vsrg 0.3.0

Data structures for vertical scrolling rhythm games
Documentation
use std::f64::consts::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_easing<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 t < 0.0 {
            from
        } else if t > 1.0 {
            to
        } else {
            let e = easing_value(ease, t);
            from * (1.0 - e) + to * e
        }
    })
}

pub(crate) fn easing_value(ease: AdvancedEasing, t: f64) -> f64 {
    match ease {
        AdvancedEasing::Linear => t,

        AdvancedEasing::InConst => {
            if t >= 1.0 {
                1.0
            } else {
                0.0
            }
        }
        AdvancedEasing::OutConst => {
            if t <= 0.0 {
                0.0
            } else {
                1.0
            }
        }
        AdvancedEasing::InOutConst => t.round(),

        AdvancedEasing::InSine => 1.0 - f64::cos(t * PI / 2.0),
        AdvancedEasing::OutSine => f64::sin(t * PI / 2.0),
        AdvancedEasing::InOutSine => (1.0 - f64::cos(t * PI)) / 2.0,

        AdvancedEasing::InQuad => t * t,
        AdvancedEasing::OutQuad => 1.0 - (1.0 - t) * (1.0 - t),
        AdvancedEasing::InOutQuad => {
            if t < 0.5 {
                2.0 * t * t
            } else {
                1.0 - f64::powi(-2.0 * t + 2.0, 2) / 2.0
            }
        }

        AdvancedEasing::InCubic => t * t * t,
        AdvancedEasing::OutCubic => 1.0 - f64::powi(1.0 - t, 3),
        AdvancedEasing::InOutCubic => {
            if t < 0.5 {
                4.0 * t * t * t
            } else {
                1.0 - f64::powi(-2.0 * t + 2.0, 3) / 2.0
            }
        }

        AdvancedEasing::InQuart => t * t * t * t,
        AdvancedEasing::OutQuart => 1.0 - f64::powi(1.0 - t, 4),
        AdvancedEasing::InOutQuart => {
            if t < 0.5 {
                8.0 * t * t * t * t
            } else {
                1.0 - f64::powi(-2.0 * t + 2.0, 4) / 2.0
            }
        }

        AdvancedEasing::InQuint => t * t * t * t * t,
        AdvancedEasing::OutQuint => 1.0 - f64::powi(1.0 - t, 5),
        AdvancedEasing::InOutQuint => {
            if t < 0.5 {
                16.0 * t * t * t * t * t
            } else {
                1.0 - f64::powi(-2.0 * t + 2.0, 5) / 2.0
            }
        }

        AdvancedEasing::InExpo => {
            if t == 0.0 {
                0.0
            } else {
                f64::powf(2.0, 10.0 * t - 10.0)
            }
        }
        AdvancedEasing::OutExpo => {
            if t == 1.0 {
                1.0
            } else {
                1.0 - f64::powf(2.0, -10.0 * t)
            }
        }
        AdvancedEasing::InOutExpo => {
            if t == 0.0 {
                0.0
            } else if t == 1.0 {
                1.0
            } else if t < 0.5 {
                f64::powf(2.0, 20.0 * t - 11.0)
            } else {
                1.0 - f64::powf(2.0, -20.0 * t + 9.0)
            }
        }

        AdvancedEasing::InCirc => 1.0 - f64::sqrt(1.0 - t * t),
        AdvancedEasing::OutCirc => f64::sqrt(1.0 - (t - 1.0) * (t - 1.0)),
        AdvancedEasing::InOutCirc => {
            if t < 0.5 {
                (1.0 - f64::sqrt(1.0 - 4.0 * t * t)) / 2.0
            } else {
                f64::sqrt(1.0 - f64::powi(-2.0 * t + 2.0, 2)) / 2.0 + 0.5
            }
        }

        AdvancedEasing::InBack => C3 * t * t * t - C1 * t * t,
        AdvancedEasing::OutBack => 1.0 + C3 * f64::powi(t - 1.0, 3) + C1 * f64::powi(t - 1.0, 2),
        AdvancedEasing::InOutBack => {
            if t < 0.5 {
                (4.0 * t * t * ((C2 + 1.0) * 2.0 * t - C2)) / 2.0
            } else {
                f64::powi(2.0 * t - 2.0, 2) * ((C2 + 1.0) * (2.0 * t - 2.0) + C2) / 2.0 + 1.0
            }
        }

        AdvancedEasing::InElastic => {
            if t == 0.0 {
                0.0
            } else if t == 1.0 {
                1.0
            } else {
                f64::powf(2.0, 10.0 * t - 10.0) * f64::sin((t * 10.0 - 10.75) * C4)
            }
        }
        AdvancedEasing::OutElastic => {
            if t == 0.0 {
                0.0
            } else if t == 1.0 {
                1.0
            } else {
                f64::powf(2.0, -10.0 * t) * f64::sin((t * 10.0 - 0.75) * C4) + 1.0
            }
        }
        AdvancedEasing::InOutElastic => {
            if t == 0.0 {
                0.0
            } else if t == 1.0 {
                1.0
            } else if t < 0.5 {
                -f64::powf(2.0, 20.0 * t - 10.0) * f64::sin((20.0 * t - 11.125) * C5) / 2.0
            } else {
                f64::powf(2.0, -20.0 * t + 10.0) * f64::sin((20.0 * t - 11.125) * C5) / 2.0 + 1.0
            }
        }

        AdvancedEasing::InBounce => 1.0 - ease_out_bounce(1.0 - t),
        AdvancedEasing::OutBounce => ease_out_bounce(t),
        AdvancedEasing::InOutBounce => {
            if t < 0.5 {
                (1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
            } else {
                (1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
            }
        }
    }
}

fn ease_out_bounce(x: f64) -> f64 {
    let n1 = 7.5625;
    let d1 = 2.75;

    if x < 1.0 / d1 {
        n1 * x * x
    } else if x < 2.0 / d1 {
        let x = x - 1.5 / d1;
        n1 * x * x + 0.75
    } else if x < 2.5 / d1 {
        let x = x - 2.25 / d1;
        n1 * x * x + 0.9375
    } else {
        let x = x - 2.625 / d1;
        n1 * x * x + 0.984375
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const FROM: f64 = 0.0;
    const TO: f64 = 100.0;
    const EPS: f64 = 1e-4;

    const INOUT_EASINGS: &[AdvancedEasing] = &[
        AdvancedEasing::InOutSine,
        AdvancedEasing::InOutQuad,
        AdvancedEasing::InOutCubic,
        AdvancedEasing::InOutQuart,
        AdvancedEasing::InOutQuint,
        AdvancedEasing::InOutExpo,
        AdvancedEasing::InOutCirc,
        AdvancedEasing::InOutBack,
        AdvancedEasing::InOutElastic,
        AdvancedEasing::InOutBounce,
    ];

    fn approx_eq(a: f64, b: f64) -> bool {
        (a - b).abs() <= EPS
    }

    #[test]
    fn t0_returns_from() {
        let mut failures: Vec<(AdvancedEasing, f64)> = Vec::new();
        for &ease in AdvancedEasing::ALL {
            let result = evaluate_easing::<f64, 1>(ease, FROM, TO, 0.0);
            if !approx_eq(result, FROM) {
                failures.push((ease, result));
            }
        }
        assert!(
            failures.is_empty(),
            "t=0 should return from ({}), but got: {}",
            FROM,
            failures
                .iter()
                .map(|(e, r)| format!("{e:?}={r}"))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    #[test]
    fn t1_returns_to() {
        let mut failures: Vec<(AdvancedEasing, f64)> = Vec::new();
        for &ease in AdvancedEasing::ALL {
            let result = evaluate_easing::<f64, 1>(ease, FROM, TO, 1.0);
            if !approx_eq(result, TO) {
                failures.push((ease, result));
            }
        }
        assert!(
            failures.is_empty(),
            "t=1 should return to ({}), but got: {}",
            TO,
            failures
                .iter()
                .map(|(e, r)| format!("{e:?}={r}"))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    #[test]
    fn t05_returns_midpoint_for_inout() {
        let midpoint = (FROM + TO) / 2.0;
        let mut failures: Vec<(AdvancedEasing, f64)> = Vec::new();
        for &ease in INOUT_EASINGS {
            if matches!(ease, AdvancedEasing::InOutConst) {
                continue;
            }
            let result = evaluate_easing::<f64, 1>(ease, FROM, TO, 0.5);
            if !approx_eq(result, midpoint) {
                failures.push((ease, result));
            }
        }
        assert!(
            failures.is_empty(),
            "t=0.5 should return midpoint ({}), but got: {}",
            midpoint,
            failures
                .iter()
                .map(|(e, r)| format!("{e:?}={r}"))
                .collect::<Vec<_>>()
                .join(", ")
        );
    }
}