1//! The grey cat: a sitting tabby on the 15 by 16 grid, with a pale belly and muzzle, tall 2//! two-step ears, a raised tail and cat eyes (an amber iris with a slit). Every pose the app has 3//! is drawn here, so grey is a complete theme. 4//! 5//! Layout, back to front: tail, feet, ears, body (x 3..12, y 6..14), belly, stripes, whiskers, 6//! muzzle and nose, eyes, mouth, and whatever the pose adds (a laptop and paws, raised paws, 7//! blush). The eyes are `Role::Eyes` groups so the gaze moves them; each eye blinks on its own 8//! about its middle. 9 10use whiskers_pet::Mood; 11 12use crate::decor::{BLUSH, INK, WHITE, spiral}; 13use crate::motion::Motion; 14use crate::node::{Color, Look, Node, eyes, moving, r, ra}; 15use crate::scene::Mouth; 16 17const FUR: Color = Color::hex(0x9DB4C0); 18const STRIPE: Color = Color::hex(0x7B93A2); 19const BELLY: Color = Color::hex(0xDDE6EA); 20const NOSE: Color = Color::hex(0xE7A9B4); 21const AMBER: Color = Color::hex(0xF2C14E); 22const WHISKER: Color = Color::hex(0xEEF3F5); 23const LID: Color = Color::hex(0x3A3A44); 24const KEYS: Color = Color::hex(0xB9B9C4); 25 26/// The standing cat in `mood` (any but sleeping). 27pub fn standing(mood: Mood, mouth: Option<Mouth>) -> Vec<Node> { 28 debug_assert!(mood != Mood::Sleeping, "a sleeping cat is lying down: use `sleeping`"); 29 let mut out = Vec::new(); 30 tail(mood, &mut out); 31 // Feet. 32 out.push(r(4.0, 14.0, 2.0, 1.0, BELLY)); 33 out.push(r(9.0, 14.0, 2.0, 1.0, BELLY)); 34 ears(mood, &mut out); 35 // Body, belly and the tabby marks: three on the brow and two on each cheek. 36 out.push(r(3.0, 6.0, 9.0, 8.0, FUR)); 37 out.push(r(5.0, 11.0, 5.0, 3.0, BELLY)); 38 for (x, h) in [(6.2, 1.4), (7.2, 1.8), (8.2, 1.4)] { 39 out.push(r(x, 6.0, 0.6, h, STRIPE)); 40 } 41 for y in [8.0, 9.4] { 42 out.push(r(3.0, y, 1.0, 0.5, STRIPE)); 43 out.push(r(11.0, y, 1.0, 0.5, STRIPE)); 44 } 45 // Whiskers, then the muzzle and nose over the middle of the face. 46 for (y, len) in [(9.8, 2.8), (10.8, 2.4)] { 47 out.push(r(3.3 - len, y, len, 0.3, WHISKER)); 48 out.push(r(11.7, y, len, 0.3, WHISKER)); 49 } 50 out.push(r(6.0, 9.3, 3.0, 2.4, BELLY)); 51 out.push(r(7.0, 9.6, 1.0, 0.8, NOSE)); 52 face(mood, mouth, &mut out); 53 out 54} 55 56fn tail(mood: Mood, out: &mut Vec<Node>) { 57 if matches!(mood, Mood::Tangled | Mood::Offline) { 58 // Hanging low behind the cat. 59 out.push(r(12.0, 13.0, 3.0, 1.0, FUR)); 60 return; 61 } 62 let sway = if mood == Mood::Tickled { Motion::TailFlick } else { Motion::TailWag }; 63 out.push(r(12.0, 12.0, 2.0, 1.0, FUR)); 64 out.push(moving(sway, (0.0, 0.0), vec![r(13.0, 9.0, 1.0, 3.0, FUR), r(13.0, 8.5, 1.0, 0.5, BELLY)])); 65} 66 67/// Ears of two steps with a pink inside. Their feet stay on the head; the tops perk up when 68/// listening and droop when the cat is unhappy. 69fn ears(mood: Mood, out: &mut Vec<Node>) { 70 let lift = match mood { 71 Mood::Listening => -1.0, 72 Mood::Tangled | Mood::Offline => 0.7, 73 _ => 0.0, 74 }; 75 let top = 3.5 + lift; 76 for (outer, tip, inner) in [(3.0, 3.0, 3.5), (10.0, 11.0, 10.5)] { 77 out.push(r(outer, top + 1.0, 2.0, 6.0 - (top + 1.0), FUR)); 78 out.push(r(tip, top, 1.0, 1.0, FUR)); 79 out.push(r(inner, top + 1.5, 1.0, (6.0 - (top + 1.5)).max(0.3), NOSE)); 80 } 81} 82 83/// One eye: an amber iris with a slit, `x` its left edge, shutting about its middle in a blink. 84fn eye(x: f32, top: f32, height: f32) -> Node { 85 moving(Motion::Blink, (x + 1.0, top + height / 2.0), vec![r(x, top, 2.0, height, AMBER), r(x + 0.7, top, 0.6, height, INK)]) 86} 87 88fn eye_pair(look: Look, top: f32, height: f32) -> Node { 89 eyes(look, vec![eye(4.5, top, height), eye(8.5, top, height)]) 90} 91 92/// A 3 cell wide arch (`up`: a happy ^) or notch (a worried v) at `cx`. 93fn arch(cx: f32, up: bool) -> Vec<Node> { 94 let (side, middle) = if up { (9.0, 8.3) } else { (8.3, 9.0) }; 95 vec![r(cx - 1.2, side, 0.8, 0.7, INK), r(cx - 0.5, middle, 1.0, 0.7, INK), r(cx + 0.4, side, 0.8, 0.7, INK)] 96} 97 98fn face(mood: Mood, mouth: Option<Mouth>, out: &mut Vec<Node>) { 99 // The line down from the nose, for the closed mouths. 100 let philtrum = || ra(7.35, 10.4, 0.3, 0.5, INK, 0.85); 101 // The closed mouth: two small steps, like a cat's. 102 let closed = |out: &mut Vec<Node>| { 103 out.push(philtrum()); 104 out.push(ra(6.5, 10.9, 1.0, 0.3, INK, 0.85)); 105 out.push(ra(7.5, 10.9, 1.0, 0.3, INK, 0.85)); 106 }; 107 match mood { 108 Mood::Dizzy => { 109 out.extend(spiral(5.5, 8.5, INK)); 110 out.extend(spiral(9.5, 8.5, INK)); 111 out.push(ra(7.0, 10.9, 1.0, 0.9, INK, 0.85)); 112 } 113 Mood::Tickled => { 114 out.extend(arch(5.5, true)); 115 out.extend(arch(9.5, true)); 116 out.push(ra(3.4, 10.3, 1.6, 0.9, BLUSH, 0.7)); 117 out.push(ra(10.0, 10.3, 1.6, 0.9, BLUSH, 0.7)); 118 out.push(r(6.7, 10.9, 1.6, 0.9, INK)); 119 out.push(r(7.2, 11.5, 0.6, 0.3, NOSE)); 120 } 121 Mood::Tangled => { 122 out.extend(arch(5.5, false)); 123 out.extend(arch(9.5, false)); 124 // A wavy mouth. 125 for (x, y) in [(6.4, 11.0), (7.1, 11.4), (7.8, 11.0)] { 126 out.push(r(x, y, 0.8, 0.4, INK)); 127 } 128 } 129 Mood::Offline => { 130 // Heavy-lidded, sad eyes and a small frown. 131 for x in [4.5, 8.5] { 132 out.push(r(x, 9.0, 2.0, 1.0, AMBER)); 133 out.push(r(x + 0.7, 9.0, 0.6, 1.0, INK)); 134 out.push(r(x - 0.1, 8.6, 2.2, 0.5, STRIPE)); 135 } 136 out.push(r(6.8, 11.2, 1.4, 0.35, INK)); 137 out.push(r(6.3, 11.6, 0.6, 0.35, INK)); 138 out.push(r(8.1, 11.6, 0.6, 0.35, INK)); 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![eye_pair(Look::Fixed(0.0, 0.0), 7.5, 2.0)])); 143 closed(out); 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, BELLY)])); 146 } 147 } 148 _ => { 149 let (look, top, height) = match mood { 150 Mood::Listening => (Look::Follow, 7.0, 3.0), 151 Mood::Thinking => (Look::Fixed(0.8, -0.8), 7.5, 2.0), 152 Mood::Working => (Look::Fixed(0.0, 0.9), 7.5, 2.0), 153 _ => (Look::Follow, 7.5, 2.0), 154 }; 155 out.push(eye_pair(look, top, height)); 156 if mood == Mood::Speaking { 157 match mouth { 158 Some(Mouth::Closed) => closed(out), 159 Some(Mouth::Small) => out.push(ra(6.9, 10.8, 1.2, 0.7, INK, 0.85)), 160 Some(Mouth::Wide) => out.push(ra(6.4, 10.7, 2.2, 1.2, INK, 0.85)), 161 Some(Mouth::Round) => out.push(ra(6.9, 10.7, 1.2, 1.2, INK, 0.85)), 162 None => out.push(moving(Motion::MouthFlap, (7.5, 10.8), vec![ra(6.6, 10.8, 1.8, 1.0, INK, 0.85)])), 163 } 164 } else { 165 closed(out); 166 } 167 if mood == Mood::Working { 168 laptop(out); 169 } 170 } 171 } 172} 173 174/// A laptop on the cat's lap with a paw on each side of the keys, taking turns. 175fn laptop(out: &mut Vec<Node>) { 176 out.push(r(4.0, 11.8, 7.0, 1.9, LID)); 177 out.push(ra(7.0, 12.4, 1.0, 0.7, WHITE, 0.8)); 178 out.push(r(3.4, 13.7, 8.2, 0.6, KEYS)); 179 for (i, x) in [(0_u8, 4.8_f32), (1, 8.6)] { 180 out.push(moving(Motion::Type(i), (0.0, 0.0), vec![r(x, 13.0, 1.6, 0.9, BELLY)])); 181 } 182} 183 184/// The flat cat: a loaf with its ears folded flat, eyes shut and its tail curled up behind. 185pub fn sleeping() -> Vec<Node> { 186 let mut out = vec![r(13.0, 13.0, 2.0, 1.0, FUR), r(14.0, 12.0, 1.0, 1.5, FUR)]; 187 for (outer, tip) in [(3.0, 3.0), (10.0, 11.0)] { 188 out.push(r(outer, 8.2, 2.0, 1.8, FUR)); 189 out.push(r(tip, 7.7, 1.0, 0.5, FUR)); 190 out.push(r(outer + 0.5, 8.8, 1.0, 1.2, NOSE)); 191 } 192 out.push(r(2.0, 10.0, 11.0, 5.0, FUR)); 193 out.push(r(5.0, 13.0, 5.0, 2.0, BELLY)); 194 for x in [5.9, 6.9, 7.9] { 195 out.push(r(x, 10.0, 0.6, 1.4, STRIPE)); 196 } 197 out.push(r(3.5, 14.2, 2.0, 0.8, BELLY)); 198 out.push(r(9.5, 14.2, 2.0, 0.8, BELLY)); 199 out.push(r(7.0, 12.6, 1.0, 0.8, NOSE)); 200 out.push(r(4.0, 12.0, 2.0, 0.4, INK)); 201 out.push(r(9.0, 12.0, 2.0, 0.4, INK)); 202 out.push(r(-0.3, 12.4, 2.6, 0.3, WHISKER)); 203 out.push(r(12.7, 12.4, 2.6, 0.3, WHISKER)); 204 out 205} 206 207#[cfg(test)] 208mod tests { 209 use super::*; 210 use crate::scene::MOODS; 211 212 #[test] 213 fn every_standing_mood_draws_a_face_with_eyes() { 214 for mood in MOODS.into_iter().filter(|m| *m != Mood::Sleeping) { 215 let nodes = standing(mood, None); 216 assert!(nodes.len() > 12, "{mood:?}"); 217 } 218 } 219 220 #[test] 221 fn the_eyes_that_look_are_the_ones_the_gaze_moves() { 222 let has_eyes = |mood| { 223 standing(mood, None).iter().any(|n| matches!(n, Node::Group(g) if matches!(g.role, crate::node::Role::Eyes(_)))) 224 }; 225 for mood in [Mood::Idle, Mood::Listening, Mood::Thinking, Mood::Working, Mood::Speaking] { 226 assert!(has_eyes(mood), "{mood:?}"); 227 } 228 // Closed, crossed or spinning eyes are drawn, not looked with. 229 for mood in [Mood::Tickled, Mood::Tangled, Mood::Offline, Mood::Dizzy] { 230 assert!(!has_eyes(mood), "{mood:?}"); 231 } 232 } 233}