fixtures/frames.txt is the grey cat flattened at a few instants, and the Kotlin evaluator
(android/.../art/Motion.kt, Flatten.kt) is tested against the very same file: the app
draws what this crate defines, or one of the two tests fails. Regenerate after a deliberate
change to the art or the motions:
WHISKERS_ART_BLESS=1 cargo test -p whiskers-art --test frames
then record the screenshots and look at them.
10use std::fmt::Write;
Instants chosen away from every step boundary (the blink's 0.12 s, the ring's tenths, the dots' 0.4 s), so that f32 sines in two languages cannot land either side of a change.
19const MOMENTS: [(f32, (f32, f32)); 3] = [(0.09, (0.0, 0.0)), (0.93, (0.9, -0.6)), (2.71, (-1.0, 0.5))];
21fn render() -> String { 22 let mut out = String::from("# mood mouth t gx gy, then one quad per line: x0 y0 x1 y1 x3 y3 rrggbb alpha\n"); 23 for mood in MOODS { 24 let mouths: Vec<Option<Mouth>> = if mood == Mood::Speaking { std::iter::once(None).chain(Mouth::ALL.map(Some)).collect() } else { vec![None] }; 25 for mouth in mouths { 26 let nodes = scene(mood, mouth); 27 for (t, gaze) in MOMENTS { 28 let _ = writeln!(out, "= {mood:?} {} {t} {} {}", mouth.map_or("flap".to_owned(), |m| format!("{m:?}")), gaze.0, gaze.1); 29 for q in flatten(&nodes, t, gaze) { 30 let [p0, p1, _, p3] = q.corners; 31 let _ = writeln!( 32 out, 33 "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} {:02x}{:02x}{:02x} {:.3}", 34 p0.0, p0.1, p1.0, p1.1, p3.0, p3.1, q.color.0, q.color.1, q.color.2, q.alpha 35 ); 36 } 37 } 38 } 39 } 40 out 41} 42 43#[test] 44fn the_frames_fixture_is_what_the_art_draws() { 45 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/frames.txt"); 46 let now = render(); 47 if std::env::var_os("WHISKERS_ART_BLESS").is_some() { 48 std::fs::write(&path, &now).unwrap(); 49 return; 50 } 51 let kept = std::fs::read_to_string(&path).expect("fixtures/frames.txt (WHISKERS_ART_BLESS=1 writes it)"); 52 if kept != now { 53 let first = kept.lines().zip(now.lines()).enumerate().find(|(_, (a, b))| a != b); 54 panic!("the art or its motions changed; first difference {first:?}. Bless and re-record if it was meant."); 55 } 56} 57 58#[test] 59fn no_instant_sits_on_a_step_boundary_of_a_stepping_motion() { 60 // The fixture is only a fair test if both languages agree which side of a step they are on. 61 for motion in Motion::all().into_iter().filter(|m| m.steps()) { 62 for (t, _) in MOMENTS { 63 let eps = 0.004; 64 let (a, b, c) = (motion.at(t - eps), motion.at(t), motion.at(t + eps)); 65 assert!(a == b && b == c, "{} changes within {eps} s of {t}", motion.name()); 66 } 67 } 68}