use crate::math::{ceil, cos, powf, powi, sin, sqrt};
use core::f32::consts::PI;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::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,
CubicBezier(f32, f32, f32, f32),
Steps(u32),
#[cfg_attr(feature = "serde", serde(skip))]
Custom(fn(f32) -> f32),
}
impl PartialEq for Easing {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Easing::Custom(_), _) | (_, Easing::Custom(_)) => false,
(Easing::CubicBezier(ax1, ay1, ax2, ay2), Easing::CubicBezier(bx1, by1, bx2, by2)) => {
ax1 == bx1 && ay1 == by1 && ax2 == bx2 && ay2 == by2
}
(Easing::Steps(a), Easing::Steps(b)) => a == b,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
impl Easing {
#[inline]
pub fn apply(&self, t: f32) -> f32 {
match self {
Easing::Custom(f) => f(t),
_ => {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseInQuad => ease_in_quad(t),
Easing::EaseOutQuad => ease_out_quad(t),
Easing::EaseInOutQuad => ease_in_out_quad(t),
Easing::EaseInCubic => ease_in_cubic(t),
Easing::EaseOutCubic => ease_out_cubic(t),
Easing::EaseInOutCubic => ease_in_out_cubic(t),
Easing::EaseInQuart => ease_in_quart(t),
Easing::EaseOutQuart => ease_out_quart(t),
Easing::EaseInOutQuart => ease_in_out_quart(t),
Easing::EaseInQuint => ease_in_quint(t),
Easing::EaseOutQuint => ease_out_quint(t),
Easing::EaseInOutQuint => ease_in_out_quint(t),
Easing::EaseInSine => ease_in_sine(t),
Easing::EaseOutSine => ease_out_sine(t),
Easing::EaseInOutSine => ease_in_out_sine(t),
Easing::EaseInExpo => ease_in_expo(t),
Easing::EaseOutExpo => ease_out_expo(t),
Easing::EaseInOutExpo => ease_in_out_expo(t),
Easing::EaseInCirc => ease_in_circ(t),
Easing::EaseOutCirc => ease_out_circ(t),
Easing::EaseInOutCirc => ease_in_out_circ(t),
Easing::EaseInBack => ease_in_back(t),
Easing::EaseOutBack => ease_out_back(t),
Easing::EaseInOutBack => ease_in_out_back(t),
Easing::EaseInElastic => ease_in_elastic(t),
Easing::EaseOutElastic => ease_out_elastic(t),
Easing::EaseInOutElastic => ease_in_out_elastic(t),
Easing::EaseInBounce => ease_in_bounce(t),
Easing::EaseOutBounce => ease_out_bounce(t),
Easing::EaseInOutBounce => ease_in_out_bounce(t),
Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(t, *x1, *y1, *x2, *y2),
Easing::Steps(count) => steps(t, *count),
Easing::Custom(_) => unreachable!(),
}
}
}
}
pub fn all_named() -> &'static [Easing] {
&[
Easing::Linear,
Easing::EaseInQuad,
Easing::EaseOutQuad,
Easing::EaseInOutQuad,
Easing::EaseInCubic,
Easing::EaseOutCubic,
Easing::EaseInOutCubic,
Easing::EaseInQuart,
Easing::EaseOutQuart,
Easing::EaseInOutQuart,
Easing::EaseInQuint,
Easing::EaseOutQuint,
Easing::EaseInOutQuint,
Easing::EaseInSine,
Easing::EaseOutSine,
Easing::EaseInOutSine,
Easing::EaseInExpo,
Easing::EaseOutExpo,
Easing::EaseInOutExpo,
Easing::EaseInCirc,
Easing::EaseOutCirc,
Easing::EaseInOutCirc,
Easing::EaseInBack,
Easing::EaseOutBack,
Easing::EaseInOutBack,
Easing::EaseInElastic,
Easing::EaseOutElastic,
Easing::EaseInOutElastic,
Easing::EaseInBounce,
Easing::EaseOutBounce,
Easing::EaseInOutBounce,
Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
Easing::Steps(1),
]
}
}
#[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 - powi(-2.0 * t + 2.0, 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 - powi(1.0 - t, 3)
}
#[inline]
pub fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - powi(-2.0 * t + 2.0, 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 - powi(1.0 - t, 4)
}
#[inline]
pub fn ease_in_out_quart(t: f32) -> f32 {
if t < 0.5 {
8.0 * t * t * t * t
} else {
1.0 - powi(-2.0 * t + 2.0, 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 - powi(1.0 - t, 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 - powi(-2.0 * t + 2.0, 5) / 2.0
}
}
#[inline]
pub fn ease_in_sine(t: f32) -> f32 {
1.0 - cos(t * PI / 2.0)
}
#[inline]
pub fn ease_out_sine(t: f32) -> f32 {
sin(t * PI / 2.0)
}
#[inline]
pub fn ease_in_out_sine(t: f32) -> f32 {
-(cos(t * PI) - 1.0) / 2.0
}
#[inline]
pub fn ease_in_expo(t: f32) -> f32 {
if t == 0.0 {
0.0
} else {
powf(2.0, 10.0 * t - 10.0)
}
}
#[inline]
pub fn ease_out_expo(t: f32) -> f32 {
if t == 1.0 {
1.0
} else {
1.0 - powf(2.0, -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 {
powf(2.0, 20.0 * t - 10.0) / 2.0
} else {
(2.0 - powf(2.0, -20.0 * t + 10.0)) / 2.0
}
}
#[inline]
pub fn ease_in_circ(t: f32) -> f32 {
1.0 - sqrt(1.0 - t * t)
}
#[inline]
pub fn ease_out_circ(t: f32) -> f32 {
sqrt(1.0 - (t - 1.0) * (t - 1.0))
}
#[inline]
pub fn ease_in_out_circ(t: f32) -> f32 {
if t < 0.5 {
(1.0 - sqrt(1.0 - powi(2.0 * t, 2))) / 2.0
} else {
(sqrt(1.0 - powi(-2.0 * t + 2.0, 2)) + 1.0) / 2.0
}
}
const BACK_C1: f32 = 1.701_58;
const BACK_C2: f32 = BACK_C1 * 1.525;
const BACK_C3: f32 = BACK_C1 + 1.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 {
(powi(2.0 * t, 2) * ((BACK_C2 + 1.0) * 2.0 * t - BACK_C2)) / 2.0
} else {
(powi(2.0 * t - 2.0, 2) * ((BACK_C2 + 1.0) * (2.0 * t - 2.0) + BACK_C2) + 2.0) / 2.0
}
}
const ELASTIC_C4: f32 = (2.0 * PI) / 3.0;
const ELASTIC_C5: f32 = (2.0 * PI) / 4.5;
#[inline]
pub fn ease_in_elastic(t: f32) -> f32 {
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
-powf(2.0, 10.0 * t - 10.0) * sin((10.0 * t - 10.75) * ELASTIC_C4)
}
#[inline]
pub fn ease_out_elastic(t: f32) -> f32 {
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
powf(2.0, -10.0 * t) * sin((10.0 * t - 0.75) * ELASTIC_C4) + 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 {
-(powf(2.0, 20.0 * t - 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0
} else {
(powf(2.0, -20.0 * t + 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0 + 1.0
}
}
#[inline]
pub fn ease_out_bounce(t: f32) -> f32 {
const N1: f32 = 7.5625;
const D1: f32 = 2.75;
let t = &mut { t };
if *t < 1.0 / D1 {
N1 * *t * *t
} else if *t < 2.0 / D1 {
*t -= 1.5 / D1;
N1 * *t * *t + 0.75
} else if *t < 2.5 / D1 {
*t -= 2.25 / D1;
N1 * *t * *t + 0.9375
} else {
*t -= 2.625 / D1;
N1 * *t * *t + 0.984_375
}
}
#[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(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t == 0.0 || t == 1.0 {
return t;
}
let x1 = x1.clamp(0.0, 1.0);
let x2 = x2.clamp(0.0, 1.0);
let mut u = t;
for _ in 0..6 {
let x = sample_cubic(x1, x2, u) - t;
if x.abs() < 1e-6 {
return sample_cubic(y1, y2, u);
}
let derivative = sample_cubic_derivative(x1, x2, u);
if derivative.abs() < 1e-6 {
break;
}
u = (u - x / derivative).clamp(0.0, 1.0);
}
let mut low = 0.0;
let mut high = 1.0;
u = t;
for _ in 0..10 {
let x = sample_cubic(x1, x2, u);
if (x - t).abs() < 1e-6 {
break;
}
if x < t {
low = u;
} else {
high = u;
}
u = (low + high) * 0.5;
}
sample_cubic(y1, y2, u)
}
#[inline]
pub fn steps(t: f32, count: u32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t == 0.0 {
return 0.0;
}
let count = count.max(1) as f32;
(ceil(t * count) / count).clamp(0.0, 1.0)
}
#[inline]
fn sample_cubic(a1: f32, a2: f32, t: f32) -> f32 {
let c = 3.0 * a1;
let b = 3.0 * (a2 - a1) - c;
let a = 1.0 - c - b;
((a * t + b) * t + c) * t
}
#[inline]
fn sample_cubic_derivative(a1: f32, a2: f32, t: f32) -> f32 {
let c = 3.0 * a1;
let b = 3.0 * (a2 - a1) - c;
let a = 1.0 - c - b;
(3.0 * a * t + 2.0 * b) * t + c
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn all_named_endpoints() {
for easing in Easing::all_named() {
let v0 = easing.apply(0.0);
let v1 = easing.apply(1.0);
assert!(
approx_eq(v0, 0.0),
"{:?}.apply(0.0) = {} (expected 0.0)",
easing,
v0
);
assert!(
approx_eq(v1, 1.0),
"{:?}.apply(1.0) = {} (expected 1.0)",
easing,
v1
);
}
}
#[test]
fn no_panic_out_of_range() {
for easing in Easing::all_named() {
let _ = easing.apply(-0.5);
let _ = easing.apply(1.5);
let _ = easing.apply(f32::INFINITY);
let _ = easing.apply(f32::NEG_INFINITY);
}
}
#[test]
fn custom_variant() {
let e = Easing::Custom(|t| t * t);
assert_eq!(e.apply(0.5), 0.25);
}
#[test]
fn custom_never_equals() {
let a = Easing::Custom(|t| t);
let b = Easing::Custom(|t| t);
let c = Easing::Linear;
assert!(a != b);
assert!(a != c);
assert!(c != a);
}
#[test]
fn named_equality() {
assert_eq!(Easing::Linear, Easing::Linear);
assert_eq!(Easing::EaseOutCubic, Easing::EaseOutCubic);
assert_eq!(
Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
Easing::CubicBezier(0.25, 0.1, 0.25, 1.0)
);
assert_eq!(Easing::Steps(4), Easing::Steps(4));
assert_ne!(Easing::EaseInQuad, Easing::EaseOutQuad);
assert_ne!(Easing::Steps(3), Easing::Steps(4));
}
#[test]
fn free_functions_match_enum() {
let cases: &[(Easing, fn(f32) -> f32)] = &[
(Easing::EaseInQuad, ease_in_quad),
(Easing::EaseOutQuad, ease_out_quad),
(Easing::EaseInCubic, ease_in_cubic),
(Easing::EaseOutCubic, ease_out_cubic),
(Easing::EaseInOutCubic, ease_in_out_cubic),
(Easing::EaseOutBounce, ease_out_bounce),
(Easing::EaseOutElastic, ease_out_elastic),
(Easing::EaseOutBack, ease_out_back),
];
for t in [0.1, 0.25, 0.5, 0.75, 0.9] {
for (easing, f) in cases {
let a = easing.apply(t);
let b = f(t);
assert!(
approx_eq(a, b),
"{:?} at t={}: enum={} free_fn={}",
easing,
t,
a,
b
);
}
}
}
#[test]
fn ease_out_frontloaded() {
for t in [0.1_f32, 0.3, 0.5, 0.7] {
assert!(
Easing::EaseOutCubic.apply(t) > t,
"EaseOutCubic at t={} should be > t",
t
);
}
}
#[test]
fn linear_is_identity() {
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
assert_eq!(Easing::Linear.apply(t), t);
}
}
#[test]
fn cubic_bezier_linear_control_points_are_identity() {
let easing = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
for t in [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
assert!(
approx_eq(easing.apply(t), t),
"linear cubic-bezier at t={} was {}",
t,
easing.apply(t)
);
}
}
#[test]
fn cubic_bezier_css_ease_shape_is_frontloaded() {
let ease = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
assert_eq!(ease.apply(0.0), 0.0);
assert_eq!(ease.apply(1.0), 1.0);
assert!(ease.apply(0.5) > 0.5);
}
#[test]
fn cubic_bezier_clamps_invalid_x_control_points() {
let invalid = Easing::CubicBezier(-2.0, 0.0, 4.0, 1.0);
let clamped = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
assert!(approx_eq(invalid.apply(0.5), clamped.apply(0.5)));
}
#[test]
fn steps_jump_end_behavior() {
let easing = Easing::Steps(4);
assert_eq!(easing.apply(0.0), 0.0);
assert_eq!(easing.apply(0.01), 0.25);
assert_eq!(easing.apply(0.25), 0.25);
assert_eq!(easing.apply(0.26), 0.5);
assert_eq!(easing.apply(1.0), 1.0);
}
#[test]
fn steps_zero_count_is_one_step() {
assert_eq!(Easing::Steps(0).apply(0.5), 1.0);
}
#[test]
fn all_named_count() {
assert_eq!(Easing::all_named().len(), 33);
}
}