frames.rs

3.7 kB · rust · 113 lines

1use mrlycore::colors::Color;2use mrlycore::errors::Result;3use mrlycore::paint::Ink;4use mrlycore::ramp::Colorizer;5use mrlycore::state::{randint, sample, shuffle};6use mrlymath::two::Cell2d;78/// Returns the thirteen secondary inks, black and white excluded.9pub fn secondaries() -> Vec<Ink> {10    Ink::all()11        .into_iter()12        .filter(|ink| !matches!(ink, Ink::Black | Ink::White))13        .collect()14}1516/// Draws a segment's neighbor palette: the accent kept, the draw cycled to the wanted length.17pub fn frame_palette(accent: Ink, simple: bool, wanted: usize) -> Vec<Color> {18    let pool = secondaries();19    let k = if simple {20        wanted.min(pool.len())21    } else {22        randint(2, pool.len() as i64) as usize23    };24    let mut inks = sample(&pool, k.max(1));25    if !inks.contains(&accent) {26        inks.pop();27        inks.push(accent);28        shuffle(&mut inks);29    }30    (0..wanted).map(|i| inks[i % inks.len()].color()).collect()31}3233/// Builds a segment's heatmap colorizer: the primary at zero, the accent fading to its opposite pole.34pub fn heat_colorizer(primary: Ink, accent: Ink, peak: usize) -> Result<Colorizer> {35    let pole = if primary == Ink::White {36        Ink::Black37    } else {38        Ink::White39    };40    Colorizer::gradient_bins(primary.color(), &[accent.color(), pole.color()], peak)41}4243/// Returns the highest cumulative visit count across a span of frames, at least one.44pub fn span_peak(grids: &[Cell2d]) -> usize {45    let Some(first) = grids.first() else {46        return 1;47    };48    let mut total = vec![0usize; first.types().size()];49    for grid in grids {50        for (i, &v) in grid.types().bytes().iter().enumerate() {51            total[i] += v as usize;52        }53    }54    total.into_iter().max().unwrap_or(0).max(1)55}5657#[cfg(test)]58mod tests {59    use super::*;60    use mrlycore::state::{guard, seed};61    use mrlycore::tensor::Tensor;62    #[test]63    fn thirteen_secondaries_skip_black_and_white() {64        let pool = secondaries();65        assert_eq!(pool.len(), 13);66        assert!(!pool.contains(&Ink::Black));67        assert!(!pool.contains(&Ink::White));68    }69    #[test]70    fn the_palette_keeps_the_accent_and_the_length() {71        let _g = guard();72        for s in 0..50 {73            seed(s);74            let palette = frame_palette(Ink::Teal, true, 9);75            assert_eq!(palette.len(), 9);76            assert!(palette.contains(&Ink::Teal.color()), "seed {s}");77        }78    }79    #[test]80    fn a_long_palette_cycles_its_draw() {81        let _g = guard();82        seed(3);83        let palette = frame_palette(Ink::Red, false, 40);84        assert_eq!(palette.len(), 40);85        let mut distinct = palette.clone();86        distinct.sort_by_key(|c| (c.r, c.g, c.b));87        distinct.dedup();88        assert!(distinct.len() <= 13);89        for (i, color) in palette.iter().enumerate() {90            assert_eq!(*color, palette[i % distinct.len().max(1)]);91        }92    }93    #[test]94    fn the_heat_ramp_grounds_on_the_primary() {95        let colorizer = heat_colorizer(Ink::Black, Ink::Teal, 8).unwrap();96        assert_eq!(colorizer.color(0, 8), Ink::Black.color());97        assert_eq!(colorizer.color(8, 8), Ink::White.color());98        let inverted = heat_colorizer(Ink::White, Ink::Teal, 8).unwrap();99        assert_eq!(inverted.color(0, 8), Ink::White.color());100        assert_eq!(inverted.color(8, 8), Ink::Black.color());101    }102    #[test]103    fn the_span_peak_counts_cumulative_visits() {104        let mut a = Tensor::new(vec![2, 2]);105        a.set(&[0, 0], 1);106        a.set(&[1, 1], 1);107        let mut b = Tensor::new(vec![2, 2]);108        b.set(&[0, 0], 1);109        let grids = vec![Cell2d::new(a), Cell2d::new(b)];110        assert_eq!(span_peak(&grids), 2);111        assert_eq!(span_peak(&[]), 1);112    }113}