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::default() }
48
49 pub fn subtle() -> Self {
51 Self {
52 enabled: true,
53 intensity: 0.05,
54 line_width: 1.0,
55 spacing: 1,
56 smoothness: 0.7,
57 ..Default::default()
58 }
59 }
60
61 pub fn arcade() -> Self {
63 Self {
64 enabled: true,
65 intensity: 0.25,
66 line_width: 1.0,
67 spacing: 1,
68 smoothness: 0.3,
69 tint: [0.9, 1.0, 0.85], ..Default::default()
71 }
72 }
73
74 pub fn damaged() -> Self {
76 Self {
77 enabled: true,
78 intensity: 0.45,
79 line_width: 1.5,
80 spacing: 1,
81 vsync_wobble: 2.5,
82 persistence: 0.4,
83 smoothness: 0.2,
84 tint: [0.8, 0.9, 0.8],
85 ..Default::default()
86 }
87 }
88
89 pub fn lofi() -> Self {
91 Self {
92 enabled: true,
93 intensity: 0.35,
94 line_width: 2.0,
95 spacing: 2,
96 smoothness: 0.1,
97 ..Default::default()
98 }
99 }
100
101 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
103 let t = t.clamp(0.0, 1.0);
104 Self {
105 enabled: if t < 0.5 { a.enabled } else { b.enabled },
106 intensity: lerp_f32(a.intensity, b.intensity, t),
107 line_width: lerp_f32(a.line_width, b.line_width, t),
108 spacing: if t < 0.5 { a.spacing } else { b.spacing },
109 horizontal: a.horizontal,
110 vsync_wobble: lerp_f32(a.vsync_wobble, b.vsync_wobble, t),
111 persistence: lerp_f32(a.persistence, b.persistence, t),
112 smoothness: lerp_f32(a.smoothness, b.smoothness, t),
113 tint: [
114 lerp_f32(a.tint[0], b.tint[0], t),
115 lerp_f32(a.tint[1], b.tint[1], t),
116 lerp_f32(a.tint[2], b.tint[2], t),
117 ],
118 }
119 }
120
121 pub fn evaluate(&self, pixel_y: f32, screen_height: f32, time: f32) -> f32 {
127 if !self.enabled { return 1.0; }
128
129 let mut y = pixel_y;
130
131 if self.vsync_wobble > 0.0 {
133 y += (time * 60.0).sin() * self.vsync_wobble;
134 }
135
136 let period = (self.spacing as f32 + 1.0) * self.line_width;
138 let phase = (y / period).fract();
139
140 let darkened = if self.smoothness > 0.0 {
142 let dip = (phase * std::f32::consts::TAU).cos() * 0.5 + 0.5;
144 let alpha = self.smoothness;
145 dip * alpha + (1.0 - alpha) * (if phase < 0.5 { 1.0 } else { 0.0 })
146 } else {
147 if phase < 0.5 { 0.0 } else { 1.0 }
148 };
149
150 1.0 - darkened * self.intensity
151 }
152}
153
154fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
155
156pub fn generate_scanline_lut(height: u32, params: &ScanlineParams) -> Vec<f32> {
163 (0..height)
164 .map(|y| params.evaluate(y as f32, height as f32, 0.0))
165 .collect()
166}
167
168#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn disabled_returns_one() {
176 let params = ScanlineParams::none();
177 assert_eq!(params.evaluate(5.0, 100.0, 0.0), 1.0);
178 }
179
180 #[test]
181 fn enabled_dims_some_pixels() {
182 let params = ScanlineParams::arcade();
183 let values: Vec<f32> = (0..20).map(|y| params.evaluate(y as f32, 100.0, 0.0)).collect();
184 let any_dimmed = values.iter().any(|&v| v < 0.99);
186 assert!(any_dimmed, "Expected some pixels to be dimmed");
187 }
188
189 #[test]
190 fn lut_has_correct_length() {
191 let params = ScanlineParams::subtle();
192 let lut = generate_scanline_lut(256, ¶ms);
193 assert_eq!(lut.len(), 256);
194 }
195
196 #[test]
197 fn all_values_in_range() {
198 let params = ScanlineParams::damaged();
199 let lut = generate_scanline_lut(480, ¶ms);
200 for v in &lut {
201 assert!(*v >= 0.0 && *v <= 1.0, "Out of range: {v}");
202 }
203 }
204
205 #[test]
206 fn lerp_halfway() {
207 let a = ScanlineParams::none();
208 let b = ScanlineParams { enabled: true, intensity: 0.4, ..Default::default() };
209 let mid = ScanlineParams::lerp(&a, &b, 0.5);
210 assert!((mid.intensity - 0.2).abs() < 0.001);
211 }
212}