1use crate::math::{ceil, cos, powf, powi, sin, sqrt};
26use core::f32::consts::PI;
27
28#[derive(Clone, Debug)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Easing {
45 Linear,
47
48 EaseInQuad,
51 EaseOutQuad,
53 EaseInOutQuad,
55
56 EaseInCubic,
58 EaseOutCubic,
60 EaseInOutCubic,
62
63 EaseInQuart,
65 EaseOutQuart,
67 EaseInOutQuart,
69
70 EaseInQuint,
72 EaseOutQuint,
74 EaseInOutQuint,
76
77 EaseInSine,
80 EaseOutSine,
82 EaseInOutSine,
84
85 EaseInExpo,
88 EaseOutExpo,
90 EaseInOutExpo,
92
93 EaseInCirc,
96 EaseOutCirc,
98 EaseInOutCirc,
100
101 EaseInBack,
104 EaseOutBack,
106 EaseInOutBack,
108
109 EaseInElastic,
112 EaseOutElastic,
114 EaseInOutElastic,
116
117 EaseInBounce,
120 EaseOutBounce,
122 EaseInOutBounce,
124
125 CubicBezier(f32, f32, f32, f32),
132
133 Steps(u32),
137
138 #[cfg_attr(feature = "serde", serde(skip))]
149 Custom(fn(f32) -> f32),
150}
151
152impl PartialEq for Easing {
154 fn eq(&self, other: &Self) -> bool {
155 match (self, other) {
156 (Easing::Custom(_), _) | (_, Easing::Custom(_)) => false,
157 (Easing::CubicBezier(ax1, ay1, ax2, ay2), Easing::CubicBezier(bx1, by1, bx2, by2)) => {
158 ax1 == bx1 && ay1 == by1 && ax2 == bx2 && ay2 == by2
159 }
160 (Easing::Steps(a), Easing::Steps(b)) => a == b,
161 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
162 }
163 }
164}
165
166impl Easing {
167 #[inline]
184 pub fn apply(&self, t: f32) -> f32 {
185 match self {
186 Easing::Custom(f) => f(t),
187 _ => {
188 let t = t.clamp(0.0, 1.0);
189 match self {
190 Easing::Linear => t,
191 Easing::EaseInQuad => ease_in_quad(t),
192 Easing::EaseOutQuad => ease_out_quad(t),
193 Easing::EaseInOutQuad => ease_in_out_quad(t),
194 Easing::EaseInCubic => ease_in_cubic(t),
195 Easing::EaseOutCubic => ease_out_cubic(t),
196 Easing::EaseInOutCubic => ease_in_out_cubic(t),
197 Easing::EaseInQuart => ease_in_quart(t),
198 Easing::EaseOutQuart => ease_out_quart(t),
199 Easing::EaseInOutQuart => ease_in_out_quart(t),
200 Easing::EaseInQuint => ease_in_quint(t),
201 Easing::EaseOutQuint => ease_out_quint(t),
202 Easing::EaseInOutQuint => ease_in_out_quint(t),
203 Easing::EaseInSine => ease_in_sine(t),
204 Easing::EaseOutSine => ease_out_sine(t),
205 Easing::EaseInOutSine => ease_in_out_sine(t),
206 Easing::EaseInExpo => ease_in_expo(t),
207 Easing::EaseOutExpo => ease_out_expo(t),
208 Easing::EaseInOutExpo => ease_in_out_expo(t),
209 Easing::EaseInCirc => ease_in_circ(t),
210 Easing::EaseOutCirc => ease_out_circ(t),
211 Easing::EaseInOutCirc => ease_in_out_circ(t),
212 Easing::EaseInBack => ease_in_back(t),
213 Easing::EaseOutBack => ease_out_back(t),
214 Easing::EaseInOutBack => ease_in_out_back(t),
215 Easing::EaseInElastic => ease_in_elastic(t),
216 Easing::EaseOutElastic => ease_out_elastic(t),
217 Easing::EaseInOutElastic => ease_in_out_elastic(t),
218 Easing::EaseInBounce => ease_in_bounce(t),
219 Easing::EaseOutBounce => ease_out_bounce(t),
220 Easing::EaseInOutBounce => ease_in_out_bounce(t),
221 Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(t, *x1, *y1, *x2, *y2),
222 Easing::Steps(count) => steps(t, *count),
223 Easing::Custom(_) => unreachable!(),
224 }
225 }
226 }
227 }
228
229 pub fn all_named() -> &'static [Easing] {
241 &[
242 Easing::Linear,
243 Easing::EaseInQuad,
244 Easing::EaseOutQuad,
245 Easing::EaseInOutQuad,
246 Easing::EaseInCubic,
247 Easing::EaseOutCubic,
248 Easing::EaseInOutCubic,
249 Easing::EaseInQuart,
250 Easing::EaseOutQuart,
251 Easing::EaseInOutQuart,
252 Easing::EaseInQuint,
253 Easing::EaseOutQuint,
254 Easing::EaseInOutQuint,
255 Easing::EaseInSine,
256 Easing::EaseOutSine,
257 Easing::EaseInOutSine,
258 Easing::EaseInExpo,
259 Easing::EaseOutExpo,
260 Easing::EaseInOutExpo,
261 Easing::EaseInCirc,
262 Easing::EaseOutCirc,
263 Easing::EaseInOutCirc,
264 Easing::EaseInBack,
265 Easing::EaseOutBack,
266 Easing::EaseInOutBack,
267 Easing::EaseInElastic,
268 Easing::EaseOutElastic,
269 Easing::EaseInOutElastic,
270 Easing::EaseInBounce,
271 Easing::EaseOutBounce,
272 Easing::EaseInOutBounce,
273 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
274 Easing::Steps(1),
275 ]
276 }
277}
278
279#[inline]
285pub fn ease_in_quad(t: f32) -> f32 {
286 t * t
287}
288
289#[inline]
291pub fn ease_out_quad(t: f32) -> f32 {
292 1.0 - (1.0 - t) * (1.0 - t)
293}
294
295#[inline]
297pub fn ease_in_out_quad(t: f32) -> f32 {
298 if t < 0.5 {
299 2.0 * t * t
300 } else {
301 1.0 - powi(-2.0 * t + 2.0, 2) / 2.0
302 }
303}
304
305#[inline]
307pub fn ease_in_cubic(t: f32) -> f32 {
308 t * t * t
309}
310
311#[inline]
313pub fn ease_out_cubic(t: f32) -> f32 {
314 1.0 - powi(1.0 - t, 3)
315}
316
317#[inline]
319pub fn ease_in_out_cubic(t: f32) -> f32 {
320 if t < 0.5 {
321 4.0 * t * t * t
322 } else {
323 1.0 - powi(-2.0 * t + 2.0, 3) / 2.0
324 }
325}
326
327#[inline]
329pub fn ease_in_quart(t: f32) -> f32 {
330 t * t * t * t
331}
332
333#[inline]
335pub fn ease_out_quart(t: f32) -> f32 {
336 1.0 - powi(1.0 - t, 4)
337}
338
339#[inline]
341pub fn ease_in_out_quart(t: f32) -> f32 {
342 if t < 0.5 {
343 8.0 * t * t * t * t
344 } else {
345 1.0 - powi(-2.0 * t + 2.0, 4) / 2.0
346 }
347}
348
349#[inline]
351pub fn ease_in_quint(t: f32) -> f32 {
352 t * t * t * t * t
353}
354
355#[inline]
357pub fn ease_out_quint(t: f32) -> f32 {
358 1.0 - powi(1.0 - t, 5)
359}
360
361#[inline]
363pub fn ease_in_out_quint(t: f32) -> f32 {
364 if t < 0.5 {
365 16.0 * t * t * t * t * t
366 } else {
367 1.0 - powi(-2.0 * t + 2.0, 5) / 2.0
368 }
369}
370
371#[inline]
373pub fn ease_in_sine(t: f32) -> f32 {
374 1.0 - cos(t * PI / 2.0)
375}
376#[inline]
378pub fn ease_out_sine(t: f32) -> f32 {
379 sin(t * PI / 2.0)
380}
381#[inline]
383pub fn ease_in_out_sine(t: f32) -> f32 {
384 -(cos(t * PI) - 1.0) / 2.0
385}
386
387#[inline]
389pub fn ease_in_expo(t: f32) -> f32 {
390 if t == 0.0 {
391 0.0
392 } else {
393 powf(2.0, 10.0 * t - 10.0)
394 }
395}
396
397#[inline]
399pub fn ease_out_expo(t: f32) -> f32 {
400 if t == 1.0 {
401 1.0
402 } else {
403 1.0 - powf(2.0, -10.0 * t)
404 }
405}
406
407#[inline]
409pub fn ease_in_out_expo(t: f32) -> f32 {
410 if t == 0.0 {
411 return 0.0;
412 }
413 if t == 1.0 {
414 return 1.0;
415 }
416 if t < 0.5 {
417 powf(2.0, 20.0 * t - 10.0) / 2.0
418 } else {
419 (2.0 - powf(2.0, -20.0 * t + 10.0)) / 2.0
420 }
421}
422
423#[inline]
425pub fn ease_in_circ(t: f32) -> f32 {
426 1.0 - sqrt(1.0 - t * t)
427}
428#[inline]
430pub fn ease_out_circ(t: f32) -> f32 {
431 sqrt(1.0 - (t - 1.0) * (t - 1.0))
432}
433
434#[inline]
436pub fn ease_in_out_circ(t: f32) -> f32 {
437 if t < 0.5 {
438 (1.0 - sqrt(1.0 - powi(2.0 * t, 2))) / 2.0
439 } else {
440 (sqrt(1.0 - powi(-2.0 * t + 2.0, 2)) + 1.0) / 2.0
441 }
442}
443
444const BACK_C1: f32 = 1.701_58;
445const BACK_C2: f32 = BACK_C1 * 1.525;
446const BACK_C3: f32 = BACK_C1 + 1.0;
447
448#[inline]
450pub fn ease_in_back(t: f32) -> f32 {
451 BACK_C3 * t * t * t - BACK_C1 * t * t
452}
453
454#[inline]
456pub fn ease_out_back(t: f32) -> f32 {
457 let t = t - 1.0;
458 1.0 + BACK_C3 * t * t * t + BACK_C1 * t * t
459}
460
461#[inline]
463pub fn ease_in_out_back(t: f32) -> f32 {
464 if t < 0.5 {
465 (powi(2.0 * t, 2) * ((BACK_C2 + 1.0) * 2.0 * t - BACK_C2)) / 2.0
466 } else {
467 (powi(2.0 * t - 2.0, 2) * ((BACK_C2 + 1.0) * (2.0 * t - 2.0) + BACK_C2) + 2.0) / 2.0
468 }
469}
470
471const ELASTIC_C4: f32 = (2.0 * PI) / 3.0;
472const ELASTIC_C5: f32 = (2.0 * PI) / 4.5;
473
474#[inline]
476pub fn ease_in_elastic(t: f32) -> f32 {
477 if t == 0.0 {
478 return 0.0;
479 }
480 if t == 1.0 {
481 return 1.0;
482 }
483 -powf(2.0, 10.0 * t - 10.0) * sin((10.0 * t - 10.75) * ELASTIC_C4)
484}
485
486#[inline]
488pub fn ease_out_elastic(t: f32) -> f32 {
489 if t == 0.0 {
490 return 0.0;
491 }
492 if t == 1.0 {
493 return 1.0;
494 }
495 powf(2.0, -10.0 * t) * sin((10.0 * t - 0.75) * ELASTIC_C4) + 1.0
496}
497
498#[inline]
500pub fn ease_in_out_elastic(t: f32) -> f32 {
501 if t == 0.0 {
502 return 0.0;
503 }
504 if t == 1.0 {
505 return 1.0;
506 }
507 if t < 0.5 {
508 -(powf(2.0, 20.0 * t - 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0
509 } else {
510 (powf(2.0, -20.0 * t + 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0 + 1.0
511 }
512}
513
514#[inline]
516pub fn ease_out_bounce(t: f32) -> f32 {
517 const N1: f32 = 7.5625;
518 const D1: f32 = 2.75;
519 let t = &mut { t };
520 if *t < 1.0 / D1 {
521 N1 * *t * *t
522 } else if *t < 2.0 / D1 {
523 *t -= 1.5 / D1;
524 N1 * *t * *t + 0.75
525 } else if *t < 2.5 / D1 {
526 *t -= 2.25 / D1;
527 N1 * *t * *t + 0.9375
528 } else {
529 *t -= 2.625 / D1;
530 N1 * *t * *t + 0.984_375
531 }
532}
533
534#[inline]
536pub fn ease_in_bounce(t: f32) -> f32 {
537 1.0 - ease_out_bounce(1.0 - t)
538}
539
540#[inline]
542pub fn ease_in_out_bounce(t: f32) -> f32 {
543 if t < 0.5 {
544 (1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
545 } else {
546 (1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
547 }
548}
549
550#[inline]
556pub fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
557 let t = t.clamp(0.0, 1.0);
558 if t == 0.0 || t == 1.0 {
559 return t;
560 }
561
562 let x1 = x1.clamp(0.0, 1.0);
563 let x2 = x2.clamp(0.0, 1.0);
564 let mut u = t;
565
566 for _ in 0..6 {
567 let x = sample_cubic(x1, x2, u) - t;
568 if x.abs() < 1e-6 {
569 return sample_cubic(y1, y2, u);
570 }
571 let derivative = sample_cubic_derivative(x1, x2, u);
572 if derivative.abs() < 1e-6 {
573 break;
574 }
575 u = (u - x / derivative).clamp(0.0, 1.0);
576 }
577
578 let mut low = 0.0;
579 let mut high = 1.0;
580 u = t;
581 for _ in 0..10 {
582 let x = sample_cubic(x1, x2, u);
583 if (x - t).abs() < 1e-6 {
584 break;
585 }
586 if x < t {
587 low = u;
588 } else {
589 high = u;
590 }
591 u = (low + high) * 0.5;
592 }
593
594 sample_cubic(y1, y2, u)
595}
596
597#[inline]
601pub fn steps(t: f32, count: u32) -> f32 {
602 let t = t.clamp(0.0, 1.0);
603 if t == 0.0 {
604 return 0.0;
605 }
606 let count = count.max(1) as f32;
607 (ceil(t * count) / count).clamp(0.0, 1.0)
608}
609
610#[inline]
611fn sample_cubic(a1: f32, a2: f32, t: f32) -> f32 {
612 let c = 3.0 * a1;
613 let b = 3.0 * (a2 - a1) - c;
614 let a = 1.0 - c - b;
615 ((a * t + b) * t + c) * t
616}
617
618#[inline]
619fn sample_cubic_derivative(a1: f32, a2: f32, t: f32) -> f32 {
620 let c = 3.0 * a1;
621 let b = 3.0 * (a2 - a1) - c;
622 let a = 1.0 - c - b;
623 (3.0 * a * t + 2.0 * b) * t + c
624}
625
626#[cfg(test)]
631mod tests {
632 use super::*;
633
634 const EPSILON: f32 = 1e-5;
635
636 fn approx_eq(a: f32, b: f32) -> bool {
637 (a - b).abs() < EPSILON
638 }
639
640 #[test]
642 fn all_named_endpoints() {
643 for easing in Easing::all_named() {
644 let v0 = easing.apply(0.0);
645 let v1 = easing.apply(1.0);
646 assert!(
647 approx_eq(v0, 0.0),
648 "{:?}.apply(0.0) = {} (expected 0.0)",
649 easing,
650 v0
651 );
652 assert!(
653 approx_eq(v1, 1.0),
654 "{:?}.apply(1.0) = {} (expected 1.0)",
655 easing,
656 v1
657 );
658 }
659 }
660
661 #[test]
663 fn no_panic_out_of_range() {
664 for easing in Easing::all_named() {
665 let _ = easing.apply(-0.5);
666 let _ = easing.apply(1.5);
667 let _ = easing.apply(f32::INFINITY);
668 let _ = easing.apply(f32::NEG_INFINITY);
669 }
672 }
673
674 #[test]
676 fn custom_variant() {
677 let e = Easing::Custom(|t| t * t);
678 assert_eq!(e.apply(0.5), 0.25);
679 }
680
681 #[test]
682 fn custom_never_equals() {
683 let a = Easing::Custom(|t| t);
684 let b = Easing::Custom(|t| t);
685 let c = Easing::Linear;
686 assert!(a != b);
687 assert!(a != c);
688 assert!(c != a);
689 }
690
691 #[test]
693 fn named_equality() {
694 assert_eq!(Easing::Linear, Easing::Linear);
695 assert_eq!(Easing::EaseOutCubic, Easing::EaseOutCubic);
696 assert_eq!(
697 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
698 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0)
699 );
700 assert_eq!(Easing::Steps(4), Easing::Steps(4));
701 assert_ne!(Easing::EaseInQuad, Easing::EaseOutQuad);
702 assert_ne!(Easing::Steps(3), Easing::Steps(4));
703 }
704
705 #[test]
707 fn free_functions_match_enum() {
708 let cases: &[(Easing, fn(f32) -> f32)] = &[
709 (Easing::EaseInQuad, ease_in_quad),
710 (Easing::EaseOutQuad, ease_out_quad),
711 (Easing::EaseInCubic, ease_in_cubic),
712 (Easing::EaseOutCubic, ease_out_cubic),
713 (Easing::EaseInOutCubic, ease_in_out_cubic),
714 (Easing::EaseOutBounce, ease_out_bounce),
715 (Easing::EaseOutElastic, ease_out_elastic),
716 (Easing::EaseOutBack, ease_out_back),
717 ];
718 for t in [0.1, 0.25, 0.5, 0.75, 0.9] {
719 for (easing, f) in cases {
720 let a = easing.apply(t);
721 let b = f(t);
722 assert!(
723 approx_eq(a, b),
724 "{:?} at t={}: enum={} free_fn={}",
725 easing,
726 t,
727 a,
728 b
729 );
730 }
731 }
732 }
733
734 #[test]
736 fn ease_out_frontloaded() {
737 for t in [0.1_f32, 0.3, 0.5, 0.7] {
738 assert!(
739 Easing::EaseOutCubic.apply(t) > t,
740 "EaseOutCubic at t={} should be > t",
741 t
742 );
743 }
744 }
745
746 #[test]
748 fn linear_is_identity() {
749 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
750 assert_eq!(Easing::Linear.apply(t), t);
751 }
752 }
753
754 #[test]
755 fn cubic_bezier_linear_control_points_are_identity() {
756 let easing = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
757 for t in [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
758 assert!(
759 approx_eq(easing.apply(t), t),
760 "linear cubic-bezier at t={} was {}",
761 t,
762 easing.apply(t)
763 );
764 }
765 }
766
767 #[test]
768 fn cubic_bezier_css_ease_shape_is_frontloaded() {
769 let ease = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
770 assert_eq!(ease.apply(0.0), 0.0);
771 assert_eq!(ease.apply(1.0), 1.0);
772 assert!(ease.apply(0.5) > 0.5);
773 }
774
775 #[test]
776 fn cubic_bezier_clamps_invalid_x_control_points() {
777 let invalid = Easing::CubicBezier(-2.0, 0.0, 4.0, 1.0);
778 let clamped = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
779 assert!(approx_eq(invalid.apply(0.5), clamped.apply(0.5)));
780 }
781
782 #[test]
783 fn steps_jump_end_behavior() {
784 let easing = Easing::Steps(4);
785 assert_eq!(easing.apply(0.0), 0.0);
786 assert_eq!(easing.apply(0.01), 0.25);
787 assert_eq!(easing.apply(0.25), 0.25);
788 assert_eq!(easing.apply(0.26), 0.5);
789 assert_eq!(easing.apply(1.0), 1.0);
790 }
791
792 #[test]
793 fn steps_zero_count_is_one_step() {
794 assert_eq!(Easing::Steps(0).apply(0.5), 1.0);
795 }
796
797 #[test]
799 fn all_named_count() {
800 assert_eq!(Easing::all_named().len(), 33);
801 }
802}