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}