#![forbid(unsafe_code)]
#[inline]
pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
a + (b - a) * t
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Easing {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
}
impl Easing {
pub fn apply(self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseIn => t * t,
Easing::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Easing::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tween {
pub from: f64,
pub to: f64,
pub duration: f64,
pub easing: Easing,
}
impl Tween {
pub fn new(from: f64, to: f64, duration: f64, easing: Easing) -> Self {
Self {
from,
to,
duration,
easing,
}
}
pub fn sample(&self, elapsed: f64) -> f64 {
if self.duration <= 0.0 {
return self.to;
}
let t = (elapsed / self.duration).clamp(0.0, 1.0);
lerp(self.from, self.to, self.easing.apply(t))
}
pub fn is_finished(&self, elapsed: f64) -> bool {
elapsed >= self.duration
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Spring {
pub position: f64,
pub velocity: f64,
pub target: f64,
pub stiffness: f64,
pub damping: f64,
}
impl Spring {
pub fn new(position: f64, target: f64) -> Self {
Self {
position,
velocity: 0.0,
target,
stiffness: 120.0,
damping: 1.0,
}
}
pub fn step(&mut self, dt: f64) {
let k = self.stiffness;
let c = 2.0 * self.damping * k.sqrt();
let force = -k * (self.position - self.target) - c * self.velocity;
self.velocity += force * dt;
self.position += self.velocity * dt;
}
pub fn is_at_rest(&self) -> bool {
(self.position - self.target).abs() < 1e-3 && self.velocity.abs() < 1e-3
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easing_endpoints_are_fixed() {
for e in [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
] {
assert!((e.apply(0.0) - 0.0).abs() < 1e-9, "{e:?} at 0");
assert!((e.apply(1.0) - 1.0).abs() < 1e-9, "{e:?} at 1");
}
assert!(Easing::EaseIn.apply(0.5) < 0.5); }
#[test]
fn tween_clamps_and_finishes() {
let tw = Tween::new(0.0, 10.0, 2.0, Easing::Linear);
assert_eq!(tw.sample(0.0), 0.0);
assert_eq!(tw.sample(1.0), 5.0);
assert_eq!(tw.sample(5.0), 10.0);
assert!(tw.is_finished(2.0));
}
#[test]
fn spring_settles_at_target() {
let mut s = Spring::new(0.0, 1.0);
for _ in 0..2000 {
s.step(1.0 / 240.0);
}
assert!(s.is_at_rest(), "spring should settle: pos={}", s.position);
assert!((s.position - 1.0).abs() < 1e-2);
}
}