1use serde::{Deserialize, Serialize};
16
17#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23#[serde(deny_unknown_fields)]
24pub struct CubicBezier {
25 pub x1: f32,
27 pub y1: f32,
29 pub x2: f32,
31 pub y2: f32,
33}
34
35const NEWTON_STEPS: usize = 8;
37const BISECTION_STEPS: usize = 64;
39const TOLERANCE: f64 = 1e-12;
41const MIN_SLOPE: f64 = 1e-9;
43
44impl CubicBezier {
45 #[must_use]
47 pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
48 Self { x1, y1, x2, y2 }
49 }
50
51 #[must_use]
53 pub fn is_valid(self) -> bool {
54 [self.x1, self.y1, self.x2, self.y2]
55 .into_iter()
56 .all(f32::is_finite)
57 && (0.0..=1.0).contains(&self.x1)
58 && (0.0..=1.0).contains(&self.x2)
59 }
60
61 #[must_use]
66 pub fn apply(self, progress: f64) -> f64 {
67 let x = progress.clamp(0.0, 1.0);
68 if x <= 0.0 {
69 return 0.0;
70 }
71 if x >= 1.0 {
72 return 1.0;
73 }
74 let parameter = self.parameter_at(x);
75 component(parameter, f64::from(self.y1), f64::from(self.y2))
76 }
77
78 fn parameter_at(self, x: f64) -> f64 {
80 self.newton(x).unwrap_or_else(|| self.bisection(x))
81 }
82
83 fn newton(self, x: f64) -> Option<f64> {
86 let (x1, x2) = (f64::from(self.x1), f64::from(self.x2));
87 let mut parameter = x;
88 for _ in 0..NEWTON_STEPS {
89 let error = component(parameter, x1, x2) - x;
90 if error.abs() < TOLERANCE {
91 return (0.0..=1.0).contains(¶meter).then_some(parameter);
92 }
93 let slope = slope(parameter, x1, x2);
94 if slope.abs() < MIN_SLOPE {
95 return None;
96 }
97 parameter -= error / slope;
98 }
99 None
100 }
101
102 fn bisection(self, x: f64) -> f64 {
105 let (x1, x2) = (f64::from(self.x1), f64::from(self.x2));
106 let (mut low, mut high) = (0.0_f64, 1.0_f64);
107 let mut parameter = x;
108 for _ in 0..BISECTION_STEPS {
109 let value = component(parameter, x1, x2);
110 if (value - x).abs() < TOLERANCE {
111 break;
112 }
113 if value < x {
114 low = parameter;
115 } else {
116 high = parameter;
117 }
118 parameter = low + (high - low) * 0.5;
119 }
120 parameter
121 }
122}
123
124fn component(t: f64, p1: f64, p2: f64) -> f64 {
128 let c = 3.0 * p1;
129 let b = 3.0 * (p2 - p1) - c;
130 let a = 1.0 - c - b;
131 ((a * t + b) * t + c) * t
132}
133
134fn slope(t: f64, p1: f64, p2: f64) -> f64 {
136 let c = 3.0 * p1;
137 let b = 3.0 * (p2 - p1) - c;
138 let a = 1.0 - c - b;
139 (3.0 * a * t + 2.0 * b) * t + c
140}
141
142#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
147#[serde(tag = "type", rename_all = "snake_case", from = "EasingWire")]
148pub enum Easing {
149 Linear,
151 CubicBezier(CubicBezier),
153}
154
155#[derive(Deserialize)]
161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
162#[cfg_attr(feature = "schema", schemars(rename = "Easing"))]
163#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
164pub(crate) enum EasingWire {
165 Linear {},
167 CubicBezier(CubicBezier),
169}
170
171impl From<EasingWire> for Easing {
172 fn from(wire: EasingWire) -> Self {
173 match wire {
174 EasingWire::Linear {} => Self::Linear,
175 EasingWire::CubicBezier(curve) => Self::CubicBezier(curve),
176 }
177 }
178}
179
180impl Easing {
181 #[must_use]
183 pub fn is_valid(self) -> bool {
184 match self {
185 Self::Linear => true,
186 Self::CubicBezier(curve) => curve.is_valid(),
187 }
188 }
189
190 #[must_use]
192 pub fn apply(self, progress: f64) -> f64 {
193 match self {
194 Self::Linear => progress.clamp(0.0, 1.0),
195 Self::CubicBezier(curve) => curve.apply(progress),
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::{CubicBezier, Easing};
203
204 const EASE: CubicBezier = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
205
206 #[test]
207 fn css_ease_matches_its_reference_value() {
208 assert!((EASE.apply(0.5) - 0.802_403_387_584_857).abs() < 1e-9);
212 assert!((EASE.parameter_at(0.5) - 0.699_572_438_706_385_6).abs() < 1e-9);
213 let ease_in_out = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
214 assert!((ease_in_out.apply(0.5) - 0.5).abs() < 1e-7);
216 for x in [0.1, 0.25, 0.4] {
217 let mirrored = 1.0 - ease_in_out.apply(1.0 - x);
218 assert!((ease_in_out.apply(x) - mirrored).abs() < 1e-7);
219 }
220 }
221
222 #[test]
223 fn ends_are_exact_and_input_is_clamped() {
224 for curve in [EASE, CubicBezier::new(0.0, -2.0, 1.0, 3.0)] {
225 assert_eq!(curve.apply(0.0).to_bits(), 0.0_f64.to_bits());
226 assert_eq!(curve.apply(1.0).to_bits(), 1.0_f64.to_bits());
227 assert_eq!(curve.apply(-4.0).to_bits(), 0.0_f64.to_bits());
228 assert_eq!(curve.apply(9.0).to_bits(), 1.0_f64.to_bits());
229 }
230 }
231
232 #[test]
233 fn the_linear_curve_is_the_identity() {
234 let linear = CubicBezier::new(0.0, 0.0, 1.0, 1.0);
235 for index in 0..=64 {
236 let x = f64::from(index) / 64.0;
237 assert!((linear.apply(x) - x).abs() < 1e-12, "{x}");
238 assert!((Easing::Linear.apply(x) - x).abs() < f64::EPSILON);
239 }
240 }
241
242 #[test]
243 fn flat_curves_fall_back_to_bisection_and_stay_accurate() {
244 let cases = [
247 (CubicBezier::new(0.0, 0.0, 0.0, 0.0), 0.001, 0.1),
248 (CubicBezier::new(1.0, 1.0, 1.0, 1.0), 0.999, 0.9),
249 ];
250 for (curve, x, parameter) in cases {
251 assert_eq!(curve.newton(x), None, "Newton must give up on {curve:?}");
252 let found = curve.parameter_at(x);
253 assert!((found - parameter).abs() < 1e-9, "{curve:?}: {found}");
254 let x_back = super::component(found, f64::from(curve.x1), f64::from(curve.x2));
255 assert!((x_back - x).abs() < 1e-12);
256 }
257 }
258
259 #[test]
260 fn overshooting_curves_leave_the_unit_interval_between_the_ends() {
261 let back = CubicBezier::new(0.34, 1.56, 0.64, 1.0);
262 let peak = (1..100)
263 .map(|index| back.apply(f64::from(index) / 100.0))
264 .fold(f64::MIN, f64::max);
265 assert!(peak > 1.05, "ease-out-back overshoots: {peak}");
266 assert!((Easing::CubicBezier(back).apply(1.0) - 1.0).abs() < f64::EPSILON);
267 }
268
269 #[test]
270 fn control_points_outside_the_unit_square_are_rejected_horizontally_only() {
271 assert!(CubicBezier::new(0.0, -5.0, 1.0, 5.0).is_valid());
272 assert!(!CubicBezier::new(-0.1, 0.0, 1.0, 1.0).is_valid());
273 assert!(!CubicBezier::new(0.0, 0.0, 1.1, 1.0).is_valid());
274 assert!(!CubicBezier::new(0.0, f32::NAN, 1.0, 1.0).is_valid());
275 assert!(!Easing::CubicBezier(CubicBezier::new(0.0, 0.0, 2.0, 1.0)).is_valid());
276 }
277
278 #[test]
279 fn the_wire_form_is_a_tagged_object() {
280 let easing: Easing =
281 serde_json::from_str(r#"{"type":"cubic_bezier","x1":0.42,"y1":0,"x2":0.58,"y2":1}"#)
282 .expect("cubic bezier easing");
283 assert_eq!(
284 easing,
285 Easing::CubicBezier(CubicBezier::new(0.42, 0.0, 0.58, 1.0))
286 );
287 let linear: Easing = serde_json::from_str(r#"{"type":"linear"}"#).expect("linear");
288 assert_eq!(linear, Easing::Linear);
289 assert!(serde_json::from_str::<Easing>(r#""linear""#).is_err());
290 assert!(serde_json::from_str::<Easing>(r#"{"type":"linear","x1":0}"#).is_err());
291 }
292}