1#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct Easing {
7 x1: f32,
8 y1: f32,
9 x2: f32,
10 y2: f32,
11}
12
13impl Default for Easing {
14 fn default() -> Self {
15 Easing::EASE
16 }
17}
18
19impl Easing {
20 pub const LINEAR: Easing = Easing { x1: 0.0, y1: 0.0, x2: 1.0, y2: 1.0 };
22 pub const EASE: Easing = Easing { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1.0 };
24 pub const EASE_IN: Easing = Easing { x1: 0.42, y1: 0.0, x2: 1.0, y2: 1.0 };
26 pub const EASE_OUT: Easing = Easing { x1: 0.0, y1: 0.0, x2: 0.58, y2: 1.0 };
28 pub const EASE_IN_OUT: Easing = Easing { x1: 0.42, y1: 0.0, x2: 0.58, y2: 1.0 };
30
31 pub const fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Easing {
33 Easing { x1: coord(x1, 0.0, 0.0, 1.0), y1: coord(y1, 0.0, -1.0, 2.0), x2: coord(x2, 1.0, 0.0, 1.0), y2: coord(y2, 1.0, -1.0, 2.0) }
34 }
35
36 pub const fn points(self) -> [f32; 4] {
38 [self.x1, self.y1, self.x2, self.y2]
39 }
40
41 fn is_linear(self) -> bool {
42 self.x1 == self.y1 && self.x2 == self.y2
43 }
44
45 pub fn ease(self, u: f32) -> f32 {
47 let u = unit(u);
48 if self.is_linear() {
49 return u;
50 }
51 let s = self.solve(f64::from(u));
52 bez(f64::from(self.y1), f64::from(self.y2), s) as f32
53 }
54
55 pub fn slope(self, u: f32) -> f32 {
57 let u = unit(u);
58 if self.is_linear() {
59 return 1.0;
60 }
61 let s = self.solve(f64::from(u));
62 let (x1, x2, y1, y2) = (f64::from(self.x1), f64::from(self.x2), f64::from(self.y1), f64::from(self.y2));
63 let dx = bez_d(x1, x2, s);
64 let slope = if dx.abs() >= 1e-9 {
65 bez_d(y1, y2, s) / dx
66 } else {
67 let ddx = bez_dd(x1, x2, s);
70 if ddx.abs() < 1e-12 {
71 return 0.0;
72 }
73 bez_dd(y1, y2, s) / ddx
74 };
75 if slope.is_finite() { slope.clamp(-1e4, 1e4) as f32 } else { 0.0 }
76 }
77
78 fn solve(self, u: f64) -> f64 {
80 let (x1, x2) = (f64::from(self.x1), f64::from(self.x2));
81 let mut s = u;
82 for _ in 0..8 {
83 let err = bez(x1, x2, s) - u;
84 if err.abs() < 1e-7 {
85 return s.clamp(0.0, 1.0);
86 }
87 let d = bez_d(x1, x2, s);
88 if d.abs() < 1e-6 {
89 break;
90 }
91 s -= err / d;
92 if !(0.0..=1.0).contains(&s) {
93 break;
94 }
95 }
96 let (mut lo, mut hi) = (0.0_f64, 1.0_f64);
98 s = u;
99 for _ in 0..40 {
100 let x = bez(x1, x2, s);
101 if (x - u).abs() < 1e-7 {
102 break;
103 }
104 if x < u {
105 lo = s;
106 } else {
107 hi = s;
108 }
109 s = (lo + hi) * 0.5;
110 }
111 s.clamp(0.0, 1.0)
112 }
113}
114
115const fn coord(v: f32, nan: f32, lo: f32, hi: f32) -> f32 {
116 if v.is_nan() { nan } else { v.clamp(lo, hi) }
117}
118
119fn unit(u: f32) -> f32 {
120 if u.is_nan() { 0.0 } else { u.clamp(0.0, 1.0) }
121}
122
123fn bez(p1: f64, p2: f64, s: f64) -> f64 {
125 let c = 3.0 * p1;
126 let b = 3.0 * (p2 - p1) - c;
127 let a = 1.0 - c - b;
128 ((a * s + b) * s + c) * s
129}
130
131fn bez_d(p1: f64, p2: f64, s: f64) -> f64 {
132 let c = 3.0 * p1;
133 let b = 3.0 * (p2 - p1) - c;
134 let a = 1.0 - c - b;
135 (3.0 * a * s + 2.0 * b) * s + c
136}
137
138fn bez_dd(p1: f64, p2: f64, s: f64) -> f64 {
139 let c = 3.0 * p1;
140 let b = 3.0 * (p2 - p1) - c;
141 let a = 1.0 - c - b;
142 6.0 * a * s + 2.0 * b
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn linear_is_identity() {
151 for e in [Easing::LINEAR, Easing::cubic_bezier(0.0, 0.0, 1.0, 1.0), Easing::cubic_bezier(0.3, 0.3, 0.7, 0.7)] {
152 for i in 0..=100 {
153 let u = i as f32 / 100.0;
154 assert!((e.ease(u) - u).abs() < 1e-5, "{e:?} {u}");
155 assert!((e.slope(u) - 1.0).abs() < 1e-3, "{e:?} {u}");
156 }
157 }
158 }
159
160 #[test]
161 fn ends_and_monotone() {
162 for e in [Easing::LINEAR, Easing::EASE, Easing::EASE_IN, Easing::EASE_OUT, Easing::EASE_IN_OUT] {
163 assert!(e.ease(0.0).abs() < 1e-6);
164 assert!((e.ease(1.0) - 1.0).abs() < 1e-6);
165 let mut last = 0.0;
166 for i in 0..=1000 {
167 let y = e.ease(i as f32 / 1000.0);
168 assert!(y >= last - 1e-6);
169 last = y;
170 }
171 }
172 assert!((Easing::EASE_IN_OUT.ease(0.5) - 0.5).abs() < 1e-4);
173 }
174
175 #[test]
176 fn slope_matches_finite_difference() {
177 for e in [Easing::EASE, Easing::EASE_IN, Easing::EASE_OUT, Easing::EASE_IN_OUT, Easing::cubic_bezier(0.3, -0.5, 0.6, 1.6)] {
178 for i in 1..100 {
179 let u = i as f32 / 100.0;
180 let h = 1e-3;
181 let fd = (e.ease(u + h) - e.ease(u - h)) / (2.0 * h);
182 assert!((fd - e.slope(u)).abs() < 2e-2 * (1.0 + fd.abs()), "{e:?} {u} {fd} {}", e.slope(u));
183 }
184 }
185 assert!(Easing::EASE_IN.slope(0.0).abs() < 1e-2);
187 assert!(Easing::EASE_OUT.slope(0.0) > 1.0);
188 }
189
190 #[test]
191 fn hostile_inputs() {
192 let e = Easing::cubic_bezier(f32::NAN, f32::INFINITY, -3.0, f32::NEG_INFINITY);
193 assert_eq!(e.points(), [0.0, 2.0, 0.0, -1.0]);
194 for u in [f32::NAN, f32::INFINITY, -1.0, 0.5, 2.0] {
195 assert!(e.ease(u).is_finite());
196 assert!(e.slope(u).is_finite());
197 }
198 }
199}