mod eclipse;
use std::time::Duration;
pub use eclipse::Eclipse;
use crate::{frame::Color, scene::Vec3};
const LIT_THRESHOLD: f32 = 0.04;
pub trait Program {
fn advance(&mut self, now: Duration, width: f32, height: f32) {
let _ = (now, width, height);
}
fn fragment(&self, x: f32, y: f32) -> (Vec3, f32);
fn particles(&self, emit: &mut dyn FnMut(f32, f32, Vec3, f32)) {
let _ = emit;
}
}
impl<F: Fn(f32, f32) -> (Vec3, f32)> Program for F {
fn fragment(&self, x: f32, y: f32) -> (Vec3, f32) {
self(x, y)
}
}
pub const fn hash(value: u32) -> u32 {
let mut mixed = value;
mixed = (mixed ^ (mixed >> 16)).wrapping_mul(0x7feb_352d);
mixed = (mixed ^ (mixed >> 15)).wrapping_mul(0x846c_a68b);
mixed ^ (mixed >> 16)
}
pub fn rand01(value: u32) -> f32 {
(hash(value) >> 8) as f32 / 16_777_216.0
}
#[derive(Default)]
pub struct Surface {
pixels: Vec<(Vec3, f32)>,
}
impl Surface {
pub const fn new() -> Self {
Self { pixels: Vec::new() }
}
pub fn render<P: Program + ?Sized>(
&mut self,
program: &mut P,
now: Duration,
cols: u16,
rows: u16,
mut put: impl FnMut(u16, u16, char, Color, Option<Color>),
) {
if cols == 0 || rows == 0 {
return;
}
let width = cols as usize;
let height = rows as usize * 2;
program.advance(now, width as f32, height as f32);
self.pixels.clear();
self.pixels.reserve(width * height);
for py in 0..height {
for px in 0..width {
let (color, alpha) = program.fragment(px as f32 + 0.5, py as f32 + 0.5);
let alpha = alpha.clamp(0.0, 1.0);
self.pixels.push((color * alpha, alpha));
}
}
program.particles(&mut |x: f32, y: f32, color: Vec3, alpha: f32| {
if x < 0.0 || y < 0.0 || x >= width as f32 || y >= height as f32 {
return;
}
let alpha = alpha.clamp(0.0, 1.0);
let pixel = &mut self.pixels[y as usize * width + x as usize];
pixel.0 = color * alpha + pixel.0 * (1.0 - alpha);
pixel.1 = pixel.1.mul_add(1.0 - alpha, alpha);
});
for row in 0..rows as usize {
for col in 0..width {
let (top, top_cover) = self.pixels[row * 2 * width + col];
let (bottom, bottom_cover) = self.pixels[(row * 2 + 1) * width + col];
let (glyph, fg, bg) = match (top_cover >= LIT_THRESHOLD, bottom_cover >= LIT_THRESHOLD)
{
(true, true) => ('▀', top, Some(bottom)),
(true, false) => ('▀', top, None),
(false, true) => ('▄', bottom, None),
(false, false) => continue,
};
put(col as u16, row as u16, glyph, Color::from(fg), bg.map(Color::from));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scene::vec3;
fn cells<P: Program>(
cols: u16,
rows: u16,
program: &mut P,
) -> Vec<(u16, u16, char, Color, Option<Color>)> {
let mut out = Vec::new();
Surface::new().render(program, Duration::ZERO, cols, rows, |x, y, glyph, fg, bg| {
out.push((x, y, glyph, fg, bg));
});
out
}
#[test]
fn opaque_field_packs_pixel_pairs_into_upper_half_blocks() {
let mut split = |_: f32, y: f32| {
if y < 1.0 {
(vec3(1.0, 0.0, 0.0), 1.0)
} else {
(vec3(0.0, 0.0, 1.0), 1.0)
}
};
let cells = cells(4, 1, &mut split);
assert_eq!(cells.len(), 4);
assert!(cells.iter().all(|&(.., glyph, fg, bg)| {
glyph == '▀' && fg == Color::Rgb(255, 0, 0) && bg == Some(Color::Rgb(0, 0, 255))
}));
}
#[test]
fn coverage_below_the_lit_threshold_stays_transparent() {
let mut haze = |_: f32, _: f32| (vec3(1.0, 1.0, 1.0), 0.02);
assert_eq!(cells(8, 4, &mut haze).len(), 0, "0.02 sits under the lit threshold");
}
#[test]
fn a_half_lit_cell_picks_the_matching_half_block() {
let mut top = |_: f32, y: f32| (vec3(1.0, 1.0, 1.0), if y < 1.0 { 1.0 } else { 0.0 });
assert_eq!(cells(1, 1, &mut top), vec![(0, 0, '▀', Color::Rgb(255, 255, 255), None)]);
let mut bottom = |_: f32, y: f32| (vec3(1.0, 1.0, 1.0), if y < 1.0 { 0.0 } else { 1.0 });
assert_eq!(cells(1, 1, &mut bottom), vec![(0, 0, '▄', Color::Rgb(255, 255, 255), None)]);
}
#[test]
fn particles_blend_over_the_field_and_ignore_off_target_splats() {
struct Dust;
impl Program for Dust {
fn fragment(&self, _: f32, _: f32) -> (Vec3, f32) {
(Vec3::ZERO, 1.0)
}
fn particles(&self, emit: &mut dyn FnMut(f32, f32, Vec3, f32)) {
emit(0.5, 0.5, vec3(1.0, 1.0, 1.0), 0.5);
emit(-3.0, 0.5, vec3(1.0, 1.0, 1.0), 1.0);
emit(0.5, 99.0, vec3(1.0, 1.0, 1.0), 1.0);
}
}
let cells = cells(2, 1, &mut Dust);
assert!(cells.contains(&(0, 0, '▀', Color::Rgb(188, 188, 188), Some(Color::Rgb(0, 0, 0)))));
assert!(cells.contains(&(1, 0, '▀', Color::Rgb(0, 0, 0), Some(Color::Rgb(0, 0, 0)))));
}
#[test]
fn rand01_is_deterministic_and_unit_range() {
for seed in 0..1000_u32 {
let sample = rand01(seed);
assert!((0.0..1.0).contains(&sample));
assert_eq!(sample, rand01(seed));
}
}
#[test]
fn advance_sees_the_pixel_resolution() {
struct Probe(f32, f32);
impl Program for Probe {
fn advance(&mut self, _: Duration, width: f32, height: f32) {
*self = Self(width, height);
}
fn fragment(&self, _: f32, _: f32) -> (Vec3, f32) {
(Vec3::ZERO, 0.0)
}
}
let mut probe = Probe(0.0, 0.0);
Surface::new().render(&mut probe, Duration::ZERO, 10, 4, |_, _, _, _, _| {});
assert_eq!((probe.0, probe.1), (10.0, 8.0), "rows double into pixel height");
}
}