use crate::tokens::EASE_IN_OUT_CUBIC;
pub const ZOOMS: [f32; 8] = [0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0];
pub const ZOOM_MIN: f32 = 0.05;
pub const ZOOM_MAX: f32 = 8.0;
pub const CAMERA_MS: f32 = 320.0;
pub const SELECTION_STROKE: f32 = 1.5;
pub const HANDLE_SIZE: f32 = 8.0;
pub const SNAP_GRID: f32 = 8.0;
#[must_use]
pub fn zoom_in(z: f32) -> f32 {
ZOOMS.iter().copied().find(|&s| s > z).unwrap_or(ZOOM_MAX)
}
#[must_use]
pub fn zoom_out(z: f32) -> f32 {
ZOOMS
.iter()
.rev()
.copied()
.find(|&s| s < z)
.unwrap_or(ZOOM_MIN)
}
#[must_use]
pub fn screen_len(world: f32, zoom: f32) -> f32 {
world * zoom
}
#[must_use]
pub fn world_len(screen: f32, zoom: f32) -> f32 {
screen / zoom.max(1e-6)
}
#[must_use]
pub fn snap(v: f32, grid: f32) -> f32 {
if grid <= 0.0 {
v
} else {
(v / grid).round() * grid
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Camera {
pub x: f32,
pub y: f32,
pub zoom: f32,
}
impl Default for Camera {
fn default() -> Self {
Self::ORIGIN
}
}
impl Camera {
pub const ORIGIN: Camera = Camera {
x: 0.0,
y: 0.0,
zoom: 1.0,
};
#[must_use]
pub fn ease_to(self, to: Camera, t: f32) -> Camera {
let e = EASE_IN_OUT_CUBIC.eval(t.clamp(0.0, 1.0));
Camera {
x: self.x + (to.x - self.x) * e,
y: self.y + (to.y - self.y) * e,
zoom: self.zoom + (to.zoom - self.zoom) * e,
}
}
#[must_use]
pub fn to_screen(self, x: f32, y: f32) -> (f32, f32) {
(x * self.zoom + self.x, y * self.zoom + self.y)
}
#[must_use]
pub fn to_world(self, x: f32, y: f32) -> (f32, f32) {
let z = self.zoom.max(1e-6);
((x - self.x) / z, (y - self.y) / z)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zoom_steps_walk_the_ladder_and_clamp() {
assert_eq!(zoom_in(1.0), 2.0);
assert_eq!(zoom_out(1.0), 0.5);
assert_eq!(zoom_in(1.5), 2.0); assert_eq!(zoom_out(1.5), 1.0); assert_eq!(zoom_in(ZOOM_MAX), ZOOM_MAX); assert_eq!(zoom_out(ZOOM_MIN), ZOOM_MIN);
assert_eq!(zoom_in(0.099_95), 0.1);
assert_eq!(zoom_out(0.100_05), 0.1);
}
#[test]
fn snap_rounds_to_the_grid() {
assert_eq!(snap(11.0, 8.0), 8.0);
assert_eq!(snap(13.0, 8.0), 16.0);
assert_eq!(snap(4.0, 8.0), 8.0); assert_eq!(snap(3.0, 0.0), 3.0); }
#[test]
fn stroke_is_zoom_compensated() {
let world = world_len(SELECTION_STROKE, 2.0);
assert!((world - 0.75).abs() < 1e-6);
assert!((screen_len(world, 2.0) - SELECTION_STROKE).abs() < 1e-6);
}
#[test]
fn camera_round_trips_world_screen_and_eases() {
let cam = Camera {
x: 40.0,
y: -10.0,
zoom: 2.0,
};
let (sx, sy) = cam.to_screen(12.0, 8.0);
let (wx, wy) = cam.to_world(sx, sy);
assert!((wx - 12.0).abs() < 1e-4 && (wy - 8.0).abs() < 1e-4);
let a = Camera::ORIGIN;
let b = Camera {
x: 0.0,
y: 0.0,
zoom: 4.0,
};
assert_eq!(a.ease_to(b, 0.0).zoom, 1.0);
assert!((a.ease_to(b, 1.0).zoom - 4.0).abs() < 1e-4);
}
}