whiskers.git / web / cat / src / scene.rs
1//! A mascot in a mood as a whole picture: shadow, the figure breathing, and what is round it.
2//! The assembly is `whiskers-art`'s, the same for every mascot; only the figure differs.
3
4use whiskers_art::assemble;
5pub use whiskers_art::scene::{CENTER, GAZE_REACH, GAZE_STEP, HALF_WIDTH, VIEW, gaze_offset};
6
7use crate::art_bunny;
8use crate::mascot::Mascot;
9use crate::mood::Mood;
10use crate::node::Node;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct Scene {
14    pub mascot: Mascot,
15    pub mood: Mood,
16    pub nodes: Vec<Node>,
17}
18
19/// `mascot` in `mood`. An exhaustive match over both the mascot and (inside each art) the mood.
20pub fn scene(mascot: Mascot, mood: Mood) -> Scene {
21    let nodes = match mascot {
22        Mascot::Grey => whiskers_art::scene(mood.pet(), None),
23        Mascot::Bunny => assemble(mood.pet(), art_bunny::figure(mood)),
24        Mascot::Ginger => assemble(mood.pet(), crate::art_ginger::figure(mood)),
25    };
26    Scene { mascot, mood, nodes }
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use crate::svg::{Mode, svg};
33    use std::collections::HashSet;
34
35    fn count(nodes: &[Node], n: &mut usize) {
36        for node in nodes {
37            match node {
38                Node::Rect(_) => *n += 1,
39                Node::Group(g) => count(&g.children, n),
40            }
41        }
42    }
43
44    fn markup(mascot: Mascot, mood: Mood) -> String {
45        svg(&scene(mascot, mood), "x", &Mode::Frame { t: 0.4, gaze: (0.0, 0.0) })
46    }
47
48    #[test]
49    fn every_mascot_draws_every_mood_and_no_two_pictures_are_the_same() {
50        let mut seen = HashSet::new();
51        for &mascot in Mascot::ALL {
52            for mood in Mood::ALL {
53                let mut rects = 0;
54                count(&scene(mascot, mood).nodes, &mut rects);
55                assert!(rects > 12, "{mascot:?} {mood:?}: {rects} rectangles");
56                assert!(seen.insert(markup(mascot, mood)), "{mascot:?} {mood:?} repeats another picture");
57            }
58        }
59        assert_eq!(seen.len(), Mascot::ALL.len() * Mood::ALL.len());
60    }
61
62    #[test]
63    fn every_mascots_eyes_follow_the_pointer_in_the_moods_that_look() {
64        for &mascot in Mascot::ALL {
65            for mood in [Mood::Idle, Mood::Listening, Mood::Speaking] {
66                let a = svg(&scene(mascot, mood), "x", &Mode::Frame { t: 0.4, gaze: (1.0, 0.0) });
67                let b = svg(&scene(mascot, mood), "x", &Mode::Frame { t: 0.4, gaze: (-1.0, 0.0) });
68                assert_ne!(a, b, "{mascot:?} {mood:?}");
69            }
70        }
71    }
72
73    #[test]
74    fn the_scenes_stay_inside_the_drawing_area() {
75        use crate::node::{Group, Node};
76        fn walk(nodes: &[Node], f: &mut impl FnMut(&crate::node::Rect)) {
77            for n in nodes {
78                match n {
79                    Node::Rect(r) => f(r),
80                    Node::Group(Group { children, .. }) => walk(children, f),
81                }
82            }
83        }
84        let (x0, y0, w, h) = VIEW;
85        for &mascot in Mascot::ALL {
86            for mood in Mood::ALL {
87                walk(&scene(mascot, mood).nodes, &mut |r| {
88                    let slack = 6.0;
89                    assert!(r.x >= x0 - slack && r.x + r.w <= x0 + w + slack, "{mascot:?} {mood:?} {r:?}");
90                    assert!(r.y >= y0 - slack && r.y + r.h <= y0 + h + slack, "{mascot:?} {mood:?} {r:?}");
91                });
92            }
93        }
94    }
95}