use core::f32::consts::PI;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
const BACK_C1: f32 = 1.70158;
const BACK_C2: f32 = BACK_C1 * 1.525;
const BACK_C3: f32 = BACK_C1 + 1.0;
const ELASTIC_C4: f32 = (2.0 * PI) / 3.0;
const ELASTIC_C5: f32 = (2.0 * PI) / 4.5;
#[derive(Clone)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Easing {
Linear,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic,
EaseInQuart,
EaseOutQuart,
EaseInOutQuart,
EaseInQuint,
EaseOutQuint,
EaseInOutQuint,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseInExpo,
EaseOutExpo,
EaseInOutExpo,
EaseInCirc,
EaseOutCirc,
EaseInOutCirc,
EaseInBack,
EaseOutBack,
EaseInOutBack,
EaseInElastic,
EaseOutElastic,
EaseInOutElastic,
EaseInBounce,
EaseOutBounce,
EaseInOutBounce,
#[cfg_attr(feature = "serde", serde(skip))]
Custom(fn(f32) -> f32),
CubicBezier(f32, f32, f32, f32),
Steps(u32),
RoughEase {
strength: f32,
points: u32,
seed: u32,
},
SlowMo {
ratio: f32,
power: f32,
yoyo_mode: bool,
},
ExpoScale {
start_scale: f32,
end_scale: f32,
},
Wiggle {
frequency: f32,
amplitude: f32,
},
CustomBounce {
strength: f32,
squash: f32,
},
}
impl Easing {
#[inline]
pub fn apply(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => linear(t),
Self::EaseInQuad => ease_in_quad(t),
Self::EaseOutQuad => ease_out_quad(t),
Self::EaseInOutQuad => ease_in_out_quad(t),
Self::EaseInCubic => ease_in_cubic(t),
Self::EaseOutCubic => ease_out_cubic(t),
Self::EaseInOutCubic => ease_in_out_cubic(t),
Self::EaseInQuart => ease_in_quart(t),
Self::EaseOutQuart => ease_out_quart(t),
Self::EaseInOutQuart => ease_in_out_quart(t),
Self::EaseInQuint => ease_in_quint(t),
Self::EaseOutQuint => ease_out_quint(t),
Self::EaseInOutQuint => ease_in_out_quint(t),
Self::EaseInSine => ease_in_sine(t),
Self::EaseOutSine => ease_out_sine(t),
Self::EaseInOutSine => ease_in_out_sine(t),
Self::EaseInExpo => ease_in_expo(t),
Self::EaseOutExpo => ease_out_expo(t),
Self::EaseInOutExpo => ease_in_out_expo(t),
Self::EaseInCirc => ease_in_circ(t),
Self::EaseOutCirc => ease_out_circ(t),
Self::EaseInOutCirc => ease_in_out_circ(t),
Self::EaseInBack => ease_in_back(t),
Self::EaseOutBack => ease_out_back(t),
Self::EaseInOutBack => ease_in_out_back(t),
Self::EaseInElastic => ease_in_elastic(t),
Self::EaseOutElastic => ease_out_elastic(t),
Self::EaseInOutElastic => ease_in_out_elastic(t),
Self::EaseInBounce => ease_in_bounce(t),
Self::EaseOutBounce => ease_out_bounce(t),
Self::EaseInOutBounce => ease_in_out_bounce(t),
Self::Custom(f) => f(t),
Self::CubicBezier(x1, y1, x2, y2) => cubic_bezier_ease(t, *x1, *y1, *x2, *y2),
Self::Steps(n) => steps_ease(t, *n),
Self::RoughEase { strength, points, seed } =>
rough_ease(t, *strength, *points, *seed),
Self::SlowMo { ratio, power, yoyo_mode } =>
slow_mo(t, *ratio, *power, *yoyo_mode),
Self::ExpoScale { start_scale, end_scale } =>
expo_scale(t, *start_scale, *end_scale),
Self::Wiggle { frequency, amplitude } =>
wiggle_ease(t, *frequency, *amplitude),
Self::CustomBounce { strength, squash } =>
custom_bounce(t, *strength, *squash),
}
}
}
impl core::fmt::Debug for Easing {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Custom(_) => write!(f, "Easing::Custom(<fn>)"),
Self::CubicBezier(x1, y1, x2, y2) => {
write!(f, "Easing::CubicBezier({x1}, {y1}, {x2}, {y2})")
}
Self::Steps(n) => write!(f, "Easing::Steps({n})"),
Self::RoughEase { strength, points, seed } => {
write!(f, "Easing::RoughEase({strength}, {points}, {seed})")
}
Self::SlowMo { ratio, power, yoyo_mode } => {
write!(f, "Easing::SlowMo({ratio}, {power}, {yoyo_mode})")
}
Self::ExpoScale { start_scale, end_scale } => {
write!(f, "Easing::ExpoScale({start_scale}, {end_scale})")
}
Self::Wiggle { frequency, amplitude } => {
write!(f, "Easing::Wiggle({frequency}, {amplitude})")
}
Self::CustomBounce { strength, squash } => {
write!(f, "Easing::CustomBounce({strength}, {squash})")
}
_ => write!(f, "Easing::{}", self.name()),
}
}
}
impl PartialEq for Easing {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Custom(a), Self::Custom(b)) => (*a as usize) == (*b as usize),
(Self::CubicBezier(x1, y1, x2, y2), Self::CubicBezier(a1, b1, a2, b2)) => {
x1 == a1 && y1 == b1 && x2 == a2 && y2 == b2
}
(Self::Steps(a), Self::Steps(b)) => a == b,
(Self::RoughEase { strength: s1, points: p1, seed: d1 },
Self::RoughEase { strength: s2, points: p2, seed: d2 }) =>
s1 == s2 && p1 == p2 && d1 == d2,
(Self::SlowMo { ratio: r1, power: p1, yoyo_mode: y1 },
Self::SlowMo { ratio: r2, power: p2, yoyo_mode: y2 }) =>
r1 == r2 && p1 == p2 && y1 == y2,
(Self::ExpoScale { start_scale: s1, end_scale: e1 },
Self::ExpoScale { start_scale: s2, end_scale: e2 }) =>
s1 == s2 && e1 == e2,
(Self::Wiggle { frequency: f1, amplitude: a1 },
Self::Wiggle { frequency: f2, amplitude: a2 }) =>
f1 == f2 && a1 == a2,
(Self::CustomBounce { strength: s1, squash: q1 },
Self::CustomBounce { strength: s2, squash: q2 }) =>
s1 == s2 && q1 == q2,
_ => self.name() == other.name(),
}
}
}
impl Easing {
pub fn name(&self) -> &'static str {
match self {
Self::Linear => "Linear",
Self::EaseInQuad => "EaseInQuad",
Self::EaseOutQuad => "EaseOutQuad",
Self::EaseInOutQuad => "EaseInOutQuad",
Self::EaseInCubic => "EaseInCubic",
Self::EaseOutCubic => "EaseOutCubic",
Self::EaseInOutCubic => "EaseInOutCubic",
Self::EaseInQuart => "EaseInQuart",
Self::EaseOutQuart => "EaseOutQuart",
Self::EaseInOutQuart => "EaseInOutQuart",
Self::EaseInQuint => "EaseInQuint",
Self::EaseOutQuint => "EaseOutQuint",
Self::EaseInOutQuint => "EaseInOutQuint",
Self::EaseInSine => "EaseInSine",
Self::EaseOutSine => "EaseOutSine",
Self::EaseInOutSine => "EaseInOutSine",
Self::EaseInExpo => "EaseInExpo",
Self::EaseOutExpo => "EaseOutExpo",
Self::EaseInOutExpo => "EaseInOutExpo",
Self::EaseInCirc => "EaseInCirc",
Self::EaseOutCirc => "EaseOutCirc",
Self::EaseInOutCirc => "EaseInOutCirc",
Self::EaseInBack => "EaseInBack",
Self::EaseOutBack => "EaseOutBack",
Self::EaseInOutBack => "EaseInOutBack",
Self::EaseInElastic => "EaseInElastic",
Self::EaseOutElastic => "EaseOutElastic",
Self::EaseInOutElastic => "EaseInOutElastic",
Self::EaseInBounce => "EaseInBounce",
Self::EaseOutBounce => "EaseOutBounce",
Self::EaseInOutBounce => "EaseInOutBounce",
Self::Custom(_) => "Custom",
Self::CubicBezier(..) => "CubicBezier",
Self::Steps(_) => "Steps",
Self::RoughEase { .. } => "RoughEase",
Self::SlowMo { .. } => "SlowMo",
Self::ExpoScale { .. } => "ExpoScale",
Self::Wiggle { .. } => "Wiggle",
Self::CustomBounce { .. } => "CustomBounce",
}
}
pub fn all_named() -> &'static [Easing] {
&[
Self::Linear,
Self::EaseInQuad, Self::EaseOutQuad, Self::EaseInOutQuad,
Self::EaseInCubic, Self::EaseOutCubic, Self::EaseInOutCubic,
Self::EaseInQuart, Self::EaseOutQuart, Self::EaseInOutQuart,
Self::EaseInQuint, Self::EaseOutQuint, Self::EaseInOutQuint,
Self::EaseInSine, Self::EaseOutSine, Self::EaseInOutSine,
Self::EaseInExpo, Self::EaseOutExpo, Self::EaseInOutExpo,
Self::EaseInCirc, Self::EaseOutCirc, Self::EaseInOutCirc,
Self::EaseInBack, Self::EaseOutBack, Self::EaseInOutBack,
Self::EaseInElastic, Self::EaseOutElastic, Self::EaseInOutElastic,
Self::EaseInBounce, Self::EaseOutBounce, Self::EaseInOutBounce,
]
}
}
#[inline] pub fn linear(t: f32) -> f32 { t }
#[inline] pub fn ease_in_quad(t: f32) -> f32 { t * t }
#[inline] pub fn ease_out_quad(t: f32) -> f32 { 1.0 - (1.0 - t) * (1.0 - t) }
#[inline] pub fn ease_in_out_quad(t: f32) -> f32 {
if t < 0.5 { 2.0 * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 }
}
#[inline] pub fn ease_in_cubic(t: f32) -> f32 { t * t * t }
#[inline] pub fn ease_out_cubic(t: f32) -> f32 { 1.0 - (1.0 - t).powi(3) }
#[inline] pub fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 { 4.0 * t * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 }
}
#[inline] pub fn ease_in_quart(t: f32) -> f32 { t * t * t * t }
#[inline] pub fn ease_out_quart(t: f32) -> f32 { 1.0 - (1.0 - t).powi(4) }
#[inline] pub fn ease_in_out_quart(t: f32) -> f32 {
if t < 0.5 { 8.0 * t * t * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0 }
}
#[inline] pub fn ease_in_quint(t: f32) -> f32 { t * t * t * t * t }
#[inline] pub fn ease_out_quint(t: f32) -> f32 { 1.0 - (1.0 - t).powi(5) }
#[inline] pub fn ease_in_out_quint(t: f32) -> f32 {
if t < 0.5 { 16.0 * t * t * t * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0 }
}
#[inline] pub fn ease_in_sine(t: f32) -> f32 { 1.0 - ((t * PI / 2.0).cos()) }
#[inline] pub fn ease_out_sine(t: f32) -> f32 { (t * PI / 2.0).sin() }
#[inline] pub fn ease_in_out_sine(t: f32) -> f32 { -(((PI * t).cos()) - 1.0) / 2.0 }
#[inline] pub fn ease_in_expo(t: f32) -> f32 {
if t == 0.0 { 0.0 } else { (2.0_f32).powf(10.0 * t - 10.0) }
}
#[inline] pub fn ease_out_expo(t: f32) -> f32 {
if t == 1.0 { 1.0 } else { 1.0 - (2.0_f32).powf(-10.0 * t) }
}
#[inline] pub fn ease_in_out_expo(t: f32) -> f32 {
if t == 0.0 { return 0.0; }
if t == 1.0 { return 1.0; }
if t < 0.5 {
(2.0_f32).powf(20.0 * t - 10.0) / 2.0
} else {
(2.0 - (2.0_f32).powf(-20.0 * t + 10.0)) / 2.0
}
}
#[inline] pub fn ease_in_circ(t: f32) -> f32 { 1.0 - (1.0 - t * t).sqrt() }
#[inline] pub fn ease_out_circ(t: f32) -> f32 { (1.0 - (t - 1.0).powi(2)).sqrt() }
#[inline] pub fn ease_in_out_circ(t: f32) -> f32 {
if t < 0.5 {
(1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0
} else {
((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0
}
}
#[inline] pub fn ease_in_back(t: f32) -> f32 {
BACK_C3 * t * t * t - BACK_C1 * t * t
}
#[inline] pub fn ease_out_back(t: f32) -> f32 {
let t = t - 1.0;
1.0 + BACK_C3 * t * t * t + BACK_C1 * t * t
}
#[inline] pub fn ease_in_out_back(t: f32) -> f32 {
if t < 0.5 {
((2.0 * t).powi(2) * ((BACK_C2 + 1.0) * 2.0 * t - BACK_C2)) / 2.0
} else {
((2.0 * t - 2.0).powi(2) * ((BACK_C2 + 1.0) * (t * 2.0 - 2.0) + BACK_C2) + 2.0) / 2.0
}
}
#[inline] pub fn ease_in_elastic(t: f32) -> f32 {
if t == 0.0 { return 0.0; }
if t == 1.0 { return 1.0; }
-(2.0_f32).powf(10.0 * t - 10.0) * ((10.0 * t - 10.75) * ELASTIC_C4).sin()
}
#[inline] pub fn ease_out_elastic(t: f32) -> f32 {
if t == 0.0 { return 0.0; }
if t == 1.0 { return 1.0; }
(2.0_f32).powf(-10.0 * t) * ((10.0 * t - 0.75) * ELASTIC_C4).sin() + 1.0
}
#[inline] pub fn ease_in_out_elastic(t: f32) -> f32 {
if t == 0.0 { return 0.0; }
if t == 1.0 { return 1.0; }
if t < 0.5 {
-((2.0_f32).powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * ELASTIC_C5).sin()) / 2.0
} else {
(2.0_f32).powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * ELASTIC_C5).sin() / 2.0 + 1.0
}
}
#[inline] pub fn ease_out_bounce(t: f32) -> f32 {
const N: f32 = 7.5625;
const D: f32 = 2.75;
if t < 1.0 / D {
N * t * t
} else if t < 2.0 / D {
let t = t - 1.5 / D;
N * t * t + 0.75
} else if t < 2.5 / D {
let t = t - 2.25 / D;
N * t * t + 0.9375
} else {
let t = t - 2.625 / D;
N * t * t + 0.984375
}
}
#[inline] pub fn ease_in_bounce(t: f32) -> f32 {
1.0 - ease_out_bounce(1.0 - t)
}
#[inline] pub fn ease_in_out_bounce(t: f32) -> f32 {
if t < 0.5 {
(1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
} else {
(1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
}
}
#[inline]
pub fn cubic_bezier_ease(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let mut u = t; for _ in 0..8 {
let x = sample_bezier(u, x1, x2) - t;
let dx = sample_bezier_derivative(u, x1, x2);
if dx.abs() < 1e-10 {
break;
}
u -= x / dx;
u = u.clamp(0.0, 1.0);
}
let mut x_val = sample_bezier(u, x1, x2) - t;
if x_val.abs() > 1e-6 {
let mut lo = 0.0_f32;
let mut hi = 1.0_f32;
u = t;
for _ in 0..20 {
x_val = sample_bezier(u, x1, x2) - t;
if x_val.abs() < 1e-7 {
break;
}
if x_val > 0.0 {
hi = u;
} else {
lo = u;
}
u = (lo + hi) * 0.5;
}
}
sample_bezier(u, y1, y2)
}
#[inline]
fn sample_bezier(u: f32, c1: f32, c2: f32) -> f32 {
let u2 = u * u;
let u3 = u2 * u;
let inv = 1.0 - u;
let inv2 = inv * inv;
3.0 * inv2 * u * c1 + 3.0 * inv * u2 * c2 + u3
}
#[inline]
fn sample_bezier_derivative(u: f32, c1: f32, c2: f32) -> f32 {
let u2 = u * u;
let inv = 1.0 - u;
3.0 * inv * inv * c1 + 6.0 * inv * u * (c2 - c1) + 3.0 * u2 * (1.0 - c2)
}
#[inline]
pub fn steps_ease(t: f32, n: u32) -> f32 {
if n == 0 || t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let n_f = n as f32;
let step = (t * n_f).floor();
step / n_f
}
#[inline]
pub fn rough_ease(t: f32, strength: f32, points: u32, seed: u32) -> f32 {
if points == 0 || strength == 0.0 {
return t;
}
let n = points as f32;
let scaled = t * n;
let idx = (scaled as u32).min(points - 1);
let frac = scaled - idx as f32;
let noise_at = |i: u32| -> f32 {
let h = i.wrapping_mul(2654435761).wrapping_add(seed.wrapping_mul(2246822519));
let h = h ^ (h >> 16);
let h = h.wrapping_mul(0x45d9f3b);
let h = h ^ (h >> 16);
(h as f32 / u32::MAX as f32) * 2.0 - 1.0
};
let n0 = noise_at(idx);
let n1 = if idx + 1 < points { noise_at(idx + 1) } else { 0.0 };
let noise = n0 + (n1 - n0) * frac;
(t + noise * strength).clamp(0.0, 1.0)
}
#[inline]
pub fn slow_mo(t: f32, ratio: f32, power: f32, yoyo_mode: bool) -> f32 {
let t = if yoyo_mode && t > 0.5 { 1.0 - t } else { t };
let ratio = ratio.clamp(0.0, 1.0);
let half_ratio = ratio * 0.5;
let slow_end = 1.0 - half_ratio;
if t < half_ratio {
let local = t / half_ratio;
let transition_y = half_ratio; transition_y * local.powf(1.0 / power.max(0.01))
} else if t > slow_end {
let local = (t - slow_end) / half_ratio;
let transition_y = slow_end;
transition_y + (1.0 - transition_y) * local.powf(power.max(0.01))
} else {
t
}
}
#[inline]
pub fn expo_scale(t: f32, start_scale: f32, end_scale: f32) -> f32 {
let start = start_scale.max(0.001); let end = end_scale.max(0.001);
if (start - end).abs() < 1e-10 {
return t;
}
let ratio = end / start;
(start * ratio.powf(t) - start) / (end - start)
}
#[inline]
pub fn wiggle_ease(t: f32, frequency: f32, amplitude: f32) -> f32 {
(t + amplitude * (frequency * 2.0 * PI * t).sin()).clamp(0.0, 1.0)
}
#[inline]
pub fn custom_bounce(t: f32, strength: f32, squash: f32) -> f32 {
if t == 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let strength = strength.clamp(0.01, 1.0);
let bounces = (1.0 / strength).ceil() as u32;
let bounces = bounces.max(2);
let mut seg_start = 0.0_f32;
let mut seg_width = 1.0_f32;
let decay = strength;
for _ in 0..bounces {
let seg_end = seg_start + seg_width;
if t < seg_end || seg_width < 0.001 {
let local = (t - seg_start) / seg_width;
let squashed = if squash > 0.0 {
let mid = 0.5;
let offset = local - mid;
(mid + offset * (1.0 + squash)).clamp(0.0, 1.0)
} else {
local
};
let height = if seg_start == 0.0 { 1.0 } else { seg_width * 4.0 };
let arc = 4.0 * squashed * (1.0 - squashed);
return (1.0 - height.min(1.0) * (1.0 - arc)).clamp(0.0, 1.0);
}
seg_start += seg_width;
seg_width *= decay;
}
1.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endpoints_for_all_named() {
for easing in Easing::all_named() {
let t0 = easing.apply(0.0);
let t1 = easing.apply(1.0);
assert!(
t0.abs() < 1e-5,
"{} apply(0.0) = {} (expected ~0.0)", easing.name(), t0
);
assert!(
(t1 - 1.0).abs() < 1e-5,
"{} apply(1.0) = {} (expected ~1.0)", easing.name(), t1
);
}
}
#[test]
fn clamping_does_not_panic() {
for easing in Easing::all_named() {
let _ = easing.apply(-0.5);
let _ = easing.apply(1.5);
}
}
#[test]
fn linear_is_identity() {
for i in 0..=10 {
let t = i as f32 / 10.0;
assert!((linear(t) - t).abs() < 1e-7, "linear({t}) != {t}");
}
}
#[test]
fn ease_in_out_cubic_is_symmetric() {
for i in 1..10 {
let t = i as f32 / 10.0;
let forward = ease_in_out_cubic(t);
let backward = 1.0 - ease_in_out_cubic(1.0 - t);
assert!(
(forward - backward).abs() < 1e-6,
"EaseInOutCubic symmetry broken at t={t}: {forward} vs {backward}"
);
}
}
#[test]
fn bounce_out_midpoint_is_above_half() {
assert!(ease_out_bounce(0.5) > 0.5);
}
#[test]
fn custom_easing_works() {
let e = Easing::Custom(|t| t * t);
assert!((e.apply(0.5) - 0.25).abs() < 1e-7);
}
#[test]
fn easing_enum_name_roundtrip() {
for e in Easing::all_named() {
assert!(!e.name().is_empty());
}
}
#[test]
fn cubic_bezier_endpoints() {
let e = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
assert!((e.apply(0.0) - 0.0).abs() < 1e-6);
assert!((e.apply(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn cubic_bezier_css_ease() {
let ease = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
let mid = ease.apply(0.5);
assert!(mid > 0.5, "Expected > 0.5, got {mid}");
}
#[test]
fn cubic_bezier_linear_equivalent() {
let lin = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
for i in 0..=10 {
let t = i as f32 / 10.0;
assert!(
(lin.apply(t) - t).abs() < 0.02,
"CubicBezier(0,0,1,1) at t={t} = {}, expected ~{t}",
lin.apply(t)
);
}
}
#[test]
fn cubic_bezier_ease_in_out() {
let eio = Easing::CubicBezier(0.42, 0.0, 0.58, 1.0);
let mid = eio.apply(0.5);
assert!((mid - 0.5).abs() < 0.05, "Expected ~0.5, got {mid}");
}
#[test]
fn steps_basic() {
let stepped = Easing::Steps(4);
assert!((stepped.apply(0.0) - 0.0).abs() < 1e-6);
assert!((stepped.apply(0.1) - 0.0).abs() < 1e-6);
assert!((stepped.apply(0.3) - 0.25).abs() < 1e-6);
assert!((stepped.apply(0.5) - 0.5).abs() < 1e-6);
assert!((stepped.apply(0.8) - 0.75).abs() < 1e-6);
assert!((stepped.apply(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn steps_one() {
let s = Easing::Steps(1);
assert!((s.apply(0.0) - 0.0).abs() < 1e-6);
assert!((s.apply(0.5) - 0.0).abs() < 1e-6);
assert!((s.apply(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn steps_zero_returns_zero() {
let s = Easing::Steps(0);
assert!((s.apply(0.5) - 0.0).abs() < 1e-6);
}
#[test]
fn rough_ease_endpoints() {
let e = Easing::RoughEase { strength: 0.5, points: 20, seed: 42 };
assert!((e.apply(0.0)).abs() < 1e-5, "RoughEase(0) should be ~0");
assert!((e.apply(1.0) - 1.0).abs() < 1e-5, "RoughEase(1) should be ~1");
}
#[test]
fn rough_ease_zero_strength_is_linear() {
let e = Easing::RoughEase { strength: 0.0, points: 20, seed: 42 };
for i in 0..=10 {
let t = i as f32 / 10.0;
assert!((e.apply(t) - t).abs() < 1e-6);
}
}
#[test]
fn slow_mo_endpoints() {
let e = Easing::SlowMo { ratio: 0.7, power: 0.7, yoyo_mode: false };
assert!((e.apply(0.0)).abs() < 1e-5);
assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
}
#[test]
fn expo_scale_endpoints() {
let e = Easing::ExpoScale { start_scale: 1.0, end_scale: 10.0 };
assert!((e.apply(0.0)).abs() < 1e-5);
assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
}
#[test]
fn expo_scale_equal_scales_is_linear() {
let e = Easing::ExpoScale { start_scale: 5.0, end_scale: 5.0 };
for i in 0..=10 {
let t = i as f32 / 10.0;
assert!((e.apply(t) - t).abs() < 1e-5);
}
}
#[test]
fn wiggle_zero_amplitude_is_linear() {
let e = Easing::Wiggle { frequency: 5.0, amplitude: 0.0 };
for i in 0..=10 {
let t = i as f32 / 10.0;
assert!((e.apply(t) - t).abs() < 1e-6);
}
}
#[test]
fn wiggle_endpoints() {
let e = Easing::Wiggle { frequency: 3.0, amplitude: 0.3 };
assert!((e.apply(0.0)).abs() < 1e-5);
assert!((e.apply(1.0) - 1.0).abs() < 1e-2);
}
#[test]
fn custom_bounce_endpoints() {
let e = Easing::CustomBounce { strength: 0.5, squash: 0.0 };
assert!((e.apply(0.0)).abs() < 1e-5);
assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
}
}