1use std::f32::consts::{FRAC_PI_2, TAU};
2
3pub use crate::motion::{
4 cubic_in, cubic_in_out, cubic_out, quad_in, quad_out, smootherstep, smoothstep,
5};
6
7const BACK: f32 = 1.70158;
8const BACK_IN_OUT: f32 = BACK * 1.525;
9const ELASTIC: f32 = 3.0;
10const ELASTIC_IN_OUT: f32 = 4.5;
11
12fn shaped(t: f32, curve: impl Fn(f32) -> f32) -> f32 {
13 let t = t.clamp(0.0, 1.0);
14 if t <= 0.0 {
15 0.0
16 } else if t >= 1.0 {
17 1.0
18 } else {
19 curve(t)
20 }
21}
22
23fn folded(t: f32, rising: impl Fn(f32) -> f32) -> f32 {
24 let t = t.clamp(0.0, 1.0);
25 let u = if t < 0.5 { t } else { 1.0 - t };
26 let y = rising(2.0 * u) * 0.5;
27 if t < 0.5 { y } else { 1.0 - y }
28}
29
30pub fn linear(t: f32) -> f32 {
31 t.clamp(0.0, 1.0)
32}
33
34pub fn sine_in(t: f32) -> f32 {
35 shaped(t, |t| 1.0 - (t * FRAC_PI_2).cos())
36}
37
38pub fn sine_out(t: f32) -> f32 {
39 shaped(t, |t| (t * FRAC_PI_2).sin())
40}
41
42pub fn sine_in_out(t: f32) -> f32 {
43 folded(t, sine_in)
44}
45
46pub fn quad_in_out(t: f32) -> f32 {
47 folded(t, quad_in)
48}
49
50pub fn quart_in(t: f32) -> f32 {
51 shaped(t, |t| {
52 let s = t * t;
53 s * s
54 })
55}
56
57pub fn quart_out(t: f32) -> f32 {
58 shaped(t, |t| {
59 let u = 1.0 - t;
60 let s = u * u;
61 1.0 - s * s
62 })
63}
64
65pub fn quart_in_out(t: f32) -> f32 {
66 folded(t, quart_in)
67}
68
69pub fn quint_in(t: f32) -> f32 {
70 shaped(t, |t| {
71 let s = t * t;
72 s * s * t
73 })
74}
75
76pub fn quint_out(t: f32) -> f32 {
77 shaped(t, |t| {
78 let u = 1.0 - t;
79 let s = u * u;
80 1.0 - s * s * u
81 })
82}
83
84pub fn quint_in_out(t: f32) -> f32 {
85 folded(t, quint_in)
86}
87
88pub fn expo_in(t: f32) -> f32 {
89 shaped(t, |t| (10.0 * t - 10.0).exp2())
90}
91
92pub fn expo_out(t: f32) -> f32 {
93 shaped(t, |t| 1.0 - (-10.0 * t).exp2())
94}
95
96pub fn expo_in_out(t: f32) -> f32 {
97 folded(t, expo_in)
98}
99
100pub fn circ_in(t: f32) -> f32 {
101 shaped(t, |t| 1.0 - (1.0 - t * t).sqrt())
102}
103
104pub fn circ_out(t: f32) -> f32 {
105 shaped(t, |t| {
106 let u = t - 1.0;
107 (1.0 - u * u).sqrt()
108 })
109}
110
111pub fn circ_in_out(t: f32) -> f32 {
112 folded(t, circ_in)
113}
114
115fn back_in_with(t: f32, overshoot: f32) -> f32 {
116 shaped(t, |t| t * t * ((overshoot + 1.0) * t - overshoot))
117}
118
119pub fn back_in(t: f32) -> f32 {
120 back_in_with(t, BACK)
121}
122
123pub fn back_out(t: f32) -> f32 {
124 shaped(t, |t| {
125 let u = t - 1.0;
126 1.0 + u * u * ((BACK + 1.0) * u + BACK)
127 })
128}
129
130pub fn back_in_out(t: f32) -> f32 {
131 folded(t, |t| back_in_with(t, BACK_IN_OUT))
132}
133
134fn elastic_in_with(t: f32, period: f32) -> f32 {
135 shaped(t, |t| {
136 let phase = (10.0 * t - 10.0 - period * 0.25) * (TAU / period);
137 -(10.0 * t - 10.0).exp2() * phase.sin()
138 })
139}
140
141pub fn elastic_in(t: f32) -> f32 {
142 elastic_in_with(t, ELASTIC)
143}
144
145pub fn elastic_out(t: f32) -> f32 {
146 1.0 - elastic_in(1.0 - t.clamp(0.0, 1.0))
147}
148
149pub fn elastic_in_out(t: f32) -> f32 {
150 folded(t, |t| elastic_in_with(t, ELASTIC_IN_OUT))
151}
152
153pub fn bounce_out(t: f32) -> f32 {
154 shaped(t, |t| {
155 const N: f32 = 7.5625;
156 const D: f32 = 2.75;
157 if t < 1.0 / D {
158 N * t * t
159 } else if t < 2.0 / D {
160 let t = t - 1.5 / D;
161 N * t * t + 0.75
162 } else if t < 2.5 / D {
163 let t = t - 2.25 / D;
164 N * t * t + 0.9375
165 } else {
166 let t = t - 2.625 / D;
167 N * t * t + 0.984375
168 }
169 })
170}
171
172pub fn bounce_in(t: f32) -> f32 {
173 1.0 - bounce_out(1.0 - t.clamp(0.0, 1.0))
174}
175
176pub fn bounce_in_out(t: f32) -> f32 {
177 folded(t, bounce_in)
178}
179
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
181pub enum Ease {
182 #[default]
183 Linear,
184 SineIn,
185 SineOut,
186 SineInOut,
187 QuadIn,
188 QuadOut,
189 QuadInOut,
190 CubicIn,
191 CubicOut,
192 CubicInOut,
193 QuartIn,
194 QuartOut,
195 QuartInOut,
196 QuintIn,
197 QuintOut,
198 QuintInOut,
199 ExpoIn,
200 ExpoOut,
201 ExpoInOut,
202 CircIn,
203 CircOut,
204 CircInOut,
205 BackIn,
206 BackOut,
207 BackInOut,
208 ElasticIn,
209 ElasticOut,
210 ElasticInOut,
211 BounceIn,
212 BounceOut,
213 BounceInOut,
214}
215
216type Curve = fn(f32) -> f32;
217
218const TABLE: [(Ease, &str, Curve); 31] = [
219 (Ease::Linear, "linear", linear),
220 (Ease::SineIn, "sine_in", sine_in),
221 (Ease::SineOut, "sine_out", sine_out),
222 (Ease::SineInOut, "sine_in_out", sine_in_out),
223 (Ease::QuadIn, "quad_in", quad_in),
224 (Ease::QuadOut, "quad_out", quad_out),
225 (Ease::QuadInOut, "quad_in_out", quad_in_out),
226 (Ease::CubicIn, "cubic_in", cubic_in),
227 (Ease::CubicOut, "cubic_out", cubic_out),
228 (Ease::CubicInOut, "cubic_in_out", cubic_in_out),
229 (Ease::QuartIn, "quart_in", quart_in),
230 (Ease::QuartOut, "quart_out", quart_out),
231 (Ease::QuartInOut, "quart_in_out", quart_in_out),
232 (Ease::QuintIn, "quint_in", quint_in),
233 (Ease::QuintOut, "quint_out", quint_out),
234 (Ease::QuintInOut, "quint_in_out", quint_in_out),
235 (Ease::ExpoIn, "expo_in", expo_in),
236 (Ease::ExpoOut, "expo_out", expo_out),
237 (Ease::ExpoInOut, "expo_in_out", expo_in_out),
238 (Ease::CircIn, "circ_in", circ_in),
239 (Ease::CircOut, "circ_out", circ_out),
240 (Ease::CircInOut, "circ_in_out", circ_in_out),
241 (Ease::BackIn, "back_in", back_in),
242 (Ease::BackOut, "back_out", back_out),
243 (Ease::BackInOut, "back_in_out", back_in_out),
244 (Ease::ElasticIn, "elastic_in", elastic_in),
245 (Ease::ElasticOut, "elastic_out", elastic_out),
246 (Ease::ElasticInOut, "elastic_in_out", elastic_in_out),
247 (Ease::BounceIn, "bounce_in", bounce_in),
248 (Ease::BounceOut, "bounce_out", bounce_out),
249 (Ease::BounceInOut, "bounce_in_out", bounce_in_out),
250];
251
252impl Ease {
253 pub const ALL: [Ease; 31] = [
254 Ease::Linear,
255 Ease::SineIn,
256 Ease::SineOut,
257 Ease::SineInOut,
258 Ease::QuadIn,
259 Ease::QuadOut,
260 Ease::QuadInOut,
261 Ease::CubicIn,
262 Ease::CubicOut,
263 Ease::CubicInOut,
264 Ease::QuartIn,
265 Ease::QuartOut,
266 Ease::QuartInOut,
267 Ease::QuintIn,
268 Ease::QuintOut,
269 Ease::QuintInOut,
270 Ease::ExpoIn,
271 Ease::ExpoOut,
272 Ease::ExpoInOut,
273 Ease::CircIn,
274 Ease::CircOut,
275 Ease::CircInOut,
276 Ease::BackIn,
277 Ease::BackOut,
278 Ease::BackInOut,
279 Ease::ElasticIn,
280 Ease::ElasticOut,
281 Ease::ElasticInOut,
282 Ease::BounceIn,
283 Ease::BounceOut,
284 Ease::BounceInOut,
285 ];
286
287 pub fn at(self, t: f32) -> f32 {
288 (TABLE[self as usize].2)(t)
289 }
290
291 pub fn name(self) -> &'static str {
292 TABLE[self as usize].1
293 }
294
295 pub fn from_name(name: &str) -> Option<Ease> {
296 TABLE
297 .iter()
298 .find(|entry| entry.1 == name)
299 .map(|entry| entry.0)
300 }
301
302 pub fn monotonic(self) -> bool {
303 !matches!(
304 self,
305 Ease::BackIn
306 | Ease::BackOut
307 | Ease::BackInOut
308 | Ease::ElasticIn
309 | Ease::ElasticOut
310 | Ease::ElasticInOut
311 | Ease::BounceIn
312 | Ease::BounceOut
313 | Ease::BounceInOut
314 )
315 }
316
317 pub fn symmetric(self) -> bool {
318 matches!(
319 self,
320 Ease::Linear
321 | Ease::SineInOut
322 | Ease::QuadInOut
323 | Ease::CubicInOut
324 | Ease::QuartInOut
325 | Ease::QuintInOut
326 | Ease::ExpoInOut
327 | Ease::CircInOut
328 | Ease::BackInOut
329 | Ease::ElasticInOut
330 | Ease::BounceInOut
331 )
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 const NAMES: [&str; 31] = [
340 "linear",
341 "sine_in",
342 "sine_out",
343 "sine_in_out",
344 "quad_in",
345 "quad_out",
346 "quad_in_out",
347 "cubic_in",
348 "cubic_out",
349 "cubic_in_out",
350 "quart_in",
351 "quart_out",
352 "quart_in_out",
353 "quint_in",
354 "quint_out",
355 "quint_in_out",
356 "expo_in",
357 "expo_out",
358 "expo_in_out",
359 "circ_in",
360 "circ_out",
361 "circ_in_out",
362 "back_in",
363 "back_out",
364 "back_in_out",
365 "elastic_in",
366 "elastic_out",
367 "elastic_in_out",
368 "bounce_in",
369 "bounce_out",
370 "bounce_in_out",
371 ];
372
373 const SAMPLES: u32 = 4000;
374
375 fn sweep(ease: Ease) -> impl Iterator<Item = (f32, f32)> {
376 (0..=SAMPLES).map(move |i| {
377 let t = i as f32 / SAMPLES as f32;
378 (t, ease.at(t))
379 })
380 }
381
382 #[test]
383 fn the_name_table_is_exhaustive_and_in_order() {
384 assert_eq!(Ease::ALL.len(), NAMES.len());
385 assert_eq!(TABLE.len(), NAMES.len());
386 for (index, (ease, name)) in Ease::ALL.iter().zip(NAMES).enumerate() {
387 assert_eq!(*ease as usize, index);
388 assert_eq!(TABLE[index].0, *ease);
389 assert_eq!(ease.name(), name);
390 assert_eq!(Ease::from_name(name), Some(*ease));
391 }
392 assert_eq!(Ease::from_name("Linear"), None);
393 assert_eq!(Ease::from_name("sine"), None);
394 assert_eq!(Ease::from_name(""), None);
395 assert_eq!(Ease::default(), Ease::Linear);
396 let mut sorted = NAMES.to_vec();
397 sorted.sort_unstable();
398 sorted.dedup();
399 assert_eq!(sorted.len(), NAMES.len());
400 }
401
402 #[test]
403 fn the_table_calls_the_named_functions() {
404 let pairs: [(Ease, Curve); 31] = [
405 (Ease::Linear, linear),
406 (Ease::SineIn, sine_in),
407 (Ease::SineOut, sine_out),
408 (Ease::SineInOut, sine_in_out),
409 (Ease::QuadIn, quad_in),
410 (Ease::QuadOut, quad_out),
411 (Ease::QuadInOut, quad_in_out),
412 (Ease::CubicIn, cubic_in),
413 (Ease::CubicOut, cubic_out),
414 (Ease::CubicInOut, cubic_in_out),
415 (Ease::QuartIn, quart_in),
416 (Ease::QuartOut, quart_out),
417 (Ease::QuartInOut, quart_in_out),
418 (Ease::QuintIn, quint_in),
419 (Ease::QuintOut, quint_out),
420 (Ease::QuintInOut, quint_in_out),
421 (Ease::ExpoIn, expo_in),
422 (Ease::ExpoOut, expo_out),
423 (Ease::ExpoInOut, expo_in_out),
424 (Ease::CircIn, circ_in),
425 (Ease::CircOut, circ_out),
426 (Ease::CircInOut, circ_in_out),
427 (Ease::BackIn, back_in),
428 (Ease::BackOut, back_out),
429 (Ease::BackInOut, back_in_out),
430 (Ease::ElasticIn, elastic_in),
431 (Ease::ElasticOut, elastic_out),
432 (Ease::ElasticInOut, elastic_in_out),
433 (Ease::BounceIn, bounce_in),
434 (Ease::BounceOut, bounce_out),
435 (Ease::BounceInOut, bounce_in_out),
436 ];
437 for (ease, function) in pairs {
438 for (t, y) in sweep(ease) {
439 assert_eq!(y.to_bits(), function(t).to_bits(), "{}", ease.name());
440 }
441 }
442 }
443
444 #[test]
445 fn every_curve_starts_at_zero_ends_at_one_and_clamps() {
446 for ease in Ease::ALL {
447 let name = ease.name();
448 assert_eq!(ease.at(0.0), 0.0, "{name}");
449 assert_eq!(ease.at(1.0), 1.0, "{name}");
450 assert_eq!(ease.at(-0.4), 0.0, "{name}");
451 assert_eq!(ease.at(-1e9), 0.0, "{name}");
452 assert_eq!(ease.at(1.7), 1.0, "{name}");
453 assert_eq!(ease.at(1e9), 1.0, "{name}");
454 assert_eq!(ease.at(f32::INFINITY), 1.0, "{name}");
455 assert_eq!(ease.at(f32::NEG_INFINITY), 0.0, "{name}");
456 for (_, y) in sweep(ease) {
457 assert!(y.is_finite(), "{name}");
458 }
459 }
460 }
461
462 #[test]
463 fn the_monotonic_curves_never_step_back() {
464 for ease in Ease::ALL {
465 let mut previous = 0.0;
466 for (t, y) in sweep(ease) {
467 if ease.monotonic() {
468 assert!(
469 (0.0..=1.0).contains(&y) && y >= previous,
470 "{} at {t}",
471 ease.name()
472 );
473 }
474 previous = y;
475 }
476 }
477 let wobbly = Ease::ALL.iter().filter(|e| !e.monotonic()).count();
478 assert_eq!(wobbly, 9);
479 for ease in Ease::ALL.iter().filter(|e| !e.monotonic()) {
480 let mut previous = 0.0;
481 let mut stepped_back = false;
482 for (_, y) in sweep(*ease) {
483 stepped_back |= y < previous;
484 previous = y;
485 }
486 assert!(stepped_back, "{}", ease.name());
487 }
488 }
489
490 #[test]
491 fn in_and_out_mirror_each_other() {
492 let pairs = [
493 (Ease::SineIn, Ease::SineOut),
494 (Ease::QuadIn, Ease::QuadOut),
495 (Ease::CubicIn, Ease::CubicOut),
496 (Ease::QuartIn, Ease::QuartOut),
497 (Ease::QuintIn, Ease::QuintOut),
498 (Ease::ExpoIn, Ease::ExpoOut),
499 (Ease::CircIn, Ease::CircOut),
500 (Ease::BackIn, Ease::BackOut),
501 (Ease::ElasticIn, Ease::ElasticOut),
502 (Ease::BounceIn, Ease::BounceOut),
503 ];
504 for (rising, falling) in pairs {
505 for i in 1..SAMPLES {
506 let t = i as f32 / SAMPLES as f32;
507 let mirrored = 1.0 - rising.at(1.0 - t);
508 let tolerance = if matches!(rising, Ease::ExpoIn | Ease::ElasticIn) {
509 1e-3
510 } else {
511 2e-6
512 };
513 assert!(
514 (falling.at(t) - mirrored).abs() <= tolerance,
515 "{} at {t}",
516 falling.name()
517 );
518 }
519 }
520 }
521
522 #[test]
523 fn the_in_out_curves_are_point_symmetric_about_the_middle() {
524 for ease in Ease::ALL.iter().filter(|e| e.symmetric()) {
525 assert_eq!(ease.at(0.5), 0.5, "{}", ease.name());
526 for i in 0..=SAMPLES / 2 {
527 let t = i as f32 / SAMPLES as f32;
528 let sum = ease.at(t) + ease.at(1.0 - t);
529 assert!((sum - 1.0).abs() < 2e-6, "{} at {t}: {sum}", ease.name());
530 }
531 }
532 let named = Ease::ALL.iter().filter(|e| e.symmetric()).count();
533 assert_eq!(named, 11);
534 }
535
536 #[test]
537 fn known_midpoints() {
538 assert_eq!(quad_in_out(0.25), 0.125);
539 assert_eq!(quad_in_out(0.75), 0.875);
540 assert_eq!(quart_in(0.5), 0.0625);
541 assert_eq!(quart_out(0.5), 0.9375);
542 assert_eq!(quart_in_out(0.25), 0.03125);
543 assert_eq!(quint_in(0.5), 0.03125);
544 assert_eq!(quint_out(0.5), 0.96875);
545 assert_eq!(quint_in_out(0.25), 0.015625);
546 assert_eq!(expo_in(0.5), 2.0f32.powi(-5));
547 assert_eq!(expo_out(0.5), 1.0 - 2.0f32.powi(-5));
548 assert!((sine_in(0.5) - (1.0 - 0.5f32.sqrt())).abs() < 1e-6);
549 assert!((sine_out(0.5) - 0.5f32.sqrt()).abs() < 1e-6);
550 assert!((circ_in(0.6) - 0.2).abs() < 1e-6);
551 assert!((circ_out(0.6) - (1.0 - 0.16f32).sqrt()).abs() < 1e-6);
552 assert!((back_in(0.5) - 0.25 * (2.70158 * 0.5 - 1.70158)).abs() < 1e-6);
553 assert!((bounce_out(0.5) - 0.765625).abs() < 1e-6);
554 assert!((bounce_out(1.0 / 2.75) - 1.0).abs() < 1e-6);
555 }
556
557 #[test]
558 fn back_overshoots_by_about_a_tenth() {
559 let low = sweep(Ease::BackIn).map(|(_, y)| y).fold(0.0f32, f32::min);
560 assert!((-0.1001..-0.09).contains(&low), "{low}");
561 assert!(sweep(Ease::BackIn).all(|(_, y)| y <= 1.0));
562
563 let high = sweep(Ease::BackOut).map(|(_, y)| y).fold(1.0f32, f32::max);
564 assert!((1.09..1.1001).contains(&high), "{high}");
565 assert!(sweep(Ease::BackOut).all(|(_, y)| y >= 0.0));
566
567 let low = sweep(Ease::BackInOut)
568 .map(|(_, y)| y)
569 .fold(0.0f32, f32::min);
570 let high = sweep(Ease::BackInOut)
571 .map(|(_, y)| y)
572 .fold(1.0f32, f32::max);
573 assert!((-0.1002..-0.09).contains(&low), "{low}");
574 assert!((1.09..1.1002).contains(&high), "{high}");
575 }
576
577 #[test]
578 fn elastic_overshoots_and_rings_down() {
579 let low = sweep(Ease::ElasticIn)
580 .map(|(_, y)| y)
581 .fold(0.0f32, f32::min);
582 assert!((-0.4..-0.3).contains(&low), "{low}");
583 let high = sweep(Ease::ElasticOut)
584 .map(|(_, y)| y)
585 .fold(1.0f32, f32::max);
586 assert!((1.3..1.4).contains(&high), "{high}");
587 assert!(sweep(Ease::ElasticIn).all(|(_, y)| y <= 1.0));
588 assert!(sweep(Ease::ElasticOut).all(|(_, y)| y >= 0.0));
589
590 let low = sweep(Ease::ElasticInOut)
591 .map(|(_, y)| y)
592 .fold(0.0f32, f32::min);
593 let high = sweep(Ease::ElasticInOut)
594 .map(|(_, y)| y)
595 .fold(1.0f32, f32::max);
596 assert!((-0.2..-0.1).contains(&low), "{low}");
597 assert!((1.1..1.2).contains(&high), "{high}");
598
599 let mut peaks = Vec::new();
600 let mut previous = [0.0f32; 2];
601 for (_, y) in sweep(Ease::ElasticOut) {
602 if previous[1] > previous[0] && previous[1] > y && previous[1] > 1.0 {
603 peaks.push(previous[1] - 1.0);
604 }
605 previous = [previous[1], y];
606 }
607 assert!(peaks.len() >= 3, "{peaks:?}");
608 assert!(peaks.windows(2).all(|pair| pair[1] < pair[0]));
609 }
610
611 #[test]
612 fn bounce_stays_inside_the_unit_range() {
613 for ease in [Ease::BounceIn, Ease::BounceOut, Ease::BounceInOut] {
614 for (t, y) in sweep(ease) {
615 assert!((0.0..=1.0).contains(&y), "{} at {t}: {y}", ease.name());
616 }
617 }
618 let mut touches = 0;
619 let mut previous = [0.0f32; 2];
620 for (_, y) in sweep(Ease::BounceOut) {
621 if previous[1] < previous[0] && previous[1] < y {
622 touches += 1;
623 }
624 previous = [previous[1], y];
625 }
626 assert_eq!(touches, 3);
627 }
628
629 #[test]
630 fn the_older_easings_keep_their_results() {
631 assert_eq!(Ease::QuadIn.at(0.5), 0.25);
632 assert_eq!(Ease::QuadOut.at(0.5), 0.75);
633 assert_eq!(Ease::CubicIn.at(0.5), 0.125);
634 assert_eq!(Ease::CubicOut.at(0.5), 0.875);
635 assert_eq!(Ease::CubicInOut.at(0.25), 4.0 * 0.25 * 0.25 * 0.25);
636 assert_eq!(smoothstep(0.25), 0.15625);
637 assert_eq!(smootherstep(0.5), 0.5);
638 assert_eq!(linear(0.3), 0.3);
639 }
640}