1use rustc_hash::FxHashMap;
52
53use crate::key::Key;
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum Easing {
58 #[default]
60 EaseOut,
61 Linear,
62 EaseIn,
63 EaseInOut,
64 Spring,
67 Bouncy,
69 Smooth,
72 Snappy,
74}
75
76impl Easing {
77 pub const ALL: &'static [Easing] = &[
80 Easing::EaseOut,
81 Easing::Linear,
82 Easing::EaseIn,
83 Easing::EaseInOut,
84 Easing::Spring,
85 Easing::Bouncy,
86 Easing::Smooth,
87 Easing::Snappy,
88 ];
89
90 pub fn name(self) -> &'static str {
92 match self {
93 Easing::EaseOut => "easeOut",
94 Easing::Linear => "linear",
95 Easing::EaseIn => "easeIn",
96 Easing::EaseInOut => "easeInOut",
97 Easing::Spring => "spring",
98 Easing::Bouncy => "bouncy",
99 Easing::Smooth => "smooth",
100 Easing::Snappy => "snappy",
101 }
102 }
103
104 pub fn from_index(i: usize) -> Easing {
107 Self::ALL.get(i).copied().unwrap_or_default()
108 }
109
110 pub fn bounce(self) -> Option<f32> {
113 match self {
114 Easing::Smooth => Some(0.0),
115 Easing::Snappy => Some(0.15),
116 Easing::Spring => Some(0.25),
117 Easing::Bouncy => Some(0.5),
118 Easing::EaseOut | Easing::Linear | Easing::EaseIn | Easing::EaseInOut => None,
119 }
120 }
121
122 pub fn is_spring(self) -> bool {
125 self.bounce().is_some()
126 }
127
128 pub fn apply(self, t: f32) -> f32 {
129 let t = t.clamp(0.0, 1.0);
130 match self {
131 Easing::Linear => t,
132 Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
133 Easing::EaseIn => t * t * t,
134 Easing::EaseInOut => {
135 if t < 0.5 {
136 4.0 * t * t * t
137 } else {
138 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
139 }
140 }
141 Easing::Spring | Easing::Bouncy | Easing::Smooth | Easing::Snappy => {
143 1.0 - (1.0 - t).powi(3)
144 }
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
151pub enum Repeat {
152 #[default]
154 Normal,
155 Reverse,
157 Alternate,
159 AlternateReverse,
161}
162
163impl Repeat {
164 pub const ALL: &'static [Repeat] = &[
167 Repeat::Normal,
168 Repeat::Reverse,
169 Repeat::Alternate,
170 Repeat::AlternateReverse,
171 ];
172
173 pub fn name(self) -> &'static str {
175 match self {
176 Repeat::Normal => "normal",
177 Repeat::Reverse => "reverse",
178 Repeat::Alternate => "alternate",
179 Repeat::AlternateReverse => "alternateReverse",
180 }
181 }
182
183 pub fn from_index(i: usize) -> Repeat {
186 Self::ALL.get(i).copied().unwrap_or_default()
187 }
188
189 fn progress(self, u: f64) -> f32 {
192 let p = match self {
195 Repeat::Normal => u.rem_euclid(1.0),
196 Repeat::Reverse => 1.0 - u.rem_euclid(1.0),
197 Repeat::Alternate => {
198 let c = u.rem_euclid(2.0);
199 if c < 1.0 { c } else { 2.0 - c }
200 }
201 Repeat::AlternateReverse => {
202 let c = u.rem_euclid(2.0);
203 if c < 1.0 { 1.0 - c } else { c - 1.0 }
204 }
205 };
206 p as f32
207 }
208}
209
210const SPRING_STEP: f64 = 0.004;
213const MAX_FRAME_DT: f64 = 0.1;
216
217pub const MAX_BOUNCE: f32 = 0.9;
220
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
231pub struct Bounce(std::num::NonZeroU16);
232
233impl Bounce {
234 pub fn new(b: f32) -> Self {
236 let q = (b.clamp(0.0, MAX_BOUNCE) * 10_000.0).round() as u16;
237 Bounce(std::num::NonZeroU16::MIN.saturating_add(q))
238 }
239
240 pub fn get(self) -> f32 {
241 (self.0.get() - 1) as f32 / 10_000.0
242 }
243}
244
245#[derive(Clone, Copy, Debug, PartialEq)]
251pub struct Transition {
252 pub duration_ms: f32,
255 pub easing: Easing,
256 pub repeat: Repeat,
258 pub bounce: Option<Bounce>,
262 pub delay_ms: f32,
265}
266
267impl Transition {
268 pub fn ms(duration_ms: f32) -> Self {
270 Self {
271 duration_ms,
272 easing: Easing::EaseOut,
273 repeat: Repeat::Normal,
274 bounce: None,
275 delay_ms: 0.0,
276 }
277 }
278
279 pub fn easing(mut self, easing: Easing) -> Self {
281 self.easing = easing;
282 self
283 }
284
285 pub fn repeat(mut self, repeat: Repeat) -> Self {
287 self.repeat = repeat;
288 self
289 }
290
291 pub fn delay(mut self, delay_ms: f32) -> Self {
293 self.delay_ms = delay_ms;
294 self
295 }
296
297 pub fn bounce(mut self, bounce: f32) -> Self {
301 self.bounce = Some(Bounce::new(bounce));
302 self
303 }
304
305 pub fn curve(&self) -> Easing {
308 match self.bounce {
309 Some(_) if !self.easing.is_spring() => Easing::Spring,
310 _ => self.easing,
311 }
312 }
313
314 fn damping(&self) -> Option<f32> {
317 let own = self.curve().bounce()?;
318 Some(1.0 - self.bounce.map_or(own, Bounce::get))
319 }
320}
321
322pub(crate) type Track = [(f32, [f32; 4])];
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328#[repr(u8)]
329pub(crate) enum Slot {
330 Width = 0,
331 Height = 1,
332 Bg = 2,
333 Border = 3,
334 Radius = 4,
335 Pos = 5,
336 Opacity = 6,
337 Shadow = 7,
339 ShadowColor = 8,
340}
341
342const SLOTS: usize = 9;
343
344impl Slot {
345 const ALL: [Slot; SLOTS] = [
347 Slot::Width,
348 Slot::Height,
349 Slot::Bg,
350 Slot::Border,
351 Slot::Radius,
352 Slot::Pos,
353 Slot::Opacity,
354 Slot::Shadow,
355 Slot::ShadowColor,
356 ];
357
358 pub(crate) fn names(mask: u16) -> Vec<&'static str> {
361 Self::ALL
362 .into_iter()
363 .filter(|s| mask & (1 << *s as u16) != 0)
364 .map(Slot::name)
365 .collect()
366 }
367
368 pub(crate) fn name(self) -> &'static str {
371 match self {
372 Slot::Width => "width",
373 Slot::Height => "height",
374 Slot::Bg => "bg",
375 Slot::Border => "borderColor",
376 Slot::Radius => "radius",
377 Slot::Pos => "position",
378 Slot::Opacity => "opacity",
379 Slot::Shadow => "shadow",
380 Slot::ShadowColor => "shadowColor",
381 }
382 }
383}
384
385#[derive(Clone, Copy, Debug)]
403struct Tween {
404 from: [f32; 4],
405 to: [f32; 4],
406 value: [f32; 4],
407 start: f64,
409 velocity: [f32; 4],
411 last_time: f64,
412 duration_ms: f32,
415 easing: Easing,
416 last_used: u64,
417}
418
419impl Tween {
420 fn settled(value: [f32; 4], start: f64, last_time: f64, t: Transition, frame_no: u64) -> Self {
424 Tween {
425 from: value,
426 to: value,
427 value,
428 start,
429 velocity: [0.0; 4],
430 last_time,
431 duration_ms: t.duration_ms,
432 easing: t.curve(),
433 last_used: frame_no,
434 }
435 }
436
437 fn leg(from: [f32; 4], to: [f32; 4], now: f64, t: Transition, frame_no: u64) -> Self {
439 Tween {
440 from,
441 to,
442 value: from,
443 start: now,
444 velocity: [0.0; 4],
445 last_time: now,
446 duration_ms: t.duration_ms,
447 easing: t.curve(),
448 last_used: frame_no,
449 }
450 }
451
452 #[inline]
455 fn progress_at(&self, now: f64) -> f32 {
456 let dur = self.duration_ms.max(0.0) as f64 / 1000.0;
457 if dur <= 0.0 {
458 1.0
459 } else {
460 (((now - self.start) / dur) as f32).clamp(0.0, 1.0)
461 }
462 }
463
464 #[inline]
466 fn at(&self, p: f32) -> [f32; 4] {
467 if p >= 1.0 {
468 return self.to;
469 }
470 let e = self.easing.apply(p);
471 std::array::from_fn(|i| self.from[i] + (self.to[i] - self.from[i]) * e)
472 }
473
474 fn eased_at(&self, now: f64) -> [f32; 4] {
477 self.at(self.progress_at(now))
478 }
479
480 fn spring_step(&mut self, now: f64, zeta: f32) -> bool {
485 let response = (self.duration_ms.max(1.0) / 1000.0) as f64;
486 let omega = std::f64::consts::TAU / response;
487 let k = (omega * omega) as f32;
488 let c = (2.0 * zeta as f64 * omega) as f32;
489 let dt = (now - self.last_time).clamp(0.0, MAX_FRAME_DT);
490 self.last_time = now;
491 let steps = (dt / SPRING_STEP).ceil().max(1.0);
492 let h = (dt / steps) as f32;
493 for _ in 0..steps as usize {
494 for i in 0..4 {
495 let a = -k * (self.value[i] - self.to[i]) - c * self.velocity[i];
496 self.velocity[i] += a * h;
497 self.value[i] += self.velocity[i] * h;
498 }
499 }
500 let moving = (0..4)
503 .any(|i| (self.value[i] - self.to[i]).abs() > 1e-3 || self.velocity[i].abs() > 5e-2);
504 if !moving {
505 self.value = self.to;
506 self.velocity = [0.0; 4];
507 }
508 moving
509 }
510}
511
512#[derive(Default)]
513pub struct AnimStore {
514 tweens: FxHashMap<Key, [Option<Tween>; SLOTS]>,
515 now: Option<f64>,
517 frame_no: u64,
519 drove_at: Option<f64>,
524 owes_transition: bool,
527 owes_cycle: bool,
532}
533
534impl AnimStore {
535 pub fn set_time(&mut self, now_secs: f64) {
538 self.now = Some(now_secs);
539 }
540
541 pub fn time(&self) -> Option<f64> {
542 self.now
543 }
544
545 pub fn animating(&self) -> bool {
549 self.owes_transition || self.owes_cycle
550 }
551
552 pub fn owes(&self) -> (bool, bool) {
555 (self.owes_transition, self.owes_cycle)
556 }
557
558 pub(crate) fn begin_frame(&mut self, frame_no: u64) {
561 self.frame_no = frame_no;
562 self.drove_at = self.now;
563 self.owes_transition = false;
564 self.owes_cycle = false;
565 if !self.tweens.is_empty()
567 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
568 {
569 self.tweens
570 .retain(|_, slots| slots.iter().flatten().any(|t| t.last_used >= cutoff));
571 }
572 }
573
574 pub(crate) fn owing(&self, mut each: impl FnMut(Key, Slot)) {
582 let Some(now) = self.drove_at.or(self.now) else {
583 return;
584 };
585 for (&key, slots) in &self.tweens {
586 for (i, tw) in slots.iter().enumerate() {
587 let Some(tw) = tw else { continue };
588 if tw.last_used != self.frame_no {
589 continue;
590 }
591 let moving = if tw.easing.is_spring() {
592 tw.value != tw.to || tw.velocity != [0.0; 4]
593 } else {
594 tw.progress_at(now) < 1.0
595 };
596 if moving {
597 each(key, Slot::ALL[i]);
598 }
599 }
600 }
601 }
602
603 pub(crate) fn drive(
613 &mut self,
614 key: Key,
615 slot: Slot,
616 enter_from: Option<[f32; 4]>,
617 target: [f32; 4],
618 transition: Transition,
619 follow: bool,
620 ) -> [f32; 4] {
621 self.node(key)
622 .drive(slot, enter_from, target, transition, follow)
623 }
624
625 pub(crate) fn node(&mut self, key: Key) -> NodeAnim<'_> {
636 NodeAnim {
637 slots: self.tweens.entry(key).or_default(),
638 now: self.now,
639 frame_no: self.frame_no,
640 active: &mut self.owes_transition,
641 cycling: &mut self.owes_cycle,
642 }
643 }
644}
645
646fn sample_track(track: &Track, transition: Transition, now: Option<f64>) -> Option<[f32; 4]> {
650 let dur = transition.duration_ms as f64 / 1000.0;
651 let (Some(now), true, Some(&(_, first))) = (now, dur > 0.0, track.first()) else {
652 return None;
653 };
654 let u = (now - transition.delay_ms as f64 / 1000.0) / dur;
655 let p = transition.repeat.progress(u);
656 let mut from = (0.0, first);
657 for &(at, value) in track {
658 if p < at {
659 let (a, va) = from;
660 let t = if at > a { (p - a) / (at - a) } else { 1.0 };
661 let e = transition.curve().apply(t);
662 let mut out = [0.0; 4];
663 for i in 0..4 {
664 out[i] = va[i] + (value[i] - va[i]) * e;
665 }
666 return Some(out);
667 }
668 from = (at, value);
669 }
670 Some(from.1)
671}
672
673pub(crate) struct NodeAnim<'a> {
677 slots: &'a mut [Option<Tween>; SLOTS],
678 now: Option<f64>,
680 frame_no: u64,
681 active: &'a mut bool,
683 cycling: &'a mut bool,
685}
686
687impl NodeAnim<'_> {
688 pub(crate) fn sample(&mut self, track: &Track, transition: Transition) -> Option<[f32; 4]> {
708 let v = sample_track(track, transition, self.now);
709 if v.is_some() {
710 *self.cycling = true;
711 }
712 v
713 }
714
715 pub(crate) fn drive(
716 &mut self,
717 slot: Slot,
718 enter_from: Option<[f32; 4]>,
719 target: [f32; 4],
720 transition: Transition,
721 follow: bool,
722 ) -> [f32; 4] {
723 let frame_no = self.frame_no;
724 let now = self.now;
725 let entry = &mut self.slots[slot as usize];
726 let stale = entry.is_none_or(|t| t.last_used + 1 < frame_no);
727 let Some(now) = now else {
728 *entry = Some(Tween::settled(target, 0.0, 0.0, transition, frame_no));
729 return target;
730 };
731 if stale {
732 match enter_from {
733 Some(from) if from != target => {
735 *entry = Some(Tween::leg(from, target, now, transition, frame_no));
736 }
737 _ => {
740 *entry = Some(Tween::settled(
741 target,
742 f64::NEG_INFINITY,
743 now,
744 transition,
745 frame_no,
746 ));
747 return target;
748 }
749 }
750 }
751 let tw = entry.as_mut().expect("checked above");
752 tw.last_used = frame_no;
753 if !follow && tw.to != target && tw.value == tw.to && tw.velocity == [0.0; 4] {
754 tw.from = target;
756 tw.to = target;
757 tw.value = target;
758 tw.start = f64::NEG_INFINITY;
759 tw.last_time = now;
760 return target;
761 }
762 if let Some(zeta) = transition.damping() {
763 tw.to = target;
765 tw.duration_ms = transition.duration_ms;
766 tw.easing = transition.curve();
767 if tw.spring_step(now, zeta) {
768 *self.active = true;
769 }
770 return tw.value;
771 }
772 if tw.to != target {
773 tw.from = tw.eased_at(now);
781 tw.to = target;
782 tw.start = now;
783 tw.duration_ms = transition.duration_ms;
784 tw.easing = transition.curve();
785 }
786 let p = tw.progress_at(now);
787 if p < 1.0 {
788 *self.active = true;
789 }
790 tw.value = tw.at(p);
791 tw.value
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 struct Frames(u64);
802 impl Frames {
803 fn next(&mut self) -> u64 {
804 self.0 += 1;
805 self.0
806 }
807 }
808
809 #[test]
821 fn a_tween_stays_small() {
822 const BOUND: usize = 96;
823 let size = std::mem::size_of::<Option<Tween>>();
824 assert!(
825 size <= BOUND,
826 "Option<Tween> is {size} bytes, over the {BOUND}-byte bound. \
827 It is held per slot per node across frames; put what the node \
828 owns on the node and what the cycle owns in the Transition."
829 );
830 assert_eq!(size, std::mem::size_of::<Tween>());
833 }
834
835 fn one(v: f32) -> [f32; 4] {
836 [v, 0.0, 0.0, 0.0]
837 }
838
839 #[test]
840 fn easings_hit_their_endpoints() {
841 for e in [
842 Easing::Linear,
843 Easing::EaseOut,
844 Easing::EaseIn,
845 Easing::EaseInOut,
846 Easing::Spring,
847 Easing::Bouncy,
848 Easing::Smooth,
849 Easing::Snappy,
850 ] {
851 assert_eq!(e.apply(0.0), 0.0);
852 assert_eq!(e.apply(1.0), 1.0);
853 assert!(e.apply(0.5) > 0.0 && e.apply(0.5) < 1.0);
854 }
855 }
856
857 #[test]
858 fn first_sight_snaps_then_retargets_ease() {
859 let mut a = AnimStore::default();
860 let mut frame = Frames(0);
861 let k = Key::ROOT.str("x");
862 let t = Transition::ms(100.0).easing(Easing::Linear);
863 a.set_time(0.0);
864 a.begin_frame(frame.next());
865 assert_eq!(a.drive(k, Slot::Width, None, one(10.0), t, true)[0], 10.0);
866 assert!(!a.animating());
867
868 a.set_time(0.0);
869 a.begin_frame(frame.next());
870 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 10.0);
871 assert!(a.animating(), "mid-flight after retarget");
872
873 a.set_time(0.05);
874 a.begin_frame(frame.next());
875 assert!((a.drive(k, Slot::Width, None, one(20.0), t, true)[0] - 15.0).abs() < 1e-4);
876
877 a.set_time(0.2);
878 a.begin_frame(frame.next());
879 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 20.0);
880 assert!(!a.animating(), "settled");
881 }
882
883 #[test]
884 fn unchanged_targets_owe_no_frames() {
885 let mut a = AnimStore::default();
886 let mut frame = Frames(0);
887 let k = Key::ROOT.str("x");
888 let t = Transition::ms(100.0);
889 for i in 0..3 {
890 a.set_time(i as f64 * 0.001);
891 a.begin_frame(frame.next());
892 a.drive(k, Slot::Width, None, one(5.0), t, true);
893 assert!(!a.animating(), "frame {i}: same value, nothing to animate");
894 }
895 }
896
897 #[test]
898 fn retarget_mid_flight_starts_from_current_value() {
899 let mut a = AnimStore::default();
900 let mut frame = Frames(0);
901 let k = Key::ROOT.str("x");
902 let t = Transition::ms(100.0).easing(Easing::Linear);
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 a.set_time(0.0);
907 a.begin_frame(frame.next());
908 a.drive(k, Slot::Width, None, one(100.0), t, true);
909 a.set_time(0.05);
910 a.begin_frame(frame.next());
911 assert!((a.drive(k, Slot::Width, None, one(100.0), t, true)[0] - 50.0).abs() < 1e-4);
912 a.set_time(0.05);
914 a.begin_frame(frame.next());
915 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 50.0).abs() < 1e-4);
916 a.set_time(0.10);
917 a.begin_frame(frame.next());
918 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 25.0).abs() < 1e-4);
919 }
920
921 #[test]
922 fn springs_overshoot_settle_and_keep_momentum() {
923 let mut a = AnimStore::default();
924 let mut frame = Frames(0);
925 let k = Key::ROOT.str("x");
926 let t = Transition::ms(200.0).easing(Easing::Bouncy);
927 a.set_time(0.0);
928 a.begin_frame(frame.next());
929 a.drive(k, Slot::Width, None, one(0.0), t, true);
930 let mut max = 0.0f32;
932 let mut settled_at = None;
933 for i in 1..=180 {
934 a.set_time(i as f64 / 60.0);
935 a.begin_frame(frame.next());
936 let v = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
937 max = max.max(v);
938 if !a.animating() && settled_at.is_none() {
939 settled_at = Some(i);
940 }
941 }
942 assert!(max > 101.0, "bouncy overshoots: peak {max}");
943 let settled = settled_at.expect("settles within 3s");
944 assert!(settled > 6, "not instant: {settled}");
945 a.set_time(4.0);
946 a.begin_frame(frame.next());
947 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
948
949 a.set_time(4.0);
953 a.begin_frame(frame.next());
954 a.drive(k, Slot::Width, None, one(200.0), t, true);
955 let mut v_prev = 100.0;
956 for i in 1..=2 {
957 a.set_time(4.0 + i as f64 / 60.0);
958 a.begin_frame(frame.next());
959 v_prev = a.drive(k, Slot::Width, None, one(200.0), t, true)[0];
960 }
961 assert!(v_prev > 100.0);
962 a.set_time(4.0 + 3.0 / 60.0);
963 a.begin_frame(frame.next());
964 let after = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
965 assert!(
966 after > v_prev,
967 "momentum carries past the retarget: {v_prev} -> {after}"
968 );
969 }
970
971 fn peak(t: Transition) -> (f32, bool) {
974 let mut a = AnimStore::default();
975 let mut frame = Frames(0);
976 let k = Key::ROOT.str("x");
977 a.set_time(0.0);
978 a.begin_frame(frame.next());
979 a.drive(k, Slot::Width, None, one(0.0), t, true);
980 let mut max = 0.0f32;
981 for i in 1..=180 {
982 a.set_time(i as f64 / 60.0);
983 a.begin_frame(frame.next());
984 max = max.max(a.drive(k, Slot::Width, None, one(100.0), t, true)[0]);
985 }
986 (max, !a.animating())
987 }
988
989 #[test]
994 fn a_spring_s_bounce_is_how_far_it_overshoots() {
995 let t = Transition::ms(200.0);
996 assert_eq!(t.easing(Easing::Spring).damping(), Some(0.75));
997 assert_eq!(t.easing(Easing::Bouncy).damping(), Some(0.5));
998 assert_eq!(t.damping(), None, "a timed curve is no spring");
999
1000 let (smooth, settled) = peak(t.easing(Easing::Smooth));
1001 assert!(settled, "smooth comes to rest");
1002 assert!(smooth <= 100.0 + 1e-3, "smooth never overshoots: {smooth}");
1003 let mut last = smooth;
1004 for e in [Easing::Snappy, Easing::Spring, Easing::Bouncy] {
1005 let (p, settled) = peak(t.easing(e));
1006 assert!(settled, "{e:?} comes to rest");
1007 assert!(
1008 p > last,
1009 "{e:?} overshoots past the one before: {p} <= {last}"
1010 );
1011 last = p;
1012 }
1013 assert_eq!(
1015 peak(t.easing(Easing::Bouncy).bounce(0.0)).0,
1016 smooth,
1017 "bouncy with no bounce is smooth"
1018 );
1019 assert_eq!(
1020 peak(t.easing(Easing::Smooth).bounce(0.5)).0,
1021 last,
1022 "smooth with bouncy's bounce is bouncy"
1023 );
1024 }
1025
1026 #[test]
1030 fn a_bounce_on_a_timed_curve_makes_it_a_spring() {
1031 let t = Transition::ms(200.0).easing(Easing::Linear).bounce(0.5);
1032 assert_eq!(t.curve(), Easing::Spring);
1033 assert_eq!(t.damping(), Some(0.5));
1034 assert_eq!(
1035 peak(t).0,
1036 peak(Transition::ms(200.0).easing(Easing::Bouncy)).0
1037 );
1038 assert_eq!(Transition::ms(200.0).curve(), Easing::EaseOut);
1039 }
1040
1041 #[test]
1042 fn a_bounce_holds_its_range() {
1043 assert!((Bounce::new(0.3).get() - 0.3).abs() < 1e-4);
1044 assert_eq!(Bounce::new(0.0).get(), 0.0);
1045 assert_eq!(Bounce::new(-1.0).get(), 0.0);
1046 assert_eq!(Bounce::new(f32::NAN).get(), 0.0);
1047 assert_eq!(Bounce::new(1.0).get(), MAX_BOUNCE, "1 would never settle");
1048 let (_, settled) = peak(Transition::ms(100.0).easing(Easing::Spring).bounce(1.0));
1049 assert!(settled, "the most bounce there is still comes to rest");
1050 }
1051
1052 #[test]
1056 fn a_transition_carries_its_bounce_in_its_padding() {
1057 assert_eq!(std::mem::size_of::<Option<Bounce>>(), 2);
1058 assert_eq!(std::mem::size_of::<Option<Transition>>(), 12);
1059 }
1060
1061 #[test]
1062 fn an_entrance_starts_its_first_leg_from_the_declared_value() {
1063 let mut a = AnimStore::default();
1064 let mut frame = Frames(0);
1065 let k = Key::ROOT.str("x");
1066 let t = Transition::ms(100.0).easing(Easing::Linear);
1067 a.set_time(0.0);
1068 a.begin_frame(frame.next());
1069 assert_eq!(
1070 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1071 -100.0,
1072 "first sight starts at the entrance"
1073 );
1074 assert!(a.animating(), "and owes a frame");
1075 a.set_time(0.05);
1076 a.begin_frame(frame.next());
1077 assert!(
1078 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0] + 50.0).abs() < 1e-3
1079 );
1080 a.set_time(0.2);
1081 a.begin_frame(frame.next());
1082 assert_eq!(
1083 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1084 0.0
1085 );
1086 assert!(!a.animating());
1087 a.set_time(0.2);
1089 a.begin_frame(frame.next());
1090 assert_eq!(
1091 a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0],
1092 0.0
1093 );
1094 a.set_time(0.25);
1095 a.begin_frame(frame.next());
1096 assert!(
1097 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0] - 20.0).abs() < 1e-3
1098 );
1099 }
1100
1101 #[test]
1102 fn an_entrance_equal_to_the_target_is_no_entrance() {
1103 let mut a = AnimStore::default();
1104 let mut frame = Frames(0);
1105 let k = Key::ROOT.str("x");
1106 let t = Transition::ms(100.0);
1107 a.set_time(0.0);
1108 a.begin_frame(frame.next());
1109 assert_eq!(
1110 a.drive(k, Slot::Bg, Some(one(5.0)), one(5.0), t, true)[0],
1111 5.0
1112 );
1113 assert!(!a.animating());
1114 }
1115
1116 #[test]
1117 fn a_spring_entrance_carries_no_velocity_in() {
1118 let mut a = AnimStore::default();
1119 let mut frame = Frames(0);
1120 let k = Key::ROOT.str("x");
1121 let t = Transition::ms(100.0).easing(Easing::Spring);
1122 a.set_time(0.0);
1123 a.begin_frame(frame.next());
1124 assert_eq!(
1125 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1126 -100.0
1127 );
1128 a.set_time(0.016);
1129 a.begin_frame(frame.next());
1130 let first = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1131 assert!(
1132 first > -100.0 && first < 0.0,
1133 "leaves the entrance toward the target: {first}"
1134 );
1135 assert!(a.animating());
1136 let mut v = first;
1137 for i in 2..=90 {
1138 a.set_time(i as f64 * 0.016);
1139 a.begin_frame(frame.next());
1140 v = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1141 }
1142 assert!(v.abs() < 1.0, "settled on the target: {v}");
1143 assert!(!a.animating());
1144 }
1145
1146 #[test]
1147 fn off_the_leash_a_settled_slot_snaps_to_a_new_target() {
1148 let mut a = AnimStore::default();
1149 let mut frame = Frames(0);
1150 let k = Key::ROOT.str("x");
1151 let t = Transition::ms(100.0).easing(Easing::Linear);
1152 a.set_time(0.0);
1153 a.begin_frame(frame.next());
1154 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, false);
1155 a.set_time(0.05);
1157 a.begin_frame(frame.next());
1158 let at = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1159 a.set_time(0.1);
1160 a.begin_frame(frame.next());
1161 let mid = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1162 assert!(
1163 at < mid && mid < 20.0,
1164 "mid-flight retarget keeps easing: {at} -> {mid}"
1165 );
1166 a.set_time(0.3);
1167 a.begin_frame(frame.next());
1168 assert_eq!(
1169 a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0],
1170 20.0
1171 );
1172 assert!(!a.animating());
1173 a.set_time(0.3);
1175 a.begin_frame(frame.next());
1176 assert_eq!(
1177 a.drive(k, Slot::Pos, Some(one(-100.0)), one(300.0), t, false)[0],
1178 300.0
1179 );
1180 assert!(!a.animating());
1181 }
1182
1183 #[test]
1189 fn a_target_that_moves_every_frame_still_advances() {
1190 let mut a = AnimStore::default();
1191 let mut frame = Frames(0);
1192 let k = Key::ROOT.str("card");
1193 let t = Transition::ms(160.0);
1194 a.set_time(0.0);
1195 a.begin_frame(frame.next());
1196 a.drive(k, Slot::Pos, None, one(0.0), t, true);
1197 let mut behind = Vec::new();
1199 let mut drawn = 0.0;
1200 for f in 1..=60 {
1201 let target = f as f32 * 9.6;
1202 a.set_time(f as f64 * 0.016);
1203 a.begin_frame(frame.next());
1204 drawn = a.drive(k, Slot::Pos, None, one(target), t, true)[0];
1205 behind.push(target - drawn);
1206 }
1207 let target = 60.0 * 9.6;
1208 assert!(
1209 drawn > target * 0.8,
1210 "the pan reaches the screen: drawn {drawn} of {target}"
1211 );
1212 let (early, late) = (behind[29], behind[59]);
1215 assert!(
1216 (early - late).abs() < 1.0,
1217 "the gap stops growing: {early} then {late}"
1218 );
1219 }
1220
1221 #[test]
1222 fn a_slot_skipped_for_a_frame_snaps() {
1223 let mut a = AnimStore::default();
1224 let mut frame = Frames(0);
1225 let k = Key::ROOT.str("x");
1226 let t = Transition::ms(100.0);
1227 a.set_time(0.0);
1228 a.begin_frame(frame.next());
1229 a.drive(k, Slot::Width, None, one(0.0), t, true);
1230 a.set_time(0.01);
1232 a.begin_frame(frame.next());
1233 a.set_time(0.02);
1234 a.begin_frame(frame.next());
1235 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1236 assert!(!a.animating());
1237 }
1238
1239 #[test]
1242 fn keyframes_cycle_in_every_direction() {
1243 let track = [(0.0, one(0.0)), (1.0, one(100.0))];
1244 let mut frame = Frames(0);
1245 let mut at = |a: &mut AnimStore, repeat: Repeat, now: f64| {
1246 let t = Transition::ms(1000.0).easing(Easing::Linear).repeat(repeat);
1247 a.set_time(now);
1248 a.begin_frame(frame.next());
1249 let v = a.node(Key::ROOT).sample(&track, t).unwrap()[0];
1250 assert!(a.animating(), "a keyframed slot always owes a frame");
1251 v
1252 };
1253 let mut a = AnimStore::default();
1254 for (repeat, expect) in [
1255 (Repeat::Normal, [0.0, 25.0, 50.0, 75.0, 0.0, 25.0]),
1256 (Repeat::Reverse, [100.0, 75.0, 50.0, 25.0, 100.0, 75.0]),
1257 (Repeat::Alternate, [0.0, 25.0, 50.0, 75.0, 100.0, 75.0]),
1258 (
1259 Repeat::AlternateReverse,
1260 [100.0, 75.0, 50.0, 25.0, 0.0, 25.0],
1261 ),
1262 ] {
1263 for (i, e) in expect.iter().enumerate() {
1264 let v = at(&mut a, repeat, i as f64 * 0.25);
1265 assert!((v - e).abs() < 1e-3, "{repeat:?} at {i}/4: {v} != {e}");
1266 }
1267 }
1268 }
1269
1270 #[test]
1271 fn delay_shifts_the_cycle_and_easing_shapes_each_segment() {
1272 let mut a = AnimStore::default();
1273 let mut frame = Frames(0);
1274 let track = [(0.0, one(0.0)), (0.5, one(10.0)), (1.0, one(0.0))];
1275 let t = Transition::ms(1000.0).easing(Easing::Linear);
1276 a.set_time(0.25);
1277 a.begin_frame(frame.next());
1278 assert!((a.node(Key::ROOT).sample(&track, t).unwrap()[0] - 5.0).abs() < 1e-3);
1279 a.set_time(0.25);
1281 a.begin_frame(frame.next());
1282 assert!((a.node(Key::ROOT).sample(&track, t.delay(250.0)).unwrap()[0]).abs() < 1e-3);
1283 a.set_time(0.125);
1286 a.begin_frame(frame.next());
1287 let v = a
1288 .node(Key::ROOT)
1289 .sample(&track, t.easing(Easing::EaseIn))
1290 .unwrap()[0];
1291 assert!(v > 0.0 && v < 2.0, "{v}");
1292 let late = [(0.5, one(3.0)), (1.0, one(9.0))];
1294 a.set_time(0.1);
1295 a.begin_frame(frame.next());
1296 assert_eq!(a.node(Key::ROOT).sample(&late, t).unwrap()[0], 3.0);
1297 }
1298
1299 #[test]
1300 fn keyframes_need_a_clock_and_a_duration() {
1301 let mut a = AnimStore::default();
1302 let mut frame = Frames(0);
1303 let track = [(0.0, one(0.0)), (1.0, one(1.0))];
1304 a.begin_frame(frame.next());
1305 assert!(
1306 a.node(Key::ROOT)
1307 .sample(&track, Transition::ms(100.0))
1308 .is_none()
1309 );
1310 assert!(!a.animating());
1311 a.set_time(1.0);
1312 a.begin_frame(frame.next());
1313 assert!(
1314 a.node(Key::ROOT)
1315 .sample(&track, Transition::ms(0.0))
1316 .is_none()
1317 );
1318 assert!(
1319 a.node(Key::ROOT)
1320 .sample(&[], Transition::ms(100.0))
1321 .is_none()
1322 );
1323 assert!(!a.animating());
1324 }
1325
1326 #[test]
1327 fn no_clock_means_no_animation() {
1328 let mut a = AnimStore::default();
1329 let mut frame = Frames(0);
1330 let k = Key::ROOT.str("x");
1331 let t = Transition::ms(100.0);
1332 a.begin_frame(frame.next());
1333 a.drive(k, Slot::Width, None, one(0.0), t, true);
1334 a.begin_frame(frame.next());
1335 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1336 assert!(!a.animating());
1337 }
1338}