vsrg 0.3.0

Data structures for vertical scrolling rhythm games
Documentation
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() {
        // solve(ease, easing_value(ease, t)) == t
        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() {
        // easing_value(ease, solve(ease, p)) == p
        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() {
        // Full wrapper with from/to bounds.
        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"
            );
            // Below from -> 0, above to -> 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() {
        // Flat: any val -> 0 (lower bound)
        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"
            );
        }
    }
}