use std::f32::consts::PI;
pub type EasingFn = fn(f32) -> f32;
pub fn linear(t: f32) -> f32 {
t.clamp(0.0, 1.0)
}
pub fn ease_in_quad(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t
}
pub fn ease_out_quad(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * (2.0 - t)
}
pub fn ease_in_out_cubic(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - ((-2.0 * t + 2.0).powi(3) / 2.0)
}
}
pub fn ease_in_out_sine(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
-(f32::cos(PI * t) - 1.0) / 2.0
}
pub fn ease_out_elastic(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
let c4 = (2.0 * PI) / 3.0;
f32::powf(2.0, -10.0 * t) * f32::sin((t * 10.0 - 0.75) * c4) + 1.0
}
const STANDARD_BACK_TENSION: f32 = 1.701_58;
pub const STANDARD_BACK_OVERSHOOT_PERMILLE: u16 = 100;
pub const MAX_BACK_OVERSHOOT_PERMILLE: u16 = 500;
pub fn ease_out_back(t: f32, overshoot_permille: u16) -> f32 {
let t = t.clamp(0.0, 1.0);
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
let c1 = back_tension(overshoot_permille);
let u = t - 1.0;
1.0 + (c1 + 1.0) * u * u * u + c1 * u * u
}
fn back_tension(overshoot_permille: u16) -> f32 {
let target = f32::from(overshoot_permille.min(MAX_BACK_OVERSHOOT_PERMILLE)) / 1000.0;
if target <= 0.0 {
return 0.0;
}
let standard = f32::from(STANDARD_BACK_OVERSHOOT_PERMILLE) / 1000.0;
let mut c1 = STANDARD_BACK_TENSION * (target / standard).sqrt();
for _ in 0..4 {
let value = 4.0 * c1 * c1 * c1 - 27.0 * target * (c1 + 1.0) * (c1 + 1.0);
let slope = 12.0 * c1 * c1 - 54.0 * target * (c1 + 1.0);
if slope.abs() <= f32::EPSILON {
break;
}
c1 -= value / slope;
}
c1.max(0.0)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Easing {
Linear,
EaseInQuad,
EaseOutQuad,
EaseInOutCubic,
EaseInOutSine,
EaseOutElastic,
EaseOutBack {
overshoot_permille: u16,
},
}
impl Easing {
pub const EASE_OUT_BACK: Self = Self::EaseOutBack {
overshoot_permille: STANDARD_BACK_OVERSHOOT_PERMILLE,
};
pub fn apply(self, t: f32) -> f32 {
match self {
Self::Linear => linear(t),
Self::EaseInQuad => ease_in_quad(t),
Self::EaseOutQuad => ease_out_quad(t),
Self::EaseInOutCubic => ease_in_out_cubic(t),
Self::EaseInOutSine => ease_in_out_sine(t),
Self::EaseOutElastic => ease_out_elastic(t),
Self::EaseOutBack { overshoot_permille } => ease_out_back(t, overshoot_permille),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easing_curves_are_clamped_for_out_of_range_inputs() {
let curves: [EasingFn; 6] = [
linear,
ease_in_quad,
ease_out_quad,
ease_in_out_cubic,
ease_in_out_sine,
ease_out_elastic,
];
for curve in curves {
let below = curve(-0.5);
let above = curve(1.5);
assert!((0.0..=1.0).contains(&below));
assert!((0.0..=1.0).contains(&above));
}
for permille in [0, STANDARD_BACK_OVERSHOOT_PERMILLE, 1_000] {
assert!((0.0..=1.0).contains(&ease_out_back(-0.5, permille)));
assert!((0.0..=1.0).contains(&ease_out_back(1.5, permille)));
}
}
#[test]
fn monotonic_curves_are_non_decreasing() {
let curves: [EasingFn; 5] = [
linear,
ease_in_quad,
ease_out_quad,
ease_in_out_cubic,
ease_in_out_sine,
];
for curve in curves {
let mut prev = curve(0.0);
for step in 1..=200 {
let t = step as f32 / 200.0;
let current = curve(t);
assert!(current + 1e-6 >= prev);
prev = current;
}
}
}
#[test]
fn ease_out_elastic_hits_expected_endpoints_and_overshoots() {
assert_eq!(ease_out_elastic(0.0), 0.0);
assert_eq!(ease_out_elastic(1.0), 1.0);
let peak = (1..=200)
.map(|step| ease_out_elastic(step as f32 / 200.0))
.fold(f32::MIN, f32::max);
assert!(
peak > 1.0,
"ease_out_elastic peak should overshoot, got {peak}"
);
}
fn back_samples(overshoot_permille: u16) -> Vec<f32> {
(0..=1000)
.map(|step| ease_out_back(step as f32 / 1000.0, overshoot_permille))
.collect()
}
#[test]
fn ease_out_back_hits_expected_endpoints_and_overshoots_once() {
let standard = STANDARD_BACK_OVERSHOOT_PERMILLE;
assert_eq!(ease_out_back(0.0, standard), 0.0);
assert_eq!(ease_out_back(1.0, standard), 1.0);
let samples = back_samples(standard);
let peak = samples.iter().copied().fold(f32::MIN, f32::max);
assert!(
(1.09..1.11).contains(&peak),
"the standard curve should peak near 1.10, got {peak}"
);
let crossings = samples
.windows(2)
.filter(|pair| (pair[0] < 1.0) != (pair[1] < 1.0))
.count();
assert_eq!(
crossings, 1,
"expected one crossing of 1.0, got {crossings}"
);
}
#[test]
fn ease_out_back_delivers_the_overshoot_it_is_asked_for() {
for permille in [10, 25, 50, STANDARD_BACK_OVERSHOOT_PERMILLE, 250] {
let peak = back_samples(permille)
.iter()
.copied()
.fold(f32::MIN, f32::max);
let wanted = 1.0 + f32::from(permille) / 1000.0;
assert!(
(peak - wanted).abs() < 1e-3,
"{permille}permille should peak at {wanted}, got {peak}"
);
}
}
#[test]
fn ease_out_back_without_overshoot_is_a_plain_ease_out() {
let samples = back_samples(0);
assert_eq!(samples[0], 0.0);
assert_eq!(samples[samples.len() - 1], 1.0);
assert!(
samples.iter().all(|value| *value <= 1.0 + 1e-6),
"a zero amplitude must not overshoot at all"
);
for window in samples.windows(2) {
assert!(
window[1] + 1e-6 >= window[0],
"a zero amplitude should rise monotonically"
);
}
}
#[test]
fn ease_out_back_is_clamped_to_a_settling_amplitude() {
let capped = back_samples(MAX_BACK_OVERSHOOT_PERMILLE)
.iter()
.copied()
.fold(f32::MIN, f32::max);
let beyond = back_samples(u16::MAX)
.iter()
.copied()
.fold(f32::MIN, f32::max);
assert!((capped - beyond).abs() < 1e-6);
}
#[test]
fn ease_out_back_rises_to_its_peak_then_settles() {
for permille in [25, STANDARD_BACK_OVERSHOOT_PERMILLE, 250] {
let samples = back_samples(permille);
let peak_index = samples
.iter()
.enumerate()
.max_by(|a, b| a.1.total_cmp(b.1))
.map(|(index, _)| index)
.expect("samples are non-empty");
for window in samples[..=peak_index].windows(2) {
assert!(
window[1] + 1e-6 >= window[0],
"{permille}permille should rise up to its peak"
);
}
for window in samples[peak_index..].windows(2) {
assert!(
window[1] <= window[0] + 1e-6,
"{permille}permille should settle back down after its peak"
);
}
}
}
}