proof_engine/render/postfx/
scanlines.rs1#[derive(Clone, Debug)]
9pub struct ScanlineParams {
10 pub enabled: bool,
11 pub intensity: f32,
13 pub line_width: f32,
15 pub spacing: u32,
17 pub horizontal: bool,
19 pub vsync_wobble: f32,
21 pub persistence: f32,
23 pub smoothness: f32,
25 pub tint: [f32; 3],
27}
28
29impl Default for ScanlineParams {
30 fn default() -> Self {
31 Self {
32 enabled: false,
33 intensity: 0.05,
34 line_width: 1.0,
35 spacing: 1,
36 horizontal: true,
37 vsync_wobble: 0.0,
38 persistence: 0.0,
39 smoothness: 0.5,
40 tint: [1.0, 1.0, 1.0],
41 }
42 }
43}
44
45impl ScanlineParams {
46 pub fn none() -> Self { Self { enabled: false, intensity: 0.0, ..Default::default() } }
50
51 pub fn subtle() -> Self {
53 Self {
54 enabled: true,
55 intensity: 0.05,
56 line_width: 1.0,
57 spacing: 1,
58 smoothness: 0.7,
59 ..Default::default()
60 }
61 }
62
63 pub fn arcade() -> Self {
65 Self {
66 enabled: true,
67 intensity: 0.25,
68 line_width: 1.0,
69 spacing: 1,
70 smoothness: 0.3,
71 tint: [0.9, 1.0, 0.85], ..Default::default()
73 }
74 }
75
76 pub fn damaged() -> Self {
78 Self {
79 enabled: true,
80 intensity: 0.45,
81 line_width: 1.5,
82 spacing: 1,
83 vsync_wobble: 2.5,
84 persistence: 0.4,
85 smoothness: 0.2,
86 tint: [0.8, 0.9, 0.8],
87 ..Default::default()
88 }
89 }
90
91 pub fn lofi() -> Self {
93 Self {
94 enabled: true,
95 intensity: 0.35,
96 line_width: 2.0,
97 spacing: 2,
98 smoothness: 0.1,
99 ..Default::default()
100 }
101 }
102
103 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
105 let t = t.clamp(0.0, 1.0);
106 Self {
107 enabled: if t < 0.5 { a.enabled } else { b.enabled },
108 intensity: lerp_f32(a.intensity, b.intensity, t),
109 line_width: lerp_f32(a.line_width, b.line_width, t),
110 spacing: if t < 0.5 { a.spacing } else { b.spacing },
111 horizontal: a.horizontal,
112 vsync_wobble: lerp_f32(a.vsync_wobble, b.vsync_wobble, t),
113 persistence: lerp_f32(a.persistence, b.persistence, t),
114 smoothness: lerp_f32(a.smoothness, b.smoothness, t),
115 tint: [
116 lerp_f32(a.tint[0], b.tint[0], t),
117 lerp_f32(a.tint[1], b.tint[1], t),
118 lerp_f32(a.tint[2], b.tint[2], t),
119 ],
120 }
121 }
122
123 pub fn evaluate(&self, pixel_y: f32, screen_height: f32, time: f32) -> f32 {
129 if !self.enabled { return 1.0; }
130
131 let mut y = pixel_y;
132
133 if self.vsync_wobble > 0.0 {
135 y += (time * 60.0).sin() * self.vsync_wobble;
136 }
137
138 let period = (self.spacing as f32 + 1.0) * self.line_width;
140 let phase = (y / period).fract();
141
142 let darkened = if self.smoothness > 0.0 {
144 let dip = (phase * std::f32::consts::TAU).cos() * 0.5 + 0.5;
146 let alpha = self.smoothness;
147 dip * alpha + (1.0 - alpha) * (if phase < 0.5 { 1.0 } else { 0.0 })
148 } else {
149 if phase < 0.5 { 0.0 } else { 1.0 }
150 };
151
152 1.0 - darkened * self.intensity
153 }
154}
155
156fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
157
158pub fn generate_scanline_lut(height: u32, params: &ScanlineParams) -> Vec<f32> {
165 (0..height)
166 .map(|y| params.evaluate(y as f32, height as f32, 0.0))
167 .collect()
168}
169
170#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn disabled_returns_one() {
178 let params = ScanlineParams::none();
179 assert_eq!(params.evaluate(5.0, 100.0, 0.0), 1.0);
180 }
181
182 #[test]
183 fn enabled_dims_some_pixels() {
184 let params = ScanlineParams::arcade();
185 let values: Vec<f32> = (0..20).map(|y| params.evaluate(y as f32, 100.0, 0.0)).collect();
186 let any_dimmed = values.iter().any(|&v| v < 0.99);
188 assert!(any_dimmed, "Expected some pixels to be dimmed");
189 }
190
191 #[test]
192 fn lut_has_correct_length() {
193 let params = ScanlineParams::subtle();
194 let lut = generate_scanline_lut(256, ¶ms);
195 assert_eq!(lut.len(), 256);
196 }
197
198 #[test]
199 fn all_values_in_range() {
200 let params = ScanlineParams::damaged();
201 let lut = generate_scanline_lut(480, ¶ms);
202 for v in &lut {
203 assert!(*v >= 0.0 && *v <= 1.0, "Out of range: {v}");
204 }
205 }
206
207 #[test]
208 fn lerp_halfway() {
209 let a = ScanlineParams::none();
210 let b = ScanlineParams { enabled: true, intensity: 0.4, ..Default::default() };
211 let mid = ScanlineParams::lerp(&a, &b, 0.5);
212 assert!((mid.intensity - 0.2).abs() < 0.001);
213 }
214}