use std::f64::consts::PI;
use super::{Easable, Easing, split_and_calc};
pub fn solve_easing_evaluation<T, const N: usize>(ease: Easing, from: T, to: T, val: T) -> T
where
T: Easable<N>,
{
split_and_calc::<T, N>(from, to, val, |from, to, val| {
if !from.is_finite() || !to.is_finite() || !val.is_finite() {
return f64::NAN;
}
if val < from {
0.0
} else if val > to {
1.0
} else if from == to {
0.0
} else {
let p = (val - from) / (to - from);
solve(ease, p)
}
})
}
fn solve(ease: Easing, p: f64) -> f64 {
let p = p.clamp(0.0, 1.0);
match ease {
Easing::Linear => p,
Easing::InConst => {
if p > 0.0 {
1.0
} else {
0.0
}
}
Easing::OutConst => 0.0,
Easing::InOutConst => {
if p > 0.0 {
0.5
} else {
0.0
}
}
Easing::InSine => 2.0 * f64::acos(1.0 - p) / PI,
Easing::OutSine => 2.0 * f64::asin(p) / PI,
Easing::InOutSine => f64::acos(1.0 - 2.0 * p) / PI,
Easing::InQuad => f64::sqrt(p),
Easing::OutQuad => 1.0 - f64::sqrt(1.0 - p),
Easing::InOutQuad => {
if p < 0.5 {
f64::sqrt(p / 2.0)
} else {
1.0 - f64::sqrt((1.0 - p) / 2.0)
}
}
Easing::InCubic => f64::cbrt(p),
Easing::OutCubic => 1.0 - f64::cbrt(1.0 - p),
Easing::InOutCubic => {
if p < 0.5 {
f64::cbrt(p / 4.0)
} else {
1.0 - f64::cbrt((1.0 - p) / 4.0)
}
}
Easing::InQuart => f64::powf(p, 0.25),
Easing::OutQuart => 1.0 - f64::powf(1.0 - p, 0.25),
Easing::InOutQuart => {
if p < 0.5 {
f64::powf(p / 8.0, 0.25)
} else {
1.0 - f64::powf((1.0 - p) / 8.0, 0.25)
}
}
Easing::InQuint => f64::powf(p, 0.2),
Easing::OutQuint => 1.0 - f64::powf(1.0 - p, 0.2),
Easing::InOutQuint => {
if p < 0.5 {
f64::powf(p / 16.0, 0.2)
} else {
1.0 - f64::powf((1.0 - p) / 16.0, 0.2)
}
}
Easing::InExpo => {
if p <= 0.0 {
0.0
} else {
(f64::log2(p) + 10.0) / 10.0
}
}
Easing::OutExpo => {
if p >= 1.0 {
1.0
} else {
-f64::log2(1.0 - p) / 10.0
}
}
Easing::InOutExpo => {
if p <= 0.0 {
0.0
} else if p >= 1.0 {
1.0
} else if p < 0.5 {
(f64::log2(p) + 11.0) / 20.0
} else {
(9.0 - f64::log2(1.0 - p)) / 20.0
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::easing::eval::easing_value;
const SAMPLE_TS: &[f64] = &[0.1, 0.25, 0.5, 0.75, 0.9];
const IN_SCOPE_EASINGS: &[Easing] = &[
Easing::Linear,
Easing::InSine,
Easing::OutSine,
Easing::InOutSine,
Easing::InQuad,
Easing::OutQuad,
Easing::InOutQuad,
Easing::InCubic,
Easing::OutCubic,
Easing::InOutCubic,
Easing::InQuart,
Easing::OutQuart,
Easing::InOutQuart,
Easing::InQuint,
Easing::OutQuint,
Easing::InOutQuint,
Easing::InExpo,
Easing::OutExpo,
Easing::InOutExpo,
];
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn round_trip_solve_then_ease() {
let mut failures: Vec<(Easing, f64, f64, f64)> = Vec::new();
for &ease in IN_SCOPE_EASINGS {
for &t in SAMPLE_TS {
let p = easing_value(ease.as_easing(), t);
let solved = solve(ease, p);
if !approx_eq(solved, t, 1e-4) {
failures.push((ease, t, p, solved));
}
}
}
assert!(
failures.is_empty(),
"solve(ease, e(t)) != t: {}",
failures
.iter()
.map(|(e, t, p, s)| format!("{e:?}: t={t}, e(t)={p}, solve={s}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn round_trip_ease_then_solve() {
let mut failures: Vec<(Easing, f64, f64)> = Vec::new();
for &ease in IN_SCOPE_EASINGS {
for &p in SAMPLE_TS {
let t = solve(ease, p);
let back = easing_value(ease.as_easing(), t);
if !approx_eq(back, p, 1e-4) {
failures.push((ease, p, back));
}
}
}
assert!(
failures.is_empty(),
"e(solve(ease, p)) != p: {}",
failures
.iter()
.map(|(e, p, b)| format!("{e:?}: p={p}, e(solve)={b}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn solve_endpoints() {
for &ease in IN_SCOPE_EASINGS {
assert!(
approx_eq(solve(ease, 0.0), 0.0, 1e-6),
"{ease:?}: solve(0) should be 0"
);
assert!(
approx_eq(solve(ease, 1.0), 1.0, 1e-6),
"{ease:?}: solve(1) should be 1"
);
}
}
#[test]
fn solve_clamps_out_of_range() {
for &ease in IN_SCOPE_EASINGS {
assert!(
approx_eq(solve(ease, -1.0), 0.0, 1e-6),
"{ease:?}: solve(-1) should clamp to 0"
);
assert!(
approx_eq(solve(ease, 2.0), 1.0, 1e-6),
"{ease:?}: solve(2) should clamp to 1"
);
}
}
#[test]
fn solve_easing_evaluation_endpoints() {
let from: f64 = 10.0;
let to: f64 = 90.0;
for &ease in IN_SCOPE_EASINGS {
assert!(
approx_eq(solve_easing_evaluation(ease, from, to, from), 0.0, 1e-6),
"{ease:?}: solve(val=from) should be 0"
);
assert!(
approx_eq(solve_easing_evaluation(ease, from, to, to), 1.0, 1e-6),
"{ease:?}: solve(val=to) should be 1"
);
assert!(
approx_eq(
solve_easing_evaluation(ease, from, to, from - 5.0),
0.0,
1e-6
),
"{ease:?}: solve(val<from) should be 0"
);
assert!(
approx_eq(solve_easing_evaluation(ease, from, to, to + 5.0), 1.0, 1e-6),
"{ease:?}: solve(val>to) should be 1"
);
}
}
#[test]
fn solve_easing_evaluation_round_trip() {
let from: f64 = 10.0;
let to: f64 = 90.0;
let mut failures: Vec<(Easing, f64, f64)> = Vec::new();
for &ease in IN_SCOPE_EASINGS {
for &t in SAMPLE_TS {
let val = from + (to - from) * easing_value(ease.as_easing(), t);
let solved = solve_easing_evaluation(ease, from, to, val);
if !approx_eq(solved, t, 1e-4) {
failures.push((ease, t, solved));
}
}
}
assert!(
failures.is_empty(),
"solve_easing_evaluation round-trip failed: {}",
failures
.iter()
.map(|(e, t, s)| format!("{e:?}: t={t}, solved={s}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn solve_easing_evaluation_from_equals_to_returns_zero() {
let c: f64 = 50.0;
for &ease in IN_SCOPE_EASINGS {
assert!(
approx_eq(solve_easing_evaluation(ease, c, c, c), 0.0, 1e-6),
"{ease:?}: from==to should return 0"
);
}
}
}