render.rs

3.8 kB · rust · 110 lines

1use super::models::Life;2use crate::core::colors::{Color, BLACK, WHITE};3use crate::core::enums::Mode;4use crate::core::errors::{value_error, Result};5use crate::math::two::{self, Cell2d};6use std::collections::HashMap;78fn default_palette() -> HashMap<u8, Vec<Color>> {9    HashMap::from([(0, vec![WHITE]), (1, vec![BLACK])])10}1112/// Renders every generation of a run to PNG bytes.13pub fn frames(life: &Life, scale: usize) -> Result<Vec<Vec<u8>>> {14    frames_of(&life.grids, scale)15}1617/// Renders grids to black-on-white PNG bytes at a pixel scale.18pub fn frames_of(grids: &[Cell2d], scale: usize) -> Result<Vec<Vec<u8>>> {19    let palette = default_palette();20    let mut out = Vec::with_capacity(grids.len());21    for grid in grids {22        let painted = grid.clone().paint(&palette, Mode::Type);23        out.push(two::png(&painted, scale)?);24    }25    Ok(out)26}2728/// Renders one grid to black-on-white PNG bytes at a pixel scale.29pub fn frame(grid: &Cell2d, scale: usize) -> Result<Vec<u8>> {30    let painted = grid.clone().paint(&default_palette(), Mode::Type);31    two::png(&painted, scale)32}3334/// Renders grids into one looping black-on-white gif, the delay in hundredths of a second.35pub fn movie(grids: &[Cell2d], scale: usize, delay: usize) -> Result<Vec<u8>> {36    let Some(first) = grids.first() else {37        return value_error("a movie needs at least one grid.");38    };39    let (width, height) = (first.width(), first.height());40    let mut frames = Vec::with_capacity(grids.len());41    for grid in grids {42        if (grid.width(), grid.height()) != (width, height) {43            return value_error("every grid must share one size.");44        }45        let types = grid.types();46        frames.push(47            (0..types.size())48                .map(|i| u8::from(types.at(i) != 0))49                .collect::<Vec<u8>>(),50        );51    }52    let views: Vec<&[u8]> = frames.iter().map(|frame| frame.as_slice()).collect();53    let palette = [54        [WHITE.r, WHITE.g, WHITE.b, WHITE.a],55        [BLACK.r, BLACK.g, BLACK.b, BLACK.a],56    ];57    crate::core::codec::gif(&views, &palette, width, height, scale, delay)58}5960#[cfg(test)]61mod tests {62    use super::*;63    use crate::core::tensor::Tensor;64    use crate::life::{animate, moore, Boundary, Config};65    fn triple() -> Cell2d {66        let mut t = Tensor::new(vec![3, 3]);67        t.set(&[1, 0], 1);68        t.set(&[1, 1], 1);69        t.set(&[1, 2], 1);70        Cell2d::new(t)71    }72    #[test]73    fn frames_are_pngs() {74        let config = Config {75            boundary: Boundary::Constant,76            max_generations: 8,77            ..Config::new(moore(), vec![3], vec![2, 3])78        };79        let mut t = Tensor::new(vec![5, 5]);80        t.set(&[1, 2], 1);81        t.set(&[2, 2], 1);82        t.set(&[3, 2], 1);83        let life = animate(&Cell2d::new(t), &config).unwrap();84        let pngs = frames(&life, 4).unwrap();85        assert_eq!(pngs.len(), life.count);86        for png in &pngs {87            assert_eq!(&png[1..4], b"PNG");88        }89    }90    #[test]91    fn a_run_becomes_one_looping_gif() {92        let config = Config {93            boundary: Boundary::Constant,94            max_generations: 8,95            ..Config::new(moore(), vec![3], vec![2, 3])96        };97        let mut t = Tensor::new(vec![5, 5]);98        t.set(&[1, 2], 1);99        t.set(&[2, 2], 1);100        t.set(&[3, 2], 1);101        let life = animate(&Cell2d::new(t), &config).unwrap();102        let gif = movie(&life.grids, 4, 20).unwrap();103        assert_eq!(&gif[0..6], b"GIF89a");104        assert_eq!(&gif[6..10], &[20, 0, 20, 0]);105        assert_eq!(gif[gif.len() - 1], 0x3b);106        assert!(gif.len() < frames(&life, 4).unwrap().iter().map(|f| f.len()).sum());107        assert!(movie(&[], 4, 20).is_err());108        assert!(movie(&[triple(), life.grids[0].clone()], 4, 20).is_err());109    }110}