bunny.rsannotatedbunny.rssource220 lines · 9.0 KB · raw

The bunny: a sitting rabbit on the 15 by 16 grid, with long upright ears with a pink inside, a pear-shaped body (a narrow head over a wide lap), round dark eyes with a highlight, pink cheeks, a small split mouth, big hind feet and a round white tail. Cream on a dark page. Every pose the app has is drawn here, so the bunny is a complete character like the grey cat.

Layout, back to front: tail, feet, ears, head and body, belly, cheeks, muzzle, eyes, mouth, and whatever the pose adds (a laptop, raised paws). The eyes are Role::Eyes groups so the gaze moves them; each blinks about its own middle. The website draws this very code.

10use whiskers_pet::Mood;
12use crate::decor::{BLUSH, INK, WHITE, arch, laptop, spiral};
13use crate::motion::Motion;
14use crate::mouth;
15use pixel_mouth::{MouthSpec, Palette, Style};
16use crate::node::{Color, Look, Node, eyes, moving, r, ra};
17use crate::scene::Mouth;
18
19const FUR: Color = Color::hex(0xF1EBDD);
20const SHADE: Color = Color::hex(0xD3C8B0);
21const INNER: Color = Color::hex(0xF0B5C0);
22const NOSE: Color = Color::hex(0xE48CA0);
23const EYE: Color = Color::hex(0x2E2430);

Where the bunny's mouth sits and how it is drawn.

26pub const MOUTH: MouthSpec = MouthSpec { anchor: (7.5, 9.0), size: 2.0, style: Style::Cat, palette: Palette { ink: 0x2E2430, beak: 0x2E2430 } };

The sitting bunny in mood (any but sleeping). mouth matters only while speaking.

29pub fn standing(mood: Mood, mouth: Option<Mouth>) -> Vec<Node> {
30    debug_assert!(mood != Mood::Sleeping, "a sleeping bunny is lying down: use `sleeping`");
31    let mut out = vec![tail(mood)];
32    // Feet: big hind feet and small front ones.
33    out.extend([
34        r(2.0, 14.0, 3.0, 1.0, FUR),
35        r(2.0, 14.7, 3.0, 0.3, SHADE),
36        r(10.0, 14.0, 3.0, 1.0, FUR),
37        r(10.0, 14.7, 3.0, 0.3, SHADE),
38        r(5.5, 14.0, 1.5, 1.0, FUR),
39        r(8.0, 14.0, 1.5, 1.0, FUR),
40    ]);
41    ears(mood, &mut out);
42    // Head over a wider lap, and the pale belly.
43    out.push(r(3.0, 5.5, 9.0, 4.5, FUR));
44    out.push(r(2.0, 9.5, 11.0, 4.5, FUR));
45    out.push(r(4.5, 11.0, 6.0, 3.0, WHITE));
46    // Cheeks, muzzle and nose.
47    out.push(ra(3.6, 8.6, 1.4, 0.8, INNER, 0.6));
48    out.push(ra(10.0, 8.6, 1.4, 0.8, INNER, 0.6));
49    out.push(r(6.0, 8.2, 3.0, 2.0, WHITE));
50    out.push(r(7.0, 8.3, 1.0, 0.7, NOSE));
51    face(mood, mouth, &mut out);
52    out
53}

The tail: a small round puff that wags, flicks when tickled and hangs still when the bunny is unhappy.

56fn tail(mood: Mood) -> Node {
57    match mood {
58        Mood::Tangled | Mood::Offline => moving(Motion::Still, (0.0, 0.0), vec![r(12.8, 13.0, 1.8, 1.6, WHITE)]),
59        Mood::Tickled => moving(Motion::TailFlick, (0.0, 0.0), vec![r(13.0, 11.8, 1.0, 2.4, WHITE), r(12.6, 12.2, 1.8, 1.6, WHITE)]),
60        _ => moving(Motion::TailWag, (0.0, 0.0), vec![r(13.0, 11.8, 1.0, 2.4, WHITE), r(12.6, 12.2, 1.8, 1.6, WHITE)]),
61    }
62}

Ears: tall, two wide, a pink strip inside. Perked when listening, one folded when thinking, flopped out to the sides when dizzy and lower when the bunny is unhappy.

66fn ears(mood: Mood, out: &mut Vec<Node>) {
67    match mood {
68        Mood::Dizzy => {
69            for (x, inner) in [(2.0, 2.5), (11.0, 11.5)] {
70                out.push(r(x, 3.5, 2.0, 3.0, FUR));
71                out.push(r(inner, 4.0, 1.0, 2.0, INNER));
72            }
73        }
74        Mood::Tangled | Mood::Offline => {
75            for (x, inner) in [(2.0, 2.5), (11.0, 11.5)] {
76                out.push(r(x, 5.0, 2.0, 3.0, FUR));
77                out.push(r(inner, 5.5, 1.0, 2.0, INNER));
78            }
79        }
80        _ => {
81            let lift = if mood == Mood::Listening { -1.0 } else { 0.0 };
82            let top = lift;
83            out.push(r(4.0, top, 2.0, 6.0 - top, FUR));
84            out.push(r(4.5, top + 1.2, 1.0, 4.3 - top, INNER));
85            if mood == Mood::Thinking {
86                // The right ear is folded over at the tip.
87                out.push(r(9.0, 1.5, 2.0, 4.5, FUR));
88                out.push(r(9.5, 2.7, 1.0, 3.0, INNER));
89                out.push(r(10.0, 1.0, 1.8, 0.9, FUR));
90            } else {
91                out.push(r(9.0, top, 2.0, 6.0 - top, FUR));
92                out.push(r(9.5, top + 1.2, 1.0, 4.3 - top, INNER));
93            }
94        }
95    }
96}

One eye: a dark round with a highlight, x its left edge, shutting about its middle.

99fn eye(x: f32, top: f32, height: f32) -> Node {
100    moving(
101        Motion::Blink,
102        (x + 0.8, top + height / 2.0),
103        vec![r(x, top, 1.6, height, EYE), r(x + 0.25, top + 0.25, 0.5, 0.5, WHITE)],
104    )
105}
107fn face(mood: Mood, mouth: Option<Mouth>, out: &mut Vec<Node>) {
108    match mood {
109        Mood::Dizzy => {
110            out.extend(spiral(5.2, 7.6, INK));
111            out.extend(spiral(9.8, 7.6, INK));
112            out.push(ra(7.0, 9.4, 1.0, 0.8, EYE, 0.85));
113        }
114        Mood::Tickled => {
115            out.extend(arch(5.2, 6.6, true));
116            out.extend(arch(9.8, 6.6, true));
117            out.push(ra(3.4, 8.2, 1.6, 0.9, BLUSH, 0.7));
118            out.push(ra(10.0, 8.2, 1.6, 0.9, BLUSH, 0.7));
119            out.push(r(6.8, 9.4, 1.4, 0.8, EYE));
120            out.push(r(7.2, 9.9, 0.6, 0.3, NOSE));
121        }
122        Mood::Tangled => {
123            out.extend(arch(5.2, 6.6, false));
124            out.extend(arch(9.8, 6.6, false));
125            // A wavy mouth.
126            for (x, y) in [(6.4, 9.4), (7.1, 9.8), (7.8, 9.4)] {
127                out.push(r(x, y, 0.8, 0.4, EYE));
128            }
129        }
130        Mood::Offline => {
131            // Heavy-lidded, sad eyes and a small frown.
132            for x in [4.4, 9.0] {
133                out.push(r(x, 7.4, 1.6, 0.9, EYE));
134                out.push(r(x - 0.1, 7.0, 1.8, 0.5, SHADE));
135            }
136            out.push(r(6.8, 9.6, 1.4, 0.35, EYE));
137            out.push(r(6.3, 9.95, 0.6, 0.3, EYE));
138            out.push(r(8.1, 9.95, 0.6, 0.3, EYE));
139        }
140        Mood::Busy => {
141            // Paws up, catching and throwing; the eyes follow the balls round.
142            out.push(moving(Motion::Scan, (0.0, 0.0), vec![eyes(Look::Fixed(0.0, 0.0), vec![eye(4.4, 6.8, 1.6), eye(9.0, 6.8, 1.6)])]));
143            out.extend(mouth::closed(&MOUTH));
144            for (i, x) in [(0_u8, 1.4_f32), (1, 11.9)] {
145                out.push(moving(Motion::Bob(i), (0.0, 0.0), vec![r(x, 6.8, 1.7, 2.4, FUR), r(x, 6.8, 1.7, 0.8, WHITE)]));
146            }
147        }
148        Mood::Idle | Mood::Listening | Mood::Thinking | Mood::Working | Mood::Speaking => {
149            let (look, top, height) = match mood {
150                Mood::Listening => (Look::Follow, 6.4, 2.2),
151                Mood::Thinking => (Look::Fixed(0.8, -0.8), 6.8, 1.6),
152                Mood::Working => (Look::Fixed(0.0, 0.9), 6.8, 1.6),
153                _ => (Look::Follow, 6.8, 1.6),
154            };
155            out.push(eyes(look, vec![eye(4.4, top, height), eye(9.0, top, height)]));
156            if mood == Mood::Speaking {
157                out.extend(mouth::speaking(&MOUTH, mouth));
158            } else {
159                out.extend(mouth::closed(&MOUTH));
160            }
161            if mood == Mood::Working {
162                out.extend(laptop(WHITE));
163            }
164        }
165        Mood::Sleeping => unreachable!("a sleeping bunny is drawn by `sleeping`"),
166    }
167}

Lying in a loaf with the ears laid back along the top and the eyes shut.

170pub fn sleeping() -> Vec<Node> {
171    vec![
172        r(13.0, 13.0, 2.0, 2.0, WHITE),
173        r(2.0, 10.0, 11.0, 5.0, FUR),
174        r(3.0, 9.2, 5.0, 1.2, FUR),
175        r(3.4, 9.6, 4.0, 0.5, INNER),
176        r(4.5, 13.0, 6.0, 2.0, WHITE),
177        r(2.0, 14.6, 3.0, 0.4, SHADE),
178        r(7.0, 12.4, 1.0, 0.7, NOSE),
179        r(4.2, 12.0, 1.8, 0.4, EYE),
180        r(9.0, 12.0, 1.8, 0.4, EYE),
181        ra(3.2, 12.6, 1.2, 0.7, INNER, 0.6),
182        ra(10.6, 12.6, 1.2, 0.7, INNER, 0.6),
183    ]
184}
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::scene::MOODS;
190
191    #[test]
192    fn every_mood_draws_a_bunny_and_only_the_flat_one_sleeps() {
193        for mood in MOODS.into_iter().filter(|m| *m != Mood::Sleeping) {
194            assert!(standing(mood, None).len() > 12, "{mood:?}");
195        }
196        assert!(sleeping().len() > 8);
197    }
198
199    #[test]
200    fn the_eyes_that_look_are_the_ones_the_gaze_moves() {
201        let has_eyes = |mood| standing(mood, None).iter().any(|n| matches!(n, Node::Group(g) if matches!(g.role, crate::node::Role::Eyes(_))));
202        for mood in [Mood::Idle, Mood::Listening, Mood::Thinking, Mood::Working, Mood::Speaking] {
203            assert!(has_eyes(mood), "{mood:?}");
204        }
205        // Closed, crossed or spinning eyes are drawn, not looked with (the busy bunny's follow the balls, inside a scan).
206        for mood in [Mood::Tickled, Mood::Tangled, Mood::Offline, Mood::Dizzy] {
207            assert!(!has_eyes(mood), "{mood:?}");
208        }
209    }
210
211    #[test]
212    fn no_two_moods_draw_the_same_bunny() {
213        let mut seen: Vec<Vec<Node>> = Vec::new();
214        for mood in MOODS {
215            let nodes = if mood == Mood::Sleeping { sleeping() } else { standing(mood, None) };
216            assert!(!seen.contains(&nodes), "{mood:?} repeats another mood");
217            seen.push(nodes);
218        }
219    }
220}