Skip to main content

xen_animation/
easing.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// A CSS-compatible cubic-bezier timing function. The curve's start and end
4/// points are implicitly (0,0) and (1,1); only the two control points are
5/// stored, matching the `cubic-bezier(x1, y1, x2, y2)` CSS syntax.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct CubicBezier {
8    x1: f32,
9    y1: f32,
10    x2: f32,
11    y2: f32,
12}
13
14impl CubicBezier {
15    /// Creates a curve from its two control points, matching the argument
16    /// order of the CSS `cubic-bezier(x1, y1, x2, y2)` function.
17    pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
18        Self { x1, y1, x2, y2 }
19    }
20
21    // Polynomial coefficients for the x(t) component of the bezier curve.
22    fn coefficients_x(&self) -> (f32, f32, f32) {
23        let cx = 3.0 * self.x1;
24        let bx = 3.0 * (self.x2 - self.x1) - cx;
25        let ax = 1.0 - cx - bx;
26        (cx, bx, ax)
27    }
28
29    // Polynomial coefficients for the y(t) component of the bezier curve.
30    fn coefficients_y(&self) -> (f32, f32, f32) {
31        let cy = 3.0 * self.y1;
32        let by = 3.0 * (self.y2 - self.y1) - cy;
33        let ay = 1.0 - cy - by;
34        (cy, by, ay)
35    }
36
37    // Evaluates x(t) for the internal bezier parameter t.
38    fn sample_curve_x(&self, t: f32) -> f32 {
39        let (cx, bx, ax) = self.coefficients_x();
40        ((ax * t + bx) * t + cx) * t
41    }
42
43    // Evaluates y(t) for the internal bezier parameter t.
44    fn sample_curve_y(&self, t: f32) -> f32 {
45        let (cy, by, ay) = self.coefficients_y();
46        ((ay * t + by) * t + cy) * t
47    }
48
49    // dx/dt, used by Newton-Raphson to invert x(t) -> t.
50    fn sample_curve_derivative_x(&self, t: f32) -> f32 {
51        let (cx, bx, ax) = self.coefficients_x();
52        (3.0 * ax * t + 2.0 * bx) * t + cx
53    }
54
55    // Solves for the bezier parameter t given progress x, using Newton-Raphson
56    // with a bisection fallback for curves where the tangent gets too flat to
57    // converge - the same approach browsers use for CSS cubic-bezier() timing.
58    fn solve_t_for_x(&self, x: f32, epsilon: f32) -> f32 {
59        let mut t = x;
60
61        for _ in 0..8 {
62            let x_est = self.sample_curve_x(t) - x;
63            if x_est.abs() < epsilon {
64                return t;
65            }
66            let d = self.sample_curve_derivative_x(t);
67            if d.abs() < 1e-6 {
68                break;
69            }
70            t -= x_est / d;
71        }
72
73        let mut lo = 0.0f32;
74        let mut hi = 1.0f32;
75        t = x.clamp(lo, hi);
76
77        while lo < hi {
78            let x_est = self.sample_curve_x(t);
79            if (x_est - x).abs() < epsilon {
80                return t;
81            }
82            if x > x_est {
83                lo = t;
84            } else {
85                hi = t;
86            }
87            t = (hi + lo) * 0.5;
88        }
89
90        t
91    }
92
93    /// Evaluates the easing curve for a normalized progress in the range `[0.0, 1.0]`.
94    ///
95    /// Values outside that range are clamped to the curve's endpoints (`0.0`
96    /// or `1.0`), matching how CSS handles cubic-bezier timing functions.
97    pub fn solve(&self, x: f32) -> f32 {
98        if x <= 0.0 {
99            return 0.0;
100        }
101        if x >= 1.0 {
102            return 1.0;
103        }
104        let t = self.solve_t_for_x(x, 1e-5);
105        self.sample_curve_y(t)
106    }
107}
108
109/// A named or custom timing function used to shape how progress `[0.0, 1.0]`
110/// maps to eased progress over the course of a [`crate::Transition`].
111#[derive(Clone, Copy, Debug, PartialEq, Default)]
112pub enum Easing {
113    /// Constant rate of change; progress and eased progress are equal.
114    #[default]
115    Linear,
116    /// Starts slow and accelerates towards the end.
117    EaseIn,
118    /// Starts fast and decelerates towards the end.
119    EaseOut,
120    /// Starts slow, speeds up through the middle, and slows down again.
121    EaseInOut,
122    /// A user-supplied cubic-bezier curve, for timing functions not covered
123    /// by the built-in presets.
124    CubicBezier(CubicBezier),
125}
126
127impl Easing {
128    // TailwindCSS --ease-in / --ease-out / --ease-in-out values.
129    pub const EASE_IN: CubicBezier = CubicBezier::new(0.4, 0.0, 1.0, 1.0);
130    pub const EASE_OUT: CubicBezier = CubicBezier::new(0.0, 0.0, 0.2, 1.0);
131    pub const EASE_IN_OUT: CubicBezier = CubicBezier::new(0.4, 0.0, 0.2, 1.0);
132
133    /// Shorthand for `Easing::CubicBezier(CubicBezier::new(x1, y1, x2, y2))`.
134    pub const fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
135        Self::CubicBezier(CubicBezier::new(x1, y1, x2, y2))
136    }
137
138    /// Applies this easing function to a normalized progress value, clamping
139    /// `t` to `[0.0, 1.0]` first.
140    pub fn apply(self, t: f32) -> f32 {
141        let t = t.clamp(0.0, 1.0);
142        match self {
143            Self::Linear => t,
144            Self::EaseIn => Self::EASE_IN.solve(t),
145            Self::EaseOut => Self::EASE_OUT.solve(t),
146            Self::EaseInOut => Self::EASE_IN_OUT.solve(t),
147            Self::CubicBezier(curve) => curve.solve(t),
148        }
149    }
150}