1mod eclipse;
33
34use std::time::Duration;
35
36pub use eclipse::Eclipse;
37
38use crate::{frame::Color, scene::Vec3};
39
40const LIT_THRESHOLD: f32 = 0.04;
43
44pub trait Program {
53 fn advance(&mut self, now: Duration, width: f32, height: f32) {
56 let _ = (now, width, height);
57 }
58
59 fn fragment(&self, x: f32, y: f32) -> (Vec3, f32);
62
63 fn particles(&self, emit: &mut dyn FnMut(f32, f32, Vec3, f32)) {
67 let _ = emit;
68 }
69}
70
71impl<F: Fn(f32, f32) -> (Vec3, f32)> Program for F {
72 fn fragment(&self, x: f32, y: f32) -> (Vec3, f32) {
73 self(x, y)
74 }
75}
76
77pub const fn hash(value: u32) -> u32 {
80 let mut mixed = value;
81 mixed = (mixed ^ (mixed >> 16)).wrapping_mul(0x7feb_352d);
82 mixed = (mixed ^ (mixed >> 15)).wrapping_mul(0x846c_a68b);
83 mixed ^ (mixed >> 16)
84}
85
86pub fn rand01(value: u32) -> f32 {
89 (hash(value) >> 8) as f32 / 16_777_216.0
90}
91
92#[derive(Default)]
97pub struct Surface {
98 pixels: Vec<(Vec3, f32)>,
102}
103
104impl Surface {
105 pub const fn new() -> Self {
107 Self { pixels: Vec::new() }
108 }
109
110 pub fn render<P: Program + ?Sized>(
118 &mut self,
119 program: &mut P,
120 now: Duration,
121 cols: u16,
122 rows: u16,
123 mut put: impl FnMut(u16, u16, char, Color, Option<Color>),
124 ) {
125 if cols == 0 || rows == 0 {
126 return;
127 }
128 let width = cols as usize;
129 let height = rows as usize * 2;
130 program.advance(now, width as f32, height as f32);
131
132 self.pixels.clear();
133 self.pixels.reserve(width * height);
134 for py in 0..height {
135 for px in 0..width {
136 let (color, alpha) = program.fragment(px as f32 + 0.5, py as f32 + 0.5);
137 let alpha = alpha.clamp(0.0, 1.0);
138 self.pixels.push((color * alpha, alpha));
139 }
140 }
141 program.particles(&mut |x: f32, y: f32, color: Vec3, alpha: f32| {
142 if x < 0.0 || y < 0.0 || x >= width as f32 || y >= height as f32 {
143 return;
144 }
145 let alpha = alpha.clamp(0.0, 1.0);
146 let pixel = &mut self.pixels[y as usize * width + x as usize];
147 pixel.0 = color * alpha + pixel.0 * (1.0 - alpha);
148 pixel.1 = pixel.1.mul_add(1.0 - alpha, alpha);
149 });
150
151 for row in 0..rows as usize {
152 for col in 0..width {
153 let (top, top_cover) = self.pixels[row * 2 * width + col];
154 let (bottom, bottom_cover) = self.pixels[(row * 2 + 1) * width + col];
155 let (glyph, fg, bg) = match (top_cover >= LIT_THRESHOLD, bottom_cover >= LIT_THRESHOLD)
156 {
157 (true, true) => ('▀', top, Some(bottom)),
158 (true, false) => ('▀', top, None),
159 (false, true) => ('▄', bottom, None),
160 (false, false) => continue,
161 };
162 put(col as u16, row as u16, glyph, Color::from(fg), bg.map(Color::from));
163 }
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::scene::vec3;
172
173 fn cells<P: Program>(
174 cols: u16,
175 rows: u16,
176 program: &mut P,
177 ) -> Vec<(u16, u16, char, Color, Option<Color>)> {
178 let mut out = Vec::new();
179 Surface::new().render(program, Duration::ZERO, cols, rows, |x, y, glyph, fg, bg| {
180 out.push((x, y, glyph, fg, bg));
181 });
182 out
183 }
184
185 #[test]
186 fn opaque_field_packs_pixel_pairs_into_upper_half_blocks() {
187 let mut split = |_: f32, y: f32| {
189 if y < 1.0 {
190 (vec3(1.0, 0.0, 0.0), 1.0)
191 } else {
192 (vec3(0.0, 0.0, 1.0), 1.0)
193 }
194 };
195 let cells = cells(4, 1, &mut split);
196 assert_eq!(cells.len(), 4);
197 assert!(cells.iter().all(|&(.., glyph, fg, bg)| {
198 glyph == '▀' && fg == Color::Rgb(255, 0, 0) && bg == Some(Color::Rgb(0, 0, 255))
199 }));
200 }
201
202 #[test]
203 fn coverage_below_the_lit_threshold_stays_transparent() {
204 let mut haze = |_: f32, _: f32| (vec3(1.0, 1.0, 1.0), 0.02);
205 assert_eq!(cells(8, 4, &mut haze).len(), 0, "0.02 sits under the lit threshold");
206 }
207
208 #[test]
209 fn a_half_lit_cell_picks_the_matching_half_block() {
210 let mut top = |_: f32, y: f32| (vec3(1.0, 1.0, 1.0), if y < 1.0 { 1.0 } else { 0.0 });
211 assert_eq!(cells(1, 1, &mut top), vec![(0, 0, '▀', Color::Rgb(255, 255, 255), None)]);
212 let mut bottom = |_: f32, y: f32| (vec3(1.0, 1.0, 1.0), if y < 1.0 { 0.0 } else { 1.0 });
213 assert_eq!(cells(1, 1, &mut bottom), vec![(0, 0, '▄', Color::Rgb(255, 255, 255), None)]);
214 }
215
216 #[test]
217 fn particles_blend_over_the_field_and_ignore_off_target_splats() {
218 struct Dust;
219 impl Program for Dust {
220 fn fragment(&self, _: f32, _: f32) -> (Vec3, f32) {
221 (Vec3::ZERO, 1.0)
222 }
223
224 fn particles(&self, emit: &mut dyn FnMut(f32, f32, Vec3, f32)) {
225 emit(0.5, 0.5, vec3(1.0, 1.0, 1.0), 0.5);
226 emit(-3.0, 0.5, vec3(1.0, 1.0, 1.0), 1.0);
227 emit(0.5, 99.0, vec3(1.0, 1.0, 1.0), 1.0);
228 }
229 }
230 let cells = cells(2, 1, &mut Dust);
231 assert!(cells.contains(&(0, 0, '▀', Color::Rgb(188, 188, 188), Some(Color::Rgb(0, 0, 0)))));
233 assert!(cells.contains(&(1, 0, '▀', Color::Rgb(0, 0, 0), Some(Color::Rgb(0, 0, 0)))));
234 }
235
236 #[test]
237 fn rand01_is_deterministic_and_unit_range() {
238 for seed in 0..1000_u32 {
239 let sample = rand01(seed);
240 assert!((0.0..1.0).contains(&sample));
241 assert_eq!(sample, rand01(seed));
242 }
243 }
244
245 #[test]
246 fn advance_sees_the_pixel_resolution() {
247 struct Probe(f32, f32);
248 impl Program for Probe {
249 fn advance(&mut self, _: Duration, width: f32, height: f32) {
250 *self = Self(width, height);
251 }
252
253 fn fragment(&self, _: f32, _: f32) -> (Vec3, f32) {
254 (Vec3::ZERO, 0.0)
255 }
256 }
257 let mut probe = Probe(0.0, 0.0);
258 Surface::new().render(&mut probe, Duration::ZERO, 10, 4, |_, _, _, _, _| {});
259 assert_eq!((probe.0, probe.1), (10.0, 8.0), "rows double into pixel height");
260 }
261}