1#[derive(Debug, Clone)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum Easing {
11 Hold,
13 Linear,
15 EaseIn,
17 EaseOut,
19 EaseInOut,
21 Bezier {
26 p1: (f64, f64),
28 p2: (f64, f64),
30 },
31}
32
33impl Easing {
34 pub(crate) fn apply(&self, t: f64) -> f64 {
40 match self {
41 Easing::Hold => {
44 if t >= 1.0 {
45 1.0
46 } else {
47 0.0
48 }
49 }
50 Easing::Linear => t,
51 Easing::EaseIn => t * t * t,
53 Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
55 Easing::EaseInOut => 3.0 * t * t - 2.0 * t * t * t,
58 Easing::Bezier { p1, p2 } => {
62 let p1x = p1.0.clamp(0.0, 1.0);
63 let p2x = p2.0.clamp(0.0, 1.0);
64
65 let mut nt = t;
67 for _ in 0..4 {
68 let bx_prime = bez_x_prime(nt, p1x, p2x);
69 if bx_prime.abs() < 1e-10 {
70 break;
71 }
72 nt -= (bez_x(nt, p1x, p2x) - t) / bx_prime;
73 nt = nt.clamp(0.0, 1.0);
74 }
75
76 bez_y(nt, p1.1, p2.1)
77 }
78 }
79 }
80}
81
82fn bez_x(t: f64, p1x: f64, p2x: f64) -> f64 {
86 let u = 1.0 - t;
87 3.0 * p1x * t * u * u + 3.0 * p2x * t * t * u + t * t * t
88}
89
90fn bez_x_prime(t: f64, p1x: f64, p2x: f64) -> f64 {
92 let u = 1.0 - t;
93 3.0 * p1x * u * u + 6.0 * (p2x - p1x) * t * u + 3.0 * (1.0 - p2x) * t * t
94}
95
96fn bez_y(t: f64, p1y: f64, p2y: f64) -> f64 {
98 let u = 1.0 - t;
99 3.0 * p1y * t * u * u + 3.0 * p2y * t * t * u + t * t * t
100}
101
102#[cfg(test)]
105mod tests {
106 use std::time::Duration;
107
108 use super::*;
109 use crate::animation::{AnimationTrack, Keyframe};
110
111 #[test]
112 fn bezier_ease_css_preset_should_match_reference_values() {
113 let ease = Easing::Bezier {
116 p1: (0.25, 0.1),
117 p2: (0.25, 1.0),
118 };
119 let v = ease.apply(0.5);
120 assert!(
121 (v - 0.8029_f64).abs() < 0.01,
122 "expected ~0.8029 for CSS ease at x=0.5, got {v}"
123 );
124
125 assert!(
127 ease.apply(0.0).abs() < f64::EPSILON,
128 "apply(0.0) must be 0.0"
129 );
130 assert!(
131 (ease.apply(1.0) - 1.0).abs() < f64::EPSILON,
132 "apply(1.0) must be 1.0"
133 );
134 }
135
136 #[test]
137 fn linear_easing_should_return_half_at_midpoint() {
138 let track = AnimationTrack::new()
140 .push(Keyframe::new(Duration::ZERO, 0.0_f64, Easing::Linear))
141 .push(Keyframe::new(
142 Duration::from_secs(1),
143 1.0_f64,
144 Easing::Linear,
145 ));
146
147 let v = track.value_at(Duration::from_millis(500));
148 assert!((v - 0.5).abs() < 0.001, "expected 0.5 at midpoint, got {v}");
149 }
150
151 #[test]
152 fn ease_in_out_should_return_half_at_midpoint() {
153 let u = Easing::EaseInOut.apply(0.5);
155 assert!((u - 0.5).abs() < 0.001, "expected 0.5 at midpoint, got {u}");
156 }
157
158 #[test]
159 fn ease_in_out_should_be_below_linear_at_quarter() {
160 let u = Easing::EaseInOut.apply(0.1);
162 assert!(u < 0.1, "ease-in-out at t=0.1 should be below 0.1, got {u}");
163 }
164
165 #[test]
166 fn ease_in_out_should_be_above_linear_at_three_quarters() {
167 let u = Easing::EaseInOut.apply(0.9);
169 assert!(u > 0.9, "ease-in-out at t=0.9 should be above 0.9, got {u}");
170 }
171
172 #[test]
173 fn ease_out_should_be_above_linear_at_midpoint() {
174 let u = Easing::EaseOut.apply(0.5);
176 assert!(u > 0.5, "ease-out at t=0.5 should be above 0.5, got {u}");
177 assert!((u - 0.875).abs() < f64::EPSILON, "expected 0.875, got {u}");
178 }
179
180 #[test]
181 fn ease_in_should_be_below_linear_at_midpoint() {
182 let u = Easing::EaseIn.apply(0.5);
184 assert!(u < 0.5, "ease-in at t=0.5 should be below 0.5, got {u}");
185 assert!((u - 0.125).abs() < f64::EPSILON, "expected 0.125, got {u}");
186 }
187
188 #[test]
189 fn hold_easing_should_return_start_value_at_midpoint() {
190 let u = Easing::Hold.apply(0.5);
192 assert!(
193 (u - 0.0).abs() < f64::EPSILON,
194 "expected 0.0 at t=0.5, got {u}"
195 );
196 }
197
198 #[test]
199 fn hold_easing_should_snap_at_keyframe_boundary() {
200 let u = Easing::Hold.apply(1.0);
202 assert!(
203 (u - 1.0).abs() < f64::EPSILON,
204 "expected 1.0 at t=1.0, got {u}"
205 );
206
207 let u2 = Easing::Hold.apply(1.5);
209 assert!(
210 (u2 - 1.0).abs() < f64::EPSILON,
211 "expected 1.0 at t=1.5, got {u2}"
212 );
213 }
214
215 fn track_with_easing(easing: Easing) -> AnimationTrack<f64> {
224 AnimationTrack::new()
225 .push(Keyframe::new(Duration::ZERO, 0.0_f64, easing))
226 .push(Keyframe::new(
227 Duration::from_secs(1),
228 1.0_f64,
229 Easing::Linear,
230 ))
231 }
232
233 #[test]
234 fn hold_easing_should_hold_at_start_value() {
235 let track = track_with_easing(Easing::Hold);
236
237 let v0 = track.value_at(Duration::ZERO);
239 assert!((v0 - 0.0).abs() < 0.001, "t=0: expected 0.0, got {v0}");
240
241 let v25 = track.value_at(Duration::from_millis(250));
243 assert!(
244 (v25 - 0.0).abs() < 0.001,
245 "t=250ms: expected 0.0, got {v25}"
246 );
247
248 let v50 = track.value_at(Duration::from_millis(500));
249 assert!(
250 (v50 - 0.0).abs() < 0.001,
251 "t=500ms: expected 0.0, got {v50}"
252 );
253
254 let v75 = track.value_at(Duration::from_millis(750));
255 assert!(
256 (v75 - 0.0).abs() < 0.001,
257 "t=750ms: expected 0.0, got {v75}"
258 );
259
260 let v100 = track.value_at(Duration::from_secs(1));
262 assert!((v100 - 1.0).abs() < 0.001, "t=1s: expected 1.0, got {v100}");
263 }
264
265 #[test]
266 fn linear_easing_should_interpolate_uniformly() {
267 let track = track_with_easing(Easing::Linear);
268
269 let v0 = track.value_at(Duration::ZERO);
270 assert!((v0 - 0.0).abs() < 0.001, "t=0: expected 0.0, got {v0}");
271
272 let v25 = track.value_at(Duration::from_millis(250));
273 assert!(
274 (v25 - 0.25).abs() < 0.001,
275 "t=250ms: expected 0.25, got {v25}"
276 );
277
278 let v50 = track.value_at(Duration::from_millis(500));
279 assert!(
280 (v50 - 0.5).abs() < 0.001,
281 "t=500ms: expected 0.5, got {v50}"
282 );
283
284 let v75 = track.value_at(Duration::from_millis(750));
285 assert!(
286 (v75 - 0.75).abs() < 0.001,
287 "t=750ms: expected 0.75, got {v75}"
288 );
289
290 let v100 = track.value_at(Duration::from_secs(1));
291 assert!((v100 - 1.0).abs() < 0.001, "t=1s: expected 1.0, got {v100}");
292 }
293
294 #[test]
295 fn ease_in_should_be_slow_at_start() {
296 let track = track_with_easing(Easing::EaseIn);
298
299 let v0 = track.value_at(Duration::ZERO);
300 assert!((v0 - 0.0).abs() < 0.001, "t=0: expected 0.0, got {v0}");
301
302 let v25 = track.value_at(Duration::from_millis(250));
304 assert!(
305 (v25 - 0.015_625).abs() < 0.001,
306 "t=250ms: expected ~0.016, got {v25}"
307 );
308 assert!(
309 v25 < 0.25,
310 "ease-in at 25% must be below linear ({v25} >= 0.25)"
311 );
312
313 let v50 = track.value_at(Duration::from_millis(500));
315 assert!(
316 (v50 - 0.125).abs() < 0.001,
317 "t=500ms: expected 0.125, got {v50}"
318 );
319 assert!(
320 v50 < 0.5,
321 "ease-in at 50% must be below linear ({v50} >= 0.5)"
322 );
323
324 let v75 = track.value_at(Duration::from_millis(750));
326 assert!(
327 (v75 - 0.421_875).abs() < 0.001,
328 "t=750ms: expected ~0.422, got {v75}"
329 );
330 assert!(
331 v75 < 0.75,
332 "ease-in at 75% must be below linear ({v75} >= 0.75)"
333 );
334
335 let v100 = track.value_at(Duration::from_secs(1));
336 assert!((v100 - 1.0).abs() < 0.001, "t=1s: expected 1.0, got {v100}");
337 }
338
339 #[test]
340 fn ease_out_should_be_fast_at_start() {
341 let track = track_with_easing(Easing::EaseOut);
343
344 let v0 = track.value_at(Duration::ZERO);
345 assert!((v0 - 0.0).abs() < 0.001, "t=0: expected 0.0, got {v0}");
346
347 let v25 = track.value_at(Duration::from_millis(250));
349 assert!(
350 (v25 - 0.578_125).abs() < 0.001,
351 "t=250ms: expected ~0.578, got {v25}"
352 );
353 assert!(
354 v25 > 0.25,
355 "ease-out at 25% must be above linear ({v25} <= 0.25)"
356 );
357
358 let v50 = track.value_at(Duration::from_millis(500));
360 assert!(
361 (v50 - 0.875).abs() < 0.001,
362 "t=500ms: expected 0.875, got {v50}"
363 );
364 assert!(
365 v50 > 0.5,
366 "ease-out at 50% must be above linear ({v50} <= 0.5)"
367 );
368
369 let v75 = track.value_at(Duration::from_millis(750));
371 assert!(
372 (v75 - 0.984_375).abs() < 0.001,
373 "t=750ms: expected ~0.984, got {v75}"
374 );
375 assert!(
376 v75 > 0.75,
377 "ease-out at 75% must be above linear ({v75} <= 0.75)"
378 );
379
380 let v100 = track.value_at(Duration::from_secs(1));
381 assert!((v100 - 1.0).abs() < 0.001, "t=1s: expected 1.0, got {v100}");
382 }
383
384 #[test]
385 fn ease_in_out_should_be_symmetric_at_midpoint() {
386 let track = track_with_easing(Easing::EaseInOut);
388
389 let v0 = track.value_at(Duration::ZERO);
390 assert!((v0 - 0.0).abs() < 0.001, "t=0: expected 0.0, got {v0}");
391
392 let v25 = track.value_at(Duration::from_millis(250));
394 assert!(
395 (v25 - 0.15625).abs() < 0.001,
396 "t=250ms: expected ~0.156, got {v25}"
397 );
398 assert!(
399 v25 < 0.25,
400 "ease-in-out at 25% must be below linear ({v25} >= 0.25)"
401 );
402
403 let v50 = track.value_at(Duration::from_millis(500));
405 assert!(
406 (v50 - 0.5).abs() < 0.001,
407 "t=500ms: expected 0.5, got {v50}"
408 );
409
410 let v75 = track.value_at(Duration::from_millis(750));
412 assert!(
413 (v75 - 0.84375).abs() < 0.001,
414 "t=750ms: expected ~0.844, got {v75}"
415 );
416 assert!(
417 v75 > 0.75,
418 "ease-in-out at 75% must be above linear ({v75} <= 0.75)"
419 );
420
421 let v100 = track.value_at(Duration::from_secs(1));
422 assert!((v100 - 1.0).abs() < 0.001, "t=1s: expected 1.0, got {v100}");
423 }
424
425 #[test]
426 fn bezier_ease_preset_should_match_css_reference() {
427 let track = track_with_easing(Easing::Bezier {
430 p1: (0.25, 0.1),
431 p2: (0.25, 1.0),
432 });
433
434 let v0 = track.value_at(Duration::ZERO);
435 assert!((v0 - 0.0).abs() < 0.001, "t=0: expected 0.0, got {v0}");
436
437 let v50 = track.value_at(Duration::from_millis(500));
439 assert!(
440 (v50 - 0.8029_f64).abs() < 0.01,
441 "t=500ms: expected ~0.803 (CSS ease midpoint), got {v50}"
442 );
443 assert!(
444 v50 > 0.5,
445 "CSS ease at 50% must be above linear ({v50} <= 0.5)"
446 );
447
448 let v100 = track.value_at(Duration::from_secs(1));
449 assert!((v100 - 1.0).abs() < 0.001, "t=1s: expected 1.0, got {v100}");
450 }
451}