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}