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 fn forward(self, k: u64) -> bool {
211 match self {
212 Repeat::Normal => true,
213 Repeat::Reverse => false,
214 Repeat::Alternate => k.is_multiple_of(2),
215 Repeat::AlternateReverse => !k.is_multiple_of(2),
216 }
217 }
218
219 fn end(self, n: f64) -> f32 {
224 let k = (n.ceil() as u64).saturating_sub(1);
225 let frac = (n - k as f64).clamp(0.0, 1.0) as f32;
226 if self.forward(k) { frac } else { 1.0 - frac }
227 }
228
229 fn start(self) -> f32 {
233 if self.forward(0) { 0.0 } else { 1.0 }
234 }
235}
236
237const SPRING_STEP: f64 = 0.004;
240const MAX_FRAME_DT: f64 = 0.1;
243
244pub const MAX_BOUNCE: f32 = 0.9;
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
258pub struct Bounce(std::num::NonZeroU16);
259
260impl Bounce {
261 pub fn new(b: f32) -> Self {
263 let q = (b.clamp(0.0, MAX_BOUNCE) * 10_000.0).round() as u16;
264 Bounce(std::num::NonZeroU16::MIN.saturating_add(q))
265 }
266
267 pub fn get(self) -> f32 {
268 (self.0.get() - 1) as f32 / 10_000.0
269 }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq)]
278pub struct Transition {
279 pub duration_ms: f32,
282 pub easing: Easing,
283 pub repeat: Repeat,
285 pub bounce: Option<Bounce>,
289 pub delay_ms: f32,
292}
293
294impl Transition {
295 pub fn ms(duration_ms: f32) -> Self {
297 Self {
298 duration_ms,
299 easing: Easing::EaseOut,
300 repeat: Repeat::Normal,
301 bounce: None,
302 delay_ms: 0.0,
303 }
304 }
305
306 pub fn easing(mut self, easing: Easing) -> Self {
308 self.easing = easing;
309 self
310 }
311
312 pub fn repeat(mut self, repeat: Repeat) -> Self {
314 self.repeat = repeat;
315 self
316 }
317
318 pub fn delay(mut self, delay_ms: f32) -> Self {
320 self.delay_ms = delay_ms;
321 self
322 }
323
324 pub fn bounce(mut self, bounce: f32) -> Self {
328 self.bounce = Some(Bounce::new(bounce));
329 self
330 }
331
332 pub fn curve(&self) -> Easing {
335 match self.bounce {
336 Some(_) if !self.easing.is_spring() => Easing::Spring,
337 _ => self.easing,
338 }
339 }
340
341 fn damping(&self) -> Option<f32> {
344 let own = self.curve().bounce()?;
345 Some(1.0 - self.bounce.map_or(own, Bounce::get))
346 }
347}
348
349pub(crate) type Track = [(f32, [f32; 4])];
352
353#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355#[repr(u8)]
356pub(crate) enum Slot {
357 Width = 0,
358 Height = 1,
359 Bg = 2,
360 Border = 3,
361 Radius = 4,
362 Pos = 5,
363 Opacity = 6,
364 Shadow = 7,
366 ShadowColor = 8,
367 Transform = 9,
372}
373
374const SLOTS: usize = 9;
380
381impl Slot {
382 const ALL: [Slot; SLOTS + 1] = [
385 Slot::Width,
386 Slot::Height,
387 Slot::Bg,
388 Slot::Border,
389 Slot::Radius,
390 Slot::Pos,
391 Slot::Opacity,
392 Slot::Shadow,
393 Slot::ShadowColor,
394 Slot::Transform,
395 ];
396
397 pub(crate) fn names(mask: u16) -> Vec<&'static str> {
400 Self::ALL
401 .into_iter()
402 .filter(|s| mask & (1 << *s as u16) != 0)
403 .map(Slot::name)
404 .collect()
405 }
406
407 pub(crate) fn name(self) -> &'static str {
410 match self {
411 Slot::Width => "width",
412 Slot::Height => "height",
413 Slot::Bg => "bg",
414 Slot::Border => "borderColor",
415 Slot::Radius => "radius",
416 Slot::Pos => "position",
417 Slot::Opacity => "opacity",
418 Slot::Shadow => "shadow",
419 Slot::ShadowColor => "shadowColor",
420 Slot::Transform => "transform",
421 }
422 }
423}
424
425#[derive(Clone, Copy, Debug)]
443struct Tween {
444 from: [f32; 4],
445 to: [f32; 4],
446 value: [f32; 4],
447 start: f64,
449 velocity: [f32; 4],
451 last_time: f64,
452 duration_ms: f32,
455 easing: Easing,
456 last_used: u64,
457}
458
459impl Tween {
460 fn settled(value: [f32; 4], start: f64, last_time: f64, t: Transition, frame_no: u64) -> Self {
464 Tween {
465 from: value,
466 to: value,
467 value,
468 start,
469 velocity: [0.0; 4],
470 last_time,
471 duration_ms: t.duration_ms,
472 easing: t.curve(),
473 last_used: frame_no,
474 }
475 }
476
477 fn leg(from: [f32; 4], to: [f32; 4], now: f64, t: Transition, frame_no: u64) -> Self {
479 Tween {
480 from,
481 to,
482 value: from,
483 start: now,
484 velocity: [0.0; 4],
485 last_time: now,
486 duration_ms: t.duration_ms,
487 easing: t.curve(),
488 last_used: frame_no,
489 }
490 }
491
492 #[inline]
495 fn progress_at(&self, now: f64) -> f32 {
496 let dur = self.duration_ms.max(0.0) as f64 / 1000.0;
497 if dur <= 0.0 {
498 1.0
499 } else {
500 (((now - self.start) / dur) as f32).clamp(0.0, 1.0)
501 }
502 }
503
504 #[inline]
506 fn at(&self, p: f32) -> [f32; 4] {
507 if p >= 1.0 {
508 return self.to;
509 }
510 let e = self.easing.apply(p);
511 std::array::from_fn(|i| self.from[i] + (self.to[i] - self.from[i]) * e)
512 }
513
514 fn eased_at(&self, now: f64) -> [f32; 4] {
517 self.at(self.progress_at(now))
518 }
519
520 fn spring_step(&mut self, now: f64, zeta: f32) -> bool {
525 let response = (self.duration_ms.max(1.0) / 1000.0) as f64;
526 let omega = std::f64::consts::TAU / response;
527 let k = (omega * omega) as f32;
528 let c = (2.0 * zeta as f64 * omega) as f32;
529 let dt = (now - self.last_time).clamp(0.0, MAX_FRAME_DT);
530 self.last_time = now;
531 let steps = (dt / SPRING_STEP).ceil().max(1.0);
532 let h = (dt / steps) as f32;
533 for _ in 0..steps as usize {
534 for i in 0..4 {
535 let a = -k * (self.value[i] - self.to[i]) - c * self.velocity[i];
536 self.velocity[i] += a * h;
537 self.value[i] += self.velocity[i] * h;
538 }
539 }
540 let moving = (0..4)
543 .any(|i| (self.value[i] - self.to[i]).abs() > 1e-3 || self.velocity[i].abs() > 5e-2);
544 if !moving {
545 self.value = self.to;
546 self.velocity = [0.0; 4];
547 }
548 moving
549 }
550}
551
552#[derive(Default)]
553pub struct AnimStore {
554 tweens: FxHashMap<Key, [Option<Tween>; SLOTS]>,
555 turns: FxHashMap<Key, Option<Tween>>,
559 starts: FxHashMap<Key, (f64, u64)>,
565 now: Option<f64>,
567 frame_no: u64,
569 drove_at: Option<f64>,
574 owes_transition: bool,
577 owes_cycle: bool,
582}
583
584impl AnimStore {
585 pub fn set_time(&mut self, now_secs: f64) {
588 self.now = Some(now_secs);
589 }
590
591 pub fn time(&self) -> Option<f64> {
592 self.now
593 }
594
595 pub fn animating(&self) -> bool {
599 self.owes_transition || self.owes_cycle
600 }
601
602 pub fn owes(&self) -> (bool, bool) {
605 (self.owes_transition, self.owes_cycle)
606 }
607
608 pub(crate) fn begin_frame(&mut self, frame_no: u64) {
611 self.frame_no = frame_no;
612 self.drove_at = self.now;
613 self.owes_transition = false;
614 self.owes_cycle = false;
615 if !self.tweens.is_empty()
617 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
618 {
619 self.tweens
620 .retain(|_, slots| slots.iter().flatten().any(|t| t.last_used >= cutoff));
621 }
622 if !self.starts.is_empty()
623 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
624 {
625 self.starts.retain(|_, (_, used)| *used >= cutoff);
626 }
627 if !self.turns.is_empty()
628 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
629 {
630 self.turns
631 .retain(|_, t| t.is_some_and(|t| t.last_used >= cutoff));
632 }
633 }
634
635 pub(crate) fn owing(&self, mut each: impl FnMut(Key, Slot)) {
643 let Some(now) = self.drove_at.or(self.now) else {
644 return;
645 };
646 let moving = |tw: &Tween| {
647 tw.last_used == self.frame_no
648 && if tw.easing.is_spring() {
649 tw.value != tw.to || tw.velocity != [0.0; 4]
650 } else {
651 tw.progress_at(now) < 1.0
652 }
653 };
654 for (&key, slots) in &self.tweens {
655 for (i, tw) in slots.iter().enumerate() {
656 if tw.as_ref().is_some_and(moving) {
657 each(key, Slot::ALL[i]);
658 }
659 }
660 }
661 for (&key, tw) in &self.turns {
662 if tw.as_ref().is_some_and(moving) {
663 each(key, Slot::Transform);
664 }
665 }
666 }
667
668 pub(crate) fn drive(
678 &mut self,
679 key: Key,
680 slot: Slot,
681 enter_from: Option<[f32; 4]>,
682 target: [f32; 4],
683 transition: Transition,
684 follow: bool,
685 ) -> [f32; 4] {
686 self.node(key)
687 .drive(slot, enter_from, target, transition, follow)
688 }
689
690 pub(crate) fn node(&mut self, key: Key) -> NodeAnim<'_> {
701 NodeAnim {
702 slots: self.tweens.entry(key).or_default(),
703 key,
704 starts: &mut self.starts,
705 now: self.now,
706 frame_no: self.frame_no,
707 active: &mut self.owes_transition,
708 cycling: &mut self.owes_cycle,
709 }
710 }
711
712 pub(crate) fn cycle_running(
717 &self,
718 key: Key,
719 transition: Transition,
720 iterations: Option<f32>,
721 ) -> bool {
722 let Some(n) = iterations else {
723 return true;
724 };
725 let (Some(now), Some(&(start, _))) = (self.drove_at.or(self.now), self.starts.get(&key))
726 else {
727 return false;
728 };
729 let dur = transition.duration_ms as f64 / 1000.0;
730 dur > 0.0 && (now - start - transition.delay_ms as f64 / 1000.0) / dur < n as f64
731 }
732
733 #[inline]
739 pub(crate) fn turn_live(&self, key: Key) -> bool {
740 !self.turns.is_empty()
741 && self
742 .turns
743 .get(&key)
744 .and_then(Option::as_ref)
745 .is_some_and(|t| t.last_used + 1 >= self.frame_no)
746 }
747
748 pub(crate) fn turn_starts_upright(&self, key: Key) -> bool {
753 let prev = self.frame_no.wrapping_sub(1);
754 !self.turn_live(key)
755 && self
756 .tweens
757 .get(&key)
758 .is_some_and(|s| s.iter().flatten().any(|t| t.last_used == prev))
759 }
760
761 pub(crate) fn forget_settled_turn(&mut self, key: Key, identity: [f32; 4]) {
764 if let Some(Some(t)) = self.turns.get(&key)
765 && t.value == identity
766 && t.to == identity
767 && t.velocity == [0.0; 4]
768 {
769 self.turns.remove(&key);
770 }
771 }
772
773 pub(crate) fn drive_turn(
774 &mut self,
775 key: Key,
776 enter_from: Option<[f32; 4]>,
777 target: [f32; 4],
778 transition: Transition,
779 ) -> [f32; 4] {
780 let entry = self.turns.entry(key).or_default();
781 drive_entry(
782 entry,
783 self.now,
784 self.frame_no,
785 &mut self.owes_transition,
786 enter_from,
787 target,
788 transition,
789 true,
790 )
791 }
792
793 pub(crate) fn sample_cycle(
796 &mut self,
797 key: Key,
798 track: &Track,
799 transition: Transition,
800 iterations: Option<f32>,
801 ) -> Option<[f32; 4]> {
802 let now = self.now?;
803 let finite =
804 iterations.map(|n| (n, cycle_start(&mut self.starts, key, now, self.frame_no)));
805 let (v, running) = sample_track(track, transition, Some(now), finite)?;
806 if running {
807 self.owes_cycle = true;
808 }
809 Some(v)
810 }
811}
812
813fn sample_track(
822 track: &Track,
823 transition: Transition,
824 now: Option<f64>,
825 finite: Option<(f32, f64)>,
826) -> Option<([f32; 4], bool)> {
827 let dur = transition.duration_ms as f64 / 1000.0;
828 let (Some(now), true, Some(&(_, first))) = (now, dur > 0.0, track.first()) else {
829 return None;
830 };
831 let delay = transition.delay_ms as f64 / 1000.0;
832 let (p, running) = match finite {
833 None => (transition.repeat.progress((now - delay) / dur), true),
834 Some((n, start)) => {
835 let u = (now - start - delay) / dur;
836 if u <= 0.0 {
837 (transition.repeat.start(), true)
838 } else if u >= n as f64 {
839 (transition.repeat.end(n as f64), false)
840 } else {
841 (transition.repeat.progress(u), true)
842 }
843 }
844 };
845 let v = segment_at(track, transition, p, first);
846 Some((v, running))
847}
848
849fn segment_at(track: &Track, transition: Transition, p: f32, first: [f32; 4]) -> [f32; 4] {
853 let mut from = (0.0, first);
854 for &(at, value) in track {
855 if p < at {
856 let (a, va) = from;
857 let t = if at > a { (p - a) / (at - a) } else { 1.0 };
858 let e = transition.curve().apply(t);
859 let mut out = [0.0; 4];
860 for i in 0..4 {
861 out[i] = va[i] + (value[i] - va[i]) * e;
862 }
863 return out;
864 }
865 from = (at, value);
866 }
867 from.1
868}
869
870fn cycle_start(starts: &mut FxHashMap<Key, (f64, u64)>, key: Key, now: f64, frame_no: u64) -> f64 {
873 let e = starts.entry(key).or_insert((now, frame_no));
874 if e.1 + 1 < frame_no {
875 e.0 = now;
876 }
877 e.1 = frame_no;
878 e.0
879}
880
881pub(crate) struct NodeAnim<'a> {
885 slots: &'a mut [Option<Tween>; SLOTS],
886 key: Key,
888 starts: &'a mut FxHashMap<Key, (f64, u64)>,
889 now: Option<f64>,
891 frame_no: u64,
892 active: &'a mut bool,
894 cycling: &'a mut bool,
896}
897
898impl NodeAnim<'_> {
899 pub(crate) fn sample(
919 &mut self,
920 track: &Track,
921 transition: Transition,
922 iterations: Option<f32>,
923 ) -> Option<[f32; 4]> {
924 let now = self.now?;
925 let finite =
926 iterations.map(|n| (n, cycle_start(self.starts, self.key, now, self.frame_no)));
927 let (v, running) = sample_track(track, transition, Some(now), finite)?;
928 if running {
929 *self.cycling = true;
930 }
931 Some(v)
932 }
933
934 #[inline]
935 pub(crate) fn drive(
936 &mut self,
937 slot: Slot,
938 enter_from: Option<[f32; 4]>,
939 target: [f32; 4],
940 transition: Transition,
941 follow: bool,
942 ) -> [f32; 4] {
943 debug_assert!(slot != Slot::Transform);
946 drive_entry(
947 &mut self.slots[slot as usize],
948 self.now,
949 self.frame_no,
950 self.active,
951 enter_from,
952 target,
953 transition,
954 follow,
955 )
956 }
957}
958
959#[inline(never)]
969#[allow(clippy::too_many_arguments)]
970fn drive_entry(
971 entry: &mut Option<Tween>,
972 now: Option<f64>,
973 frame_no: u64,
974 active: &mut bool,
975 enter_from: Option<[f32; 4]>,
976 target: [f32; 4],
977 transition: Transition,
978 follow: bool,
979) -> [f32; 4] {
980 {
981 let stale = entry.is_none_or(|t| t.last_used + 1 < frame_no);
982 let Some(now) = now else {
983 *entry = Some(Tween::settled(target, 0.0, 0.0, transition, frame_no));
984 return target;
985 };
986 if stale {
987 match enter_from {
988 Some(from) if from != target => {
990 *entry = Some(Tween::leg(from, target, now, transition, frame_no));
991 }
992 _ => {
995 *entry = Some(Tween::settled(
996 target,
997 f64::NEG_INFINITY,
998 now,
999 transition,
1000 frame_no,
1001 ));
1002 return target;
1003 }
1004 }
1005 }
1006 let tw = entry.as_mut().expect("checked above");
1007 tw.last_used = frame_no;
1008 if !follow && tw.to != target && tw.value == tw.to && tw.velocity == [0.0; 4] {
1009 tw.from = target;
1011 tw.to = target;
1012 tw.value = target;
1013 tw.start = f64::NEG_INFINITY;
1014 tw.last_time = now;
1015 return target;
1016 }
1017 if let Some(zeta) = transition.damping() {
1018 tw.to = target;
1020 tw.duration_ms = transition.duration_ms;
1021 tw.easing = transition.curve();
1022 if tw.spring_step(now, zeta) {
1023 *active = true;
1024 }
1025 return tw.value;
1026 }
1027 if tw.to != target {
1028 tw.from = tw.eased_at(now);
1036 tw.to = target;
1037 tw.start = now;
1038 tw.duration_ms = transition.duration_ms;
1039 tw.easing = transition.curve();
1040 }
1041 let p = tw.progress_at(now);
1042 if p < 1.0 {
1043 *active = true;
1044 }
1045 tw.value = tw.at(p);
1046 tw.value
1047 }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::*;
1053
1054 struct Frames(u64);
1057 impl Frames {
1058 fn next(&mut self) -> u64 {
1059 self.0 += 1;
1060 self.0
1061 }
1062 }
1063
1064 #[test]
1076 fn a_tween_stays_small() {
1077 const BOUND: usize = 96;
1078 let size = std::mem::size_of::<Option<Tween>>();
1079 assert!(
1080 size <= BOUND,
1081 "Option<Tween> is {size} bytes, over the {BOUND}-byte bound. \
1082 It is held per slot per node across frames; put what the node \
1083 owns on the node and what the cycle owns in the Transition."
1084 );
1085 assert_eq!(size, std::mem::size_of::<Tween>());
1088 }
1089
1090 fn one(v: f32) -> [f32; 4] {
1091 [v, 0.0, 0.0, 0.0]
1092 }
1093
1094 #[test]
1095 fn easings_hit_their_endpoints() {
1096 for e in [
1097 Easing::Linear,
1098 Easing::EaseOut,
1099 Easing::EaseIn,
1100 Easing::EaseInOut,
1101 Easing::Spring,
1102 Easing::Bouncy,
1103 Easing::Smooth,
1104 Easing::Snappy,
1105 ] {
1106 assert_eq!(e.apply(0.0), 0.0);
1107 assert_eq!(e.apply(1.0), 1.0);
1108 assert!(e.apply(0.5) > 0.0 && e.apply(0.5) < 1.0);
1109 }
1110 }
1111
1112 #[test]
1113 fn first_sight_snaps_then_retargets_ease() {
1114 let mut a = AnimStore::default();
1115 let mut frame = Frames(0);
1116 let k = Key::ROOT.str("x");
1117 let t = Transition::ms(100.0).easing(Easing::Linear);
1118 a.set_time(0.0);
1119 a.begin_frame(frame.next());
1120 assert_eq!(a.drive(k, Slot::Width, None, one(10.0), t, true)[0], 10.0);
1121 assert!(!a.animating());
1122
1123 a.set_time(0.0);
1124 a.begin_frame(frame.next());
1125 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 10.0);
1126 assert!(a.animating(), "mid-flight after retarget");
1127
1128 a.set_time(0.05);
1129 a.begin_frame(frame.next());
1130 assert!((a.drive(k, Slot::Width, None, one(20.0), t, true)[0] - 15.0).abs() < 1e-4);
1131
1132 a.set_time(0.2);
1133 a.begin_frame(frame.next());
1134 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 20.0);
1135 assert!(!a.animating(), "settled");
1136 }
1137
1138 #[test]
1139 fn unchanged_targets_owe_no_frames() {
1140 let mut a = AnimStore::default();
1141 let mut frame = Frames(0);
1142 let k = Key::ROOT.str("x");
1143 let t = Transition::ms(100.0);
1144 for i in 0..3 {
1145 a.set_time(i as f64 * 0.001);
1146 a.begin_frame(frame.next());
1147 a.drive(k, Slot::Width, None, one(5.0), t, true);
1148 assert!(!a.animating(), "frame {i}: same value, nothing to animate");
1149 }
1150 }
1151
1152 #[test]
1153 fn retarget_mid_flight_starts_from_current_value() {
1154 let mut a = AnimStore::default();
1155 let mut frame = Frames(0);
1156 let k = Key::ROOT.str("x");
1157 let t = Transition::ms(100.0).easing(Easing::Linear);
1158 a.set_time(0.0);
1159 a.begin_frame(frame.next());
1160 a.drive(k, Slot::Width, None, one(0.0), t, true);
1161 a.set_time(0.0);
1162 a.begin_frame(frame.next());
1163 a.drive(k, Slot::Width, None, one(100.0), t, true);
1164 a.set_time(0.05);
1165 a.begin_frame(frame.next());
1166 assert!((a.drive(k, Slot::Width, None, one(100.0), t, true)[0] - 50.0).abs() < 1e-4);
1167 a.set_time(0.05);
1169 a.begin_frame(frame.next());
1170 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 50.0).abs() < 1e-4);
1171 a.set_time(0.10);
1172 a.begin_frame(frame.next());
1173 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 25.0).abs() < 1e-4);
1174 }
1175
1176 #[test]
1177 fn springs_overshoot_settle_and_keep_momentum() {
1178 let mut a = AnimStore::default();
1179 let mut frame = Frames(0);
1180 let k = Key::ROOT.str("x");
1181 let t = Transition::ms(200.0).easing(Easing::Bouncy);
1182 a.set_time(0.0);
1183 a.begin_frame(frame.next());
1184 a.drive(k, Slot::Width, None, one(0.0), t, true);
1185 let mut max = 0.0f32;
1187 let mut settled_at = None;
1188 for i in 1..=180 {
1189 a.set_time(i as f64 / 60.0);
1190 a.begin_frame(frame.next());
1191 let v = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
1192 max = max.max(v);
1193 if !a.animating() && settled_at.is_none() {
1194 settled_at = Some(i);
1195 }
1196 }
1197 assert!(max > 101.0, "bouncy overshoots: peak {max}");
1198 let settled = settled_at.expect("settles within 3s");
1199 assert!(settled > 6, "not instant: {settled}");
1200 a.set_time(4.0);
1201 a.begin_frame(frame.next());
1202 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1203
1204 a.set_time(4.0);
1208 a.begin_frame(frame.next());
1209 a.drive(k, Slot::Width, None, one(200.0), t, true);
1210 let mut v_prev = 100.0;
1211 for i in 1..=2 {
1212 a.set_time(4.0 + i as f64 / 60.0);
1213 a.begin_frame(frame.next());
1214 v_prev = a.drive(k, Slot::Width, None, one(200.0), t, true)[0];
1215 }
1216 assert!(v_prev > 100.0);
1217 a.set_time(4.0 + 3.0 / 60.0);
1218 a.begin_frame(frame.next());
1219 let after = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
1220 assert!(
1221 after > v_prev,
1222 "momentum carries past the retarget: {v_prev} -> {after}"
1223 );
1224 }
1225
1226 fn peak(t: Transition) -> (f32, bool) {
1229 let mut a = AnimStore::default();
1230 let mut frame = Frames(0);
1231 let k = Key::ROOT.str("x");
1232 a.set_time(0.0);
1233 a.begin_frame(frame.next());
1234 a.drive(k, Slot::Width, None, one(0.0), t, true);
1235 let mut max = 0.0f32;
1236 for i in 1..=180 {
1237 a.set_time(i as f64 / 60.0);
1238 a.begin_frame(frame.next());
1239 max = max.max(a.drive(k, Slot::Width, None, one(100.0), t, true)[0]);
1240 }
1241 (max, !a.animating())
1242 }
1243
1244 #[test]
1249 fn a_spring_s_bounce_is_how_far_it_overshoots() {
1250 let t = Transition::ms(200.0);
1251 assert_eq!(t.easing(Easing::Spring).damping(), Some(0.75));
1252 assert_eq!(t.easing(Easing::Bouncy).damping(), Some(0.5));
1253 assert_eq!(t.damping(), None, "a timed curve is no spring");
1254
1255 let (smooth, settled) = peak(t.easing(Easing::Smooth));
1256 assert!(settled, "smooth comes to rest");
1257 assert!(smooth <= 100.0 + 1e-3, "smooth never overshoots: {smooth}");
1258 let mut last = smooth;
1259 for e in [Easing::Snappy, Easing::Spring, Easing::Bouncy] {
1260 let (p, settled) = peak(t.easing(e));
1261 assert!(settled, "{e:?} comes to rest");
1262 assert!(
1263 p > last,
1264 "{e:?} overshoots past the one before: {p} <= {last}"
1265 );
1266 last = p;
1267 }
1268 assert_eq!(
1270 peak(t.easing(Easing::Bouncy).bounce(0.0)).0,
1271 smooth,
1272 "bouncy with no bounce is smooth"
1273 );
1274 assert_eq!(
1275 peak(t.easing(Easing::Smooth).bounce(0.5)).0,
1276 last,
1277 "smooth with bouncy's bounce is bouncy"
1278 );
1279 }
1280
1281 #[test]
1285 fn a_bounce_on_a_timed_curve_makes_it_a_spring() {
1286 let t = Transition::ms(200.0).easing(Easing::Linear).bounce(0.5);
1287 assert_eq!(t.curve(), Easing::Spring);
1288 assert_eq!(t.damping(), Some(0.5));
1289 assert_eq!(
1290 peak(t).0,
1291 peak(Transition::ms(200.0).easing(Easing::Bouncy)).0
1292 );
1293 assert_eq!(Transition::ms(200.0).curve(), Easing::EaseOut);
1294 }
1295
1296 #[test]
1297 fn a_bounce_holds_its_range() {
1298 assert!((Bounce::new(0.3).get() - 0.3).abs() < 1e-4);
1299 assert_eq!(Bounce::new(0.0).get(), 0.0);
1300 assert_eq!(Bounce::new(-1.0).get(), 0.0);
1301 assert_eq!(Bounce::new(f32::NAN).get(), 0.0);
1302 assert_eq!(Bounce::new(1.0).get(), MAX_BOUNCE, "1 would never settle");
1303 let (_, settled) = peak(Transition::ms(100.0).easing(Easing::Spring).bounce(1.0));
1304 assert!(settled, "the most bounce there is still comes to rest");
1305 }
1306
1307 #[test]
1311 fn a_transition_carries_its_bounce_in_its_padding() {
1312 assert_eq!(std::mem::size_of::<Option<Bounce>>(), 2);
1313 assert_eq!(std::mem::size_of::<Option<Transition>>(), 12);
1314 }
1315
1316 #[test]
1317 fn an_entrance_starts_its_first_leg_from_the_declared_value() {
1318 let mut a = AnimStore::default();
1319 let mut frame = Frames(0);
1320 let k = Key::ROOT.str("x");
1321 let t = Transition::ms(100.0).easing(Easing::Linear);
1322 a.set_time(0.0);
1323 a.begin_frame(frame.next());
1324 assert_eq!(
1325 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1326 -100.0,
1327 "first sight starts at the entrance"
1328 );
1329 assert!(a.animating(), "and owes a frame");
1330 a.set_time(0.05);
1331 a.begin_frame(frame.next());
1332 assert!(
1333 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0] + 50.0).abs() < 1e-3
1334 );
1335 a.set_time(0.2);
1336 a.begin_frame(frame.next());
1337 assert_eq!(
1338 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1339 0.0
1340 );
1341 assert!(!a.animating());
1342 a.set_time(0.2);
1344 a.begin_frame(frame.next());
1345 assert_eq!(
1346 a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0],
1347 0.0
1348 );
1349 a.set_time(0.25);
1350 a.begin_frame(frame.next());
1351 assert!(
1352 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0] - 20.0).abs() < 1e-3
1353 );
1354 }
1355
1356 #[test]
1357 fn an_entrance_equal_to_the_target_is_no_entrance() {
1358 let mut a = AnimStore::default();
1359 let mut frame = Frames(0);
1360 let k = Key::ROOT.str("x");
1361 let t = Transition::ms(100.0);
1362 a.set_time(0.0);
1363 a.begin_frame(frame.next());
1364 assert_eq!(
1365 a.drive(k, Slot::Bg, Some(one(5.0)), one(5.0), t, true)[0],
1366 5.0
1367 );
1368 assert!(!a.animating());
1369 }
1370
1371 #[test]
1372 fn a_spring_entrance_carries_no_velocity_in() {
1373 let mut a = AnimStore::default();
1374 let mut frame = Frames(0);
1375 let k = Key::ROOT.str("x");
1376 let t = Transition::ms(100.0).easing(Easing::Spring);
1377 a.set_time(0.0);
1378 a.begin_frame(frame.next());
1379 assert_eq!(
1380 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1381 -100.0
1382 );
1383 a.set_time(0.016);
1384 a.begin_frame(frame.next());
1385 let first = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1386 assert!(
1387 first > -100.0 && first < 0.0,
1388 "leaves the entrance toward the target: {first}"
1389 );
1390 assert!(a.animating());
1391 let mut v = first;
1392 for i in 2..=90 {
1393 a.set_time(i as f64 * 0.016);
1394 a.begin_frame(frame.next());
1395 v = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1396 }
1397 assert!(v.abs() < 1.0, "settled on the target: {v}");
1398 assert!(!a.animating());
1399 }
1400
1401 #[test]
1402 fn off_the_leash_a_settled_slot_snaps_to_a_new_target() {
1403 let mut a = AnimStore::default();
1404 let mut frame = Frames(0);
1405 let k = Key::ROOT.str("x");
1406 let t = Transition::ms(100.0).easing(Easing::Linear);
1407 a.set_time(0.0);
1408 a.begin_frame(frame.next());
1409 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, false);
1410 a.set_time(0.05);
1412 a.begin_frame(frame.next());
1413 let at = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1414 a.set_time(0.1);
1415 a.begin_frame(frame.next());
1416 let mid = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1417 assert!(
1418 at < mid && mid < 20.0,
1419 "mid-flight retarget keeps easing: {at} -> {mid}"
1420 );
1421 a.set_time(0.3);
1422 a.begin_frame(frame.next());
1423 assert_eq!(
1424 a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0],
1425 20.0
1426 );
1427 assert!(!a.animating());
1428 a.set_time(0.3);
1430 a.begin_frame(frame.next());
1431 assert_eq!(
1432 a.drive(k, Slot::Pos, Some(one(-100.0)), one(300.0), t, false)[0],
1433 300.0
1434 );
1435 assert!(!a.animating());
1436 }
1437
1438 #[test]
1444 fn a_target_that_moves_every_frame_still_advances() {
1445 let mut a = AnimStore::default();
1446 let mut frame = Frames(0);
1447 let k = Key::ROOT.str("card");
1448 let t = Transition::ms(160.0);
1449 a.set_time(0.0);
1450 a.begin_frame(frame.next());
1451 a.drive(k, Slot::Pos, None, one(0.0), t, true);
1452 let mut behind = Vec::new();
1454 let mut drawn = 0.0;
1455 for f in 1..=60 {
1456 let target = f as f32 * 9.6;
1457 a.set_time(f as f64 * 0.016);
1458 a.begin_frame(frame.next());
1459 drawn = a.drive(k, Slot::Pos, None, one(target), t, true)[0];
1460 behind.push(target - drawn);
1461 }
1462 let target = 60.0 * 9.6;
1463 assert!(
1464 drawn > target * 0.8,
1465 "the pan reaches the screen: drawn {drawn} of {target}"
1466 );
1467 let (early, late) = (behind[29], behind[59]);
1470 assert!(
1471 (early - late).abs() < 1.0,
1472 "the gap stops growing: {early} then {late}"
1473 );
1474 }
1475
1476 #[test]
1477 fn a_slot_skipped_for_a_frame_snaps() {
1478 let mut a = AnimStore::default();
1479 let mut frame = Frames(0);
1480 let k = Key::ROOT.str("x");
1481 let t = Transition::ms(100.0);
1482 a.set_time(0.0);
1483 a.begin_frame(frame.next());
1484 a.drive(k, Slot::Width, None, one(0.0), t, true);
1485 a.set_time(0.01);
1487 a.begin_frame(frame.next());
1488 a.set_time(0.02);
1489 a.begin_frame(frame.next());
1490 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1491 assert!(!a.animating());
1492 }
1493
1494 #[test]
1497 fn keyframes_cycle_in_every_direction() {
1498 let track = [(0.0, one(0.0)), (1.0, one(100.0))];
1499 let mut frame = Frames(0);
1500 let mut at = |a: &mut AnimStore, repeat: Repeat, now: f64| {
1501 let t = Transition::ms(1000.0).easing(Easing::Linear).repeat(repeat);
1502 a.set_time(now);
1503 a.begin_frame(frame.next());
1504 let v = a.node(Key::ROOT).sample(&track, t, None).unwrap()[0];
1505 assert!(a.animating(), "a keyframed slot always owes a frame");
1506 v
1507 };
1508 let mut a = AnimStore::default();
1509 for (repeat, expect) in [
1510 (Repeat::Normal, [0.0, 25.0, 50.0, 75.0, 0.0, 25.0]),
1511 (Repeat::Reverse, [100.0, 75.0, 50.0, 25.0, 100.0, 75.0]),
1512 (Repeat::Alternate, [0.0, 25.0, 50.0, 75.0, 100.0, 75.0]),
1513 (
1514 Repeat::AlternateReverse,
1515 [100.0, 75.0, 50.0, 25.0, 0.0, 25.0],
1516 ),
1517 ] {
1518 for (i, e) in expect.iter().enumerate() {
1519 let v = at(&mut a, repeat, i as f64 * 0.25);
1520 assert!((v - e).abs() < 1e-3, "{repeat:?} at {i}/4: {v} != {e}");
1521 }
1522 }
1523 }
1524
1525 #[test]
1526 fn delay_shifts_the_cycle_and_easing_shapes_each_segment() {
1527 let mut a = AnimStore::default();
1528 let mut frame = Frames(0);
1529 let track = [(0.0, one(0.0)), (0.5, one(10.0)), (1.0, one(0.0))];
1530 let t = Transition::ms(1000.0).easing(Easing::Linear);
1531 a.set_time(0.25);
1532 a.begin_frame(frame.next());
1533 assert!((a.node(Key::ROOT).sample(&track, t, None).unwrap()[0] - 5.0).abs() < 1e-3);
1534 a.set_time(0.25);
1536 a.begin_frame(frame.next());
1537 assert!(
1538 (a.node(Key::ROOT)
1539 .sample(&track, t.delay(250.0), None)
1540 .unwrap()[0])
1541 .abs()
1542 < 1e-3
1543 );
1544 a.set_time(0.125);
1547 a.begin_frame(frame.next());
1548 let v = a
1549 .node(Key::ROOT)
1550 .sample(&track, t.easing(Easing::EaseIn), None)
1551 .unwrap()[0];
1552 assert!(v > 0.0 && v < 2.0, "{v}");
1553 let late = [(0.5, one(3.0)), (1.0, one(9.0))];
1555 a.set_time(0.1);
1556 a.begin_frame(frame.next());
1557 assert_eq!(a.node(Key::ROOT).sample(&late, t, None).unwrap()[0], 3.0);
1558 }
1559
1560 #[test]
1561 fn keyframes_need_a_clock_and_a_duration() {
1562 let mut a = AnimStore::default();
1563 let mut frame = Frames(0);
1564 let track = [(0.0, one(0.0)), (1.0, one(1.0))];
1565 a.begin_frame(frame.next());
1566 assert!(
1567 a.node(Key::ROOT)
1568 .sample(&track, Transition::ms(100.0), None)
1569 .is_none()
1570 );
1571 assert!(!a.animating());
1572 a.set_time(1.0);
1573 a.begin_frame(frame.next());
1574 assert!(
1575 a.node(Key::ROOT)
1576 .sample(&track, Transition::ms(0.0), None)
1577 .is_none()
1578 );
1579 assert!(
1580 a.node(Key::ROOT)
1581 .sample(&[], Transition::ms(100.0), None)
1582 .is_none()
1583 );
1584 assert!(!a.animating());
1585 }
1586
1587 #[test]
1588 fn no_clock_means_no_animation() {
1589 let mut a = AnimStore::default();
1590 let mut frame = Frames(0);
1591 let k = Key::ROOT.str("x");
1592 let t = Transition::ms(100.0);
1593 a.begin_frame(frame.next());
1594 a.drive(k, Slot::Width, None, one(0.0), t, true);
1595 a.begin_frame(frame.next());
1596 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1597 assert!(!a.animating());
1598 }
1599}