use crate::math::easing::eval::easing_value;
use crate::math::{AdvancedEasing, Easable};
pub fn evaluate_integration_reciprocal<T, const N: usize>(
ease: AdvancedEasing,
from: T,
to: T,
t: T,
samples: usize,
) -> Option<T>
where
T: Easable<N>,
{
let from = from.components();
let to = to.components();
let t = t.components();
let mut out = [0.0f64; N];
for i in 0..N {
out[i] = integrate_reciprocal_scalar(ease, from[i], to[i], t[i], samples)?;
}
Some(T::compose(out))
}
fn integrate_reciprocal_scalar(
ease: AdvancedEasing,
from: f64,
to: f64,
t: f64,
samples: usize,
) -> Option<f64> {
if !t.is_finite() || !from.is_finite() || !to.is_finite() {
return None;
}
if t < 0.0 {
div_checked(t, from)
} else if t > 1.0 {
let inner = integrate_reciprocal_in_range(ease, from, to, 1.0, samples)?;
let tail = div_checked(t - 1.0, to)?;
Some(inner + tail)
} else {
integrate_reciprocal_in_range(ease, from, to, t, samples)
}
}
fn integrate_reciprocal_in_range(
ease: AdvancedEasing,
from: f64,
to: f64,
t: f64,
samples: usize,
) -> Option<f64> {
if t <= 0.0 {
return Some(0.0);
}
match ease {
AdvancedEasing::Linear => {
if from == to {
div_checked(t, from)
} else if from * to <= 0.0 {
None
} else {
let denom_t = from * (1.0 - t) + to * t;
Some(f64::ln(denom_t / from) / (to - from))
}
}
AdvancedEasing::InConst => {
div_checked(t, from)
}
AdvancedEasing::OutConst => {
div_checked(t, to)
}
AdvancedEasing::InOutConst => {
if t <= 0.5 {
div_checked(t, from)
} else {
let first = div_checked(0.5, from)?;
let second = div_checked(t - 0.5, to)?;
Some(first + second)
}
}
_ => {
if has_pole(ease, from, to) {
None
} else {
Some(simpson_reciprocal(ease, from, to, t, samples))
}
}
}
}
#[allow(unused_variables)]
pub fn overshoots(ease: AdvancedEasing) -> bool {
{
matches!(
ease,
AdvancedEasing::InBack
| AdvancedEasing::OutBack
| AdvancedEasing::InOutBack
| AdvancedEasing::InElastic
| AdvancedEasing::OutElastic
| AdvancedEasing::InOutElastic
)
}
}
pub fn overshoot_bounds(ease: AdvancedEasing) -> (f64, f64) {
match ease {
AdvancedEasing::InBack => (-0.100005, 1.000001),
AdvancedEasing::OutBack => (-0.000001, 1.100005),
AdvancedEasing::InOutBack => (-0.100152, 1.100152),
AdvancedEasing::InElastic => (-1.000000, 1.000001),
AdvancedEasing::OutElastic => (-0.000001, 1.373099),
AdvancedEasing::InOutElastic => (-0.118349, 1.118349),
_ => (0.0, 1.0),
}
}
fn has_pole(ease: AdvancedEasing, from: f64, to: f64) -> bool {
if from * to <= 0.0 {
return true;
}
if !overshoots(ease) {
return false;
}
let (e_min, e_max) = overshoot_bounds(ease);
let e_crit = from / (from - to);
e_crit >= e_min && e_crit <= e_max
}
fn simpson_reciprocal(ease: AdvancedEasing, from: f64, to: f64, t: f64, samples: usize) -> f64 {
let denom = |s: f64| from * (1.0 - easing_value(ease, s)) + to * easing_value(ease, s);
let f = |s: f64| 1.0 / denom(s);
let n = if samples < 2 {
2
} else if samples % 2 == 1 {
samples + 1
} else {
samples
};
let h = t / n as f64;
let mut sum = f(0.0) + f(t);
for k in 1..n {
let s = k as f64 * h;
let w = if k % 2 == 0 { 2.0 } else { 4.0 };
sum += w * f(s);
}
sum * h / 3.0
}
#[inline]
fn div_checked(a: f64, b: f64) -> Option<f64> {
if b.abs() <= f64::EPSILON {
None
} else {
Some(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::easing::eval::evaluate_easing;
const FROM: f64 = 1.0;
const TO: f64 = 100.0;
const SAMPLES: usize = 2000;
const H: f64 = 1e-4;
const SAMPLE_TS: &[f64] = &[0.1, 0.25, 0.5, 0.75, 0.9];
const CLOSED_FORM_EASINGS: &[AdvancedEasing] = &[
AdvancedEasing::Linear,
AdvancedEasing::InConst,
AdvancedEasing::OutConst,
AdvancedEasing::InOutConst,
];
const WELL_BEHAVED_EASINGS: &[AdvancedEasing] = &[
AdvancedEasing::Linear,
AdvancedEasing::InSine,
AdvancedEasing::OutSine,
AdvancedEasing::InOutSine,
AdvancedEasing::InQuad,
AdvancedEasing::OutQuad,
AdvancedEasing::InOutQuad,
AdvancedEasing::InCubic,
AdvancedEasing::OutCubic,
AdvancedEasing::InOutCubic,
AdvancedEasing::InQuart,
AdvancedEasing::OutQuart,
AdvancedEasing::InOutQuart,
AdvancedEasing::InQuint,
AdvancedEasing::OutQuint,
AdvancedEasing::InOutQuint,
AdvancedEasing::InExpo,
AdvancedEasing::OutExpo,
AdvancedEasing::InOutExpo,
AdvancedEasing::InCirc,
AdvancedEasing::OutCirc,
AdvancedEasing::InOutCirc,
];
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn t0_returns_zero() {
for &ease in AdvancedEasing::ALL {
let result = evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, 0.0, SAMPLES);
assert!(
approx_eq(result.unwrap(), 0.0, 1e-6),
"{ease:?}: expected reciprocal(0)=0, got {result:?}"
);
}
}
#[test]
fn negative_t_returns_t_over_from() {
for &ease in AdvancedEasing::ALL {
let t = -0.3;
let result = evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, t, SAMPLES);
assert!(
approx_eq(result.unwrap(), t / FROM, 1e-6),
"{ease:?}: expected t/from for t<0, got {result:?}"
);
}
}
#[test]
fn t_above_1_extends_with_to() {
for &ease in WELL_BEHAVED_EASINGS {
let t = 1.4;
let inner =
evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, 1.0, SAMPLES).unwrap();
let result =
evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, t, SAMPLES).unwrap();
assert!(
approx_eq(result, inner + (t - 1.0) / TO, 1e-5),
"{ease:?}: t>1 should be inner + (t-1)/to, got {result}"
);
}
}
#[test]
fn derivative_matches_reciprocal_of_ease() {
let mut failures: Vec<(AdvancedEasing, f64, f64, f64)> = Vec::new();
for &ease in WELL_BEHAVED_EASINGS {
if matches!(
ease,
AdvancedEasing::InConst | AdvancedEasing::OutConst | AdvancedEasing::InOutConst
) {
continue;
}
for &t in SAMPLE_TS {
let vp = evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, t + H, SAMPLES)
.unwrap();
let vm = evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, t - H, SAMPLES)
.unwrap();
let numerical = (vp - vm) / (2.0 * H);
let expected = 1.0 / evaluate_easing::<f64, 1>(ease, FROM, TO, t);
if !approx_eq(numerical, expected, 1e-2) {
failures.push((ease, t, numerical, expected));
}
}
}
assert!(
failures.is_empty(),
"d/dt reciprocal did not match 1/ease: {}",
failures
.iter()
.map(|(e, t, n, v)| format!("{e:?}@t={t}: d/dt={n}, expected={v}"))
.collect::<Vec<_>>()
.join("; ")
);
}
#[test]
fn linear_matches_closed_form() {
let delta = TO - FROM;
for &t in SAMPLE_TS {
let result = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::Linear,
FROM,
TO,
t,
SAMPLES,
)
.unwrap();
let expected = (f64::ln(FROM + delta * t) - f64::ln(FROM)) / delta;
assert!(
approx_eq(result, expected, 1e-4),
"Linear: t={t}, expected {expected}, got {result}"
);
}
}
#[test]
fn const_variants_match_closed_form() {
for &t in SAMPLE_TS {
let in_const = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::InConst,
FROM,
TO,
t,
SAMPLES,
)
.unwrap();
assert!(
approx_eq(in_const, t / FROM, 1e-6),
"InConst t={t}: {in_const}"
);
let out_const = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::OutConst,
FROM,
TO,
t,
SAMPLES,
)
.unwrap();
assert!(
approx_eq(out_const, t / TO, 1e-6),
"OutConst t={t}: {out_const}"
);
let inout_const = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::InOutConst,
FROM,
TO,
t,
SAMPLES,
)
.unwrap();
let expected = if t <= 0.5 {
t / FROM
} else {
0.5 / FROM + (t - 0.5) / TO
};
assert!(
approx_eq(inout_const, expected, 1e-6),
"InOutConst t={t}: {inout_const}"
);
}
}
#[test]
fn samples_count_does_not_break_closed_forms() {
for &ease in CLOSED_FORM_EASINGS {
for &t in SAMPLE_TS {
let low = evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, t, 2).unwrap();
let high =
evaluate_integration_reciprocal::<f64, 1>(ease, FROM, TO, t, 10000).unwrap();
assert!(
approx_eq(low, high, 1e-6),
"{ease:?}: closed form should be sample-invariant, {low} vs {high}"
);
}
}
}
#[test]
fn zero_from_returns_none() {
for &ease in AdvancedEasing::ALL {
if matches!(ease, AdvancedEasing::OutConst) {
continue;
}
let result = evaluate_integration_reciprocal::<f64, 1>(ease, 0.0, TO, 0.5, SAMPLES);
assert!(
result.is_none(),
"{ease:?}: from=0 should diverge and return None, got {result:?}"
);
}
}
#[test]
fn out_const_with_zero_from_stays_finite() {
let result = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::OutConst,
0.0,
TO,
0.5,
SAMPLES,
);
assert!(
approx_eq(result.unwrap(), 0.5 / TO, 1e-6),
"OutConst with from=0 should be t/to, got {result:?}"
);
}
#[test]
fn zero_to_returns_none_for_t_above_1() {
for &ease in AdvancedEasing::ALL {
let result = evaluate_integration_reciprocal::<f64, 1>(ease, FROM, 0.0, 1.5, SAMPLES);
assert!(
result.is_none(),
"{ease:?}: to=0 with t>1 should diverge and return None, got {result:?}"
);
}
}
#[test]
fn same_sign_no_pole_for_bounded_easings() {
let from: f64 = 1e-10;
let to: f64 = 1e10;
for &ease in WELL_BEHAVED_EASINGS {
let result = evaluate_integration_reciprocal::<f64, 1>(ease, from, to, 0.5, SAMPLES);
assert!(
result.is_some(),
"{ease:?}: from*to>0 and bounded → should be finite, got {result:?}"
);
}
}
#[test]
fn back_overshoot_same_sign_safe_when_far() {
let result = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::InBack,
100.0,
200.0,
0.5,
SAMPLES,
);
assert!(
result.is_some(),
"InBack with from=100, to=200: e_crit=-1.0 < e_min≈-0.1 → safe, got {result:?}"
);
}
#[test]
fn back_overshoot_same_sign_pole_when_close() {
let result = evaluate_integration_reciprocal::<f64, 1>(
AdvancedEasing::InBack,
1.0,
12.0,
0.5,
SAMPLES,
);
assert!(
result.is_none(),
"InBack with from=1, to=12: e_crit≈-0.09 ∈ [-0.1, 1.0] → pole, got {result:?}"
);
}
#[test]
fn hardcoded_overshoot_bounds_match_numeric() {
for &ease in AdvancedEasing::ALL {
if !overshoots(ease) {
continue;
}
const N: usize = 1_000_000;
let mut e_min = f64::INFINITY;
let mut e_max = f64::NEG_INFINITY;
for k in 0..=N {
let s = k as f64 / N as f64;
let e = crate::math::easing::eval::easing_value(ease, s);
if e < e_min {
e_min = e;
}
if e > e_max {
e_max = e;
}
}
let (hardcoded_min, hardcoded_max) = overshoot_bounds(ease);
assert!(
hardcoded_min <= e_min + 1e-10,
"{ease:?}: hardcoded e_min {hardcoded_min} > scanned {e_min}"
);
assert!(
hardcoded_max >= e_max - 1e-10,
"{ease:?}: hardcoded e_max {hardcoded_max} < scanned {e_max}"
);
}
}
#[test]
fn opposite_sign_returns_none() {
for &ease in AdvancedEasing::ALL {
if matches!(
ease,
AdvancedEasing::InConst | AdvancedEasing::OutConst | AdvancedEasing::InOutConst
) {
continue;
}
let result = evaluate_integration_reciprocal::<f64, 1>(ease, -1.0, 1.0, 0.5, SAMPLES);
assert!(
result.is_none(),
"{ease:?}: from*to<0 → pole, got {result:?}"
);
}
}
}