1//! A mouth for pixel-art characters. See the README: a [`MouthSpec`] says where it sits and how it is 2//! drawn, [`rects`] gives the rectangles for a [`Level`], and the timing functions step the level over a 3//! syllable. The units are the host's own grid units. 4 5use std::f32::consts::TAU; 6 7/// 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} 15 16impl Level { 17 /// Every level, in the order a host lists them. 18 pub const ALL: [Level; 4] = [Level::Closed, Level::Small, Level::Wide, Level::Round]; 19 20 /// 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 } 29 30 /// 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} 43 44/// 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} 54 55/// 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} 61 62/// The kind of mouth the animal has. 63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 64pub enum Style { 65 /// 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, 67 /// A plain line when shut, the same open mouths. The anchor is the middle of the line. 68 Flat, 69 /// 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), 71 /// 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, 73 /// A character facing front with a beak: the opening shows under it. The anchor is the cell of the beak. 74 BeakFront, 75} 76 77/// The colours a mouth draws with, `0xrrggbb`. 78#[derive(Clone, Copy, Debug, PartialEq, Eq)] 79pub struct Palette { 80 /// The colour of the inside of the mouth and of its lines. 81 pub ink: u32, 82 /// The colour of a beak, where the style has one (otherwise unused). 83 pub beak: u32, 84} 85 86/// Where a character's mouth is and how it is drawn. 87#[derive(Clone, Copy, Debug, PartialEq)] 88pub struct MouthSpec { 89 /// Where it sits, in the host's grid units; what exactly it points at is the style's. 90 pub anchor: (f32, f32), 91 /// `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} 96 97/// The opacity a cat's mouth is drawn with. 98const MOUTH_ALPHA: f32 = 0.85; 99 100/// 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} 155 156/// How far through the gap between two syllables the mouth stays open. 157pub const OPEN_UNTIL: f32 = 0.7; 158 159/// The letters a syllable's level is read from, and the levels they give: `a` is the widest, `o` and `u` are round, every 160/// other vowel is a small opening. 161pub const VOWELS: &str = "aeiouy"; 162pub const WIDE_VOWELS: &str = "a"; 163pub const ROUND_VOWELS: &str = "ou"; 164 165/// 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} 173 174/// 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 175/// one (`hopping`: a next syllable is under way, `phase` how far through the gap, 0 to 1). The last syllable has no gap, 176/// 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} 180 181/// 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; 183 184/// How open the beat has the mouth at `t` seconds. 185pub fn flap_open(t: f32) -> f32 { 186 (t * FLAP_RATE).sin().abs() 187} 188 189/// 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} 193 194/// Seconds until the beat repeats. 195pub const FLAP_PERIOD: f32 = TAU / 2.0 / FLAP_RATE; 196 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}