The ginger cat: an upright cat on the 15 by 16 grid with a block torso, four legs, arms at its sides, ears that perk and droop, whiskers, and a pink nose. Every pose the app has is drawn here, so ginger is a complete character like the grey cat and the bunny, and the website draws the very same code.
Layout, back to front: legs, tail, ears, arms, torso, nose and whiskers, eyes, mouth, and whatever the pose
adds (a laptop under typing paws). The eyes are Role::Eyes groups so the gaze moves them; each blinks
about its own middle. The mouth is pixel-mouth's, drawn only while speaking.
12use crate::decor::{BLUSH, INK, WHITE, spiral}; 13use crate::motion::Motion; 14use crate::mouth; 15use crate::node::{Color, Look, Node, eyes, moving, r, ra}; 16use crate::scene::Mouth; 17 18const FUR: Color = Color::hex(0xDE886D); 19const INNER_EAR: Color = Color::hex(0xF2A7B0); 20const WHISKER: Color = Color::hex(0xF5EDE6); 21const LAPTOP_BODY: Color = Color::hex(0xB9B9C4); 22const LAPTOP_LID: Color = Color::hex(0x3A3A44);
Where ginger's mouth sits and how it is drawn: a plain line when shut, drawn only while speaking.
25pub const MOUTH: MouthSpec = MouthSpec { anchor: (7.5, 11.0), size: 2.4, style: Style::Flat, palette: Palette { ink: 0x000000, beak: 0x000000 } };
The ginger cat in mood (any but sleeping). mouth matters only while speaking.
28pub fn standing(mood: Mood, mouth: Option<Mouth>) -> Vec<Node> { 29 debug_assert!(mood != Mood::Sleeping, "a sleeping cat is lying down: use `sleeping`"); 30 let mut out = Vec::new(); 31 // Legs. 32 for x in [3.0, 5.0, 9.0, 11.0] { 33 out.push(r(x, 13.0, 1.0, 2.0, FUR)); 34 } 35 // Tail: the elbow stays, the upright wags (whipping when tickled, hanging when unhappy). 36 out.push(r(13.0, 12.0, 2.0, 1.0, FUR)); 37 match mood { 38 Mood::Tangled => out.push(r(14.0, 11.3, 1.0, 0.7, FUR)), 39 Mood::Tickled => out.push(moving(Motion::TailFlick, (0.0, 0.0), vec![r(14.0, 10.5, 1.0, 1.5, FUR)])), 40 _ => out.push(moving(Motion::TailWag, (0.0, 0.0), vec![r(14.0, 10.5, 1.0, 1.5, FUR)])), 41 } 42 // Ears: bottoms fixed on the head, tops perk up when listening and droop when unhappy. 43 let lift = match mood { 44 Mood::Listening => -1.0, 45 Mood::Tangled | Mood::Offline => 0.7, 46 _ => 0.0, 47 }; 48 for (x, top) in [(2.0, 4.0), (3.0, 3.0), (11.0, 3.0), (12.0, 4.0)] { 49 out.push(r(x, top + lift, 1.0, 6.0 - (top + lift), FUR)); 50 } 51 out.push(r(3.0, 4.0 + lift, 1.0, 2.0 - lift, INNER_EAR)); 52 out.push(r(11.0, 4.0 + lift, 1.0, 2.0 - lift, INNER_EAR)); 53 // Arms (before the torso so the laptop and torso sit over them). 54 match mood { 55 Mood::Working => { 56 out.push(moving(Motion::Type(0), (0.0, 0.0), vec![r(1.5, 11.0, 2.0, 1.5, FUR)])); 57 out.push(moving(Motion::Type(1), (0.0, 0.0), vec![r(11.5, 11.0, 2.0, 1.5, FUR)])); 58 } 59 Mood::Busy => { 60 // Hands up, catching and throwing. 61 out.push(moving(Motion::Bob(0), (0.0, 0.0), vec![r(0.0, 6.5, 2.0, 2.0, FUR)])); 62 out.push(moving(Motion::Bob(1), (0.0, 0.0), vec![r(13.0, 6.5, 2.0, 2.0, FUR)])); 63 } 64 _ => { 65 out.push(r(0.0, 9.0, 2.0, 2.0, FUR)); 66 out.push(r(13.0, 9.0, 2.0, 2.0, FUR)); 67 } 68 } 69 out.push(r(2.0, 6.0, 11.0, 7.0, FUR)); // torso 70 face(mood, mouth, &mut out); 71 if mood == Mood::Working { 72 out.extend([r(4.5, 10.9, 6.0, 2.2, LAPTOP_LID), ra(7.0, 11.6, 1.0, 0.8, WHITE, 0.8), r(3.5, 13.0, 8.0, 0.9, LAPTOP_BODY)]); 73 } 74 out 75}
77fn face(mood: Mood, mouth: Option<Mouth>, out: &mut Vec<Node>) { 78 // Nose and whiskers. 79 out.push(r(7.0, 9.5, 1.0, 1.0, INNER_EAR)); 80 for y in [8.0, 11.3] { 81 out.push(r(-1.0, y, 4.0, 0.35, WHISKER)); 82 out.push(r(12.0, y, 4.0, 0.35, WHISKER)); 83 } 84 match mood { 85 Mood::Dizzy => { 86 out.extend(spiral(4.5, 9.0, INK)); 87 out.extend(spiral(10.5, 9.0, INK)); 88 } 89 Mood::Tickled => { 90 for x in [4.0, 10.0] { 91 out.extend([r(x - 1.0, 9.0, 1.0, 1.0, INK), r(x, 8.0, 1.0, 1.0, INK), r(x + 1.0, 9.0, 1.0, 1.0, INK)]); 92 } 93 out.push(ra(3.0, 10.4, 1.6, 1.0, BLUSH, 0.7)); 94 out.push(ra(10.4, 10.4, 1.6, 1.0, BLUSH, 0.7)); 95 out.push(r(6.5, 10.8, 2.0, 1.0, INK)); 96 out.push(r(7.0, 11.3, 1.0, 0.5, INNER_EAR)); 97 } 98 Mood::Tangled => { 99 for x in [4.0, 10.0] { 100 out.extend([r(x - 1.0, 8.0, 1.0, 1.0, INK), r(x, 9.0, 1.0, 1.0, INK), r(x + 1.0, 8.0, 1.0, 1.0, INK)]); 101 } 102 out.extend([r(6.5, 10.8, 0.7, 0.4, INK), r(7.2, 11.1, 0.7, 0.4, INK), r(7.9, 10.8, 0.7, 0.4, INK)]); 103 } 104 Mood::Offline => { 105 // Sad, heavy-lidded eyes and a small frown. 106 for x in [4.0, 10.0] { 107 out.push(r(x - 0.5, 9.0, 2.0, 0.5, INK)); 108 out.push(r(x, 9.5, 1.0, 1.0, INK)); 109 } 110 out.extend([r(6.5, 11.3, 2.0, 0.4, INK), r(6.0, 11.7, 0.6, 0.4, INK), r(8.4, 11.7, 0.6, 0.4, INK)]); 111 } 112 Mood::Busy => { 113 // The eyes follow the balls round. 114 out.push(moving(Motion::Scan, (0.0, 0.0), vec![eyes(Look::Fixed(0.0, 0.0), vec![pupil(4.0, 8.0, 2.0), pupil(10.0, 8.0, 2.0)])])); 115 } 116 _ => { 117 let (height, top, look) = match mood { 118 Mood::Listening => (3.0, 7.5, Look::Follow), 119 Mood::Thinking => (2.0, 8.0, Look::Fixed(0.8, -0.8)), 120 Mood::Working => (2.0, 8.0, Look::Fixed(0.0, 0.9)), 121 _ => (2.0, 8.0, Look::Follow), 122 }; 123 out.push(eyes(look, vec![pupil(4.0, top, height), pupil(10.0, top, height)])); 124 if mood == Mood::Speaking { 125 out.extend(mouth::speaking(&MOUTH, mouth)); 126 } 127 } 128 } 129}
One eye, shutting about its middle in a blink.
The flat cat: a loaf with its ears folded, eyes shut and its tail out behind.
137pub fn sleeping() -> Vec<Node> { 138 let mut out = Vec::new(); 139 for x in [3.0, 5.0, 9.0, 11.0] { 140 out.push(r(x, 9.0, 1.0, 1.0, FUR)); 141 } 142 out.push(r(14.0, 12.5, 2.0, 1.0, FUR)); // tail 143 out.extend([r(2.0, 9.0, 1.0, 1.0, FUR), r(3.0, 8.0, 1.0, 2.0, FUR), r(11.0, 8.0, 1.0, 2.0, FUR), r(12.0, 9.0, 1.0, 1.0, FUR)]); 144 out.extend([r(3.0, 9.0, 1.0, 1.0, INNER_EAR), r(11.0, 9.0, 1.0, 1.0, INNER_EAR)]); 145 out.push(r(1.0, 10.0, 13.0, 5.0, FUR)); 146 out.push(r(-1.0, 13.0, 2.0, 2.0, FUR)); 147 out.push(r(14.0, 13.0, 2.0, 2.0, FUR)); 148 out.push(r(7.0, 12.6, 1.0, 1.0, INNER_EAR)); 149 out.push(r(3.5, 12.0, 2.0, 0.4, INK)); 150 out.push(r(9.5, 12.0, 2.0, 0.4, INK)); 151 for y in [11.5, 14.0] { 152 out.push(r(-1.0, y, 3.0, 0.35, WHISKER)); 153 out.push(r(13.0, y, 4.0, 0.35, WHISKER)); 154 } 155 out 156}
158#[cfg(test)] 159mod tests { 160 use super::*; 161 use crate::scene::MOODS; 162 163 #[test] 164 fn every_standing_mood_draws_a_cat_and_no_two_moods_draw_alike() { 165 let mut seen: Vec<Vec<Node>> = Vec::new(); 166 for mood in MOODS { 167 let nodes = if mood == Mood::Sleeping { sleeping() } else { standing(mood, None) }; 168 assert!(nodes.len() > 8, "{mood:?}"); 169 assert!(!seen.contains(&nodes), "{mood:?} repeats another mood"); 170 seen.push(nodes); 171 } 172 } 173 174 #[test] 175 fn only_the_speaking_cat_has_a_mouth_and_its_shape_changes_it() { 176 assert_eq!(standing(Mood::Idle, None), standing(Mood::Idle, Some(Mouth::Wide))); 177 assert_ne!(standing(Mood::Speaking, Some(Mouth::Closed)), standing(Mood::Speaking, Some(Mouth::Wide))); 178 assert_ne!(standing(Mood::Speaking, None), standing(Mood::Idle, None)); 179 } 180 181 #[test] 182 fn the_eyes_that_look_are_the_ones_the_gaze_moves() { 183 let has_eyes = |mood| standing(mood, None).iter().any(|n| matches!(n, Node::Group(g) if matches!(g.role, crate::node::Role::Eyes(_)))); 184 for mood in [Mood::Idle, Mood::Listening, Mood::Thinking, Mood::Working, Mood::Speaking] { 185 assert!(has_eyes(mood), "{mood:?}"); 186 } 187 for mood in [Mood::Tickled, Mood::Tangled, Mood::Offline, Mood::Dizzy] { 188 assert!(!has_eyes(mood), "{mood:?}"); 189 } 190 } 191}