eazy_core/easing/bezier/
cubic_bezier.rs1use crate::easing::Curve;
7
8const SAMPLE_TABLE_SIZE: usize = 11;
10
11const NEWTON_ITERATIONS: usize = 4;
13
14const NEWTON_MIN_SLOPE: f32 = 0.001;
16
17const SUBDIVISION_PRECISION: f32 = 0.0000001;
19
20const SUBDIVISION_MAX_ITERATIONS: usize = 10;
22
23#[derive(Clone, Copy, Debug)]
33pub struct CubicBezier {
34 p1x: f32,
36 p1y: f32,
38 p2x: f32,
40 p2y: f32,
42 samples: [f32; SAMPLE_TABLE_SIZE],
44}
45
46impl CubicBezier {
47 #[inline(always)]
49 fn a(p1: f32, p2: f32) -> f32 {
50 1.0 - 3.0 * p2 + 3.0 * p1
51 }
52
53 #[inline(always)]
55 fn b(p1: f32, p2: f32) -> f32 {
56 3.0 * p2 - 6.0 * p1
57 }
58
59 #[inline(always)]
61 fn c(p1: f32) -> f32 {
62 3.0 * p1
63 }
64
65 #[inline(always)]
67 fn bezier_at(t: f32, p1: f32, p2: f32) -> f32 {
68 ((Self::a(p1, p2) * t + Self::b(p1, p2)) * t + Self::c(p1)) * t
69 }
70
71 #[inline(always)]
73 fn bezier_slope(t: f32, p1: f32, p2: f32) -> f32 {
74 3.0 * Self::a(p1, p2) * t * t + 2.0 * Self::b(p1, p2) * t + Self::c(p1)
75 }
76
77 #[inline(always)]
79 fn newton_raphson(&self, x: f32, guess: f32) -> f32 {
80 let mut t = guess;
81
82 for _ in 0..NEWTON_ITERATIONS {
83 let slope = Self::bezier_slope(t, self.p1x, self.p2x);
84
85 if slope == 0.0 {
86 return t;
87 }
88
89 let current_x = Self::bezier_at(t, self.p1x, self.p2x) - x;
90
91 t -= current_x / slope;
92 }
93
94 t
95 }
96
97 #[inline(always)]
99 fn binary_subdivide(&self, x: f32, mut a: f32, mut b: f32) -> f32 {
100 let mut t = 0.0;
101
102 for _ in 0..SUBDIVISION_MAX_ITERATIONS {
103 t = a + (b - a) / 2.0;
104
105 let current_x = Self::bezier_at(t, self.p1x, self.p2x) - x;
106
107 if current_x.abs() < SUBDIVISION_PRECISION {
108 return t;
109 }
110
111 if current_x > 0.0 {
112 b = t;
113 } else {
114 a = t;
115 }
116 }
117
118 t
119 }
120
121 #[inline(always)]
123 fn t_for_x(&self, x: f32) -> f32 {
124 let sample_step = 1.0 / (SAMPLE_TABLE_SIZE - 1) as f32;
125
126 let mut interval_start = 0.0;
128 let mut current_sample = 1;
129
130 while current_sample < SAMPLE_TABLE_SIZE - 1
131 && self.samples[current_sample] <= x
132 {
133 interval_start += sample_step;
134 current_sample += 1;
135 }
136
137 current_sample -= 1;
138
139 let dist = (x - self.samples[current_sample])
141 / (self.samples[current_sample + 1] - self.samples[current_sample]);
142 let guess = interval_start + dist * sample_step;
143
144 let initial_slope = Self::bezier_slope(guess, self.p1x, self.p2x);
146
147 if initial_slope >= NEWTON_MIN_SLOPE {
148 self.newton_raphson(x, guess)
149 } else if initial_slope == 0.0 {
150 guess
151 } else {
152 self.binary_subdivide(x, interval_start, interval_start + sample_step)
153 }
154 }
155
156 #[inline(always)]
184 pub fn curve(p1x: f32, p1y: f32, p2x: f32, p2y: f32) -> Self {
185 let mut samples = [0.0; SAMPLE_TABLE_SIZE];
187
188 for (i, sample) in samples.iter_mut().enumerate() {
189 let t = i as f32 / (SAMPLE_TABLE_SIZE - 1) as f32;
190
191 *sample = Self::bezier_at(t, p1x, p2x);
192 }
193
194 Self {
195 p1x,
196 p1y,
197 p2x,
198 p2y,
199 samples,
200 }
201 }
202
203 #[inline(always)]
206 pub fn ease() -> Self {
207 Self::curve(0.25, 0.1, 0.25, 1.0)
208 }
209
210 #[inline(always)]
213 pub fn in_ease() -> Self {
214 Self::curve(0.42, 0.0, 1.0, 1.0)
215 }
216
217 #[inline(always)]
220 pub fn out_ease() -> Self {
221 Self::curve(0.0, 0.0, 0.58, 1.0)
222 }
223
224 #[inline(always)]
227 pub fn in_out_ease() -> Self {
228 Self::curve(0.42, 0.0, 0.58, 1.0)
229 }
230}
231
232impl Curve for CubicBezier {
233 #[inline(always)]
234 fn y(&self, x: f32) -> f32 {
235 if x <= 0.0 {
237 return 0.0;
238 }
239
240 if x >= 1.0 {
241 return 1.0;
242 }
243
244 if self.p1x == self.p1y && self.p2x == self.p2y {
246 return x;
247 }
248
249 let t = self.t_for_x(x);
251
252 Self::bezier_at(t, self.p1y, self.p2y)
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_cubic_bezier_endpoints() {
262 let ease = CubicBezier::ease();
263
264 assert_eq!(ease.y(0.0), 0.0);
265 assert_eq!(ease.y(1.0), 1.0);
266 }
267
268 #[test]
269 fn test_cubic_bezier_ease_midpoint() {
270 let ease = CubicBezier::ease();
273 let y = ease.y(0.5);
274
275 assert!(y > 0.75 && y < 0.85, "ease(0.5) = {y}, expected ~0.8");
276 }
277
278 #[test]
279 fn test_cubic_bezier_linear() {
280 let linear = CubicBezier::curve(0.0, 0.0, 1.0, 1.0);
282
283 assert!((linear.y(0.25) - 0.25).abs() < 0.01);
284 assert!((linear.y(0.5) - 0.5).abs() < 0.01);
285 assert!((linear.y(0.75) - 0.75).abs() < 0.01);
286 }
287
288 #[test]
289 fn test_cubic_bezier_ease_in() {
290 let ease_in = CubicBezier::in_ease();
292
293 assert!(ease_in.y(0.5) < 0.5);
295 }
296
297 #[test]
298 fn test_cubic_bezier_ease_out() {
299 let ease_out = CubicBezier::out_ease();
301
302 assert!(ease_out.y(0.5) > 0.5);
304 }
305
306 #[test]
307 fn test_cubic_bezier_ease_in_out() {
308 let ease_in_out = CubicBezier::in_out_ease();
310 let y = ease_in_out.y(0.5);
311
312 assert!((y - 0.5).abs() < 0.05, "ease-in-out(0.5) = {y}");
314 }
315}