Skip to main content

ravenrs_animation/
easing.rs

1/// A timing curve from `(0, 0)` to `(1, 1)` with two control points, the same
2/// as CSS `cubic-bezier()`.
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct CubicBezier {
5    /// First control point, x then y.
6    pub p1: (f32, f32),
7    /// Second control point, x then y.
8    pub p2: (f32, f32),
9}
10
11impl CubicBezier {
12    /// A curve with these control points.
13    pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
14        Self { p1: (x1, y1), p2: (x2, y2) }
15    }
16
17    /// The curve's height at horizontal position `x`. Newton's method converges
18    /// in a few steps almost everywhere; bisection covers the flat spots.
19    pub fn sample(&self, x: f32) -> f32 {
20        if x <= 0.0 {
21            return 0.0;
22        }
23        if x >= 1.0 {
24            return 1.0;
25        }
26        let curve = |a: f32, b: f32, t: f32| {
27            let u = 1.0 - t;
28            3.0 * u * u * t * a + 3.0 * u * t * t * b + t * t * t
29        };
30        let slope = |a: f32, b: f32, t: f32| {
31            let u = 1.0 - t;
32            3.0 * u * u * a + 6.0 * u * t * (b - a) + 3.0 * t * t * (1.0 - b)
33        };
34        let (x1, x2) = (self.p1.0.clamp(0.0, 1.0), self.p2.0.clamp(0.0, 1.0));
35        let mut t = x;
36        let mut solved = false;
37        for _ in 0..6 {
38            let error = curve(x1, x2, t) - x;
39            if error.abs() < 1e-4 {
40                solved = true;
41                break;
42            }
43            let d = slope(x1, x2, t);
44            if d.abs() < 1e-5 {
45                break;
46            }
47            t = (t - error / d).clamp(0.0, 1.0);
48        }
49        if !solved {
50            let (mut lo, mut hi) = (0.0_f32, 1.0_f32);
51            for _ in 0..20 {
52                t = (lo + hi) / 2.0;
53                if curve(x1, x2, t) < x {
54                    lo = t;
55                } else {
56                    hi = t;
57                }
58            }
59        }
60        curve(self.p1.1, self.p2.1, t)
61    }
62}
63
64/// How a timed transition moves from start to end.
65#[derive(Clone, Copy, Debug, Default, PartialEq)]
66#[non_exhaustive]
67pub enum Easing {
68    /// Constant speed; for loops and scrubbing.
69    Linear,
70    /// Starts slowly; for things that leave for good.
71    EaseIn,
72    /// Starts fast and settles; acknowledges input at once.
73    #[default]
74    EaseOut,
75    /// Slow at both ends; for things moving from one resting place to another.
76    EaseInOut,
77    /// A long, soft landing; for large surfaces entering.
78    Emphasized,
79    /// Any other curve.
80    Bezier(CubicBezier),
81}
82
83impl Easing {
84    /// Progress at `t`, where both run from 0 to 1 (a [`CubicBezier`] may overshoot).
85    pub fn sample(&self, t: f32) -> f32 {
86        match self {
87            Self::Linear => t.clamp(0.0, 1.0),
88            Self::EaseIn => CubicBezier::new(0.42, 0.0, 1.0, 1.0).sample(t),
89            Self::EaseOut => CubicBezier::new(0.0, 0.0, 0.2, 1.0).sample(t),
90            Self::EaseInOut => CubicBezier::new(0.4, 0.0, 0.2, 1.0).sample(t),
91            Self::Emphasized => CubicBezier::new(0.05, 0.7, 0.1, 1.0).sample(t),
92            Self::Bezier(curve) => curve.sample(t),
93        }
94    }
95}