1use crate::math::{cos, powf, powi, sin, sqrt};
26use core::f32::consts::PI;
27
28#[derive(Clone, Debug)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub enum Easing {
43 Linear,
45
46 EaseInQuad,
49 EaseOutQuad,
51 EaseInOutQuad,
53
54 EaseInCubic,
56 EaseOutCubic,
58 EaseInOutCubic,
60
61 EaseInQuart,
63 EaseOutQuart,
65 EaseInOutQuart,
67
68 EaseInQuint,
70 EaseOutQuint,
72 EaseInOutQuint,
74
75 EaseInSine,
78 EaseOutSine,
80 EaseInOutSine,
82
83 EaseInExpo,
86 EaseOutExpo,
88 EaseInOutExpo,
90
91 EaseInCirc,
94 EaseOutCirc,
96 EaseInOutCirc,
98
99 EaseInBack,
102 EaseOutBack,
104 EaseInOutBack,
106
107 EaseInElastic,
110 EaseOutElastic,
112 EaseInOutElastic,
114
115 EaseInBounce,
118 EaseOutBounce,
120 EaseInOutBounce,
122
123 #[cfg_attr(feature = "serde", serde(skip))]
134 Custom(fn(f32) -> f32),
135}
136
137impl PartialEq for Easing {
139 fn eq(&self, other: &Self) -> bool {
140 match (self, other) {
141 (Easing::Custom(_), _) | (_, Easing::Custom(_)) => false,
142 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
143 }
144 }
145}
146
147impl Easing {
148 #[inline]
165 pub fn apply(&self, t: f32) -> f32 {
166 match self {
167 Easing::Custom(f) => f(t),
168 _ => {
169 let t = t.clamp(0.0, 1.0);
170 match self {
171 Easing::Linear => t,
172 Easing::EaseInQuad => ease_in_quad(t),
173 Easing::EaseOutQuad => ease_out_quad(t),
174 Easing::EaseInOutQuad => ease_in_out_quad(t),
175 Easing::EaseInCubic => ease_in_cubic(t),
176 Easing::EaseOutCubic => ease_out_cubic(t),
177 Easing::EaseInOutCubic => ease_in_out_cubic(t),
178 Easing::EaseInQuart => ease_in_quart(t),
179 Easing::EaseOutQuart => ease_out_quart(t),
180 Easing::EaseInOutQuart => ease_in_out_quart(t),
181 Easing::EaseInQuint => ease_in_quint(t),
182 Easing::EaseOutQuint => ease_out_quint(t),
183 Easing::EaseInOutQuint => ease_in_out_quint(t),
184 Easing::EaseInSine => ease_in_sine(t),
185 Easing::EaseOutSine => ease_out_sine(t),
186 Easing::EaseInOutSine => ease_in_out_sine(t),
187 Easing::EaseInExpo => ease_in_expo(t),
188 Easing::EaseOutExpo => ease_out_expo(t),
189 Easing::EaseInOutExpo => ease_in_out_expo(t),
190 Easing::EaseInCirc => ease_in_circ(t),
191 Easing::EaseOutCirc => ease_out_circ(t),
192 Easing::EaseInOutCirc => ease_in_out_circ(t),
193 Easing::EaseInBack => ease_in_back(t),
194 Easing::EaseOutBack => ease_out_back(t),
195 Easing::EaseInOutBack => ease_in_out_back(t),
196 Easing::EaseInElastic => ease_in_elastic(t),
197 Easing::EaseOutElastic => ease_out_elastic(t),
198 Easing::EaseInOutElastic => ease_in_out_elastic(t),
199 Easing::EaseInBounce => ease_in_bounce(t),
200 Easing::EaseOutBounce => ease_out_bounce(t),
201 Easing::EaseInOutBounce => ease_in_out_bounce(t),
202 Easing::Custom(_) => unreachable!(),
203 }
204 }
205 }
206 }
207
208 pub fn all_named() -> &'static [Easing] {
220 &[
221 Easing::Linear,
222 Easing::EaseInQuad,
223 Easing::EaseOutQuad,
224 Easing::EaseInOutQuad,
225 Easing::EaseInCubic,
226 Easing::EaseOutCubic,
227 Easing::EaseInOutCubic,
228 Easing::EaseInQuart,
229 Easing::EaseOutQuart,
230 Easing::EaseInOutQuart,
231 Easing::EaseInQuint,
232 Easing::EaseOutQuint,
233 Easing::EaseInOutQuint,
234 Easing::EaseInSine,
235 Easing::EaseOutSine,
236 Easing::EaseInOutSine,
237 Easing::EaseInExpo,
238 Easing::EaseOutExpo,
239 Easing::EaseInOutExpo,
240 Easing::EaseInCirc,
241 Easing::EaseOutCirc,
242 Easing::EaseInOutCirc,
243 Easing::EaseInBack,
244 Easing::EaseOutBack,
245 Easing::EaseInOutBack,
246 Easing::EaseInElastic,
247 Easing::EaseOutElastic,
248 Easing::EaseInOutElastic,
249 Easing::EaseInBounce,
250 Easing::EaseOutBounce,
251 Easing::EaseInOutBounce,
252 ]
253 }
254}
255
256#[inline]
262pub fn ease_in_quad(t: f32) -> f32 {
263 t * t
264}
265
266#[inline]
268pub fn ease_out_quad(t: f32) -> f32 {
269 1.0 - (1.0 - t) * (1.0 - t)
270}
271
272#[inline]
274pub fn ease_in_out_quad(t: f32) -> f32 {
275 if t < 0.5 {
276 2.0 * t * t
277 } else {
278 1.0 - powi(-2.0 * t + 2.0, 2) / 2.0
279 }
280}
281
282#[inline]
284pub fn ease_in_cubic(t: f32) -> f32 {
285 t * t * t
286}
287
288#[inline]
290pub fn ease_out_cubic(t: f32) -> f32 {
291 1.0 - powi(1.0 - t, 3)
292}
293
294#[inline]
296pub fn ease_in_out_cubic(t: f32) -> f32 {
297 if t < 0.5 {
298 4.0 * t * t * t
299 } else {
300 1.0 - powi(-2.0 * t + 2.0, 3) / 2.0
301 }
302}
303
304#[inline]
306pub fn ease_in_quart(t: f32) -> f32 {
307 t * t * t * t
308}
309
310#[inline]
312pub fn ease_out_quart(t: f32) -> f32 {
313 1.0 - powi(1.0 - t, 4)
314}
315
316#[inline]
318pub fn ease_in_out_quart(t: f32) -> f32 {
319 if t < 0.5 {
320 8.0 * t * t * t * t
321 } else {
322 1.0 - powi(-2.0 * t + 2.0, 4) / 2.0
323 }
324}
325
326#[inline]
328pub fn ease_in_quint(t: f32) -> f32 {
329 t * t * t * t * t
330}
331
332#[inline]
334pub fn ease_out_quint(t: f32) -> f32 {
335 1.0 - powi(1.0 - t, 5)
336}
337
338#[inline]
340pub fn ease_in_out_quint(t: f32) -> f32 {
341 if t < 0.5 {
342 16.0 * t * t * t * t * t
343 } else {
344 1.0 - powi(-2.0 * t + 2.0, 5) / 2.0
345 }
346}
347
348#[inline]
350pub fn ease_in_sine(t: f32) -> f32 {
351 1.0 - cos(t * PI / 2.0)
352}
353#[inline]
355pub fn ease_out_sine(t: f32) -> f32 {
356 sin(t * PI / 2.0)
357}
358#[inline]
360pub fn ease_in_out_sine(t: f32) -> f32 {
361 -(cos(t * PI) - 1.0) / 2.0
362}
363
364#[inline]
366pub fn ease_in_expo(t: f32) -> f32 {
367 if t == 0.0 {
368 0.0
369 } else {
370 powf(2.0, 10.0 * t - 10.0)
371 }
372}
373
374#[inline]
376pub fn ease_out_expo(t: f32) -> f32 {
377 if t == 1.0 {
378 1.0
379 } else {
380 1.0 - powf(2.0, -10.0 * t)
381 }
382}
383
384#[inline]
386pub fn ease_in_out_expo(t: f32) -> f32 {
387 if t == 0.0 {
388 return 0.0;
389 }
390 if t == 1.0 {
391 return 1.0;
392 }
393 if t < 0.5 {
394 powf(2.0, 20.0 * t - 10.0) / 2.0
395 } else {
396 (2.0 - powf(2.0, -20.0 * t + 10.0)) / 2.0
397 }
398}
399
400#[inline]
402pub fn ease_in_circ(t: f32) -> f32 {
403 1.0 - sqrt(1.0 - t * t)
404}
405#[inline]
407pub fn ease_out_circ(t: f32) -> f32 {
408 sqrt(1.0 - (t - 1.0) * (t - 1.0))
409}
410
411#[inline]
413pub fn ease_in_out_circ(t: f32) -> f32 {
414 if t < 0.5 {
415 (1.0 - sqrt(1.0 - powi(2.0 * t, 2))) / 2.0
416 } else {
417 (sqrt(1.0 - powi(-2.0 * t + 2.0, 2)) + 1.0) / 2.0
418 }
419}
420
421const BACK_C1: f32 = 1.701_58;
422const BACK_C2: f32 = BACK_C1 * 1.525;
423const BACK_C3: f32 = BACK_C1 + 1.0;
424
425#[inline]
427pub fn ease_in_back(t: f32) -> f32 {
428 BACK_C3 * t * t * t - BACK_C1 * t * t
429}
430
431#[inline]
433pub fn ease_out_back(t: f32) -> f32 {
434 let t = t - 1.0;
435 1.0 + BACK_C3 * t * t * t + BACK_C1 * t * t
436}
437
438#[inline]
440pub fn ease_in_out_back(t: f32) -> f32 {
441 if t < 0.5 {
442 (powi(2.0 * t, 2) * ((BACK_C2 + 1.0) * 2.0 * t - BACK_C2)) / 2.0
443 } else {
444 (powi(2.0 * t - 2.0, 2) * ((BACK_C2 + 1.0) * (2.0 * t - 2.0) + BACK_C2) + 2.0) / 2.0
445 }
446}
447
448const ELASTIC_C4: f32 = (2.0 * PI) / 3.0;
449const ELASTIC_C5: f32 = (2.0 * PI) / 4.5;
450
451#[inline]
453pub fn ease_in_elastic(t: f32) -> f32 {
454 if t == 0.0 {
455 return 0.0;
456 }
457 if t == 1.0 {
458 return 1.0;
459 }
460 -powf(2.0, 10.0 * t - 10.0) * sin((10.0 * t - 10.75) * ELASTIC_C4)
461}
462
463#[inline]
465pub fn ease_out_elastic(t: f32) -> f32 {
466 if t == 0.0 {
467 return 0.0;
468 }
469 if t == 1.0 {
470 return 1.0;
471 }
472 powf(2.0, -10.0 * t) * sin((10.0 * t - 0.75) * ELASTIC_C4) + 1.0
473}
474
475#[inline]
477pub fn ease_in_out_elastic(t: f32) -> f32 {
478 if t == 0.0 {
479 return 0.0;
480 }
481 if t == 1.0 {
482 return 1.0;
483 }
484 if t < 0.5 {
485 -(powf(2.0, 20.0 * t - 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0
486 } else {
487 (powf(2.0, -20.0 * t + 10.0) * sin((20.0 * t - 11.125) * ELASTIC_C5)) / 2.0 + 1.0
488 }
489}
490
491#[inline]
493pub fn ease_out_bounce(t: f32) -> f32 {
494 const N1: f32 = 7.5625;
495 const D1: f32 = 2.75;
496 let t = &mut { t };
497 if *t < 1.0 / D1 {
498 N1 * *t * *t
499 } else if *t < 2.0 / D1 {
500 *t -= 1.5 / D1;
501 N1 * *t * *t + 0.75
502 } else if *t < 2.5 / D1 {
503 *t -= 2.25 / D1;
504 N1 * *t * *t + 0.9375
505 } else {
506 *t -= 2.625 / D1;
507 N1 * *t * *t + 0.984_375
508 }
509}
510
511#[inline]
513pub fn ease_in_bounce(t: f32) -> f32 {
514 1.0 - ease_out_bounce(1.0 - t)
515}
516
517#[inline]
519pub fn ease_in_out_bounce(t: f32) -> f32 {
520 if t < 0.5 {
521 (1.0 - ease_out_bounce(1.0 - 2.0 * t)) / 2.0
522 } else {
523 (1.0 + ease_out_bounce(2.0 * t - 1.0)) / 2.0
524 }
525}
526
527#[cfg(test)]
532mod tests {
533 use super::*;
534
535 const EPSILON: f32 = 1e-5;
536
537 fn approx_eq(a: f32, b: f32) -> bool {
538 (a - b).abs() < EPSILON
539 }
540
541 #[test]
543 fn all_named_endpoints() {
544 for easing in Easing::all_named() {
545 let v0 = easing.apply(0.0);
546 let v1 = easing.apply(1.0);
547 assert!(
548 approx_eq(v0, 0.0),
549 "{:?}.apply(0.0) = {} (expected 0.0)",
550 easing,
551 v0
552 );
553 assert!(
554 approx_eq(v1, 1.0),
555 "{:?}.apply(1.0) = {} (expected 1.0)",
556 easing,
557 v1
558 );
559 }
560 }
561
562 #[test]
564 fn no_panic_out_of_range() {
565 for easing in Easing::all_named() {
566 let _ = easing.apply(-0.5);
567 let _ = easing.apply(1.5);
568 let _ = easing.apply(f32::INFINITY);
569 let _ = easing.apply(f32::NEG_INFINITY);
570 }
573 }
574
575 #[test]
577 fn custom_variant() {
578 let e = Easing::Custom(|t| t * t);
579 assert_eq!(e.apply(0.5), 0.25);
580 }
581
582 #[test]
583 fn custom_never_equals() {
584 let a = Easing::Custom(|t| t);
585 let b = Easing::Custom(|t| t);
586 let c = Easing::Linear;
587 assert!(a != b);
588 assert!(a != c);
589 assert!(c != a);
590 }
591
592 #[test]
594 fn named_equality() {
595 assert_eq!(Easing::Linear, Easing::Linear);
596 assert_eq!(Easing::EaseOutCubic, Easing::EaseOutCubic);
597 assert_ne!(Easing::EaseInQuad, Easing::EaseOutQuad);
598 }
599
600 #[test]
602 fn free_functions_match_enum() {
603 let cases: &[(Easing, fn(f32) -> f32)] = &[
604 (Easing::EaseInQuad, ease_in_quad),
605 (Easing::EaseOutQuad, ease_out_quad),
606 (Easing::EaseInCubic, ease_in_cubic),
607 (Easing::EaseOutCubic, ease_out_cubic),
608 (Easing::EaseInOutCubic, ease_in_out_cubic),
609 (Easing::EaseOutBounce, ease_out_bounce),
610 (Easing::EaseOutElastic, ease_out_elastic),
611 (Easing::EaseOutBack, ease_out_back),
612 ];
613 for t in [0.1, 0.25, 0.5, 0.75, 0.9] {
614 for (easing, f) in cases {
615 let a = easing.apply(t);
616 let b = f(t);
617 assert!(
618 approx_eq(a, b),
619 "{:?} at t={}: enum={} free_fn={}",
620 easing,
621 t,
622 a,
623 b
624 );
625 }
626 }
627 }
628
629 #[test]
631 fn ease_out_frontloaded() {
632 for t in [0.1_f32, 0.3, 0.5, 0.7] {
633 assert!(
634 Easing::EaseOutCubic.apply(t) > t,
635 "EaseOutCubic at t={} should be > t",
636 t
637 );
638 }
639 }
640
641 #[test]
643 fn linear_is_identity() {
644 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
645 assert_eq!(Easing::Linear.apply(t), t);
646 }
647 }
648
649 #[test]
651 fn all_named_count() {
652 assert_eq!(Easing::all_named().len(), 31);
653 }
654}