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