blizz_ui/components/mascot/
math.rs1pub(super) const ANCHOR_ROW: f64 = 10.0;
2pub(super) const ANCHOR_COL: f64 = 19.0;
3pub(super) const CORE_ROW_OFFSET: f64 = 7.0;
4pub(super) const CHAR_ASPECT_RATIO: f64 = 0.5;
5
6pub(super) const CORE_GROW_SPEED: f64 = 0.2;
7pub(super) const CORE_MAX_RADIUS: f64 = 2.0;
8pub(super) const CORE_HOLD_TICKS: usize = 70;
9pub(super) const CROSSFADE_TICKS: usize = 20;
10
11#[derive(Clone, Debug)]
12pub struct DistanceMap {
13 pub(super) distances: Vec<Vec<f64>>,
14 pub max_distance: f64,
15}
16
17pub fn build_distance_map(lines: &[&str]) -> DistanceMap {
18 let mut max_distance: f64 = 0.0;
19 let distances: Vec<Vec<f64>> = lines
20 .iter()
21 .enumerate()
22 .map(|(row, line)| {
23 line
24 .chars()
25 .enumerate()
26 .map(|(col, ch)| char_distance(row, col, ch, &mut max_distance))
27 .collect()
28 })
29 .collect();
30
31 DistanceMap {
32 distances,
33 max_distance,
34 }
35}
36
37fn char_distance(row: usize, col: usize, ch: char, max_distance: &mut f64) -> f64 {
38 if ch == ' ' {
39 return f64::INFINITY;
40 }
41 let d = euclidean_distance(row as f64, col as f64);
42 if d > *max_distance {
43 *max_distance = d;
44 }
45 d
46}
47
48pub fn distance_at(map: &DistanceMap, row: usize, col: usize) -> f64 {
49 map.distances[row][col]
50}
51
52pub(super) fn weighted_distance(deltas: &[(f64, f64)]) -> f64 {
58 deltas
59 .iter()
60 .map(|(d, w)| {
61 let scaled = d * w;
62 scaled * scaled
63 })
64 .sum::<f64>()
65 .sqrt()
66}
67
68pub(super) fn euclidean_distance(row: f64, col: f64) -> f64 {
69 weighted_distance(&[
70 (row - ANCHOR_ROW, 1.0),
71 ((col - ANCHOR_COL), CHAR_ASPECT_RATIO),
72 ])
73}
74
75pub(super) fn core_distance(row: f64, col: f64) -> f64 {
76 weighted_distance(&[
77 (row - (ANCHOR_ROW + CORE_ROW_OFFSET), 1.0),
78 ((col - ANCHOR_COL), CHAR_ASPECT_RATIO),
79 ])
80}
81
82pub(super) fn core_total_ticks() -> usize {
83 core_grow_ticks() + CORE_HOLD_TICKS
84}
85
86pub(super) fn core_grow_ticks() -> usize {
87 (CORE_MAX_RADIUS / CORE_GROW_SPEED).ceil() as usize
88}
89
90pub(super) fn core_radius(tick: usize) -> f64 {
91 let grow_ticks = core_grow_ticks();
92 if tick < grow_ticks {
93 tick as f64 * CORE_GROW_SPEED
94 } else {
95 CORE_MAX_RADIUS
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn sample_lines() -> Vec<&'static str> {
104 vec![" ▓▓▓ ", " ▓▓▓▓▓ ", "▓▓▓▓▓▓▓", " ▓▓▓▓▓ ", " ▓▓▓ "]
105 }
106
107 #[test]
108 fn distance_map_assigns_infinity_to_spaces() {
109 let lines = sample_lines();
110 let map = build_distance_map(&lines);
111
112 assert_eq!(distance_at(&map, 0, 0), f64::INFINITY);
113 assert_eq!(distance_at(&map, 0, 1), f64::INFINITY);
114 }
115
116 #[test]
117 fn distance_map_computes_finite_for_visible_chars() {
118 let lines = sample_lines();
119 let map = build_distance_map(&lines);
120
121 assert!(distance_at(&map, 2, 3).is_finite());
122 assert!(map.max_distance > 0.0);
123 }
124
125 #[test]
126 fn euclidean_distance_at_anchor_is_zero() {
127 let d = euclidean_distance(ANCHOR_ROW, ANCHOR_COL);
128 assert!(d < 0.01);
129 }
130
131 #[test]
132 fn core_distance_is_finite_for_all_positions() {
133 assert!(core_distance(0.0, 0.0).is_finite());
134 assert!(core_distance(10.0, 19.0).is_finite());
135 }
136
137 #[test]
138 fn core_radius_grows_then_caps() {
139 assert_eq!(core_radius(0), 0.0);
140 let grow = core_grow_ticks();
141 assert!((core_radius(grow) - CORE_MAX_RADIUS).abs() < 0.01);
142 assert!((core_radius(grow + 100) - CORE_MAX_RADIUS).abs() < 0.01);
143 }
144
145 #[test]
146 fn core_total_ticks_is_sum_of_grow_and_hold() {
147 let total = core_total_ticks();
148 assert_eq!(total, core_grow_ticks() + CORE_HOLD_TICKS);
149 }
150}