1use rustc_hash::FxHashMap;
33
34use crate::key::Key;
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum Easing {
39 #[default]
41 EaseOut,
42 Linear,
43 EaseIn,
44 EaseInOut,
45 Spring,
48 Bouncy,
50 Smooth,
53 Snappy,
55}
56
57impl Easing {
58 pub const ALL: &'static [Easing] = &[
62 Easing::EaseOut,
63 Easing::Linear,
64 Easing::EaseIn,
65 Easing::EaseInOut,
66 Easing::Spring,
67 Easing::Bouncy,
68 Easing::Smooth,
69 Easing::Snappy,
70 ];
71
72 pub fn name(self) -> &'static str {
74 match self {
75 Easing::EaseOut => "easeOut",
76 Easing::Linear => "linear",
77 Easing::EaseIn => "easeIn",
78 Easing::EaseInOut => "easeInOut",
79 Easing::Spring => "spring",
80 Easing::Bouncy => "bouncy",
81 Easing::Smooth => "smooth",
82 Easing::Snappy => "snappy",
83 }
84 }
85
86 pub fn from_index(i: usize) -> Easing {
89 Self::ALL.get(i).copied().unwrap_or_default()
90 }
91
92 pub fn bounce(self) -> Option<f32> {
95 match self {
96 Easing::Smooth => Some(0.0),
97 Easing::Snappy => Some(0.15),
98 Easing::Spring => Some(0.25),
99 Easing::Bouncy => Some(0.5),
100 Easing::EaseOut | Easing::Linear | Easing::EaseIn | Easing::EaseInOut => None,
101 }
102 }
103
104 pub fn is_spring(self) -> bool {
107 self.bounce().is_some()
108 }
109
110 pub fn apply(self, t: f32) -> f32 {
111 let t = t.clamp(0.0, 1.0);
112 match self {
113 Easing::Linear => t,
114 Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
115 Easing::EaseIn => t * t * t,
116 Easing::EaseInOut => {
117 if t < 0.5 {
118 4.0 * t * t * t
119 } else {
120 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
121 }
122 }
123 Easing::Spring | Easing::Bouncy | Easing::Smooth | Easing::Snappy => {
125 1.0 - (1.0 - t).powi(3)
126 }
127 }
128 }
129}
130
131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
133pub enum Repeat {
134 #[default]
136 Normal,
137 Reverse,
139 Alternate,
141 AlternateReverse,
143}
144
145impl Repeat {
146 pub const ALL: &'static [Repeat] = &[
149 Repeat::Normal,
150 Repeat::Reverse,
151 Repeat::Alternate,
152 Repeat::AlternateReverse,
153 ];
154
155 pub fn name(self) -> &'static str {
157 match self {
158 Repeat::Normal => "normal",
159 Repeat::Reverse => "reverse",
160 Repeat::Alternate => "alternate",
161 Repeat::AlternateReverse => "alternateReverse",
162 }
163 }
164
165 pub fn from_index(i: usize) -> Repeat {
168 Self::ALL.get(i).copied().unwrap_or_default()
169 }
170
171 fn progress(self, u: f64) -> f32 {
174 let p = match self {
177 Repeat::Normal => u.rem_euclid(1.0),
178 Repeat::Reverse => 1.0 - u.rem_euclid(1.0),
179 Repeat::Alternate => {
180 let c = u.rem_euclid(2.0);
181 if c < 1.0 { c } else { 2.0 - c }
182 }
183 Repeat::AlternateReverse => {
184 let c = u.rem_euclid(2.0);
185 if c < 1.0 { 1.0 - c } else { c - 1.0 }
186 }
187 };
188 p as f32
189 }
190}
191
192const SPRING_STEP: f64 = 0.004;
195const MAX_FRAME_DT: f64 = 0.1;
198
199pub const MAX_BOUNCE: f32 = 0.9;
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
213pub struct Bounce(std::num::NonZeroU16);
214
215impl Bounce {
216 pub fn new(b: f32) -> Self {
218 let q = (b.clamp(0.0, MAX_BOUNCE) * 10_000.0).round() as u16;
219 Bounce(std::num::NonZeroU16::MIN.saturating_add(q))
220 }
221
222 pub fn get(self) -> f32 {
223 (self.0.get() - 1) as f32 / 10_000.0
224 }
225}
226
227#[derive(Clone, Copy, Debug, PartialEq)]
229pub struct Transition {
230 pub duration_ms: f32,
233 pub easing: Easing,
234 pub repeat: Repeat,
236 pub bounce: Option<Bounce>,
240 pub delay_ms: f32,
243}
244
245impl Transition {
246 pub fn ms(duration_ms: f32) -> Self {
247 Self {
248 duration_ms,
249 easing: Easing::EaseOut,
250 repeat: Repeat::Normal,
251 bounce: None,
252 delay_ms: 0.0,
253 }
254 }
255
256 pub fn easing(mut self, easing: Easing) -> Self {
257 self.easing = easing;
258 self
259 }
260
261 pub fn repeat(mut self, repeat: Repeat) -> Self {
262 self.repeat = repeat;
263 self
264 }
265
266 pub fn delay(mut self, delay_ms: f32) -> Self {
267 self.delay_ms = delay_ms;
268 self
269 }
270
271 pub fn bounce(mut self, bounce: f32) -> Self {
275 self.bounce = Some(Bounce::new(bounce));
276 self
277 }
278
279 pub fn curve(&self) -> Easing {
282 match self.bounce {
283 Some(_) if !self.easing.is_spring() => Easing::Spring,
284 _ => self.easing,
285 }
286 }
287
288 fn damping(&self) -> Option<f32> {
291 let own = self.curve().bounce()?;
292 Some(1.0 - self.bounce.map_or(own, Bounce::get))
293 }
294}
295
296pub(crate) type Track = [(f32, [f32; 4])];
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302#[repr(u8)]
303pub(crate) enum Slot {
304 Width = 0,
305 Height = 1,
306 Bg = 2,
307 Border = 3,
308 Radius = 4,
309 Pos = 5,
310 Opacity = 6,
311 Shadow = 7,
313 ShadowColor = 8,
314}
315
316const SLOTS: usize = 9;
317
318impl Slot {
319 const ALL: [Slot; SLOTS] = [
321 Slot::Width,
322 Slot::Height,
323 Slot::Bg,
324 Slot::Border,
325 Slot::Radius,
326 Slot::Pos,
327 Slot::Opacity,
328 Slot::Shadow,
329 Slot::ShadowColor,
330 ];
331
332 pub(crate) fn names(mask: u16) -> Vec<&'static str> {
335 Self::ALL
336 .into_iter()
337 .filter(|s| mask & (1 << *s as u16) != 0)
338 .map(Slot::name)
339 .collect()
340 }
341
342 pub(crate) fn name(self) -> &'static str {
345 match self {
346 Slot::Width => "width",
347 Slot::Height => "height",
348 Slot::Bg => "bg",
349 Slot::Border => "borderColor",
350 Slot::Radius => "radius",
351 Slot::Pos => "position",
352 Slot::Opacity => "opacity",
353 Slot::Shadow => "shadow",
354 Slot::ShadowColor => "shadowColor",
355 }
356 }
357}
358
359#[derive(Clone, Copy, Debug)]
377struct Tween {
378 from: [f32; 4],
379 to: [f32; 4],
380 value: [f32; 4],
381 start: f64,
383 velocity: [f32; 4],
385 last_time: f64,
386 duration_ms: f32,
389 easing: Easing,
390 last_used: u64,
391}
392
393impl Tween {
394 fn settled(value: [f32; 4], start: f64, last_time: f64, t: Transition, frame_no: u64) -> Self {
398 Tween {
399 from: value,
400 to: value,
401 value,
402 start,
403 velocity: [0.0; 4],
404 last_time,
405 duration_ms: t.duration_ms,
406 easing: t.curve(),
407 last_used: frame_no,
408 }
409 }
410
411 fn leg(from: [f32; 4], to: [f32; 4], now: f64, t: Transition, frame_no: u64) -> Self {
413 Tween {
414 from,
415 to,
416 value: from,
417 start: now,
418 velocity: [0.0; 4],
419 last_time: now,
420 duration_ms: t.duration_ms,
421 easing: t.curve(),
422 last_used: frame_no,
423 }
424 }
425
426 #[inline]
429 fn progress_at(&self, now: f64) -> f32 {
430 let dur = self.duration_ms.max(0.0) as f64 / 1000.0;
431 if dur <= 0.0 {
432 1.0
433 } else {
434 (((now - self.start) / dur) as f32).clamp(0.0, 1.0)
435 }
436 }
437
438 #[inline]
440 fn at(&self, p: f32) -> [f32; 4] {
441 if p >= 1.0 {
442 return self.to;
443 }
444 let e = self.easing.apply(p);
445 std::array::from_fn(|i| self.from[i] + (self.to[i] - self.from[i]) * e)
446 }
447
448 fn eased_at(&self, now: f64) -> [f32; 4] {
451 self.at(self.progress_at(now))
452 }
453
454 fn spring_step(&mut self, now: f64, zeta: f32) -> bool {
459 let response = (self.duration_ms.max(1.0) / 1000.0) as f64;
460 let omega = std::f64::consts::TAU / response;
461 let k = (omega * omega) as f32;
462 let c = (2.0 * zeta as f64 * omega) as f32;
463 let dt = (now - self.last_time).clamp(0.0, MAX_FRAME_DT);
464 self.last_time = now;
465 let steps = (dt / SPRING_STEP).ceil().max(1.0);
466 let h = (dt / steps) as f32;
467 for _ in 0..steps as usize {
468 for i in 0..4 {
469 let a = -k * (self.value[i] - self.to[i]) - c * self.velocity[i];
470 self.velocity[i] += a * h;
471 self.value[i] += self.velocity[i] * h;
472 }
473 }
474 let moving = (0..4)
477 .any(|i| (self.value[i] - self.to[i]).abs() > 1e-3 || self.velocity[i].abs() > 5e-2);
478 if !moving {
479 self.value = self.to;
480 self.velocity = [0.0; 4];
481 }
482 moving
483 }
484}
485
486#[derive(Default)]
487pub struct AnimStore {
488 tweens: FxHashMap<Key, [Option<Tween>; SLOTS]>,
489 now: Option<f64>,
491 frame_no: u64,
493 drove_at: Option<f64>,
498 owes_transition: bool,
501 owes_cycle: bool,
506}
507
508impl AnimStore {
509 pub fn set_time(&mut self, now_secs: f64) {
512 self.now = Some(now_secs);
513 }
514
515 pub fn time(&self) -> Option<f64> {
516 self.now
517 }
518
519 pub fn animating(&self) -> bool {
523 self.owes_transition || self.owes_cycle
524 }
525
526 pub fn owes(&self) -> (bool, bool) {
529 (self.owes_transition, self.owes_cycle)
530 }
531
532 pub(crate) fn begin_frame(&mut self, frame_no: u64) {
537 self.frame_no = frame_no;
538 self.drove_at = self.now;
539 self.owes_transition = false;
540 self.owes_cycle = false;
541 if !self.tweens.is_empty()
543 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
544 {
545 self.tweens
546 .retain(|_, slots| slots.iter().flatten().any(|t| t.last_used >= cutoff));
547 }
548 }
549
550 pub(crate) fn owing(&self, mut each: impl FnMut(Key, Slot)) {
558 let Some(now) = self.drove_at.or(self.now) else {
559 return;
560 };
561 for (&key, slots) in &self.tweens {
562 for (i, tw) in slots.iter().enumerate() {
563 let Some(tw) = tw else { continue };
564 if tw.last_used != self.frame_no {
565 continue;
566 }
567 let moving = if tw.easing.is_spring() {
568 tw.value != tw.to || tw.velocity != [0.0; 4]
569 } else {
570 tw.progress_at(now) < 1.0
571 };
572 if moving {
573 each(key, Slot::ALL[i]);
574 }
575 }
576 }
577 }
578
579 pub(crate) fn drive(
589 &mut self,
590 key: Key,
591 slot: Slot,
592 enter_from: Option<[f32; 4]>,
593 target: [f32; 4],
594 transition: Transition,
595 follow: bool,
596 ) -> [f32; 4] {
597 self.node(key)
598 .drive(slot, enter_from, target, transition, follow)
599 }
600
601 pub(crate) fn node(&mut self, key: Key) -> NodeAnim<'_> {
612 NodeAnim {
613 slots: self.tweens.entry(key).or_default(),
614 now: self.now,
615 frame_no: self.frame_no,
616 active: &mut self.owes_transition,
617 cycling: &mut self.owes_cycle,
618 }
619 }
620}
621
622fn sample_track(track: &Track, transition: Transition, now: Option<f64>) -> Option<[f32; 4]> {
626 let dur = transition.duration_ms as f64 / 1000.0;
627 let (Some(now), true, Some(&(_, first))) = (now, dur > 0.0, track.first()) else {
628 return None;
629 };
630 let u = (now - transition.delay_ms as f64 / 1000.0) / dur;
631 let p = transition.repeat.progress(u);
632 let mut from = (0.0, first);
633 for &(at, value) in track {
634 if p < at {
635 let (a, va) = from;
636 let t = if at > a { (p - a) / (at - a) } else { 1.0 };
637 let e = transition.curve().apply(t);
638 let mut out = [0.0; 4];
639 for i in 0..4 {
640 out[i] = va[i] + (value[i] - va[i]) * e;
641 }
642 return Some(out);
643 }
644 from = (at, value);
645 }
646 Some(from.1)
647}
648
649pub(crate) struct NodeAnim<'a> {
653 slots: &'a mut [Option<Tween>; SLOTS],
654 now: Option<f64>,
656 frame_no: u64,
657 active: &'a mut bool,
659 cycling: &'a mut bool,
661}
662
663impl NodeAnim<'_> {
664 pub(crate) fn sample(&mut self, track: &Track, transition: Transition) -> Option<[f32; 4]> {
684 let v = sample_track(track, transition, self.now);
685 if v.is_some() {
686 *self.cycling = true;
687 }
688 v
689 }
690
691 pub(crate) fn drive(
692 &mut self,
693 slot: Slot,
694 enter_from: Option<[f32; 4]>,
695 target: [f32; 4],
696 transition: Transition,
697 follow: bool,
698 ) -> [f32; 4] {
699 let frame_no = self.frame_no;
700 let now = self.now;
701 let entry = &mut self.slots[slot as usize];
702 let stale = entry.is_none_or(|t| t.last_used + 1 < frame_no);
703 let Some(now) = now else {
704 *entry = Some(Tween::settled(target, 0.0, 0.0, transition, frame_no));
705 return target;
706 };
707 if stale {
708 match enter_from {
709 Some(from) if from != target => {
711 *entry = Some(Tween::leg(from, target, now, transition, frame_no));
712 }
713 _ => {
716 *entry = Some(Tween::settled(
717 target,
718 f64::NEG_INFINITY,
719 now,
720 transition,
721 frame_no,
722 ));
723 return target;
724 }
725 }
726 }
727 let tw = entry.as_mut().expect("checked above");
728 tw.last_used = frame_no;
729 if !follow && tw.to != target && tw.value == tw.to && tw.velocity == [0.0; 4] {
730 tw.from = target;
732 tw.to = target;
733 tw.value = target;
734 tw.start = f64::NEG_INFINITY;
735 tw.last_time = now;
736 return target;
737 }
738 if let Some(zeta) = transition.damping() {
739 tw.to = target;
741 tw.duration_ms = transition.duration_ms;
742 tw.easing = transition.curve();
743 if tw.spring_step(now, zeta) {
744 *self.active = true;
745 }
746 return tw.value;
747 }
748 if tw.to != target {
749 tw.from = tw.eased_at(now);
757 tw.to = target;
758 tw.start = now;
759 tw.duration_ms = transition.duration_ms;
760 tw.easing = transition.curve();
761 }
762 let p = tw.progress_at(now);
763 if p < 1.0 {
764 *self.active = true;
765 }
766 tw.value = tw.at(p);
767 tw.value
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 struct Frames(u64);
778 impl Frames {
779 fn next(&mut self) -> u64 {
780 self.0 += 1;
781 self.0
782 }
783 }
784
785 #[test]
797 fn a_tween_stays_small() {
798 const BOUND: usize = 96;
799 let size = std::mem::size_of::<Option<Tween>>();
800 assert!(
801 size <= BOUND,
802 "Option<Tween> is {size} bytes, over the {BOUND}-byte bound. \
803 It is held per slot per node across frames; put what the node \
804 owns on the node and what the cycle owns in the Transition."
805 );
806 assert_eq!(size, std::mem::size_of::<Tween>());
809 }
810
811 fn one(v: f32) -> [f32; 4] {
812 [v, 0.0, 0.0, 0.0]
813 }
814
815 #[test]
816 fn easings_hit_their_endpoints() {
817 for e in [
818 Easing::Linear,
819 Easing::EaseOut,
820 Easing::EaseIn,
821 Easing::EaseInOut,
822 Easing::Spring,
823 Easing::Bouncy,
824 Easing::Smooth,
825 Easing::Snappy,
826 ] {
827 assert_eq!(e.apply(0.0), 0.0);
828 assert_eq!(e.apply(1.0), 1.0);
829 assert!(e.apply(0.5) > 0.0 && e.apply(0.5) < 1.0);
830 }
831 }
832
833 #[test]
834 fn first_sight_snaps_then_retargets_ease() {
835 let mut a = AnimStore::default();
836 let mut frame = Frames(0);
837 let k = Key::ROOT.str("x");
838 let t = Transition::ms(100.0).easing(Easing::Linear);
839 a.set_time(0.0);
840 a.begin_frame(frame.next());
841 assert_eq!(a.drive(k, Slot::Width, None, one(10.0), t, true)[0], 10.0);
842 assert!(!a.animating());
843
844 a.set_time(0.0);
845 a.begin_frame(frame.next());
846 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 10.0);
847 assert!(a.animating(), "mid-flight after retarget");
848
849 a.set_time(0.05);
850 a.begin_frame(frame.next());
851 assert!((a.drive(k, Slot::Width, None, one(20.0), t, true)[0] - 15.0).abs() < 1e-4);
852
853 a.set_time(0.2);
854 a.begin_frame(frame.next());
855 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 20.0);
856 assert!(!a.animating(), "settled");
857 }
858
859 #[test]
860 fn unchanged_targets_owe_no_frames() {
861 let mut a = AnimStore::default();
862 let mut frame = Frames(0);
863 let k = Key::ROOT.str("x");
864 let t = Transition::ms(100.0);
865 for i in 0..3 {
866 a.set_time(i as f64 * 0.001);
867 a.begin_frame(frame.next());
868 a.drive(k, Slot::Width, None, one(5.0), t, true);
869 assert!(!a.animating(), "frame {i}: same value, nothing to animate");
870 }
871 }
872
873 #[test]
874 fn retarget_mid_flight_starts_from_current_value() {
875 let mut a = AnimStore::default();
876 let mut frame = Frames(0);
877 let k = Key::ROOT.str("x");
878 let t = Transition::ms(100.0).easing(Easing::Linear);
879 a.set_time(0.0);
880 a.begin_frame(frame.next());
881 a.drive(k, Slot::Width, None, one(0.0), t, true);
882 a.set_time(0.0);
883 a.begin_frame(frame.next());
884 a.drive(k, Slot::Width, None, one(100.0), t, true);
885 a.set_time(0.05);
886 a.begin_frame(frame.next());
887 assert!((a.drive(k, Slot::Width, None, one(100.0), t, true)[0] - 50.0).abs() < 1e-4);
888 a.set_time(0.05);
890 a.begin_frame(frame.next());
891 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 50.0).abs() < 1e-4);
892 a.set_time(0.10);
893 a.begin_frame(frame.next());
894 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 25.0).abs() < 1e-4);
895 }
896
897 #[test]
898 fn springs_overshoot_settle_and_keep_momentum() {
899 let mut a = AnimStore::default();
900 let mut frame = Frames(0);
901 let k = Key::ROOT.str("x");
902 let t = Transition::ms(200.0).easing(Easing::Bouncy);
903 a.set_time(0.0);
904 a.begin_frame(frame.next());
905 a.drive(k, Slot::Width, None, one(0.0), t, true);
906 let mut max = 0.0f32;
908 let mut settled_at = None;
909 for i in 1..=180 {
910 a.set_time(i as f64 / 60.0);
911 a.begin_frame(frame.next());
912 let v = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
913 max = max.max(v);
914 if !a.animating() && settled_at.is_none() {
915 settled_at = Some(i);
916 }
917 }
918 assert!(max > 101.0, "bouncy overshoots: peak {max}");
919 let settled = settled_at.expect("settles within 3s");
920 assert!(settled > 6, "not instant: {settled}");
921 a.set_time(4.0);
922 a.begin_frame(frame.next());
923 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
924
925 a.set_time(4.0);
929 a.begin_frame(frame.next());
930 a.drive(k, Slot::Width, None, one(200.0), t, true);
931 let mut v_prev = 100.0;
932 for i in 1..=2 {
933 a.set_time(4.0 + i as f64 / 60.0);
934 a.begin_frame(frame.next());
935 v_prev = a.drive(k, Slot::Width, None, one(200.0), t, true)[0];
936 }
937 assert!(v_prev > 100.0);
938 a.set_time(4.0 + 3.0 / 60.0);
939 a.begin_frame(frame.next());
940 let after = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
941 assert!(
942 after > v_prev,
943 "momentum carries past the retarget: {v_prev} -> {after}"
944 );
945 }
946
947 fn peak(t: Transition) -> (f32, bool) {
950 let mut a = AnimStore::default();
951 let mut frame = Frames(0);
952 let k = Key::ROOT.str("x");
953 a.set_time(0.0);
954 a.begin_frame(frame.next());
955 a.drive(k, Slot::Width, None, one(0.0), t, true);
956 let mut max = 0.0f32;
957 for i in 1..=180 {
958 a.set_time(i as f64 / 60.0);
959 a.begin_frame(frame.next());
960 max = max.max(a.drive(k, Slot::Width, None, one(100.0), t, true)[0]);
961 }
962 (max, !a.animating())
963 }
964
965 #[test]
970 fn a_spring_s_bounce_is_how_far_it_overshoots() {
971 let t = Transition::ms(200.0);
972 assert_eq!(t.easing(Easing::Spring).damping(), Some(0.75));
973 assert_eq!(t.easing(Easing::Bouncy).damping(), Some(0.5));
974 assert_eq!(t.damping(), None, "a timed curve is no spring");
975
976 let (smooth, settled) = peak(t.easing(Easing::Smooth));
977 assert!(settled, "smooth comes to rest");
978 assert!(smooth <= 100.0 + 1e-3, "smooth never overshoots: {smooth}");
979 let mut last = smooth;
980 for e in [Easing::Snappy, Easing::Spring, Easing::Bouncy] {
981 let (p, settled) = peak(t.easing(e));
982 assert!(settled, "{e:?} comes to rest");
983 assert!(
984 p > last,
985 "{e:?} overshoots past the one before: {p} <= {last}"
986 );
987 last = p;
988 }
989 assert_eq!(
991 peak(t.easing(Easing::Bouncy).bounce(0.0)).0,
992 smooth,
993 "bouncy with no bounce is smooth"
994 );
995 assert_eq!(
996 peak(t.easing(Easing::Smooth).bounce(0.5)).0,
997 last,
998 "smooth with bouncy's bounce is bouncy"
999 );
1000 }
1001
1002 #[test]
1006 fn a_bounce_on_a_timed_curve_makes_it_a_spring() {
1007 let t = Transition::ms(200.0).easing(Easing::Linear).bounce(0.5);
1008 assert_eq!(t.curve(), Easing::Spring);
1009 assert_eq!(t.damping(), Some(0.5));
1010 assert_eq!(
1011 peak(t).0,
1012 peak(Transition::ms(200.0).easing(Easing::Bouncy)).0
1013 );
1014 assert_eq!(Transition::ms(200.0).curve(), Easing::EaseOut);
1015 }
1016
1017 #[test]
1018 fn a_bounce_holds_its_range() {
1019 assert!((Bounce::new(0.3).get() - 0.3).abs() < 1e-4);
1020 assert_eq!(Bounce::new(0.0).get(), 0.0);
1021 assert_eq!(Bounce::new(-1.0).get(), 0.0);
1022 assert_eq!(Bounce::new(f32::NAN).get(), 0.0);
1023 assert_eq!(Bounce::new(1.0).get(), MAX_BOUNCE, "1 would never settle");
1024 let (_, settled) = peak(Transition::ms(100.0).easing(Easing::Spring).bounce(1.0));
1025 assert!(settled, "the most bounce there is still comes to rest");
1026 }
1027
1028 #[test]
1032 fn a_transition_carries_its_bounce_in_its_padding() {
1033 assert_eq!(std::mem::size_of::<Option<Bounce>>(), 2);
1034 assert_eq!(std::mem::size_of::<Option<Transition>>(), 12);
1035 }
1036
1037 #[test]
1038 fn an_entrance_starts_its_first_leg_from_the_declared_value() {
1039 let mut a = AnimStore::default();
1040 let mut frame = Frames(0);
1041 let k = Key::ROOT.str("x");
1042 let t = Transition::ms(100.0).easing(Easing::Linear);
1043 a.set_time(0.0);
1044 a.begin_frame(frame.next());
1045 assert_eq!(
1046 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1047 -100.0,
1048 "first sight starts at the entrance"
1049 );
1050 assert!(a.animating(), "and owes a frame");
1051 a.set_time(0.05);
1052 a.begin_frame(frame.next());
1053 assert!(
1054 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0] + 50.0).abs() < 1e-3
1055 );
1056 a.set_time(0.2);
1057 a.begin_frame(frame.next());
1058 assert_eq!(
1059 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1060 0.0
1061 );
1062 assert!(!a.animating());
1063 a.set_time(0.2);
1065 a.begin_frame(frame.next());
1066 assert_eq!(
1067 a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0],
1068 0.0
1069 );
1070 a.set_time(0.25);
1071 a.begin_frame(frame.next());
1072 assert!(
1073 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0] - 20.0).abs() < 1e-3
1074 );
1075 }
1076
1077 #[test]
1078 fn an_entrance_equal_to_the_target_is_no_entrance() {
1079 let mut a = AnimStore::default();
1080 let mut frame = Frames(0);
1081 let k = Key::ROOT.str("x");
1082 let t = Transition::ms(100.0);
1083 a.set_time(0.0);
1084 a.begin_frame(frame.next());
1085 assert_eq!(
1086 a.drive(k, Slot::Bg, Some(one(5.0)), one(5.0), t, true)[0],
1087 5.0
1088 );
1089 assert!(!a.animating());
1090 }
1091
1092 #[test]
1093 fn a_spring_entrance_carries_no_velocity_in() {
1094 let mut a = AnimStore::default();
1095 let mut frame = Frames(0);
1096 let k = Key::ROOT.str("x");
1097 let t = Transition::ms(100.0).easing(Easing::Spring);
1098 a.set_time(0.0);
1099 a.begin_frame(frame.next());
1100 assert_eq!(
1101 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1102 -100.0
1103 );
1104 a.set_time(0.016);
1105 a.begin_frame(frame.next());
1106 let first = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1107 assert!(
1108 first > -100.0 && first < 0.0,
1109 "leaves the entrance toward the target: {first}"
1110 );
1111 assert!(a.animating());
1112 let mut v = first;
1113 for i in 2..=90 {
1114 a.set_time(i as f64 * 0.016);
1115 a.begin_frame(frame.next());
1116 v = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1117 }
1118 assert!(v.abs() < 1.0, "settled on the target: {v}");
1119 assert!(!a.animating());
1120 }
1121
1122 #[test]
1123 fn off_the_leash_a_settled_slot_snaps_to_a_new_target() {
1124 let mut a = AnimStore::default();
1125 let mut frame = Frames(0);
1126 let k = Key::ROOT.str("x");
1127 let t = Transition::ms(100.0).easing(Easing::Linear);
1128 a.set_time(0.0);
1129 a.begin_frame(frame.next());
1130 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, false);
1131 a.set_time(0.05);
1133 a.begin_frame(frame.next());
1134 let at = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1135 a.set_time(0.1);
1136 a.begin_frame(frame.next());
1137 let mid = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1138 assert!(
1139 at < mid && mid < 20.0,
1140 "mid-flight retarget keeps easing: {at} -> {mid}"
1141 );
1142 a.set_time(0.3);
1143 a.begin_frame(frame.next());
1144 assert_eq!(
1145 a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0],
1146 20.0
1147 );
1148 assert!(!a.animating());
1149 a.set_time(0.3);
1151 a.begin_frame(frame.next());
1152 assert_eq!(
1153 a.drive(k, Slot::Pos, Some(one(-100.0)), one(300.0), t, false)[0],
1154 300.0
1155 );
1156 assert!(!a.animating());
1157 }
1158
1159 #[test]
1165 fn a_target_that_moves_every_frame_still_advances() {
1166 let mut a = AnimStore::default();
1167 let mut frame = Frames(0);
1168 let k = Key::ROOT.str("card");
1169 let t = Transition::ms(160.0);
1170 a.set_time(0.0);
1171 a.begin_frame(frame.next());
1172 a.drive(k, Slot::Pos, None, one(0.0), t, true);
1173 let mut behind = Vec::new();
1175 let mut drawn = 0.0;
1176 for f in 1..=60 {
1177 let target = f as f32 * 9.6;
1178 a.set_time(f as f64 * 0.016);
1179 a.begin_frame(frame.next());
1180 drawn = a.drive(k, Slot::Pos, None, one(target), t, true)[0];
1181 behind.push(target - drawn);
1182 }
1183 let target = 60.0 * 9.6;
1184 assert!(
1185 drawn > target * 0.8,
1186 "the pan reaches the screen: drawn {drawn} of {target}"
1187 );
1188 let (early, late) = (behind[29], behind[59]);
1191 assert!(
1192 (early - late).abs() < 1.0,
1193 "the gap stops growing: {early} then {late}"
1194 );
1195 }
1196
1197 #[test]
1198 fn a_slot_skipped_for_a_frame_snaps() {
1199 let mut a = AnimStore::default();
1200 let mut frame = Frames(0);
1201 let k = Key::ROOT.str("x");
1202 let t = Transition::ms(100.0);
1203 a.set_time(0.0);
1204 a.begin_frame(frame.next());
1205 a.drive(k, Slot::Width, None, one(0.0), t, true);
1206 a.set_time(0.01);
1208 a.begin_frame(frame.next());
1209 a.set_time(0.02);
1210 a.begin_frame(frame.next());
1211 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1212 assert!(!a.animating());
1213 }
1214
1215 #[test]
1218 fn keyframes_cycle_in_every_direction() {
1219 let track = [(0.0, one(0.0)), (1.0, one(100.0))];
1220 let mut frame = Frames(0);
1221 let mut at = |a: &mut AnimStore, repeat: Repeat, now: f64| {
1222 let t = Transition::ms(1000.0).easing(Easing::Linear).repeat(repeat);
1223 a.set_time(now);
1224 a.begin_frame(frame.next());
1225 let v = a.node(Key::ROOT).sample(&track, t).unwrap()[0];
1226 assert!(a.animating(), "a keyframed slot always owes a frame");
1227 v
1228 };
1229 let mut a = AnimStore::default();
1230 for (repeat, expect) in [
1231 (Repeat::Normal, [0.0, 25.0, 50.0, 75.0, 0.0, 25.0]),
1232 (Repeat::Reverse, [100.0, 75.0, 50.0, 25.0, 100.0, 75.0]),
1233 (Repeat::Alternate, [0.0, 25.0, 50.0, 75.0, 100.0, 75.0]),
1234 (
1235 Repeat::AlternateReverse,
1236 [100.0, 75.0, 50.0, 25.0, 0.0, 25.0],
1237 ),
1238 ] {
1239 for (i, e) in expect.iter().enumerate() {
1240 let v = at(&mut a, repeat, i as f64 * 0.25);
1241 assert!((v - e).abs() < 1e-3, "{repeat:?} at {i}/4: {v} != {e}");
1242 }
1243 }
1244 }
1245
1246 #[test]
1247 fn delay_shifts_the_cycle_and_easing_shapes_each_segment() {
1248 let mut a = AnimStore::default();
1249 let mut frame = Frames(0);
1250 let track = [(0.0, one(0.0)), (0.5, one(10.0)), (1.0, one(0.0))];
1251 let t = Transition::ms(1000.0).easing(Easing::Linear);
1252 a.set_time(0.25);
1253 a.begin_frame(frame.next());
1254 assert!((a.node(Key::ROOT).sample(&track, t).unwrap()[0] - 5.0).abs() < 1e-3);
1255 a.set_time(0.25);
1257 a.begin_frame(frame.next());
1258 assert!((a.node(Key::ROOT).sample(&track, t.delay(250.0)).unwrap()[0]).abs() < 1e-3);
1259 a.set_time(0.125);
1262 a.begin_frame(frame.next());
1263 let v = a
1264 .node(Key::ROOT)
1265 .sample(&track, t.easing(Easing::EaseIn))
1266 .unwrap()[0];
1267 assert!(v > 0.0 && v < 2.0, "{v}");
1268 let late = [(0.5, one(3.0)), (1.0, one(9.0))];
1270 a.set_time(0.1);
1271 a.begin_frame(frame.next());
1272 assert_eq!(a.node(Key::ROOT).sample(&late, t).unwrap()[0], 3.0);
1273 }
1274
1275 #[test]
1276 fn keyframes_need_a_clock_and_a_duration() {
1277 let mut a = AnimStore::default();
1278 let mut frame = Frames(0);
1279 let track = [(0.0, one(0.0)), (1.0, one(1.0))];
1280 a.begin_frame(frame.next());
1281 assert!(
1282 a.node(Key::ROOT)
1283 .sample(&track, Transition::ms(100.0))
1284 .is_none()
1285 );
1286 assert!(!a.animating());
1287 a.set_time(1.0);
1288 a.begin_frame(frame.next());
1289 assert!(
1290 a.node(Key::ROOT)
1291 .sample(&track, Transition::ms(0.0))
1292 .is_none()
1293 );
1294 assert!(
1295 a.node(Key::ROOT)
1296 .sample(&[], Transition::ms(100.0))
1297 .is_none()
1298 );
1299 assert!(!a.animating());
1300 }
1301
1302 #[test]
1303 fn no_clock_means_no_animation() {
1304 let mut a = AnimStore::default();
1305 let mut frame = Frames(0);
1306 let k = Key::ROOT.str("x");
1307 let t = Transition::ms(100.0);
1308 a.begin_frame(frame.next());
1309 a.drive(k, Slot::Width, None, one(0.0), t, true);
1310 a.begin_frame(frame.next());
1311 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1312 assert!(!a.animating());
1313 }
1314}