use crate::math::easing::eval::easing_value;
use crate::math::easing::integration::integrate_simple;
use crate::math::{Easable, Easing};
pub fn solve_integration_evaluation<T, const N: usize>(
ease: Easing,
from: T,
to: T,
val: T,
) -> Option<T>
where
T: Easable<N>,
{
let from = from.components();
let to = to.components();
let val = val.components();
for i in 0..N {
if from[i] * to[i] < 0.0 {
return None;
}
if !from[i].is_finite() || !to[i].is_finite() || !val[i].is_finite() {
return None;
}
}
Some(T::compose(std::array::from_fn(|i| {
let from = from[i];
let to = to[i];
let val = val[i];
if from < 0.0 || to < 0.0 {
solve_integration(ease, -from, -to, -val)
} else {
solve_integration(ease, from, to, val)
}
})))
}
fn solve_integration(ease: Easing, from: f64, to: f64, val: f64) -> f64 {
let delta = to - from;
let f0 = 0.0;
let i1 = integrate_simple(ease, 1.0);
let f1 = from * (1.0 - i1) + to * i1;
let (lo, hi) = if f0 <= f1 { (f0, f1) } else { (f1, f0) };
if val <= lo {
return 0.0;
}
if val >= hi {
return 1.0;
}
match ease {
Easing::Linear => {
if delta.abs() <= 1e-12 {
if from.abs() <= 1e-12 {
0.0
} else {
(val / from).clamp(0.0, 1.0)
}
} else {
let disc = from * from + 2.0 * delta * val;
let t = (-from + f64::sqrt(disc.max(0.0))) / delta;
t.clamp(0.0, 1.0)
}
}
Easing::InConst => {
if from.abs() <= 1e-12 {
0.0
} else {
(val / from).clamp(0.0, 1.0)
}
}
Easing::OutConst => {
if to.abs() <= 1e-12 {
0.0
} else {
(val / to).clamp(0.0, 1.0)
}
}
Easing::InOutConst => {
let f_at_half = from * 0.5;
if val <= f_at_half {
if from.abs() <= 1e-12 {
0.0
} else {
(val / from).clamp(0.0, 0.5)
}
} else if to.abs() <= 1e-12 {
0.5
} else {
0.5 + ((val - f_at_half) / to).clamp(0.0, 0.5)
}
}
_ => newton_bisect_solve(ease, from, to, val),
}
}
fn newton_bisect_solve(ease: Easing, from: f64, to: f64, target: f64) -> f64 {
let f = |t: f64| {
let i = integrate_simple(ease, t);
from * (t - i) + to * i
};
let fp = |t: f64| {
let e = easing_value(ease.as_easing(), t);
from * (1.0 - e) + to * e
};
let mut lo = 0.0_f64;
let mut hi = 1.0_f64;
for _ in 0..24 {
if hi - lo <= f64::EPSILON * 4.0 {
break;
}
let mid = 0.5 * (lo + hi);
let fm = f(mid);
let d = fp(mid);
let probe = if d.abs() > 1e-10 {
let t_n = mid - (fm - target) / d;
if t_n > lo && t_n < hi { t_n } else { mid }
} else {
mid
};
let pv = if probe == mid { fm } else { f(probe) };
if pv >= target {
hi = probe;
} else {
lo = probe;
}
}
hi
}
#[cfg(test)]
mod tests {
use crate::math::easing::integration;
use super::*;
const FROM: f64 = 0.0;
const TO: f64 = 100.0;
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_cumulate_then_solve() {
let mut failures: Vec<(Easing, f64, f64)> = Vec::new();
for &ease in IN_SCOPE_EASINGS {
for &t in SAMPLE_TS {
let val = integration::evaluate_simple(ease, FROM, TO, t);
let solved = solve_integration_evaluation(ease, FROM, TO, val).unwrap();
if !approx_eq(solved, t, 1e-3) {
failures.push((ease, t, solved));
}
}
}
assert!(
failures.is_empty(),
"solve(cumulate(t)) != t: {}",
failures
.iter()
.map(|(e, t, s)| format!("{e:?}: t={t}, solved={s}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn round_trip_solve_then_cumulate() {
let mut failures: Vec<(Easing, f64, f64)> = Vec::new();
for &ease in IN_SCOPE_EASINGS {
for &t in SAMPLE_TS {
let v = integration::evaluate_simple(ease, FROM, TO, t);
let back = integration::evaluate_simple(
ease,
FROM,
TO,
solve_integration_evaluation(ease, FROM, TO, v).unwrap(),
);
if !approx_eq(back, v, 1e-2) {
failures.push((ease, v, back));
}
}
}
assert!(
failures.is_empty(),
"cumulate(solve(V)) != V: {}",
failures
.iter()
.map(|(e, v, b)| format!("{e:?}: V={v}, back={b}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn solve_integration_endpoints() {
for &ease in IN_SCOPE_EASINGS {
assert!(
approx_eq(
solve_integration_evaluation(ease, FROM, TO, 0.0).unwrap(),
0.0,
1e-6
),
"{ease:?}: solve(V=0) should be 0"
);
let v1 = integration::evaluate_simple(ease, FROM, TO, 1.0);
assert!(
approx_eq(
solve_integration_evaluation(ease, FROM, TO, v1).unwrap(),
1.0,
1e-3
),
"{ease:?}: solve(V=F(1)) should be 1"
);
}
}
#[test]
fn solve_integration_clamps_out_of_range() {
for &ease in IN_SCOPE_EASINGS {
assert!(
approx_eq(
solve_integration_evaluation(ease, FROM, TO, -10.0).unwrap(),
0.0,
1e-6
),
"{ease:?}: solve(V<0) should clamp to 0"
);
let v1 = integration::evaluate_simple(ease, FROM, TO, 1.0);
assert!(
approx_eq(
solve_integration_evaluation(ease, FROM, TO, v1 + 10.0).unwrap(),
1.0,
1e-6
),
"{ease:?}: solve(V>F(1)) should clamp to 1"
);
}
}
#[test]
fn sign_flip_endpoints_return_none() {
for &ease in IN_SCOPE_EASINGS {
assert!(
solve_integration_evaluation(ease, -1.0, 10.0, 5.0).is_none(),
"{ease:?}: from<0, to>0 should return None"
);
assert!(
solve_integration_evaluation(ease, 10.0, -1.0, 5.0).is_none(),
"{ease:?}: from>0, to<0 should return None"
);
}
}
#[test]
fn negative_same_sign_endpoints_solve() {
let r = solve_integration_evaluation(Easing::Linear, -100.0, 0.0, -37.5).unwrap();
assert!(
approx_eq(r, 0.5, 1e-4),
"Linear with negative endpoints: expected t=0.5, got {r}"
);
}
#[test]
fn from_equals_to_returns_lower_bound() {
let c: f64 = 50.0;
for &ease in IN_SCOPE_EASINGS {
let r = solve_integration_evaluation(ease, c, c, 25.0).unwrap();
assert!(
approx_eq(r, 0.5, 1e-6),
"{ease:?}: from==to=c, V=c/2 should give t=0.5, got {r}"
);
}
}
#[test]
fn linear_matches_closed_form() {
for &v in &[2.0_f64, 12.5, 25.0, 40.0, 50.0] {
let t = solve_integration_evaluation(Easing::Linear, 0.0, 100.0, v).unwrap();
let expected = f64::sqrt(v / 50.0);
assert!(
approx_eq(t, expected, 1e-5),
"Linear: V={v}, expected t={expected}, got {t}"
);
}
}
}