use std::f64::consts::{LN_2, PI};
use super::{AdvancedEasing, Easable, split_and_calc};
const C1: f64 = 1.70158;
const C2: f64 = C1 * 1.525;
const C3: f64 = C1 + 1.0;
const C4: f64 = 2.0 * PI / 3.0;
const C5: f64 = 2.0 * PI / 4.5;
pub fn evaluate_derivative<T, const N: usize>(ease: AdvancedEasing, from: T, to: T, t: T) -> T
where
T: Easable<N>,
{
split_and_calc::<T, N>(from, to, t, |from, to, t| {
if !(0.0..=1.0).contains(&t) {
0.0
} else {
let d = easing_derivative(ease, t);
to * d - from * d
}
})
}
fn easing_derivative(ease: AdvancedEasing, t: f64) -> f64 {
match ease {
AdvancedEasing::Linear => 1.0,
AdvancedEasing::InConst | AdvancedEasing::OutConst | AdvancedEasing::InOutConst => 0.0,
AdvancedEasing::InSine => (PI / 2.0) * f64::sin(t * PI / 2.0),
AdvancedEasing::OutSine => (PI / 2.0) * f64::cos(t * PI / 2.0),
AdvancedEasing::InOutSine => (PI / 2.0) * f64::sin(t * PI),
AdvancedEasing::InQuad => 2.0 * t,
AdvancedEasing::OutQuad => 2.0 * (1.0 - t),
AdvancedEasing::InOutQuad => {
if t < 0.5 {
4.0 * t
} else {
4.0 * (1.0 - t)
}
}
AdvancedEasing::InCubic => 3.0 * t * t,
AdvancedEasing::OutCubic => 3.0 * (1.0 - t) * (1.0 - t),
AdvancedEasing::InOutCubic => {
if t < 0.5 {
12.0 * t * t
} else {
12.0 * (1.0 - t) * (1.0 - t)
}
}
AdvancedEasing::InQuart => 4.0 * t * t * t,
AdvancedEasing::OutQuart => 4.0 * (1.0 - t) * (1.0 - t) * (1.0 - t),
AdvancedEasing::InOutQuart => {
if t < 0.5 {
32.0 * t * t * t
} else {
32.0 * (1.0 - t) * (1.0 - t) * (1.0 - t)
}
}
AdvancedEasing::InQuint => 5.0 * t * t * t * t,
AdvancedEasing::OutQuint => 5.0 * (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t),
AdvancedEasing::InOutQuint => {
if t < 0.5 {
80.0 * t * t * t * t
} else {
80.0 * (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t)
}
}
AdvancedEasing::InExpo => {
if t == 0.0 {
0.0
} else {
10.0 * LN_2 * f64::powf(2.0, 10.0 * t - 10.0)
}
}
AdvancedEasing::OutExpo => {
if t == 1.0 {
0.0
} else {
10.0 * LN_2 * f64::powf(2.0, -10.0 * t)
}
}
AdvancedEasing::InOutExpo => {
if t == 0.0 || t == 1.0 {
0.0
} else if t < 0.5 {
10.0 * LN_2 * f64::powf(2.0, 20.0 * t - 10.0)
} else {
10.0 * LN_2 * f64::powf(2.0, -20.0 * t + 10.0)
}
}
AdvancedEasing::InCirc => t / f64::sqrt(1.0 - t * t),
AdvancedEasing::OutCirc => (1.0 - t) / f64::sqrt(1.0 - (t - 1.0) * (t - 1.0)),
AdvancedEasing::InOutCirc => {
if t < 0.5 {
2.0 * t / f64::sqrt(1.0 - 4.0 * t * t)
} else {
2.0 * (1.0 - t) / f64::sqrt(1.0 - f64::powi(-2.0 * t + 2.0, 2))
}
}
AdvancedEasing::InBack => 3.0 * C3 * t * t - 2.0 * C1 * t,
AdvancedEasing::OutBack => 3.0 * C3 * (t - 1.0) * (t - 1.0) + 2.0 * C1 * (t - 1.0),
AdvancedEasing::InOutBack => {
if t < 0.5 {
12.0 * (C2 + 1.0) * t * t - 4.0 * C2 * t
} else {
3.0 * (C2 + 1.0) * f64::powi(2.0 * t - 2.0, 2) + 2.0 * C2 * (2.0 * t - 2.0)
}
}
AdvancedEasing::InElastic => {
if t == 0.0 || t == 1.0 {
0.0
} else {
let arg = (t * 10.0 - 10.75) * C4;
f64::powf(2.0, 10.0 * t - 10.0)
* (10.0 * LN_2 * f64::sin(arg) + 10.0 * C4 * f64::cos(arg))
}
}
AdvancedEasing::OutElastic => {
if t == 0.0 || t == 1.0 {
0.0
} else {
let arg = (t * 10.0 - 0.75) * C4;
f64::powf(2.0, -10.0 * t)
* (-10.0 * LN_2 * f64::sin(arg) + 10.0 * C4 * f64::cos(arg))
}
}
AdvancedEasing::InOutElastic => {
if t == 0.0 || t == 1.0 {
0.0
} else if t < 0.5 {
let arg = (20.0 * t - 11.125) * C5;
-f64::powf(2.0, 20.0 * t - 10.0)
* (10.0 * LN_2 * f64::sin(arg) + 10.0 * C5 * f64::cos(arg))
} else {
let arg = (20.0 * t - 11.125) * C5;
f64::powf(2.0, -20.0 * t + 10.0)
* (-10.0 * LN_2 * f64::sin(arg) + 10.0 * C5 * f64::cos(arg))
}
}
AdvancedEasing::InBounce => ease_out_bounce_derivative(1.0 - t),
AdvancedEasing::OutBounce => ease_out_bounce_derivative(t),
AdvancedEasing::InOutBounce => {
if t < 0.5 {
ease_out_bounce_derivative(1.0 - 2.0 * t)
} else {
ease_out_bounce_derivative(2.0 * t - 1.0)
}
}
}
}
fn ease_out_bounce_derivative(x: f64) -> f64 {
let n1 = 7.5625;
let d1 = 2.75;
if x < 1.0 / d1 {
2.0 * n1 * x
} else if x < 2.0 / d1 {
2.0 * n1 * (x - 1.5 / d1)
} else if x < 2.5 / d1 {
2.0 * n1 * (x - 2.25 / d1)
} else {
2.0 * n1 * (x - 2.625 / d1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::easing::eval::evaluate_easing;
const FROM: f64 = 0.0;
const TO: f64 = 100.0;
const H: f64 = 1e-4;
const SAMPLE_TS: &[f64] = &[0.123, 0.37, 0.62, 0.88];
fn numerical_derivative(ease: AdvancedEasing, t: f64) -> f64 {
let vp = evaluate_easing::<f64, 1>(ease, FROM, TO, t + H);
let vm = evaluate_easing::<f64, 1>(ease, FROM, TO, t - H);
(vp - vm) / (2.0 * H)
}
fn matches(analytical: f64, numerical: f64) -> bool {
let abs = (analytical - numerical).abs();
abs <= 0.05 || abs <= 0.01 * analytical.abs()
}
#[test]
fn derivative_matches_finite_difference() {
let mut failures: Vec<(AdvancedEasing, f64, f64, f64)> = Vec::new();
for &ease in AdvancedEasing::ALL {
if matches!(
ease,
AdvancedEasing::InConst | AdvancedEasing::OutConst | AdvancedEasing::InOutConst
) {
continue;
}
for &t in SAMPLE_TS {
let analytical = evaluate_derivative::<f64, 1>(ease, FROM, TO, t);
let numerical = numerical_derivative(ease, t);
if !matches(analytical, numerical) {
failures.push((ease, t, analytical, numerical));
}
}
}
assert!(
failures.is_empty(),
"analytical derivative did not match finite-difference: {}",
failures
.iter()
.map(|(e, t, a, n)| format!("{e:?}@t={t}: analytical={a}, numerical={n}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn linear_velocity_is_constant_delta() {
for t in [0.0_f64, 0.25, 0.5, 0.75, 1.0] {
let v = evaluate_derivative::<f64, 1>(AdvancedEasing::Linear, FROM, TO, t);
assert!(
(v - (TO - FROM)).abs() <= 1e-4,
"Linear velocity at t={t} should be {}, got {v}",
TO - FROM
);
}
}
}