grey.rsannotatedgrey.rssource233 lines · 9.3 KB · raw
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}