#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Easing {
#[default]
Linear,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseOutElastic,
EaseOutBounce,
}
impl Easing {
#[must_use]
pub fn apply(self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::EaseInQuad => t * t,
Self::EaseOutQuad => libm::fmaf(t, -t, 2.0 * t),
Self::EaseInOutQuad => {
if t < 0.5 {
2.0 * t * t
} else {
let u = libm::fmaf(-2.0, t, 2.0);
1.0 - u * u / 2.0
}
}
Self::EaseInCubic => t * t * t,
Self::EaseOutCubic => {
let u = 1.0 - t;
libm::fmaf(u * u, -u, 1.0)
}
Self::EaseInOutCubic => {
if t < 0.5 {
4.0 * t * t * t
} else {
let u = libm::fmaf(-2.0, t, 2.0);
1.0 - u * u * u / 2.0
}
}
Self::EaseInSine => 1.0 - libm::cosf(t * core::f32::consts::FRAC_PI_2),
Self::EaseOutSine => libm::sinf(t * core::f32::consts::FRAC_PI_2),
Self::EaseInOutSine => -(libm::cosf(core::f32::consts::PI * t) - 1.0) / 2.0,
Self::EaseOutElastic => ease_out_elastic(t),
Self::EaseOutBounce => ease_out_bounce(t),
}
}
}
fn ease_out_elastic(t: f32) -> f32 {
const C4: f32 = 2.0 * core::f32::consts::PI / 3.0;
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
libm::fmaf(
libm::powf(2.0, -10.0 * t),
libm::sinf(libm::fmaf(10.0, t, -0.75) * C4),
1.0,
)
}
fn ease_out_bounce(t: f32) -> f32 {
const N1: f32 = 7.5625;
const D1: f32 = 2.75;
if t < 1.0 / D1 {
N1 * t * t
} else if t < 2.0 / D1 {
let t = t - 1.5 / D1;
libm::fmaf(N1 * t, t, 0.75)
} else if t < 2.5 / D1 {
let t = t - 2.25 / D1;
libm::fmaf(N1 * t, t, 0.9375)
} else {
let t = t - 2.625 / D1;
libm::fmaf(N1 * t, t, 0.984_375)
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
#[test]
fn linear_is_identity() {
assert_eq!(Easing::Linear.apply(0.0), 0.0);
assert_eq!(Easing::Linear.apply(0.5), 0.5);
assert_eq!(Easing::Linear.apply(1.0), 1.0);
}
#[test]
fn every_curve_starts_at_0_and_ends_at_1() {
for easing in [
Easing::Linear,
Easing::EaseInQuad,
Easing::EaseOutQuad,
Easing::EaseInOutQuad,
Easing::EaseInCubic,
Easing::EaseOutCubic,
Easing::EaseInOutCubic,
Easing::EaseInSine,
Easing::EaseOutSine,
Easing::EaseInOutSine,
Easing::EaseOutElastic,
Easing::EaseOutBounce,
] {
assert!(
(easing.apply(0.0) - 0.0).abs() < 1e-5,
"{easing:?} should start at 0"
);
assert!(
(easing.apply(1.0) - 1.0).abs() < 1e-5,
"{easing:?} should end at 1"
);
}
}
#[test]
fn ease_in_quad_starts_slower_than_linear() {
assert!(Easing::EaseInQuad.apply(0.25) < 0.25);
}
#[test]
fn ease_out_quad_starts_faster_than_linear() {
assert!(Easing::EaseOutQuad.apply(0.25) > 0.25);
}
#[test]
fn out_of_range_input_is_clamped() {
assert_eq!(Easing::Linear.apply(-1.0), 0.0);
assert_eq!(Easing::Linear.apply(2.0), 1.0);
}
#[test]
#[allow(clippy::cast_precision_loss)] fn elastic_overshoots_past_the_target() {
let overshoots = (0..100)
.map(|i| Easing::EaseOutElastic.apply(i as f32 / 100.0))
.any(|v| !(0.0..=1.0).contains(&v));
assert!(overshoots);
}
}