render.rs

6.1 kB · rust · 178 lines

1use super::models::Life;2use super::Boundary;3use crate::two::{self, Cell2d};4use mrlycore::colors::{Color, BLACK, WHITE};5use mrlycore::enums::Mode;6use mrlycore::errors::{value_error, Result};7use mrlycore::paint::Edition;8use std::collections::HashMap;910fn default_palette() -> HashMap<u8, Vec<Color>> {11    HashMap::from([(0, vec![WHITE]), (1, vec![BLACK])])12}1314fn paint_neighbors(15    grid: &Cell2d,16    mask: &Cell2d,17    boundary: Boundary,18    mapping: &HashMap<u8, Vec<Color>>,19) -> Result<Cell2d> {20    let tagged = grid.clone().neighbors(mask.types(), 1, boundary.wrap())?;21    Ok(tagged.paint(mapping, Edition::Neighbors.mode()))22}2324/// Renders grids colored by live-neighbor count under the mask: the primary on dead25/// cells, the secondary palette of mask popcount plus one on live cells, one PNG each.26pub fn frames_with(27    grids: &[Cell2d],28    mask: &Cell2d,29    boundary: Boundary,30    scale: usize,31    primary: Color,32    secondary: &[Color],33) -> Result<Vec<Vec<u8>>> {34    let wanted = mask.types().sum() as usize + 1;35    if secondary.len() != wanted {36        return value_error(format!(37            "mask wants {wanted} neighbor colors, got {}.",38            secondary.len()39        ));40    }41    let mapping = HashMap::from([(0, vec![primary]), (1, secondary.to_vec())]);42    let mut out = Vec::with_capacity(grids.len());43    for grid in grids {44        let painted = paint_neighbors(grid, mask, boundary, &mapping)?;45        out.push(two::png(&painted, scale)?);46    }47    Ok(out)48}4950/// Renders every generation of a run to PNG bytes.51pub fn frames(life: &Life, scale: usize) -> Result<Vec<Vec<u8>>> {52    frames_of(&life.grids, scale)53}5455/// Renders grids to black-on-white PNG bytes at a pixel scale.56pub fn frames_of(grids: &[Cell2d], scale: usize) -> Result<Vec<Vec<u8>>> {57    let palette = default_palette();58    let mut out = Vec::with_capacity(grids.len());59    for grid in grids {60        let painted = grid.clone().paint(&palette, Mode::Type);61        out.push(two::png(&painted, scale)?);62    }63    Ok(out)64}6566/// Renders one grid to black-on-white PNG bytes at a pixel scale.67pub fn frame(grid: &Cell2d, scale: usize) -> Result<Vec<u8>> {68    let painted = grid.clone().paint(&default_palette(), Mode::Type);69    two::png(&painted, scale)70}7172/// Renders grids into one looping black-on-white gif, the delay in hundredths of a second.73pub fn movie(grids: &[Cell2d], scale: usize, delay: usize) -> Result<Vec<u8>> {74    let Some(first) = grids.first() else {75        return value_error("a movie needs at least one grid.");76    };77    let (width, height) = (first.width(), first.height());78    let mut frames = Vec::with_capacity(grids.len());79    for grid in grids {80        if (grid.width(), grid.height()) != (width, height) {81            return value_error("every grid must share one size.");82        }83        let types = grid.types();84        frames.push(85            (0..types.size())86                .map(|i| u8::from(types.at(i) != 0))87                .collect::<Vec<u8>>(),88        );89    }90    let views: Vec<&[u8]> = frames.iter().map(|frame| frame.as_slice()).collect();91    let palette = [92        [WHITE.r, WHITE.g, WHITE.b, WHITE.a],93        [BLACK.r, BLACK.g, BLACK.b, BLACK.a],94    ];95    mrlycore::codec::gif(&views, &palette, width, height, scale, delay)96}9798#[cfg(test)]99mod tests {100    use super::*;101    use crate::life::{animate, moore, Config};102    use mrlycore::tensor::Tensor;103    fn triple() -> Cell2d {104        let mut t = Tensor::new(vec![3, 3]);105        t.set(&[1, 0], 1);106        t.set(&[1, 1], 1);107        t.set(&[1, 2], 1);108        Cell2d::new(t)109    }110    #[test]111    fn frames_are_pngs() {112        let config = Config {113            boundary: Boundary::Constant,114            max_generations: 8,115            ..Config::new(moore(), vec![3], vec![2, 3])116        };117        let mut t = Tensor::new(vec![5, 5]);118        t.set(&[1, 2], 1);119        t.set(&[2, 2], 1);120        t.set(&[3, 2], 1);121        let life = animate(&Cell2d::new(t), &config).unwrap();122        let pngs = frames(&life, 4).unwrap();123        assert_eq!(pngs.len(), life.count);124        for png in &pngs {125            assert_eq!(&png[1..4], b"PNG");126        }127    }128    #[test]129    fn a_run_becomes_one_looping_gif() {130        let config = Config {131            boundary: Boundary::Constant,132            max_generations: 8,133            ..Config::new(moore(), vec![3], vec![2, 3])134        };135        let mut t = Tensor::new(vec![5, 5]);136        t.set(&[1, 2], 1);137        t.set(&[2, 2], 1);138        t.set(&[3, 2], 1);139        let life = animate(&Cell2d::new(t), &config).unwrap();140        let gif = movie(&life.grids, 4, 20).unwrap();141        assert_eq!(&gif[0..6], b"GIF89a");142        assert_eq!(&gif[6..10], &[20, 0, 20, 0]);143        assert_eq!(gif[gif.len() - 1], 0x3b);144        assert!(gif.len() < frames(&life, 4).unwrap().iter().map(|f| f.len()).sum());145        assert!(movie(&[], 4, 20).is_err());146        assert!(movie(&[triple(), life.grids[0].clone()], 4, 20).is_err());147    }148    #[test]149    fn neighbor_paint_follows_the_counts() {150        let primary = WHITE;151        let secondary: Vec<Color> = (0..9).map(|i| Color::rgb(10 * i, i, 255 - i)).collect();152        let mapping = HashMap::from([(0, vec![primary]), (1, secondary.clone())]);153        let painted = paint_neighbors(&triple(), &moore(), Boundary::Constant, &mapping).unwrap();154        let colors = painted.cell.colors.as_ref().unwrap();155        let rgba = |c: Color| [c.r, c.g, c.b, c.a];156        assert_eq!(colors[0], rgba(primary));157        assert_eq!(colors[3], rgba(secondary[1]));158        assert_eq!(colors[4], rgba(secondary[2]));159        assert_eq!(colors[5], rgba(secondary[1]));160    }161    #[test]162    fn neighbor_frames_want_a_full_palette() {163        let secondary: Vec<Color> = (0..9).map(|_| BLACK).collect();164        let grids = [triple()];165        let pngs = frames_with(&grids, &moore(), Boundary::Constant, 2, WHITE, &secondary).unwrap();166        assert_eq!(pngs.len(), 1);167        assert_eq!(&pngs[0][1..4], b"PNG");168        let short = frames_with(169            &grids,170            &moore(),171            Boundary::Constant,172            2,173            WHITE,174            &secondary[..3],175        );176        assert!(short.is_err());177    }178}