lib.rsannotatedlib.rssource297 lines · 11.7 KB · raw

A mouth for pixel-art characters. See the README: a [MouthSpec] says where it sits and how it is drawn, [rects] gives the rectangles for a [Level], and the timing functions step the level over a syllable. The units are the host's own grid units.

5use std::f32::consts::TAU;

How open the mouth is, in four steps.

8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum Level {
10    Closed,
11    Small,
12    Wide,
13    Round,
14}
16impl Level {

Every level, in the order a host lists them.

18    pub const ALL: [Level; 4] = [Level::Closed, Level::Small, Level::Wide, Level::Round];

How open it is, 0 (shut) to 1 (as wide as it goes).

21    pub fn open(self) -> f32 {
22        match self {
23            Level::Closed => 0.0,
24            Level::Small => 0.5,
25            Level::Round => 0.8,
26            Level::Wide => 1.0,
27        }
28    }

The level nearest to an openness.

31    pub fn nearest(open: f32) -> Level {
32        if open < 0.25 {
33            Level::Closed
34        } else if open < 0.65 {
35            Level::Small
36        } else if open < 0.9 {
37            Level::Round
38        } else {
39            Level::Wide
40        }
41    }
42}

A rectangle in the host's grid units, with the colour (0xrrggbb) and opacity it is drawn with.

45#[derive(Clone, Copy, Debug, PartialEq)]
46pub struct Rect {
47    pub x: f32,
48    pub y: f32,
49    pub w: f32,
50    pub h: f32,
51    pub rgb: u32,
52    pub alpha: f32,
53}

Which way a muzzle's round opening grows from its first cell.

56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub enum Grow {
58    Across,
59    Down,
60}

The kind of mouth the animal has.

63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub enum Style {

A nose line and two small steps when shut, a dark mouth when open. The anchor is the middle of the top of the nose line.

66    Cat,

A plain line when shut, the same open mouths. The anchor is the middle of the line.

68    Flat,

Cells under a nose: nothing when shut, one cell, three across, or two (across or down) when open. The anchor is the cell under the nose.

70    Muzzle(Grow),

A character in profile with a beak: the lower beak drops (further when wide). The anchor is the cell of the upper beak.

72    BeakSide,

A character facing front with a beak: the opening shows under it. The anchor is the cell of the beak.

74    BeakFront,
75}

The colours a mouth draws with, 0xrrggbb.

78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct Palette {

The colour of the inside of the mouth and of its lines.

81    pub ink: u32,

The colour of a beak, where the style has one (otherwise unused).

83    pub beak: u32,
84}

Where a character's mouth is and how it is drawn.

87#[derive(Clone, Copy, Debug, PartialEq)]
88pub struct MouthSpec {

Where it sits, in the host's grid units; what exactly it points at is the style's.

90    pub anchor: (f32, f32),

Cat and Flat: the width of the widest mouth. The cell styles: the side of a cell.

92    pub size: f32,
93    pub style: Style,
94    pub palette: Palette,
95}

The opacity a cat's mouth is drawn with.

98const MOUTH_ALPHA: f32 = 0.85;

The rectangles of the mouth at level, in paint order.

101pub fn rects(spec: &MouthSpec, level: Level) -> Vec<Rect> {
102    let (ax, ay) = spec.anchor;
103    let ink = spec.palette.ink;
104    match spec.style {
105        Style::Cat | Style::Flat => {
106            // Everything is drawn at a scale `s` of the widest mouth of the first cat this was drawn for (2.2 wide).
107            let s = spec.size / 2.2;
108            let top = if spec.style == Style::Cat { ay } else { ay - 0.6 * s };
109            let bar = |x: f32, y: f32, w: f32, h: f32| Rect { x: ax + x * s, y: top + y * s, w: w * s, h: h * s, rgb: ink, alpha: MOUTH_ALPHA };
110            match (level, spec.style) {
111                (Level::Closed, Style::Cat) => vec![bar(-0.15, 0.0, 0.3, 0.5), bar(-1.0, 0.5, 1.0, 0.3), bar(0.0, 0.5, 1.0, 0.3)],
112                (Level::Closed, _) => vec![Rect { x: ax - 0.375 * spec.size, y: ay, w: 0.75 * spec.size, h: 0.3 * s, rgb: ink, alpha: MOUTH_ALPHA }],
113                (Level::Small, _) => vec![bar(-0.6, 0.4, 1.2, 0.7)],
114                (Level::Wide, _) => vec![bar(-1.1, 0.3, 2.2, 1.2)],
115                (Level::Round, _) => vec![bar(-0.6, 0.3, 1.2, 1.2)],
116            }
117        }
118        Style::Muzzle(grow) => {
119            let k = spec.size;
120            let cell = |dx: f32, dy: f32| Rect { x: (ax + dx) * k, y: (ay + dy) * k, w: k, h: k, rgb: ink, alpha: 1.0 };
121            match level {
122                Level::Closed => vec![],
123                Level::Small => vec![cell(0.0, 0.0)],
124                Level::Wide => vec![cell(-1.0, 0.0), cell(0.0, 0.0), cell(1.0, 0.0)],
125                Level::Round => match grow {
126                    Grow::Across => vec![cell(0.0, 0.0), cell(1.0, 0.0)],
127                    Grow::Down => vec![cell(0.0, 0.0), cell(0.0, 1.0)],
128                },
129            }
130        }
131        Style::BeakSide => {
132            let k = spec.size;
133            let beak = spec.palette.beak;
134            let cell = |dx: f32, dy: f32, rgb: u32| Rect { x: (ax + dx) * k, y: (ay + dy) * k, w: k, h: k, rgb, alpha: 1.0 };
135            match level {
136                Level::Closed => vec![],
137                Level::Small => vec![cell(0.0, 1.0, beak)],
138                Level::Round => vec![cell(0.0, 1.0, beak), cell(1.0, 1.0, ink)],
139                Level::Wide => vec![cell(0.0, 1.0, beak), cell(0.0, 2.0, beak), cell(1.0, 1.0, ink)],
140            }
141        }
142        Style::BeakFront => {
143            let k = spec.size;
144            let beak = spec.palette.beak;
145            let cell = |dx: f32, rgb: u32| Rect { x: (ax + dx) * k, y: (ay + 1.0) * k, w: k, h: k, rgb, alpha: 1.0 };
146            match level {
147                Level::Closed => vec![],
148                Level::Small => vec![cell(0.0, beak)],
149                Level::Round => vec![cell(0.0, ink)],
150                Level::Wide => vec![cell(-1.0, ink), cell(0.0, ink), cell(1.0, ink)],
151            }
152        }
153    }
154}

How far through the gap between two syllables the mouth stays open.

157pub const OPEN_UNTIL: f32 = 0.7;

The letters a syllable's level is read from, and the levels they give: a is the widest, o and u are round, every other vowel is a small opening.

161pub const VOWELS: &str = "aeiouy";
162pub const WIDE_VOWELS: &str = "a";
163pub const ROUND_VOWELS: &str = "ou";

The level a syllable with this first vowel is said with (none: a small opening).

166pub fn level_for_vowel(vowel: Option<char>) -> Level {
167    match vowel {
168        Some(v) if WIDE_VOWELS.contains(v) => Level::Wide,
169        Some(v) if ROUND_VOWELS.contains(v) => Level::Round,
170        _ => Level::Small,
171    }
172}

The level at a moment of a syllable: the syllable's own while it is said, shut in the last of the gap before the next one (hopping: a next syllable is under way, phase how far through the gap, 0 to 1). The last syllable has no gap, so it stays open until the line is said.

177pub fn level_in_syllable(vowel: Option<char>, hopping: bool, phase: f32) -> Level {
178    if hopping && phase > OPEN_UNTIL { Level::Closed } else { level_for_vowel(vowel) }
179}

How fast the beat of a mouth that is not told its syllable goes, in radians a second.

182pub const FLAP_RATE: f32 = 13.0;

How open the beat has the mouth at t seconds.

185pub fn flap_open(t: f32) -> f32 {
186    (t * FLAP_RATE).sin().abs()
187}

The level the beat has the mouth in at t seconds.

190pub fn flap_level(t: f32) -> Level {
191    Level::nearest(flap_open(t))
192}

Seconds until the beat repeats.

195pub const FLAP_PERIOD: f32 = TAU / 2.0 / FLAP_RATE;
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    const PAL: Palette = Palette { ink: 0x111111, beak: 0xFFB100 };
202
203    fn spec(style: Style, size: f32) -> MouthSpec {
204        MouthSpec { anchor: (4.0, 6.0), size, style, palette: PAL }
205    }
206
207    #[test]
208    fn a_cats_mouth_at_scale_one_is_the_one_it_was_drawn_for() {
209        let s = MouthSpec { anchor: (7.5, 10.4), size: 2.2, style: Style::Cat, palette: PAL };
210        let w = rects(&s, Level::Wide)[0];
211        assert!((w.x - 6.4).abs() < 1e-4 && (w.y - 10.7).abs() < 1e-4 && (w.w - 2.2).abs() < 1e-4 && (w.h - 1.2).abs() < 1e-4);
212        let closed = rects(&s, Level::Closed);
213        assert_eq!(closed.len(), 3);
214        assert!((closed[0].x - 7.35).abs() < 1e-4);
215    }
216
217    #[test]
218    fn every_style_draws_every_level_and_the_open_ones_are_not_alike() {
219        for style in [Style::Cat, Style::Flat, Style::Muzzle(Grow::Across), Style::Muzzle(Grow::Down), Style::BeakSide, Style::BeakFront] {
220            let s = spec(style, if matches!(style, Style::Cat | Style::Flat) { 2.0 } else { 1.0 });
221            let shapes: Vec<Vec<Rect>> = Level::ALL.iter().map(|l| rects(&s, *l)).collect();
222            for (i, a) in shapes.iter().enumerate() {
223                for (j, b) in shapes.iter().enumerate() {
224                    if i < j {
225                        assert_ne!(a, b, "{style:?}: {:?} and {:?} are alike", Level::ALL[i], Level::ALL[j]);
226                    }
227                }
228            }
229            for r in shapes.iter().flatten() {
230                assert!(r.w > 0.0 && r.h > 0.0 && r.x.is_finite() && r.y.is_finite());
231            }
232        }
233    }
234
235    #[test]
236    fn a_muzzle_and_a_beak_add_nothing_when_shut_and_a_cat_draws_its_line() {
237        for style in [Style::Muzzle(Grow::Across), Style::BeakSide, Style::BeakFront] {
238            assert!(rects(&spec(style, 1.0), Level::Closed).is_empty());
239        }
240        assert!(!rects(&spec(Style::Cat, 2.0), Level::Closed).is_empty());
241        assert_eq!(rects(&spec(Style::Flat, 2.0), Level::Closed).len(), 1);
242    }
243
244    #[test]
245    fn the_cell_styles_stay_on_whole_cells() {
246        for style in [Style::Muzzle(Grow::Across), Style::Muzzle(Grow::Down), Style::BeakSide, Style::BeakFront] {
247            for level in Level::ALL {
248                for r in rects(&spec(style, 1.0), level) {
249                    assert_eq!((r.x.fract(), r.y.fract(), r.w, r.h), (0.0, 0.0, 1.0, 1.0));
250                }
251            }
252        }
253    }
254
255    #[test]
256    fn a_muzzle_grows_the_way_it_is_told() {
257        let across = rects(&spec(Style::Muzzle(Grow::Across), 1.0), Level::Round);
258        let down = rects(&spec(Style::Muzzle(Grow::Down), 1.0), Level::Round);
259        assert_eq!((across[1].x, across[1].y), (5.0, 6.0));
260        assert_eq!((down[1].x, down[1].y), (4.0, 7.0));
261    }
262
263    #[test]
264    fn a_beak_draws_in_its_own_colour_and_the_inside_in_ink() {
265        let r = rects(&spec(Style::BeakSide, 1.0), Level::Round);
266        assert_eq!((r[0].rgb, r[1].rgb), (PAL.beak, PAL.ink));
267    }
268
269    #[test]
270    fn openness_and_its_nearest_level_agree() {
271        for l in Level::ALL {
272            assert_eq!(Level::nearest(l.open()), l);
273        }
274        assert!(Level::Closed.open() < Level::Small.open() && Level::Small.open() < Level::Round.open() && Level::Round.open() < Level::Wide.open());
275    }
276
277    #[test]
278    fn a_syllable_is_said_with_its_vowel_and_shuts_at_the_end_of_the_gap() {
279        assert_eq!(level_for_vowel(Some('a')), Level::Wide);
280        assert_eq!(level_for_vowel(Some('o')), Level::Round);
281        assert_eq!(level_for_vowel(Some('u')), Level::Round);
282        assert_eq!(level_for_vowel(Some('e')), Level::Small);
283        assert_eq!(level_for_vowel(None), Level::Small);
284        assert_eq!(level_in_syllable(Some('a'), true, 0.5), Level::Wide);
285        assert_eq!(level_in_syllable(Some('a'), true, 0.9), Level::Closed);
286        assert_eq!(level_in_syllable(Some('a'), false, 0.9), Level::Wide);
287    }
288
289    #[test]
290    fn the_beat_visits_every_level_and_repeats_after_its_period() {
291        let seen: std::collections::HashSet<Level> = (0..400).map(|i| flap_level(i as f32 * 0.003)).collect();
292        assert_eq!(seen.len(), 4);
293        for t in [0.013, 0.31, 1.7] {
294            assert!((flap_open(t) - flap_open(t + FLAP_PERIOD)).abs() < 1e-3);
295        }
296    }
297}