use core::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EasingCurve {
CubicBezier(f32, f32, f32, f32),
Spring {
stiffness: f32,
damping: f32,
},
}
impl EasingCurve {
pub const LINEAR: Self = Self::CubicBezier(0.0, 0.0, 1.0, 1.0);
pub const EASE_IN: Self = Self::CubicBezier(0.42, 0.0, 1.0, 1.0);
pub const EASE_OUT: Self = Self::CubicBezier(0.0, 0.0, 0.58, 1.0);
pub const EASE_IN_OUT: Self = Self::CubicBezier(0.42, 0.0, 0.58, 1.0);
pub const EASE: Self = Self::CubicBezier(0.25, 0.1, 0.25, 1.0);
#[must_use]
pub const fn bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
Self::CubicBezier(x1, y1, x2, y2)
}
#[must_use]
pub const fn spring(stiffness: f32, damping: f32) -> Self {
Self::Spring { stiffness, damping }
}
#[must_use]
pub fn ease(&self, t: f32) -> f32 {
match self {
Self::CubicBezier(x1, y1, x2, y2) => cubic_bezier_ease(t, *x1, *y1, *x2, *y2),
Self::Spring { stiffness, damping } => spring_ease(t, *stiffness, *damping),
}
}
#[must_use]
pub const fn is_spring(&self) -> bool {
matches!(self, Self::Spring { .. })
}
}
impl Default for EasingCurve {
fn default() -> Self {
Self::EASE_IN_OUT
}
}
fn cubic_bezier_ease(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
const EPSILON: f32 = 0.0001;
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
if (x1 - y1).abs() < 0.0001 && (x2 - y2).abs() < 0.0001 {
return t;
}
let mut guess = t;
let mut converged = false;
for _ in 0..8 {
let x = bezier_sample(guess, x1, x2) - t;
if x.abs() < EPSILON {
converged = true;
break;
}
let dx = bezier_derivative(guess, x1, x2);
if dx.abs() < 0.000_001 {
break;
}
let next = guess - x / dx;
if !(0.0..=1.0).contains(&next) {
break;
}
guess = next;
}
if !converged {
let mut low = 0.0;
let mut high = 1.0;
guess = t.clamp(0.0, 1.0);
for _ in 0..16 {
let sample = bezier_sample(guess, x1, x2);
let delta = sample - t;
if delta.abs() < EPSILON {
break;
}
if delta > 0.0 {
high = guess;
} else {
low = guess;
}
guess = f32::midpoint(low, high);
}
}
guess = guess.clamp(0.0, 1.0);
bezier_sample(guess, y1, y2)
}
#[inline]
fn bezier_sample(t: f32, p1: f32, p2: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let mt = 1.0 - t;
let mt2 = mt * mt;
(3.0 * mt2 * t).mul_add(p1, (3.0 * mt * t2).mul_add(p2, t3))
}
#[inline]
fn bezier_derivative(t: f32, p1: f32, p2: f32) -> f32 {
let t2 = t * t;
let mt = 1.0 - t;
let mt2 = mt * mt;
(3.0 * mt2).mul_add(p1, (6.0 * mt * t).mul_add(p2 - p1, 3.0 * t2 * (1.0 - p2)))
}
fn spring_ease(t: f32, stiffness: f32, damping: f32) -> f32 {
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
if !stiffness.is_finite() || !damping.is_finite() || stiffness <= 0.0 || damping < 0.0 {
return t;
}
let omega = stiffness.sqrt();
let zeta = damping / (2.0 * omega);
if zeta >= 1.0 {
let decay = (-omega * zeta * t).exp();
decay.mul_add(-(omega * zeta).mul_add(t, 1.0), 1.0)
} else {
let omega_d = omega * zeta.mul_add(-zeta, 1.0).sqrt();
let decay = (-zeta * omega * t).exp();
let cos_part = (omega_d * t).cos();
let sin_part = (zeta * omega / omega_d) * (omega_d * t).sin();
decay.mul_add(-(cos_part + sin_part), 1.0)
}
}
pub trait Interpolatable: Clone {
#[must_use]
fn lerp(&self, other: &Self, t: f32) -> Self;
}
impl Interpolatable for f32 {
fn lerp(&self, other: &Self, t: f32) -> Self {
self + (other - self) * t
}
}
impl Interpolatable for f64 {
fn lerp(&self, other: &Self, t: f32) -> Self {
self + (other - self) * Self::from(t)
}
}
impl<A: Interpolatable, B: Interpolatable> Interpolatable for (A, B) {
fn lerp(&self, other: &Self, t: f32) -> Self {
(self.0.lerp(&other.0, t), self.1.lerp(&other.1, t))
}
}
impl<A: Interpolatable, B: Interpolatable, C: Interpolatable> Interpolatable for (A, B, C) {
fn lerp(&self, other: &Self, t: f32) -> Self {
(
self.0.lerp(&other.0, t),
self.1.lerp(&other.1, t),
self.2.lerp(&other.2, t),
)
}
}
impl<A: Interpolatable, B: Interpolatable, C: Interpolatable, D: Interpolatable> Interpolatable
for (A, B, C, D)
{
fn lerp(&self, other: &Self, t: f32) -> Self {
(
self.0.lerp(&other.0, t),
self.1.lerp(&other.1, t),
self.2.lerp(&other.2, t),
self.3.lerp(&other.3, t),
)
}
}
impl<T: Interpolatable + Copy, const N: usize> Interpolatable for [T; N] {
fn lerp(&self, other: &Self, t: f32) -> Self {
let mut result = *self;
for i in 0..N {
result[i] = self[i].lerp(&other[i], t);
}
result
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AnimationSegment {
pub duration: Duration,
pub curve: EasingCurve,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_easing() {
let curve = EasingCurve::LINEAR;
assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
assert!((curve.ease(0.5) - 0.5).abs() < 0.001);
assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
}
#[test]
fn test_ease_in() {
let curve = EasingCurve::EASE_IN;
assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
assert!(curve.ease(0.5) < 0.5);
}
#[test]
fn test_ease_out() {
let curve = EasingCurve::EASE_OUT;
assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
assert!(curve.ease(0.5) > 0.5);
}
#[test]
fn test_ease_in_out() {
let curve = EasingCurve::EASE_IN_OUT;
assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
assert!((curve.ease(0.5) - 0.5).abs() < 0.1);
}
#[test]
fn test_spring_settles_to_one() {
let curve = EasingCurve::spring(100.0, 10.0);
assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
}
#[test]
fn test_bezier_solver_handles_extreme_control_points() {
let curve = EasingCurve::bezier(0.0, 1.0, 1.0, 0.0);
for step in 0_u16..=100 {
let t = f32::from(step) / 100.0;
let eased = curve.ease(t);
assert!(eased.is_finite(), "eased must be finite at t={t}");
assert!(
(0.0..=1.0).contains(&eased),
"eased out of range at t={t}: {eased}"
);
}
}
#[test]
fn test_f32_lerp() {
let a = 0.0_f32;
let b = 10.0_f32;
assert!((a.lerp(&b, 0.0) - 0.0).abs() < 0.001);
assert!((a.lerp(&b, 0.5) - 5.0).abs() < 0.001);
assert!((a.lerp(&b, 1.0) - 10.0).abs() < 0.001);
}
#[test]
fn test_tuple_lerp() {
let a = (0.0_f32, 0.0_f32);
let b = (10.0_f32, 20.0_f32);
let mid = a.lerp(&b, 0.5);
assert!((mid.0 - 5.0).abs() < 0.001);
assert!((mid.1 - 10.0).abs() < 0.001);
}
}