use std::f32::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Easing {
Linear,
QuadIn,
QuadOut,
QuadInOut,
CubicIn,
CubicOut,
CubicInOut,
QuartIn,
QuartOut,
QuartInOut,
QuintIn,
QuintOut,
QuintInOut,
SineIn,
SineOut,
SineInOut,
ExpoIn,
ExpoOut,
ExpoInOut,
CircIn,
CircOut,
CircInOut,
BackIn,
BackOut,
BackInOut,
ElasticIn,
ElasticOut,
ElasticInOut,
BounceIn,
BounceOut,
BounceInOut,
}
#[must_use]
pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
t.mul_add(b - a, a)
}
#[must_use]
pub fn clamp01(t: f32) -> f32 {
t.clamp(0.0, 1.0)
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn ease(kind: Easing, t: f32) -> f32 {
let t = clamp01(t);
match kind {
Easing::Linear => t,
Easing::QuadIn => t * t,
Easing::QuadOut => t * (2.0 - t),
Easing::QuadInOut => {
if t < 0.5 {
2.0 * t * t
} else {
(4.0 - 2.0 * t).mul_add(t, -1.0)
}
}
Easing::CubicIn => t * t * t,
Easing::CubicOut => {
let f = t - 1.0;
f * f * f + 1.0
}
Easing::CubicInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
let f = 2.0f32.mul_add(t, -2.0);
0.5f32.mul_add(f * f * f, 1.0)
}
}
Easing::QuartIn => t * t * t * t,
Easing::QuartOut => {
let f = t - 1.0;
1.0 - f * f * f * f
}
Easing::QuartInOut => {
if t < 0.5 {
8.0 * t * t * t * t
} else {
let f = t - 1.0;
(-8.0f32).mul_add(f * f * f * f, 1.0)
}
}
Easing::QuintIn => t * t * t * t * t,
Easing::QuintOut => {
let f = t - 1.0;
f * f * f * f * f + 1.0
}
Easing::QuintInOut => {
if t < 0.5 {
16.0 * t * t * t * t * t
} else {
let f = 2.0f32.mul_add(t, -2.0);
0.5f32.mul_add(f * f * f * f * f, 1.0)
}
}
Easing::SineIn => 1.0 - (t * PI / 2.0).cos(),
Easing::SineOut => (t * PI / 2.0).sin(),
Easing::SineInOut => 0.5 * (1.0 - (PI * t).cos()),
Easing::ExpoIn => {
if t <= 0.0 {
0.0
} else {
(10.0f32 * (t - 1.0)).exp2()
}
}
Easing::ExpoOut => {
if t >= 1.0 {
1.0
} else {
1.0 - (-10.0f32 * t).exp2()
}
}
Easing::ExpoInOut => {
if t <= 0.0 {
0.0
} else if t >= 1.0 {
1.0
} else if t < 0.5 {
0.5 * (20.0f32 * t - 10.0).exp2()
} else {
(-0.5f32).mul_add((-20.0f32 * t + 10.0).exp2(), 1.0)
}
}
Easing::CircIn => 1.0 - (1.0 - t * t).sqrt(),
Easing::CircOut => {
let f = t - 1.0;
(1.0 - f * f).sqrt()
}
Easing::CircInOut => {
if t < 0.5 {
0.5 * (1.0 - (1.0 - 4.0 * t * t).sqrt())
} else {
let f = (-2.0f32).mul_add(t, 2.0);
0.5 * ((1.0 - f * f).sqrt() + 1.0)
}
}
Easing::BackIn => {
const C1: f32 = 1.701_58;
const C3: f32 = C1 + 1.0;
C3.mul_add(t * t * t, -(C1 * t * t))
}
Easing::BackOut => {
const C1: f32 = 1.701_58;
const C3: f32 = C1 + 1.0;
let f = t - 1.0;
C3.mul_add(f * f * f, C1 * f * f) + 1.0
}
Easing::BackInOut => {
const C1: f32 = 1.701_58;
const C2: f32 = C1 * 1.525;
if t < 0.5 {
let f = 2.0 * t;
0.5 * (f * f * (C2.mul_add(f, f) - C2))
} else {
let f = 2.0f32.mul_add(t, -2.0);
0.5 * f.mul_add(f * C2.mul_add(f, f) + C2, 2.0)
}
}
Easing::ElasticIn => {
if t <= 0.0 {
0.0
} else if t >= 1.0 {
1.0
} else {
const C4: f32 = 2.0 * PI / 3.0;
-(10.0f32 * (t - 1.0)).exp2() * ((t - 1.0) * 10.0 - 0.75).mul_add(C4, 0.0).sin()
}
}
Easing::ElasticOut => {
if t <= 0.0 {
0.0
} else if t >= 1.0 {
1.0
} else {
const C4: f32 = 2.0 * PI / 3.0;
(-10.0f32 * t).exp2() * (t * 10.0 - 0.75).mul_add(C4, 0.0).sin() + 1.0
}
}
Easing::ElasticInOut => {
if t <= 0.0 {
0.0
} else if t >= 1.0 {
1.0
} else {
const C5: f32 = 2.0 * PI / 4.5;
let s = (20.0f32 * t - 11.125) * C5;
if t < 0.5 {
-0.5 * (20.0f32 * t - 10.0).exp2() * s.sin()
} else {
0.5f32.mul_add((-20.0f32 * t + 10.0).exp2() * s.sin(), 1.0)
}
}
}
Easing::BounceIn => 1.0 - bounce_out(1.0 - t),
Easing::BounceOut => bounce_out(t),
Easing::BounceInOut => {
if t < 0.5 {
0.5 * (1.0 - bounce_out(1.0 - 2.0 * t))
} else {
0.5f32.mul_add(bounce_out(2.0f32.mul_add(t, -1.0)), 0.5)
}
}
}
}
fn bounce_out(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;
N1.mul_add(t * t, 0.75)
} else if t < 2.5 / D1 {
let t = t - 2.25 / D1;
N1.mul_add(t * t, 0.9375)
} else {
let t = t - 2.625 / D1;
N1.mul_add(t * t, 0.984_375)
}
}
pub const ALL_EASINGS: [Easing; 31] = [
Easing::Linear,
Easing::QuadIn,
Easing::QuadOut,
Easing::QuadInOut,
Easing::CubicIn,
Easing::CubicOut,
Easing::CubicInOut,
Easing::QuartIn,
Easing::QuartOut,
Easing::QuartInOut,
Easing::QuintIn,
Easing::QuintOut,
Easing::QuintInOut,
Easing::SineIn,
Easing::SineOut,
Easing::SineInOut,
Easing::ExpoIn,
Easing::ExpoOut,
Easing::ExpoInOut,
Easing::CircIn,
Easing::CircOut,
Easing::CircInOut,
Easing::BackIn,
Easing::BackOut,
Easing::BackInOut,
Easing::ElasticIn,
Easing::ElasticOut,
Easing::ElasticInOut,
Easing::BounceIn,
Easing::BounceOut,
Easing::BounceInOut,
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endpoints_are_pinned() {
for &e in &ALL_EASINGS {
assert!(ease(e, 0.0).abs() < 1e-3, "{e:?} at 0");
assert!((ease(e, 1.0) - 1.0).abs() < 1e-3, "{e:?} at 1");
}
}
#[test]
fn input_is_clamped() {
assert_eq!(ease(Easing::Linear, -5.0), 0.0);
assert_eq!(ease(Easing::Linear, 5.0), 1.0);
}
#[test]
fn lerp_basics() {
assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
assert_eq!(lerp(10.0, 20.0, 0.0), 10.0);
}
}