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.
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}