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.
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).
The level nearest to an openness.
A rectangle in the host's grid units, with the colour (0xrrggbb) and opacity it is drawn with.
Which way a muzzle's round opening grows from its first cell.
The kind of mouth the animal has.
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.
The colours a mouth draws with, 0xrrggbb.
The colour of the inside of the mouth and of its lines.
81 pub ink: u32,
Where a character's mouth is and how it is drawn.
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.
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.
The level a syllable with this first vowel is said with (none: a small opening).
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.
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.
The level the beat has the mouth in at t seconds.
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}