use crate::types::{Grid, Point, Viewport};
const DISSOLVED: f64 = 6.0;
const RUN: f64 = 5.0;
const FINE_RADIUS: f64 = 1.1;
const HEAVY_RADIUS: f64 = 1.45;
pub const INVISIBLE: f64 = 0.02;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Level {
pub pitch: f64,
pub first: Point,
pub alpha: f64,
pub weight: f64,
}
impl Level {
pub fn radius(self) -> f64 {
FINE_RADIUS + (HEAVY_RADIUS - FINE_RADIUS) * self.weight.clamp(0.0, 1.0)
}
pub fn visible(self) -> bool {
self.alpha > INVISIBLE && self.pitch.is_finite() && self.pitch > 0.0
}
pub fn heaviness(self) -> f64 {
self.weight.clamp(0.0, 1.0) * 100.0
}
}
pub type Paper = [Level; 3];
pub fn paper(viewport: Viewport, grid: Grid) -> Paper {
let drawn = grid.size();
let zoom = if viewport.zoom.is_finite() && viewport.zoom > 0.0 {
viewport.zoom
} else {
1.0
};
let mut pitch = grid.size() * zoom;
for _ in 0..8 {
if pitch >= DISSOLVED {
break;
}
pitch *= RUN;
}
let blend = ((pitch - DISSOLVED) / (drawn - DISSOLVED)).clamp(0.0, 1.0);
[
level(viewport, pitch, blend, 0.0),
level(viewport, pitch * RUN, 1.0, blend),
level(viewport, pitch * RUN * RUN, 1.0 - blend, 1.0),
]
}
fn level(viewport: Viewport, pitch: f64, alpha: f64, weight: f64) -> Level {
Level {
pitch,
first: Point::new(viewport.x.rem_euclid(pitch), viewport.y.rem_euclid(pitch)),
alpha,
weight,
}
}
#[cfg(test)]
mod tests {
use super::*;
const MIN_ZOOM: f64 = 0.2;
const MAX_ZOOM: f64 = 3.0;
const GRID: Grid = Grid::new(12.0);
fn paper(zoom: f64) -> Paper {
super::paper(view(zoom), GRID)
}
fn view(zoom: f64) -> Viewport {
Viewport {
x: 544.3,
y: -217.8,
zoom,
}
}
fn zooms() -> Vec<f64> {
let mut zooms = vec![MIN_ZOOM, MAX_ZOOM, 1.0];
let mut zoom = MIN_ZOOM;
while zoom < MAX_ZOOM {
zooms.push(zoom);
zoom *= 1.01;
}
for boundary in [DISSOLVED / GRID.size(), DISSOLVED * RUN / GRID.size()] {
zooms.extend([boundary - 1e-9, boundary, boundary + 1e-9]);
}
zooms.retain(|zoom| (MIN_ZOOM..=MAX_ZOOM).contains(zoom));
zooms
}
#[test]
fn every_level_lands_on_the_maps_own_grid() {
for zoom in zooms() {
let viewport = view(zoom);
for level in paper(zoom) {
for (axis, first, pan) in [
("x", level.first.x, viewport.x),
("y", level.first.y, viewport.y),
] {
let world = (first - pan) / zoom;
let cell = world.rem_euclid(GRID.size());
assert!(
cell.abs() < 1e-6 || (cell - GRID.size()).abs() < 1e-6,
"zoom {zoom}: pitch {} sits {cell} into an {axis} cell, not on its boundary",
level.pitch,
);
}
let step = level.pitch / zoom;
assert!(
(step / GRID.size() - (step / GRID.size()).round()).abs() < 1e-6,
"zoom {zoom}: pitch {} is not a whole number of cells",
level.pitch,
);
}
}
}
#[test]
fn promotion_changes_nothing_that_is_drawn() {
for boundary in [DISSOLVED / GRID.size(), DISSOLVED * RUN / GRID.size()] {
let drawn = |zoom: f64| {
paper(zoom)
.into_iter()
.filter(|level| level.visible())
.collect::<Vec<_>>()
};
let before = drawn(boundary + 1e-6);
let after = drawn(boundary - 1e-6);
assert_eq!(
before.len(),
after.len(),
"a level appears or vanishes across the promotion at zoom {boundary}"
);
for (before, after) in before.iter().zip(&after) {
let same = |name: &str, before: f64, after: f64, tolerance: f64| {
assert!(
(before - after).abs() <= tolerance,
"{name} jumps from {before} to {after} \
across the promotion at zoom {boundary}"
);
};
same("pitch", before.pitch, after.pitch, 0.01);
same("alpha", before.alpha, after.alpha, 0.001);
same("weight", before.weight, after.weight, 0.001);
same("phase", before.first.x, after.first.x, 0.01);
same("phase", before.first.y, after.first.y, 0.01);
}
}
}
#[test]
fn visible_dots_never_crowd_together() {
for zoom in zooms() {
for level in paper(zoom) {
assert!(
!level.visible() || level.pitch >= DISSOLVED,
"zoom {zoom}: a visible level has dots {}px apart",
level.pitch,
);
}
}
}
#[test]
fn some_legible_lattice_is_always_drawn() {
for zoom in zooms() {
let paper = paper(zoom);
let solid = paper
.into_iter()
.filter(|level| level.alpha >= 0.5)
.map(|level| level.pitch)
.fold(f64::INFINITY, f64::min);
assert!(
(DISSOLVED..=GRID.size() * RUN * RUN).contains(&solid),
"zoom {zoom}: the finest solid lattice is {solid}px",
);
}
}
#[test]
fn levels_stay_within_their_ranges() {
for zoom in zooms() {
for level in paper(zoom) {
assert!((0.0..=1.0).contains(&level.alpha), "alpha {}", level.alpha);
assert!(
(0.0..=1.0).contains(&level.weight),
"weight {}",
level.weight
);
assert!(level.pitch.is_finite() && level.pitch > 0.0);
assert!(level.first.x >= 0.0 && level.first.x < level.pitch);
assert!(level.first.y >= 0.0 && level.first.y < level.pitch);
assert!((FINE_RADIUS..=HEAVY_RADIUS).contains(&level.radius()));
}
}
}
#[test]
fn a_nonsense_viewport_still_describes_a_drawable_paper() {
for zoom in [0.0, -1.0, f64::NAN, f64::INFINITY, 1e9] {
for level in super::paper(
Viewport {
x: 0.0,
y: 0.0,
zoom,
},
GRID,
) {
assert!(level.pitch.is_finite() && level.pitch > 0.0, "zoom {zoom}");
assert!(level.first.x.is_finite() && level.first.y.is_finite());
}
}
}
}