1use crate::math::{ceil, cos, log, 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 RoughEase {
144 strength: f32,
146 points: u32,
148 },
149
150 SlowMo {
155 linear_ratio: f32,
157 power: f32,
159 },
160
161 Wiggle {
165 wiggles: u32,
167 },
168
169 CustomBounce {
173 strength: f32,
175 },
176
177 ExpoScale {
181 start: f32,
183 end: f32,
185 },
186
187 #[cfg_attr(feature = "serde", serde(skip))]
198 Custom(fn(f32) -> f32),
199}
200
201impl PartialEq for Easing {
203 fn eq(&self, other: &Self) -> bool {
204 use Easing::*;
205 match (self, other) {
206 (Custom(_), _) | (_, Custom(_)) => false,
207 (CubicBezier(ax1, ay1, ax2, ay2), CubicBezier(bx1, by1, bx2, by2)) => {
208 ax1 == bx1 && ay1 == by1 && ax2 == bx2 && ay2 == by2
209 }
210 (Steps(a), Steps(b)) => a == b,
211 (
212 RoughEase {
213 strength: sa,
214 points: pa,
215 },
216 RoughEase {
217 strength: sb,
218 points: pb,
219 },
220 ) => sa == sb && pa == pb,
221 (
222 SlowMo {
223 linear_ratio: la,
224 power: pa,
225 },
226 SlowMo {
227 linear_ratio: lb,
228 power: pb,
229 },
230 ) => la == lb && pa == pb,
231 (Wiggle { wiggles: a }, Wiggle { wiggles: b }) => a == b,
232 (CustomBounce { strength: a }, CustomBounce { strength: b }) => a == b,
233 (ExpoScale { start: sa, end: ea }, ExpoScale { start: sb, end: eb }) => {
234 sa == sb && ea == eb
235 }
236 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
237 }
238 }
239}
240
241impl Easing {
242 #[inline]
259 pub fn apply(&self, t: f32) -> f32 {
260 match self {
261 Easing::Custom(f) => f(t),
262 _ => {
263 let t = t.clamp(0.0, 1.0);
264 match self {
265 Easing::Linear => t,
266 Easing::EaseInQuad => ease_in_quad(t),
267 Easing::EaseOutQuad => ease_out_quad(t),
268 Easing::EaseInOutQuad => ease_in_out_quad(t),
269 Easing::EaseInCubic => ease_in_cubic(t),
270 Easing::EaseOutCubic => ease_out_cubic(t),
271 Easing::EaseInOutCubic => ease_in_out_cubic(t),
272 Easing::EaseInQuart => ease_in_quart(t),
273 Easing::EaseOutQuart => ease_out_quart(t),
274 Easing::EaseInOutQuart => ease_in_out_quart(t),
275 Easing::EaseInQuint => ease_in_quint(t),
276 Easing::EaseOutQuint => ease_out_quint(t),
277 Easing::EaseInOutQuint => ease_in_out_quint(t),
278 Easing::EaseInSine => ease_in_sine(t),
279 Easing::EaseOutSine => ease_out_sine(t),
280 Easing::EaseInOutSine => ease_in_out_sine(t),
281 Easing::EaseInExpo => ease_in_expo(t),
282 Easing::EaseOutExpo => ease_out_expo(t),
283 Easing::EaseInOutExpo => ease_in_out_expo(t),
284 Easing::EaseInCirc => ease_in_circ(t),
285 Easing::EaseOutCirc => ease_out_circ(t),
286 Easing::EaseInOutCirc => ease_in_out_circ(t),
287 Easing::EaseInBack => ease_in_back(t),
288 Easing::EaseOutBack => ease_out_back(t),
289 Easing::EaseInOutBack => ease_in_out_back(t),
290 Easing::EaseInElastic => ease_in_elastic(t),
291 Easing::EaseOutElastic => ease_out_elastic(t),
292 Easing::EaseInOutElastic => ease_in_out_elastic(t),
293 Easing::EaseInBounce => ease_in_bounce(t),
294 Easing::EaseOutBounce => ease_out_bounce(t),
295 Easing::EaseInOutBounce => ease_in_out_bounce(t),
296 Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(t, *x1, *y1, *x2, *y2),
297 Easing::Steps(count) => steps(t, *count),
298 Easing::RoughEase { strength, points } => rough_ease(t, *strength, *points),
299 Easing::SlowMo {
300 linear_ratio,
301 power,
302 } => slow_mo(t, *linear_ratio, *power),
303 Easing::Wiggle { wiggles } => wiggle(t, *wiggles),
304 Easing::CustomBounce { strength } => custom_bounce(t, *strength),
305 Easing::ExpoScale { start, end } => expo_scale(t, *start, *end),
306 Easing::Custom(_) => unreachable!(),
307 }
308 }
309 }
310 }
311
312 pub fn all_named() -> &'static [Easing] {
324 &[
325 Easing::Linear,
326 Easing::EaseInQuad,
327 Easing::EaseOutQuad,
328 Easing::EaseInOutQuad,
329 Easing::EaseInCubic,
330 Easing::EaseOutCubic,
331 Easing::EaseInOutCubic,
332 Easing::EaseInQuart,
333 Easing::EaseOutQuart,
334 Easing::EaseInOutQuart,
335 Easing::EaseInQuint,
336 Easing::EaseOutQuint,
337 Easing::EaseInOutQuint,
338 Easing::EaseInSine,
339 Easing::EaseOutSine,
340 Easing::EaseInOutSine,
341 Easing::EaseInExpo,
342 Easing::EaseOutExpo,
343 Easing::EaseInOutExpo,
344 Easing::EaseInCirc,
345 Easing::EaseOutCirc,
346 Easing::EaseInOutCirc,
347 Easing::EaseInBack,
348 Easing::EaseOutBack,
349 Easing::EaseInOutBack,
350 Easing::EaseInElastic,
351 Easing::EaseOutElastic,
352 Easing::EaseInOutElastic,
353 Easing::EaseInBounce,
354 Easing::EaseOutBounce,
355 Easing::EaseInOutBounce,
356 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
357 Easing::Steps(1),
358 Easing::RoughEase {
360 strength: 0.5,
361 points: 8,
362 },
363 Easing::SlowMo {
364 linear_ratio: 0.5,
365 power: 0.7,
366 },
367 Easing::Wiggle { wiggles: 5 },
368 Easing::CustomBounce { strength: 0.7 },
369 Easing::ExpoScale {
370 start: 0.5,
371 end: 2.0,
372 },
373 ]
374 }
375}
376
377#[inline]
383pub fn ease_in_quad(t: f32) -> f32 {
384 t * t
385}
386
387#[inline]
389pub fn ease_out_quad(t: f32) -> f32 {
390 1.0 - (1.0 - t) * (1.0 - t)
391}
392
393#[inline]
395pub fn ease_in_out_quad(t: f32) -> f32 {
396 if t < 0.5 {
397 2.0 * t * t
398 } else {
399 1.0 - powi(-2.0 * t + 2.0, 2) / 2.0
400 }
401}
402
403#[inline]
405pub fn ease_in_cubic(t: f32) -> f32 {
406 t * t * t
407}
408
409#[inline]
411pub fn ease_out_cubic(t: f32) -> f32 {
412 1.0 - powi(1.0 - t, 3)
413}
414
415#[inline]
417pub fn ease_in_out_cubic(t: f32) -> f32 {
418 if t < 0.5 {
419 4.0 * t * t * t
420 } else {
421 1.0 - powi(-2.0 * t + 2.0, 3) / 2.0
422 }
423}
424
425#[inline]
427pub fn ease_in_quart(t: f32) -> f32 {
428 t * t * t * t
429}
430
431#[inline]
433pub fn ease_out_quart(t: f32) -> f32 {
434 1.0 - powi(1.0 - t, 4)
435}
436
437#[inline]
439pub fn ease_in_out_quart(t: f32) -> f32 {
440 if t < 0.5 {
441 8.0 * t * t * t * t
442 } else {
443 1.0 - powi(-2.0 * t + 2.0, 4) / 2.0
444 }
445}
446
447#[inline]
449pub fn ease_in_quint(t: f32) -> f32 {
450 t * t * t * t * t
451}
452
453#[inline]
455pub fn ease_out_quint(t: f32) -> f32 {
456 1.0 - powi(1.0 - t, 5)
457}
458
459#[inline]
461pub fn ease_in_out_quint(t: f32) -> f32 {
462 if t < 0.5 {
463 16.0 * t * t * t * t * t
464 } else {
465 1.0 - powi(-2.0 * t + 2.0, 5) / 2.0
466 }
467}
468
469#[inline]
471pub fn ease_in_sine(t: f32) -> f32 {
472 1.0 - cos(t * PI / 2.0)
473}
474#[inline]
476pub fn ease_out_sine(t: f32) -> f32 {
477 sin(t * PI / 2.0)
478}
479#[inline]
481pub fn ease_in_out_sine(t: f32) -> f32 {
482 -(cos(t * PI) - 1.0) / 2.0
483}
484
485#[inline]
487pub fn ease_in_expo(t: f32) -> f32 {
488 if t == 0.0 {
489 0.0
490 } else {
491 powf(2.0, 10.0 * t - 10.0)
492 }
493}
494
495#[inline]
497pub fn ease_out_expo(t: f32) -> f32 {
498 if t == 1.0 {
499 1.0
500 } else {
501 1.0 - powf(2.0, -10.0 * t)
502 }
503}
504
505#[inline]
507pub fn ease_in_out_expo(t: f32) -> f32 {
508 if t == 0.0 {
509 return 0.0;
510 }
511 if t == 1.0 {
512 return 1.0;
513 }
514 if t < 0.5 {
515 powf(2.0, 20.0 * t - 10.0) / 2.0
516 } else {
517 (2.0 - powf(2.0, -20.0 * t + 10.0)) / 2.0
518 }
519}
520
521#[inline]
523pub fn ease_in_circ(t: f32) -> f32 {
524 1.0 - sqrt(1.0 - t * t)
525}
526#[inline]
528pub fn ease_out_circ(t: f32) -> f32 {
529 sqrt(1.0 - (t - 1.0) * (t - 1.0))
530}
531
532#[inline]
534pub fn ease_in_out_circ(t: f32) -> f32 {
535 if t < 0.5 {
536 (1.0 - sqrt(1.0 - powi(2.0 * t, 2))) / 2.0
537 } else {
538 (sqrt(1.0 - powi(-2.0 * t + 2.0, 2)) + 1.0) / 2.0
539 }
540}
541
542const BACK_C1: f32 = 1.701_58;
543const BACK_C2: f32 = BACK_C1 * 1.525;
544const BACK_C3: f32 = BACK_C1 + 1.0;
545
546#[inline]
548pub fn ease_in_back(t: f32) -> f32 {
549 BACK_C3 * t * t * t - BACK_C1 * t * t
550}
551
552#[inline]
554pub fn ease_out_back(t: f32) -> f32 {
555 let t = t - 1.0;
556 1.0 + BACK_C3 * t * t * t + BACK_C1 * t * t
557}
558
559#[inline]
561pub fn ease_in_out_back(t: f32) -> f32 {
562 if t < 0.5 {
563 (powi(2.0 * t, 2) * ((BACK_C2 + 1.0) * 2.0 * t - BACK_C2)) / 2.0
564 } else {
565 (powi(2.0 * t - 2.0, 2) * ((BACK_C2 + 1.0) * (2.0 * t - 2.0) + BACK_C2) + 2.0) / 2.0
566 }
567}
568
569const ELASTIC_C4: f32 = (2.0 * PI) / 3.0;
570const ELASTIC_C5: f32 = (2.0 * PI) / 4.5;
571
572#[inline]
574pub fn ease_in_elastic(t: f32) -> f32 {
575 if t == 0.0 {
576 return 0.0;
577 }
578 if t == 1.0 {
579 return 1.0;
580 }
581 -powf(2.0, 10.0 * t - 10.0) * sin((10.0 * t - 10.75) * ELASTIC_C4)
582}
583
584#[inline]
586pub fn ease_out_elastic(t: f32) -> f32 {
587 if t == 0.0 {
588 return 0.0;
589 }
590 if t == 1.0 {
591 return 1.0;
592 }
593 powf(2.0, -10.0 * t) * sin((10.0 * t - 0.75) * ELASTIC_C4) + 1.0
594}
595
596#[inline]
598pub fn ease_in_out_elastic(t: f32) -> f32 {
599 if t == 0.0 {
600 return 0.0;
601 }
602 if t == 1.0 {
603 return 1.0;
604 }
605 if t < 0.5 {
606 -(powf(2.0, 20.0 * t - 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0
607 } else {
608 (powf(2.0, -20.0 * t + 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0 + 1.0
609 }
610}
611
612#[inline]
614pub fn ease_out_bounce(t: f32) -> f32 {
615 const N1: f32 = 7.5625;
616 const D1: f32 = 2.75;
617 let t = &mut { t };
618 if *t < 1.0 / D1 {
619 N1 * *t * *t
620 } else if *t < 2.0 / D1 {
621 *t -= 1.5 / D1;
622 N1 * *t * *t + 0.75
623 } else if *t < 2.5 / D1 {
624 *t -= 2.25 / D1;
625 N1 * *t * *t + 0.9375
626 } else {
627 *t -= 2.625 / D1;
628 N1 * *t * *t + 0.984_375
629 }
630}
631
632#[inline]
634pub fn ease_in_bounce(t: f32) -> f32 {
635 1.0 - ease_out_bounce(1.0 - t)
636}
637
638#[inline]
640pub fn ease_in_out_bounce(t: f32) -> f32 {
641 if t < 0.5 {
642 (1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
643 } else {
644 (1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
645 }
646}
647
648#[inline]
654pub fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
655 let t = t.clamp(0.0, 1.0);
656 if t == 0.0 || t == 1.0 {
657 return t;
658 }
659
660 let x1 = x1.clamp(0.0, 1.0);
661 let x2 = x2.clamp(0.0, 1.0);
662 let mut u = t;
663
664 for _ in 0..6 {
665 let x = sample_cubic(x1, x2, u) - t;
666 if x.abs() < 1e-6 {
667 return sample_cubic(y1, y2, u);
668 }
669 let derivative = sample_cubic_derivative(x1, x2, u);
670 if derivative.abs() < 1e-6 {
671 break;
672 }
673 u = (u - x / derivative).clamp(0.0, 1.0);
674 }
675
676 let mut low = 0.0;
677 let mut high = 1.0;
678 u = t;
679 for _ in 0..10 {
680 let x = sample_cubic(x1, x2, u);
681 if (x - t).abs() < 1e-6 {
682 break;
683 }
684 if x < t {
685 low = u;
686 } else {
687 high = u;
688 }
689 u = (low + high) * 0.5;
690 }
691
692 sample_cubic(y1, y2, u)
693}
694
695#[inline]
699pub fn steps(t: f32, count: u32) -> f32 {
700 let t = t.clamp(0.0, 1.0);
701 if t == 0.0 {
702 return 0.0;
703 }
704 let count = count.max(1) as f32;
705 (ceil(t * count) / count).clamp(0.0, 1.0)
706}
707
708#[inline]
721pub fn rough_ease(t: f32, strength: f32, points: u32) -> f32 {
722 if t <= 0.0 {
723 return 0.0;
724 }
725 if t >= 1.0 {
726 return 1.0;
727 }
728
729 let n = points.clamp(2, 20);
730 let boundary = 4.0 * t * (1.0 - t);
732 let mut noise = 0.0_f32;
733 for i in 1..=n {
734 let freq = i as f32 * PI;
735 noise += sin(freq * t) / i as f32;
736 }
737 let norm = log(n as f32).max(1.0);
739 noise /= norm;
740
741 t + boundary * strength.clamp(0.0, 2.0) * noise
742}
743
744#[inline]
755pub fn slow_mo(t: f32, linear_ratio: f32, power: f32) -> f32 {
756 if t <= 0.0 {
757 return 0.0;
758 }
759 if t >= 1.0 {
760 return 1.0;
761 }
762
763 let lr = linear_ratio.clamp(0.0, 1.0);
764 let p = power.max(0.0);
765
766 if lr >= 1.0 {
767 return t;
768 }
769
770 let t_mid_start = (1.0 - lr) * 0.5;
772 let t_mid_end = t_mid_start + lr;
773
774 let s_mid = 1.0 / (1.0 + p * (1.0 - lr));
780 let s_edge = if (1.0 - lr) > f32::EPSILON {
781 (1.0 - s_mid * lr) / (1.0 - lr)
782 } else {
783 1.0
784 };
785
786 if t < t_mid_start {
787 t * s_edge
789 } else if t > t_mid_end {
790 let out_at_mid_end = t_mid_start * s_edge + lr * s_mid;
792 out_at_mid_end + (t - t_mid_end) * s_edge
793 } else {
794 t_mid_start * s_edge + (t - t_mid_start) * s_mid
796 }
797}
798
799#[inline]
810pub fn wiggle(t: f32, wiggles: u32) -> f32 {
811 if t <= 0.0 {
812 return 0.0;
813 }
814 if t >= 1.0 {
815 return 1.0;
816 }
817 let n = wiggles.max(1) as f32;
818 let envelope = sin(t * PI);
820 let oscillation = sin(t * n * PI * 2.0) * envelope;
821 t + oscillation * 0.25
822}
823
824#[inline]
834pub fn custom_bounce(t: f32, strength: f32) -> f32 {
835 if t <= 0.0 {
836 return 0.0;
837 }
838 if t >= 1.0 {
839 return 1.0;
840 }
841 let s = strength.clamp(0.0, 1.0);
842 t * (1.0 - s) + ease_out_bounce(t) * s
844}
845
846#[inline]
857pub fn expo_scale(t: f32, start: f32, end: f32) -> f32 {
858 if t <= 0.0 {
859 return 0.0;
860 }
861 if t >= 1.0 {
862 return 1.0;
863 }
864
865 let s = start.max(0.001_f32);
866 let e = end.max(0.001_f32);
867
868 if (s - e).abs() < 0.001 {
870 return t;
871 }
872
873 let k = e / s;
874 if (k - 1.0).abs() < 0.001 {
875 return t;
876 }
877
878 (powf(k, t) - 1.0) / (k - 1.0)
881}
882
883#[inline]
884fn sample_cubic(a1: f32, a2: f32, t: f32) -> f32 {
885 let c = 3.0 * a1;
886 let b = 3.0 * (a2 - a1) - c;
887 let a = 1.0 - c - b;
888 ((a * t + b) * t + c) * t
889}
890
891#[inline]
892fn sample_cubic_derivative(a1: f32, a2: f32, t: f32) -> f32 {
893 let c = 3.0 * a1;
894 let b = 3.0 * (a2 - a1) - c;
895 let a = 1.0 - c - b;
896 (3.0 * a * t + 2.0 * b) * t + c
897}
898
899#[cfg(test)]
904mod tests {
905 use super::*;
906
907 const EPSILON: f32 = 1e-5;
908
909 fn approx_eq(a: f32, b: f32) -> bool {
910 (a - b).abs() < EPSILON
911 }
912
913 #[test]
915 fn all_named_endpoints() {
916 for easing in Easing::all_named() {
917 let v0 = easing.apply(0.0);
918 let v1 = easing.apply(1.0);
919 assert!(
920 approx_eq(v0, 0.0),
921 "{:?}.apply(0.0) = {} (expected 0.0)",
922 easing,
923 v0
924 );
925 assert!(
926 approx_eq(v1, 1.0),
927 "{:?}.apply(1.0) = {} (expected 1.0)",
928 easing,
929 v1
930 );
931 }
932 }
933
934 #[test]
936 fn no_panic_out_of_range() {
937 for easing in Easing::all_named() {
938 let _ = easing.apply(-0.5);
939 let _ = easing.apply(1.5);
940 let _ = easing.apply(f32::INFINITY);
941 let _ = easing.apply(f32::NEG_INFINITY);
942 }
945 }
946
947 #[test]
948 fn all_named_count() {
949 assert_eq!(Easing::all_named().len(), 38);
950 }
951
952 #[test]
953 fn custom_variant_applies_fn() {
954 let e = Easing::Custom(|t| t * t);
955 assert_eq!(e.apply(0.5), 0.25);
956 }
957
958 #[test]
959 fn custom_never_equals() {
960 let a = Easing::Custom(|t| t);
961 let b = Easing::Custom(|t| t);
962 assert!(a != b);
963 assert!(a != Easing::Linear);
964 }
965
966 #[test]
967 fn named_equality() {
968 assert_eq!(Easing::Linear, Easing::Linear);
969 assert_eq!(Easing::EaseOutCubic, Easing::EaseOutCubic);
970 assert_eq!(
971 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0),
972 Easing::CubicBezier(0.25, 0.1, 0.25, 1.0)
973 );
974 assert_eq!(Easing::Steps(4), Easing::Steps(4));
975 assert_ne!(Easing::EaseInQuad, Easing::EaseOutQuad);
976 assert_eq!(
978 Easing::RoughEase {
979 strength: 0.5,
980 points: 8
981 },
982 Easing::RoughEase {
983 strength: 0.5,
984 points: 8
985 }
986 );
987 assert_ne!(
988 Easing::RoughEase {
989 strength: 0.5,
990 points: 8
991 },
992 Easing::RoughEase {
993 strength: 0.5,
994 points: 4
995 }
996 );
997 assert_eq!(Easing::Wiggle { wiggles: 5 }, Easing::Wiggle { wiggles: 5 });
998 assert_eq!(
999 Easing::CustomBounce { strength: 0.7 },
1000 Easing::CustomBounce { strength: 0.7 }
1001 );
1002 assert_eq!(
1003 Easing::ExpoScale {
1004 start: 0.5,
1005 end: 2.0
1006 },
1007 Easing::ExpoScale {
1008 start: 0.5,
1009 end: 2.0
1010 }
1011 );
1012 }
1013
1014 #[test]
1015 fn rough_ease_monotonic_bias() {
1016 let sum: f32 = (1..10).map(|i| rough_ease(i as f32 / 10.0, 0.3, 6)).sum();
1018 assert!(sum > 0.0, "rough ease should have positive trend");
1019 }
1020
1021 #[test]
1022 fn slow_mo_middle_is_slow() {
1023 let dt = 0.01_f32;
1025 let mid_vel = (slow_mo(0.5 + dt, 0.5, 1.0) - slow_mo(0.5, 0.5, 1.0)) / dt;
1026 let edge_vel = (slow_mo(0.05 + dt, 0.5, 1.0) - slow_mo(0.05, 0.5, 1.0)) / dt;
1027 assert!(
1028 mid_vel < edge_vel,
1029 "middle should be slower than edges: mid={mid_vel}, edge={edge_vel}"
1030 );
1031 }
1032
1033 #[test]
1034 fn slow_mo_zero_linear_ratio() {
1035 assert!(approx_eq(slow_mo(0.0, 0.0, 1.0), 0.0));
1037 assert!(approx_eq(slow_mo(1.0, 0.0, 1.0), 1.0));
1038 }
1039
1040 #[test]
1041 fn wiggle_stays_finite() {
1042 for i in 0..=100 {
1043 let t = i as f32 / 100.0;
1044 let v = wiggle(t, 5);
1045 assert!(v.is_finite(), "wiggle at t={t} produced non-finite {v}");
1046 }
1047 }
1048
1049 #[test]
1050 fn custom_bounce_blends_correctly() {
1051 for i in 1..10 {
1053 let t = i as f32 / 10.0;
1054 assert!(
1055 approx_eq(custom_bounce(t, 0.0), t),
1056 "strength=0 should be linear at t={t}"
1057 );
1058 }
1059 for i in 1..10 {
1061 let t = i as f32 / 10.0;
1062 assert!(
1063 approx_eq(custom_bounce(t, 1.0), ease_out_bounce(t)),
1064 "strength=1 should equal ease_out_bounce at t={t}"
1065 );
1066 }
1067 }
1068
1069 #[test]
1070 fn expo_scale_is_monotonic() {
1071 let mut prev = 0.0_f32;
1072 for i in 1..=20 {
1073 let t = i as f32 / 20.0;
1074 let v = expo_scale(t, 0.5, 2.0);
1075 assert!(v >= prev - 1e-5, "expo_scale should be monotonic at t={t}");
1076 prev = v;
1077 }
1078 }
1079
1080 #[test]
1081 fn expo_scale_equal_start_end_is_linear() {
1082 for i in 1..10 {
1083 let t = i as f32 / 10.0;
1084 assert!(approx_eq(expo_scale(t, 1.0, 1.0), t));
1085 }
1086 }
1087
1088 #[test]
1089 fn free_functions_match_enum() {
1090 type EasingCase = (Easing, fn(f32) -> f32);
1091
1092 let cases: &[EasingCase] = &[
1093 (Easing::EaseInQuad, ease_in_quad),
1094 (Easing::EaseOutCubic, ease_out_cubic),
1095 (Easing::EaseOutBounce, ease_out_bounce),
1096 ];
1097 for t in [0.1, 0.5, 0.9] {
1098 for (easing, f) in cases {
1099 let a = easing.apply(t);
1100 let b = f(t);
1101 assert!(
1102 approx_eq(a, b),
1103 "{:?} at t={}: enum={} free_fn={}",
1104 easing,
1105 t,
1106 a,
1107 b
1108 );
1109 }
1110 }
1111 }
1112
1113 #[test]
1114 fn cubic_bezier_linear_is_identity() {
1115 let easing = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
1116 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
1117 assert!(
1118 approx_eq(easing.apply(t), t),
1119 "linear cubic-bezier at t={t} was {}",
1120 easing.apply(t)
1121 );
1122 }
1123 }
1124
1125 #[test]
1126 fn steps_jump_end_behavior() {
1127 let easing = Easing::Steps(4);
1128 assert_eq!(easing.apply(0.0), 0.0);
1129 assert_eq!(easing.apply(0.01), 0.25);
1130 assert_eq!(easing.apply(1.0), 1.0);
1131 }
1132
1133 #[test]
1134 fn linear_is_identity() {
1135 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
1136 assert_eq!(Easing::Linear.apply(t), t);
1137 }
1138 }
1139}