1use std::f64::consts::PI;
2
3pub trait Ease {
8 fn ease(&self, t: f64) -> f64;
10}
11
12impl<F: Fn(f64) -> f64> Ease for F {
13 fn ease(&self, t: f64) -> f64 {
14 self(t)
15 }
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct CubicBezier {
22 pub x1: f64,
24 pub y1: f64,
26 pub x2: f64,
28 pub y2: f64,
30}
31
32impl CubicBezier {
33 const NEWTON_ITERATIONS: usize = 8;
34 const PRECISION: f64 = 1e-7;
35
36 pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
38 Self { x1, y1, x2, y2 }
39 }
40
41 pub const fn from_array([x1, y1, x2, y2]: [f64; 4]) -> Self {
43 Self::new(x1, y1, x2, y2)
44 }
45
46 pub const fn to_array(self) -> [f64; 4] {
48 [self.x1, self.y1, self.x2, self.y2]
49 }
50
51 fn coeffs(p1: f64, p2: f64) -> (f64, f64, f64) {
53 let c = 3.0 * p1;
54 let b = 3.0 * (p2 - p1) - c;
55 (1.0 - c - b, b, c)
56 }
57
58 #[cfg(test)]
59 fn bezier(p1: f64, p2: f64, s: f64) -> f64 {
60 let (a, b, c) = Self::coeffs(p1, p2);
61 ((a * s + b) * s + c) * s
62 }
63
64 fn solve_s(&self, x: f64) -> f64 {
66 let (a, b, c) = Self::coeffs(self.x1, self.x2);
67 let eval = |s: f64| ((a * s + b) * s + c) * s;
68 let mut s = x;
69 for _ in 0..Self::NEWTON_ITERATIONS {
70 let err = eval(s) - x;
71 if err.abs() < Self::PRECISION {
72 return s;
73 }
74 let slope = (3.0 * a * s + 2.0 * b) * s + c;
75 if slope.abs() < 1e-6 {
76 break;
77 }
78 s = (s - err / slope).clamp(0.0, 1.0);
79 }
80 let (mut lo, mut hi) = (0.0, 1.0);
83 s = x;
84 for _ in 0..40 {
85 let v = eval(s);
86 if (v - x).abs() < Self::PRECISION {
87 break;
88 }
89 if v < x {
90 lo = s;
91 } else {
92 hi = s;
93 }
94 s = 0.5 * (lo + hi);
95 }
96 s
97 }
98}
99
100impl Ease for CubicBezier {
101 fn ease(&self, t: f64) -> f64 {
102 if t <= 0.0 {
103 return 0.0;
104 }
105 if t >= 1.0 {
106 return 1.0;
107 }
108 if self.x1 == self.y1 && self.x2 == self.y2 {
109 return t;
110 }
111 let (a, b, c) = Self::coeffs(self.y1, self.y2);
112 let s = self.solve_s(t);
113 ((a * s + b) * s + c) * s
114 }
115}
116
117pub mod presets {
119 use super::CubicBezier;
120
121 pub const LINEAR: CubicBezier = CubicBezier::new(0.0, 0.0, 1.0, 1.0);
123 pub const EASE: CubicBezier = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
125 pub const EASE_IN: CubicBezier = CubicBezier::new(0.42, 0.0, 1.0, 1.0);
127 pub const EASE_OUT: CubicBezier = CubicBezier::new(0.0, 0.0, 0.58, 1.0);
129 pub const EASE_IN_OUT: CubicBezier = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
131
132 pub const IN_SINE: CubicBezier = CubicBezier::new(0.12, 0.0, 0.39, 0.0);
134 pub const OUT_SINE: CubicBezier = CubicBezier::new(0.61, 1.0, 0.88, 1.0);
136 pub const IN_OUT_SINE: CubicBezier = CubicBezier::new(0.37, 0.0, 0.63, 1.0);
138 pub const IN_QUAD: CubicBezier = CubicBezier::new(0.11, 0.0, 0.5, 0.0);
140 pub const OUT_QUAD: CubicBezier = CubicBezier::new(0.5, 1.0, 0.89, 1.0);
142 pub const IN_OUT_QUAD: CubicBezier = CubicBezier::new(0.45, 0.0, 0.55, 1.0);
144 pub const IN_CUBIC: CubicBezier = CubicBezier::new(0.32, 0.0, 0.67, 0.0);
146 pub const OUT_CUBIC: CubicBezier = CubicBezier::new(0.33, 1.0, 0.68, 1.0);
148 pub const IN_OUT_CUBIC: CubicBezier = CubicBezier::new(0.65, 0.0, 0.35, 1.0);
150 pub const IN_QUART: CubicBezier = CubicBezier::new(0.5, 0.0, 0.75, 0.0);
152 pub const OUT_QUART: CubicBezier = CubicBezier::new(0.25, 1.0, 0.5, 1.0);
154 pub const IN_OUT_QUART: CubicBezier = CubicBezier::new(0.76, 0.0, 0.24, 1.0);
156 pub const IN_EXPO: CubicBezier = CubicBezier::new(0.7, 0.0, 0.84, 0.0);
158 pub const OUT_EXPO: CubicBezier = CubicBezier::new(0.16, 1.0, 0.3, 1.0);
160 pub const IN_OUT_EXPO: CubicBezier = CubicBezier::new(0.87, 0.0, 0.13, 1.0);
162 pub const IN_BACK: CubicBezier = CubicBezier::new(0.36, 0.0, 0.66, -0.56);
164 pub const OUT_BACK: CubicBezier = CubicBezier::new(0.34, 1.56, 0.64, 1.0);
166 pub const IN_OUT_BACK: CubicBezier = CubicBezier::new(0.68, -0.6, 0.32, 1.6);
168
169 pub const DEFAULT: CubicBezier = IN_OUT_CUBIC;
171}
172
173pub fn elastic_out(t: f64) -> f64 {
175 if t <= 0.0 || t >= 1.0 {
176 return t.clamp(0.0, 1.0);
177 }
178 let c4 = 2.0 * PI / 3.0;
179 2f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
180}
181
182pub fn elastic_in(t: f64) -> f64 {
184 1.0 - elastic_out(1.0 - t)
185}
186
187pub fn elastic_in_out(t: f64) -> f64 {
189 if t <= 0.0 || t >= 1.0 {
190 return t.clamp(0.0, 1.0);
191 }
192 let c5 = 2.0 * PI / 4.5;
193 if t < 0.5 {
194 -(2f64.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
195 } else {
196 2f64.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin() / 2.0 + 1.0
197 }
198}
199
200pub fn bounce_out(t: f64) -> f64 {
202 const N: f64 = 7.5625;
203 const D: f64 = 2.75;
204 let t = t.clamp(0.0, 1.0);
205 if t < 1.0 / D {
206 N * t * t
207 } else if t < 2.0 / D {
208 let t = t - 1.5 / D;
209 N * t * t + 0.75
210 } else if t < 2.5 / D {
211 let t = t - 2.25 / D;
212 N * t * t + 0.9375
213 } else {
214 let t = t - 2.625 / D;
215 N * t * t + 0.984_375
216 }
217}
218
219pub fn bounce_in(t: f64) -> f64 {
221 1.0 - bounce_out(1.0 - t)
222}
223
224pub fn bounce_in_out(t: f64) -> f64 {
226 if t < 0.5 {
227 (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
228 } else {
229 (1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::presets::*;
236 use super::*;
237
238 fn reference(c: &CubicBezier, x: f64) -> f64 {
240 let (mut lo, mut hi) = (0.0f64, 1.0f64);
241 for _ in 0..200 {
242 let mid = 0.5 * (lo + hi);
243 if CubicBezier::bezier(c.x1, c.x2, mid) < x {
244 lo = mid;
245 } else {
246 hi = mid;
247 }
248 }
249 CubicBezier::bezier(c.y1, c.y2, 0.5 * (lo + hi))
250 }
251
252 #[test]
253 fn bezier_matches_reference_within_tolerance() {
254 let curves = [
255 EASE,
256 EASE_IN,
257 EASE_OUT,
258 EASE_IN_OUT,
259 IN_OUT_CUBIC,
260 IN_EXPO,
261 OUT_EXPO,
262 IN_OUT_EXPO,
263 IN_OUT_BACK,
264 OUT_BACK,
265 IN_BACK,
266 CubicBezier::new(1.0, 0.0, 0.0, 1.0),
267 CubicBezier::new(0.0, 1.0, 1.0, 0.0),
268 ];
269 for c in curves {
270 for i in 0..=1000 {
271 let x = i as f64 / 1000.0;
272 let got = c.ease(x);
273 let want = reference(&c, x);
274 assert!((got - want).abs() <= 1e-4, "{c:?} at {x}: {got} vs {want}");
275 }
276 }
277 }
278
279 #[test]
280 fn endpoints_and_linear() {
281 for c in [LINEAR, EASE, IN_OUT_BACK] {
282 assert_eq!(c.ease(0.0), 0.0);
283 assert_eq!(c.ease(1.0), 1.0);
284 }
285 assert_eq!(LINEAR.ease(0.3), 0.3);
286 for f in [
287 elastic_in,
288 elastic_out,
289 elastic_in_out,
290 bounce_in,
291 bounce_out,
292 bounce_in_out,
293 ] {
294 assert!(f.ease(0.0).abs() < 1e-9);
295 assert!((f.ease(1.0) - 1.0).abs() < 1e-9);
296 }
297 }
298}