pub const MOTION_DURATION_MS: u32 = 150;
pub const EASE_X1: f32 = 0.25;
pub const EASE_Y1: f32 = 0.10;
pub const EASE_X2: f32 = 0.25;
pub const EASE_Y2: f32 = 1.00;
pub fn ease(t: f32) -> f32 {
cubic_bezier(t, EASE_X1, EASE_Y1, EASE_X2, EASE_Y2)
}
pub fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
let u = solve_bezier_t(t, x1, x2);
bezier_axis(u, y1, y2)
}
fn bezier_axis(u: f32, c1: f32, c2: f32) -> f32 {
let a = 1.0 - 3.0 * c2 + 3.0 * c1;
let b = 3.0 * c2 - 6.0 * c1;
let c = 3.0 * c1;
((a * u + b) * u + c) * u
}
fn bezier_axis_deriv(u: f32, c1: f32, c2: f32) -> f32 {
let a = 1.0 - 3.0 * c2 + 3.0 * c1;
let b = 3.0 * c2 - 6.0 * c1;
let c = 3.0 * c1;
3.0 * a * u * u + 2.0 * b * u + c
}
fn solve_bezier_t(target: f32, x1: f32, x2: f32) -> f32 {
let mut u = target;
for _ in 0..16 {
let cur = bezier_axis(u, x1, x2);
let err = cur - target;
if err.abs() < 1e-6 {
return u;
}
let d = bezier_axis_deriv(u, x1, x2);
if d.abs() < 1e-9 {
return bisect_bezier_t(target, x1, x2);
}
u -= err / d;
}
u
}
fn bisect_bezier_t(target: f32, x1: f32, x2: f32) -> f32 {
let mut lo = 0.0f32;
let mut hi = 1.0f32;
for _ in 0..32 {
let mid = 0.5 * (lo + hi);
let cur = bezier_axis(mid, x1, x2);
if cur < target {
lo = mid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
pub trait Interpolate: Copy {
fn lerp(self, other: Self, t: f32) -> Self;
}
impl Interpolate for f32 {
fn lerp(self, other: Self, t: f32) -> Self {
self + (other - self) * t
}
}
impl Interpolate for u32 {
fn lerp(self, other: Self, t: f32) -> Self {
let a = self as f32;
let b = other as f32;
(a + (b - a) * t).round().max(0.0) as u32
}
}
impl Interpolate for i32 {
fn lerp(self, other: Self, t: f32) -> Self {
let a = self as f32;
let b = other as f32;
(a + (b - a) * t).round() as i32
}
}
#[derive(Debug, Clone, Copy)]
pub struct MotionState<T: Interpolate> {
pub from: T,
pub to: T,
pub elapsed_ms: u32,
}
impl<T: Interpolate> MotionState<T> {
pub fn new(from: T, to: T) -> Self {
Self {
from,
to,
elapsed_ms: 0,
}
}
pub fn tick(&mut self, dt_ms: u32) {
self.elapsed_ms = self.elapsed_ms.saturating_add(dt_ms);
}
pub fn current(self) -> T {
let t = (self.elapsed_ms as f32 / MOTION_DURATION_MS as f32).clamp(0.0, 1.0);
self.from.lerp(self.to, ease(t))
}
pub fn is_done(self) -> bool {
self.elapsed_ms >= MOTION_DURATION_MS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ease_at_endpoints() {
assert_eq!(ease(0.0), 0.0);
assert_eq!(ease(1.0), 1.0);
}
#[test]
fn ease_is_monotone() {
let mut prev = 0.0;
for k in 0..=100 {
let t = (k as f32) / 100.0;
let e = ease(t);
assert!(e + 1e-5 >= prev, "ease must be monotone non-decreasing");
prev = e;
}
}
#[test]
fn ease_midpoint_is_above_linear() {
assert!(ease(0.5) > 0.5);
}
#[test]
fn f32_lerp() {
assert_eq!(0.0f32.lerp(10.0, 0.0), 0.0);
assert_eq!(0.0f32.lerp(10.0, 1.0), 10.0);
assert!((0.0f32.lerp(10.0, 0.5) - 5.0).abs() < 1e-6);
}
#[test]
fn u32_lerp_rounds() {
assert_eq!(0u32.lerp(10, 0.5), 5);
assert_eq!(0u32.lerp(11, 0.5), 6); }
#[test]
fn motion_state_progresses() {
let mut m = MotionState::new(0.0f32, 100.0);
assert_eq!(m.current(), 0.0);
m.tick(MOTION_DURATION_MS / 2);
let mid = m.current();
assert!(mid > 0.0 && mid < 100.0);
m.tick(MOTION_DURATION_MS / 2);
assert!(m.is_done());
assert!((m.current() - 100.0).abs() < 1e-3);
}
#[test]
fn motion_clamps_at_end() {
let mut m = MotionState::new(0u32, 50);
m.tick(MOTION_DURATION_MS * 10);
assert_eq!(m.current(), 50);
}
}