animate.rs

8.8 kB · rust · 287 lines

1use super::paths::strokes;2use super::raster::{layout, Block, Layout};3use std::collections::BTreeSet;45/// The playback rate of every animation, in frames per second.6pub const FPS: usize = 25;78/// The default number of frames a cycle rests between movements.9pub const HOLD: usize = 25;1011/// A frame-by-frame animation over a fixed board.12#[derive(Clone, Debug, PartialEq, Eq)]13pub struct Anim {14    /// The board height in cells.15    pub rows: usize,16    /// The board width in cells.17    pub cols: usize,18    /// The playback rate in frames per second.19    pub fps: usize,20    /// The frames, each a sorted list of lit row-major cell indices.21    pub frames: Vec<Vec<usize>>,22}2324/// Writes the text in stroke order, one cell per frame, from an empty padded board to the full raster.25///26/// ```27/// let write = mrlyfont::animate("MRLYPROD", 1);28/// assert_eq!((write.rows, write.cols, write.fps), (7, 49, 25));29/// assert!(write.frames[0].is_empty());30/// ```31pub fn animate(text: &str, pad: usize) -> Anim {32    let laid = layout(text);33    let (rows, cols) = board(&laid, pad);34    let mut frames = vec![Vec::new()];35    let mut current: Vec<usize> = Vec::new();36    for block in &laid.blocks {37        for (r, c) in strokes(block.char).into_iter().flatten() {38            current.push((pad + block.offset + r) * cols + (pad + block.col + c));39            let mut frame = current.clone();40            frame.sort_unstable();41            frames.push(frame);42        }43    }44    Anim {45        rows,46        cols,47        fps: FPS,48        frames,49    }50}5152/// Folds the written text's glyphs, frame by frame, into one centered stack.53pub fn merge(text: &str, pad: usize) -> Vec<Vec<usize>> {54    let laid = layout(text);55    let (rows, cols) = board(&laid, pad);56    let n = laid.blocks.len();57    let phases = n / 2;58    let starts: Vec<(i64, i64)> = laid59        .blocks60        .iter()61        .map(|b| ((pad + b.col) as i64, (pad + b.offset) as i64))62        .collect();63    let targets: Vec<(i64, i64)> = laid64        .blocks65        .iter()66        .map(|b| (((cols - b.width()) / 2) as i64, (pad + b.offset) as i64))67        .collect();68    if phases == 0 {69        return vec![stamp(&laid.blocks, &starts, rows, cols)];70    }71    let total = cols / 2;72    let mut frames: Vec<Vec<usize>> = Vec::new();73    let mut prev: Option<Vec<usize>> = None;74    for i in 0..=total {75        let (phase, progress) = beat(i, total, phases);76        let spots: Vec<(i64, i64)> = (0..n)77            .map(|idx| place(idx, n, phases, phase, progress, &starts, &targets))78            .collect();79        let frame = stamp(&laid.blocks, &spots, rows, cols);80        if prev.as_ref() != Some(&frame) {81            frames.push(frame.clone());82            prev = Some(frame);83        }84    }85    frames86}8788/// Chains the write, the merge and their reversals into one loop, resting hold frames after each.89pub fn cycle(write: &Anim, merge: &[Vec<usize>], hold: usize) -> Anim {90    let mut frames: Vec<Vec<usize>> = Vec::new();91    let rest = |frame: &Vec<usize>, out: &mut Vec<Vec<usize>>| {92        for _ in 0..hold {93            out.push(frame.clone());94        }95    };96    frames.extend(write.frames.iter().cloned());97    rest(write.frames.last().unwrap(), &mut frames);98    frames.extend(merge.iter().cloned());99    rest(merge.last().unwrap(), &mut frames);100    frames.extend(merge.iter().rev().cloned());101    rest(merge.first().unwrap(), &mut frames);102    frames.extend(write.frames.iter().rev().cloned());103    rest(write.frames.first().unwrap(), &mut frames);104    Anim {105        rows: write.rows,106        cols: write.cols,107        fps: write.fps,108        frames,109    }110}111112fn board(laid: &Layout, pad: usize) -> (usize, usize) {113    if laid.blocks.is_empty() {114        return (0, 0);115    }116    (laid.height + 2 * pad, laid.width + 2 * pad)117}118119fn stamp(blocks: &[Block], spots: &[(i64, i64)], rows: usize, cols: usize) -> Vec<usize> {120    let mut active: BTreeSet<usize> = BTreeSet::new();121    for (block, &(cx, cy)) in blocks.iter().zip(spots) {122        for (r, row) in block.rows.iter().enumerate() {123            for (c, ch) in row.chars().enumerate() {124                if ch != '1' {125                    continue;126                }127                let (y, x) = (cy + r as i64, cx + c as i64);128                if y >= 0 && y < rows as i64 && x >= 0 && x < cols as i64 {129                    active.insert(y as usize * cols + x as usize);130                }131            }132        }133    }134    active.into_iter().collect()135}136137fn beat(frame: usize, total: usize, phases: usize) -> (usize, f64) {138    let len = total / phases;139    for p in 1..phases {140        if frame < len * p {141            return (p, (frame - len * (p - 1)) as f64 / len as f64);142        }143    }144    let done = len * (phases - 1);145    (phases, (frame - done) as f64 / (total - done) as f64)146}147148fn lerp(start: i64, end: i64, p: f64) -> i64 {149    start + ((end - start) as f64 * p).trunc() as i64150}151152fn place(153    idx: usize,154    n: usize,155    phases: usize,156    phase: usize,157    progress: f64,158    starts: &[(i64, i64)],159    targets: &[(i64, i64)],160) -> (i64, i64) {161    let slide = |a: (i64, i64), b: (i64, i64)| (lerp(a.0, b.0, progress), lerp(a.1, b.1, progress));162    if phase < phases {163        if idx < phase {164            return slide(starts[phase - 1], starts[phase]);165        }166        if idx >= n - phase {167            return slide(starts[n - phase], starts[n - phase - 1]);168        }169        return starts[idx];170    }171    let anchor = if idx < phases {172        starts[phases - 1]173    } else if idx >= n - phases {174        starts[n - phases]175    } else {176        starts[idx]177    };178    slide(anchor, targets[idx])179}180181#[cfg(test)]182mod tests {183    use super::*;184    use crate::raster;185    const WORDMARK: &str = "MRLYPROD";186187    fn lit(rows: &[Vec<u8>]) -> usize {188        rows.iter().flatten().filter(|&&v| v == 1).count()189    }190191    #[test]192    fn the_wordmark_board_is_seven_by_forty_nine() {193        let write = animate(WORDMARK, 1);194        assert_eq!((write.rows, write.cols, write.fps), (7, 49, 25));195    }196197    #[test]198    fn writing_starts_empty_and_grows_one_cell_a_frame() {199        let write = animate(WORDMARK, 1);200        assert!(write.frames[0].is_empty());201        for pair in write.frames.windows(2) {202            assert_eq!(pair[1].len(), pair[0].len() + 1);203        }204        assert_eq!(write.frames.len(), 104);205        assert_eq!(206            write.frames.last().unwrap().len(),207            lit(&raster(WORDMARK)),208            "the last frame is the whole wordmark"209        );210    }211212    #[test]213    fn the_last_frame_is_the_padded_raster() {214        let write = animate(WORDMARK, 1);215        let grid = raster(WORDMARK);216        let want: Vec<usize> = grid217            .iter()218            .enumerate()219            .flat_map(|(r, row)| {220                row.iter()221                    .enumerate()222                    .filter(|&(_, &v)| v == 1)223                    .map(move |(c, _)| (1 + r) * 49 + 1 + c)224            })225            .collect();226        assert_eq!(*write.frames.last().unwrap(), want);227    }228229    #[test]230    fn merging_collapses_in_twenty_two_frames() {231        let merged = merge(WORDMARK, 1);232        assert_eq!(merged.len(), 22);233        assert_eq!(merged[0], animate(WORDMARK, 1).frames[103]);234        let x = crate::glyph('X').unwrap();235        let stacked: Vec<usize> = x236            .rows237            .iter()238            .enumerate()239            .flat_map(|(r, row)| {240                row.chars()241                    .enumerate()242                    .filter(|&(_, ch)| ch == '1')243                    .map(move |(c, _)| (1 + r) * 49 + 22 + c)244            })245            .collect();246        assert_eq!(*merged.last().unwrap(), stacked, "the eight fold into X");247    }248249    #[test]250    fn frames_are_sorted_and_in_bounds() {251        let write = animate(WORDMARK, 1);252        let anim = cycle(&write, &merge(WORDMARK, 1), HOLD);253        for frame in &anim.frames {254            assert!(frame.windows(2).all(|w| w[0] < w[1]));255            assert!(frame.iter().all(|&i| i < anim.rows * anim.cols));256        }257    }258259    #[test]260    fn the_cycle_loops_through_both_halves() {261        let write = animate(WORDMARK, 1);262        let merged = merge(WORDMARK, 1);263        let anim = cycle(&write, &merged, HOLD);264        assert_eq!(265            anim.frames.len(),266            2 * write.frames.len() + 2 * merged.len() + 4 * HOLD267        );268        assert_eq!(anim.frames.len(), 352);269    }270271    #[test]272    fn any_string_writes_itself() {273        for text in ["a", "hi", "mrly.net", "(1)"] {274            let write = animate(text, 2);275            let grid = raster(text);276            assert_eq!(write.rows, grid.len() + 4);277            assert_eq!(write.cols, grid[0].len() + 4);278            assert_eq!(write.frames.last().unwrap().len(), lit(&grid));279            assert_eq!(write.frames.len(), lit(&grid) + 1);280        }281    }282283    #[test]284    fn a_lone_glyph_has_nothing_to_merge() {285        assert_eq!(merge("A", 1).len(), 1);286    }287}