whiskers.git / web / cat / src / art_bunny.rs
1//! Bunny: a sitting rabbit on the app's 15 by 16 grid, drawn for the site. Long upright ears
2//! with a pink inside, a pear-shaped body (a narrow head over a wide lap), round dark eyes with
3//! a highlight, pink cheeks, a small split mouth, big hind feet and a round white tail. Cream on
4//! a dark page.
5//!
6//! Layout, back to front: tail, feet, ears, head and body, belly, cheeks, muzzle, eyes, mouth.
7//! The eyes are `Role::Eyes` groups so the gaze moves them; each blinks about its own middle.
8
9use whiskers_art::decor::{INK, WHITE, spiral};
10
11use crate::mood::Mood;
12use crate::motion::Motion;
13use crate::node::{Color, Look, Node, eyes, moving, r, ra};
14
15const FUR: Color = Color::hex(0xF1EBDD);
16const SHADE: Color = Color::hex(0xD3C8B0);
17const INNER: Color = Color::hex(0xF0B5C0);
18const NOSE: Color = Color::hex(0xE48CA0);
19const EYE: Color = Color::hex(0x2E2430);
20
21/// The figure in `mood`: standing up, or lying down when asleep.
22pub fn figure(mood: Mood) -> Vec<Node> {
23    match mood {
24        Mood::Sleeping => sleeping(),
25        Mood::Idle | Mood::Listening | Mood::Thinking | Mood::Speaking | Mood::Dizzy => sitting(mood),
26    }
27}
28
29fn sitting(mood: Mood) -> Vec<Node> {
30    let mut out = vec![
31        // The tail: a small round puff that wags.
32        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)]),
33        // Feet: big hind feet and small front ones.
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, &mut out);
52    out
53}
54
55/// Ears: tall, two wide, a pink strip inside. Perked when listening, one folded when thinking,
56/// flopped out to the sides when dizzy.
57fn ears(mood: Mood, out: &mut Vec<Node>) {
58    match mood {
59        Mood::Dizzy => {
60            for (x, inner) in [(2.0, 2.5), (11.0, 11.5)] {
61                out.push(r(x, 3.5, 2.0, 3.0, FUR));
62                out.push(r(inner, 4.0, 1.0, 2.0, INNER));
63            }
64        }
65        _ => {
66            let lift = if mood == Mood::Listening { -1.0 } else { 0.0 };
67            let top = lift;
68            out.push(r(4.0, top, 2.0, 6.0 - top, FUR));
69            out.push(r(4.5, top + 1.2, 1.0, 4.3 - top, INNER));
70            if mood == Mood::Thinking {
71                // The right ear is folded over at the tip.
72                out.push(r(9.0, 1.5, 2.0, 4.5, FUR));
73                out.push(r(9.5, 2.7, 1.0, 3.0, INNER));
74                out.push(r(10.0, 1.0, 1.8, 0.9, FUR));
75            } else {
76                out.push(r(9.0, top, 2.0, 6.0 - top, FUR));
77                out.push(r(9.5, top + 1.2, 1.0, 4.3 - top, INNER));
78            }
79        }
80    }
81}
82
83/// One eye: a dark round with a highlight, `x` its left edge, shutting about its middle.
84fn eye(x: f32, top: f32, height: f32) -> Node {
85    moving(
86        Motion::Blink,
87        (x + 0.8, top + height / 2.0),
88        vec![r(x, top, 1.6, height, EYE), r(x + 0.25, top + 0.25, 0.5, 0.5, WHITE)],
89    )
90}
91
92fn face(mood: Mood, out: &mut Vec<Node>) {
93    // The small split mouth under the nose.
94    let closed = |out: &mut Vec<Node>| {
95        out.push(ra(7.35, 9.0, 0.3, 0.5, EYE, 0.85));
96        out.push(ra(6.7, 9.5, 0.7, 0.3, EYE, 0.85));
97        out.push(ra(7.6, 9.5, 0.7, 0.3, EYE, 0.85));
98    };
99    match mood {
100        Mood::Dizzy => {
101            out.extend(spiral(5.2, 7.6, INK));
102            out.extend(spiral(9.8, 7.6, INK));
103            out.push(ra(7.0, 9.4, 1.0, 0.8, EYE, 0.85));
104        }
105        Mood::Idle | Mood::Listening | Mood::Thinking | Mood::Speaking => {
106            let (look, top, height) = match mood {
107                Mood::Listening => (Look::Follow, 6.4, 2.2),
108                Mood::Thinking => (Look::Fixed(0.8, -0.8), 6.8, 1.6),
109                _ => (Look::Follow, 6.8, 1.6),
110            };
111            out.push(eyes(look, vec![eye(4.4, top, height), eye(9.0, top, height)]));
112            if mood == Mood::Speaking {
113                out.push(moving(Motion::MouthFlap, (7.5, 9.4), vec![ra(6.7, 9.4, 1.6, 1.0, EYE, 0.85)]));
114            } else {
115                closed(out);
116            }
117        }
118        Mood::Sleeping => unreachable!("a sleeping rabbit is drawn by `sleeping`"),
119    }
120}
121
122/// Lying in a loaf with the ears laid back along the top and the eyes shut.
123fn sleeping() -> Vec<Node> {
124    vec![
125        r(13.0, 13.0, 2.0, 2.0, WHITE),
126        r(2.0, 10.0, 11.0, 5.0, FUR),
127        r(3.0, 9.2, 5.0, 1.2, FUR),
128        r(3.4, 9.6, 4.0, 0.5, INNER),
129        r(4.5, 13.0, 6.0, 2.0, WHITE),
130        r(2.0, 14.6, 3.0, 0.4, SHADE),
131        r(7.0, 12.4, 1.0, 0.7, NOSE),
132        r(4.2, 12.0, 1.8, 0.4, EYE),
133        r(9.0, 12.0, 1.8, 0.4, EYE),
134        ra(3.2, 12.6, 1.2, 0.7, INNER, 0.6),
135        ra(10.6, 12.6, 1.2, 0.7, INNER, 0.6),
136    ]
137}