pub(super) const ANCHOR_ROW: f64 = 10.0;
pub(super) const ANCHOR_COL: f64 = 19.0;
pub(super) const CORE_ROW_OFFSET: f64 = 7.0;
pub(super) const CHAR_ASPECT_RATIO: f64 = 0.5;
pub(super) const CORE_GROW_SPEED: f64 = 0.2;
pub(super) const CORE_MAX_RADIUS: f64 = 2.0;
pub(super) const CORE_HOLD_TICKS: usize = 70;
pub(super) const CROSSFADE_TICKS: usize = 20;
#[derive(Clone, Debug)]
pub struct DistanceMap {
pub(super) distances: Vec<Vec<f64>>,
pub max_distance: f64,
}
pub fn build_distance_map(lines: &[&str]) -> DistanceMap {
let mut max_distance: f64 = 0.0;
let distances: Vec<Vec<f64>> = lines
.iter()
.enumerate()
.map(|(row, line)| {
line
.chars()
.enumerate()
.map(|(col, ch)| char_distance(row, col, ch, &mut max_distance))
.collect()
})
.collect();
DistanceMap {
distances,
max_distance,
}
}
fn char_distance(row: usize, col: usize, ch: char, max_distance: &mut f64) -> f64 {
if ch == ' ' {
return f64::INFINITY;
}
let d = euclidean_distance(row as f64, col as f64);
if d > *max_distance {
*max_distance = d;
}
d
}
pub fn distance_at(map: &DistanceMap, row: usize, col: usize) -> f64 {
map.distances[row][col]
}
pub(super) fn weighted_distance(deltas: &[(f64, f64)]) -> f64 {
deltas
.iter()
.map(|(d, w)| {
let scaled = d * w;
scaled * scaled
})
.sum::<f64>()
.sqrt()
}
pub(super) fn euclidean_distance(row: f64, col: f64) -> f64 {
weighted_distance(&[
(row - ANCHOR_ROW, 1.0),
((col - ANCHOR_COL), CHAR_ASPECT_RATIO),
])
}
pub(super) fn core_distance(row: f64, col: f64) -> f64 {
weighted_distance(&[
(row - (ANCHOR_ROW + CORE_ROW_OFFSET), 1.0),
((col - ANCHOR_COL), CHAR_ASPECT_RATIO),
])
}
pub(super) fn core_total_ticks() -> usize {
core_grow_ticks() + CORE_HOLD_TICKS
}
pub(super) fn core_grow_ticks() -> usize {
(CORE_MAX_RADIUS / CORE_GROW_SPEED).ceil() as usize
}
pub(super) fn core_radius(tick: usize) -> f64 {
let grow_ticks = core_grow_ticks();
if tick < grow_ticks {
tick as f64 * CORE_GROW_SPEED
} else {
CORE_MAX_RADIUS
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_lines() -> Vec<&'static str> {
vec![" ▓▓▓ ", " ▓▓▓▓▓ ", "▓▓▓▓▓▓▓", " ▓▓▓▓▓ ", " ▓▓▓ "]
}
#[test]
fn distance_map_assigns_infinity_to_spaces() {
let lines = sample_lines();
let map = build_distance_map(&lines);
assert_eq!(distance_at(&map, 0, 0), f64::INFINITY);
assert_eq!(distance_at(&map, 0, 1), f64::INFINITY);
}
#[test]
fn distance_map_computes_finite_for_visible_chars() {
let lines = sample_lines();
let map = build_distance_map(&lines);
assert!(distance_at(&map, 2, 3).is_finite());
assert!(map.max_distance > 0.0);
}
#[test]
fn euclidean_distance_at_anchor_is_zero() {
let d = euclidean_distance(ANCHOR_ROW, ANCHOR_COL);
assert!(d < 0.01);
}
#[test]
fn core_distance_is_finite_for_all_positions() {
assert!(core_distance(0.0, 0.0).is_finite());
assert!(core_distance(10.0, 19.0).is_finite());
}
#[test]
fn core_radius_grows_then_caps() {
assert_eq!(core_radius(0), 0.0);
let grow = core_grow_ticks();
assert!((core_radius(grow) - CORE_MAX_RADIUS).abs() < 0.01);
assert!((core_radius(grow + 100) - CORE_MAX_RADIUS).abs() < 0.01);
}
#[test]
fn core_total_ticks_is_sum_of_grow_and_hold() {
let total = core_total_ticks();
assert_eq!(total, core_grow_ticks() + CORE_HOLD_TICKS);
}
}