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}