Skip to main content

shuyi_ui/
ease.rs

1//! Ease curves for a skin that works in floats: [`ease_f32`] eases through
2//! `shuyi_core`'s integer curves ([`shuyi_core::ease`]), so a curve is the
3//! same on every machine.
4
5#[cfg(not(feature = "std"))]
6use crate::float::Float;
7use shuyi_core::ease::Easing;
8
9/// [`shuyi_core::ease::ONE`], the whole way, as an `f32`.
10const ONE: f32 = 65_536.0;
11
12/// `p` (0 to 1; clamped, not a number counts as 0) eased by `ease`: turned
13/// into 1/65536, eased by the integer curve and turned back. A curve that
14/// overshoots (`Back`, `Elastic`) gives values below 0 and above 1.
15///
16/// ```
17/// use shuyi_core::{Ease, Family};
18/// use shuyi_ui::ease::ease_f32;
19///
20/// assert_eq!(ease_f32(&Ease::Smooth, 0.5), 0.5);
21/// assert!(ease_f32(&Ease::Out(Family::Back), 0.6) > 1.0);
22/// ```
23pub fn ease_f32(ease: &impl Easing, p: f32) -> f32 {
24    let p = if p.is_nan() { 0.0 } else { p.clamp(0.0, 1.0) };
25    // At most 65 536, which an `f32` and an `i32` hold exactly.
26    #[expect(clippy::cast_possible_truncation, reason = "0 to 65 536")]
27    let units = (p * ONE).round() as i32;
28    #[expect(
29        clippy::cast_precision_loss,
30        reason = "every curve stays between -65 536 and 131 072"
31    )]
32    let eased = ease.ease(units) as f32;
33    eased / ONE
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::cinematic::ease_in_out;
40    use shuyi_core::{Ease, Family};
41
42    #[test]
43    fn smooth_is_the_cinematics_ease_in_out() {
44        for k in 0..=10_000_u16 {
45            let p = f32::from(k) / 10_000.0;
46            let off = (ease_f32(&Ease::Smooth, p) - ease_in_out(p)).abs();
47            assert!(off <= 2.0 / 65_536.0, "{p}: {off}");
48        }
49    }
50
51    #[test]
52    fn not_a_number_and_input_out_of_range_are_clamped() {
53        let curve = Ease::In(Family::Cubic);
54        assert_eq!(ease_f32(&curve, f32::NAN), 0.0);
55        assert_eq!(ease_f32(&curve, -3.0), 0.0);
56        assert_eq!(ease_f32(&curve, f32::NEG_INFINITY), 0.0);
57        assert_eq!(ease_f32(&curve, 7.0), 1.0);
58        assert_eq!(ease_f32(&curve, f32::INFINITY), 1.0);
59        assert_eq!(ease_f32(&curve, 0.5), 0.125);
60    }
61
62    #[test]
63    fn an_overshoot_goes_past_0_and_1() {
64        assert!(ease_f32(&Ease::In(Family::Back), 0.25) < 0.0);
65        assert!(ease_f32(&Ease::Out(Family::Elastic), 0.1) > 1.0);
66    }
67}