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)]
153pub enum Repeat {
154 #[default]
156 Normal,
157 Reverse,
159 Alternate,
161 AlternateReverse,
163}
164
165impl Repeat {
166 pub const ALL: &'static [Repeat] = &[
169 Repeat::Normal,
170 Repeat::Reverse,
171 Repeat::Alternate,
172 Repeat::AlternateReverse,
173 ];
174
175 pub fn name(self) -> &'static str {
177 match self {
178 Repeat::Normal => "normal",
179 Repeat::Reverse => "reverse",
180 Repeat::Alternate => "alternate",
181 Repeat::AlternateReverse => "alternateReverse",
182 }
183 }
184
185 pub fn from_index(i: usize) -> Repeat {
188 Self::ALL.get(i).copied().unwrap_or_default()
189 }
190
191 fn progress(self, u: f64) -> f32 {
194 let p = match self {
197 Repeat::Normal => u.rem_euclid(1.0),
198 Repeat::Reverse => 1.0 - u.rem_euclid(1.0),
199 Repeat::Alternate => {
200 let c = u.rem_euclid(2.0);
201 if c < 1.0 { c } else { 2.0 - c }
202 }
203 Repeat::AlternateReverse => {
204 let c = u.rem_euclid(2.0);
205 if c < 1.0 { 1.0 - c } else { c - 1.0 }
206 }
207 };
208 p as f32
209 }
210
211 fn forward(self, k: u64) -> bool {
213 match self {
214 Repeat::Normal => true,
215 Repeat::Reverse => false,
216 Repeat::Alternate => k.is_multiple_of(2),
217 Repeat::AlternateReverse => !k.is_multiple_of(2),
218 }
219 }
220
221 fn end(self, n: f64) -> f32 {
226 let k = (n.ceil() as u64).saturating_sub(1);
227 let frac = (n - k as f64).clamp(0.0, 1.0) as f32;
228 if self.forward(k) { frac } else { 1.0 - frac }
229 }
230
231 fn start(self) -> f32 {
235 if self.forward(0) { 0.0 } else { 1.0 }
236 }
237}
238
239const SPRING_STEP: f64 = 0.004;
242const MAX_FRAME_DT: f64 = 0.1;
245
246pub const MAX_BOUNCE: f32 = 0.9;
249
250#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
260pub struct Bounce(std::num::NonZeroU16);
261
262impl Bounce {
263 pub fn new(b: f32) -> Self {
265 let q = (b.clamp(0.0, MAX_BOUNCE) * 10_000.0).round() as u16;
266 Bounce(std::num::NonZeroU16::MIN.saturating_add(q))
267 }
268
269 pub fn get(self) -> f32 {
270 (self.0.get() - 1) as f32 / 10_000.0
271 }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq)]
280pub struct Transition {
281 pub duration_ms: f32,
284 pub easing: Easing,
285 pub repeat: Repeat,
287 pub bounce: Option<Bounce>,
291 pub delay_ms: f32,
294}
295
296impl Transition {
297 pub fn ms(duration_ms: f32) -> Self {
299 Self {
300 duration_ms,
301 easing: Easing::EaseOut,
302 repeat: Repeat::Normal,
303 bounce: None,
304 delay_ms: 0.0,
305 }
306 }
307
308 pub fn easing(mut self, easing: Easing) -> Self {
310 self.easing = easing;
311 self
312 }
313
314 pub fn repeat(mut self, repeat: Repeat) -> Self {
316 self.repeat = repeat;
317 self
318 }
319
320 pub fn delay(mut self, delay_ms: f32) -> Self {
322 self.delay_ms = delay_ms;
323 self
324 }
325
326 pub fn bounce(mut self, bounce: f32) -> Self {
330 self.bounce = Some(Bounce::new(bounce));
331 self
332 }
333
334 pub fn curve(&self) -> Easing {
337 match self.bounce {
338 Some(_) if !self.easing.is_spring() => Easing::Spring,
339 _ => self.easing,
340 }
341 }
342
343 fn damping(&self) -> Option<f32> {
346 let own = self.curve().bounce()?;
347 Some(1.0 - self.bounce.map_or(own, Bounce::get))
348 }
349}
350
351pub(crate) type Track = [(f32, [f32; 4])];
354
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
357#[repr(u8)]
358pub(crate) enum Slot {
359 Width = 0,
360 Height = 1,
361 Bg = 2,
362 Border = 3,
363 Radius = 4,
364 Pos = 5,
365 Opacity = 6,
366 Shadow = 7,
368 ShadowColor = 8,
369 Transform = 9,
374}
375
376const SLOTS: usize = 9;
382
383impl Slot {
384 const ALL: [Slot; SLOTS + 1] = [
387 Slot::Width,
388 Slot::Height,
389 Slot::Bg,
390 Slot::Border,
391 Slot::Radius,
392 Slot::Pos,
393 Slot::Opacity,
394 Slot::Shadow,
395 Slot::ShadowColor,
396 Slot::Transform,
397 ];
398
399 pub(crate) fn names(mask: u16) -> Vec<&'static str> {
402 Self::ALL
403 .into_iter()
404 .filter(|s| mask & (1 << *s as u16) != 0)
405 .map(Slot::name)
406 .collect()
407 }
408
409 pub(crate) fn name(self) -> &'static str {
412 match self {
413 Slot::Width => "width",
414 Slot::Height => "height",
415 Slot::Bg => "bg",
416 Slot::Border => "borderColor",
417 Slot::Radius => "radius",
418 Slot::Pos => "position",
419 Slot::Opacity => "opacity",
420 Slot::Shadow => "shadow",
421 Slot::ShadowColor => "shadowColor",
422 Slot::Transform => "transform",
423 }
424 }
425}
426
427#[derive(Clone, Copy, Debug)]
445struct Tween {
446 from: [f32; 4],
447 to: [f32; 4],
448 value: [f32; 4],
449 start: f64,
451 velocity: [f32; 4],
453 last_time: f64,
454 duration_ms: f32,
457 easing: Easing,
458 last_used: u64,
459}
460
461impl Tween {
462 fn settled(value: [f32; 4], start: f64, last_time: f64, t: Transition, frame_no: u64) -> Self {
466 Tween {
467 from: value,
468 to: value,
469 value,
470 start,
471 velocity: [0.0; 4],
472 last_time,
473 duration_ms: t.duration_ms,
474 easing: t.curve(),
475 last_used: frame_no,
476 }
477 }
478
479 fn leg(from: [f32; 4], to: [f32; 4], now: f64, t: Transition, frame_no: u64) -> Self {
481 Tween {
482 from,
483 to,
484 value: from,
485 start: now,
486 velocity: [0.0; 4],
487 last_time: now,
488 duration_ms: t.duration_ms,
489 easing: t.curve(),
490 last_used: frame_no,
491 }
492 }
493
494 #[inline]
497 fn progress_at(&self, now: f64) -> f32 {
498 let dur = self.duration_ms.max(0.0) as f64 / 1000.0;
499 if dur <= 0.0 {
500 1.0
501 } else {
502 (((now - self.start) / dur) as f32).clamp(0.0, 1.0)
503 }
504 }
505
506 #[inline]
508 fn at(&self, p: f32) -> [f32; 4] {
509 if p >= 1.0 {
510 return self.to;
511 }
512 let e = self.easing.apply(p);
513 std::array::from_fn(|i| self.from[i] + (self.to[i] - self.from[i]) * e)
514 }
515
516 fn eased_at(&self, now: f64) -> [f32; 4] {
519 self.at(self.progress_at(now))
520 }
521
522 fn spring_step(&mut self, now: f64, zeta: f32) -> bool {
527 let response = (self.duration_ms.max(1.0) / 1000.0) as f64;
528 let omega = std::f64::consts::TAU / response;
529 let k = (omega * omega) as f32;
530 let c = (2.0 * zeta as f64 * omega) as f32;
531 let dt = (now - self.last_time).clamp(0.0, MAX_FRAME_DT);
532 self.last_time = now;
533 let steps = (dt / SPRING_STEP).ceil().max(1.0);
534 let h = (dt / steps) as f32;
535 for _ in 0..steps as usize {
536 for i in 0..4 {
537 let a = -k * (self.value[i] - self.to[i]) - c * self.velocity[i];
538 self.velocity[i] += a * h;
539 self.value[i] += self.velocity[i] * h;
540 }
541 }
542 let moving = (0..4)
545 .any(|i| (self.value[i] - self.to[i]).abs() > 1e-3 || self.velocity[i].abs() > 5e-2);
546 if !moving {
547 self.value = self.to;
548 self.velocity = [0.0; 4];
549 }
550 moving
551 }
552}
553
554#[derive(Default)]
555pub struct AnimStore {
556 tweens: FxHashMap<Key, [Option<Tween>; SLOTS]>,
557 turns: FxHashMap<Key, Option<Tween>>,
561 starts: FxHashMap<Key, (f64, u64)>,
567 now: Option<f64>,
569 frame_no: u64,
571 drove_at: Option<f64>,
576 owes_transition: bool,
579 owes_cycle: bool,
584}
585
586impl AnimStore {
587 pub fn set_time(&mut self, now_secs: f64) {
590 self.now = Some(now_secs);
591 }
592
593 pub fn time(&self) -> Option<f64> {
594 self.now
595 }
596
597 pub fn animating(&self) -> bool {
601 self.owes_transition || self.owes_cycle
602 }
603
604 pub fn owes(&self) -> (bool, bool) {
607 (self.owes_transition, self.owes_cycle)
608 }
609
610 pub(crate) fn begin_frame(&mut self, frame_no: u64) {
613 self.frame_no = frame_no;
614 self.drove_at = self.now;
615 self.owes_transition = false;
616 self.owes_cycle = false;
617 if !self.tweens.is_empty()
619 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
620 {
621 self.tweens
622 .retain(|_, slots| slots.iter().flatten().any(|t| t.last_used >= cutoff));
623 }
624 if !self.starts.is_empty()
625 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
626 {
627 self.starts.retain(|_, (_, used)| *used >= cutoff);
628 }
629 if !self.turns.is_empty()
630 && let Some(cutoff) = crate::retain::sweep_cutoff(self.frame_no)
631 {
632 self.turns
633 .retain(|_, t| t.is_some_and(|t| t.last_used >= cutoff));
634 }
635 }
636
637 pub(crate) fn owing(&self, mut each: impl FnMut(Key, Slot)) {
645 let Some(now) = self.drove_at.or(self.now) else {
646 return;
647 };
648 let moving = |tw: &Tween| {
649 tw.last_used == self.frame_no
650 && if tw.easing.is_spring() {
651 tw.value != tw.to || tw.velocity != [0.0; 4]
652 } else {
653 tw.progress_at(now) < 1.0
654 }
655 };
656 for (&key, slots) in &self.tweens {
657 for (i, tw) in slots.iter().enumerate() {
658 if tw.as_ref().is_some_and(moving) {
659 each(key, Slot::ALL[i]);
660 }
661 }
662 }
663 for (&key, tw) in &self.turns {
664 if tw.as_ref().is_some_and(moving) {
665 each(key, Slot::Transform);
666 }
667 }
668 }
669
670 pub(crate) fn drive(
680 &mut self,
681 key: Key,
682 slot: Slot,
683 enter_from: Option<[f32; 4]>,
684 target: [f32; 4],
685 transition: Transition,
686 follow: bool,
687 ) -> [f32; 4] {
688 self.node(key)
689 .drive(slot, enter_from, target, transition, follow)
690 }
691
692 pub(crate) fn node(&mut self, key: Key) -> NodeAnim<'_> {
703 NodeAnim {
704 slots: self.tweens.entry(key).or_default(),
705 key,
706 starts: &mut self.starts,
707 now: self.now,
708 frame_no: self.frame_no,
709 active: &mut self.owes_transition,
710 cycling: &mut self.owes_cycle,
711 }
712 }
713
714 pub(crate) fn cycle_running(
719 &self,
720 key: Key,
721 transition: Transition,
722 iterations: Option<f32>,
723 ) -> bool {
724 let Some(n) = iterations else {
725 return true;
726 };
727 let (Some(now), Some(&(start, _))) = (self.drove_at.or(self.now), self.starts.get(&key))
728 else {
729 return false;
730 };
731 let dur = transition.duration_ms as f64 / 1000.0;
732 dur > 0.0 && (now - start - transition.delay_ms as f64 / 1000.0) / dur < n as f64
733 }
734
735 #[inline]
741 pub(crate) fn turn_live(&self, key: Key) -> bool {
742 !self.turns.is_empty()
743 && self
744 .turns
745 .get(&key)
746 .and_then(Option::as_ref)
747 .is_some_and(|t| t.last_used + 1 >= self.frame_no)
748 }
749
750 pub(crate) fn turn_starts_upright(&self, key: Key) -> bool {
755 let prev = self.frame_no.wrapping_sub(1);
756 !self.turn_live(key)
757 && self
758 .tweens
759 .get(&key)
760 .is_some_and(|s| s.iter().flatten().any(|t| t.last_used == prev))
761 }
762
763 pub(crate) fn forget_settled_turn(&mut self, key: Key, identity: [f32; 4]) {
766 if let Some(Some(t)) = self.turns.get(&key)
767 && t.value == identity
768 && t.to == identity
769 && t.velocity == [0.0; 4]
770 {
771 self.turns.remove(&key);
772 }
773 }
774
775 pub(crate) fn drive_turn(
776 &mut self,
777 key: Key,
778 enter_from: Option<[f32; 4]>,
779 target: [f32; 4],
780 transition: Transition,
781 ) -> [f32; 4] {
782 let entry = self.turns.entry(key).or_default();
783 drive_entry(
784 entry,
785 self.now,
786 self.frame_no,
787 &mut self.owes_transition,
788 enter_from,
789 target,
790 transition,
791 true,
792 )
793 }
794
795 pub(crate) fn sample_cycle(
798 &mut self,
799 key: Key,
800 track: &Track,
801 transition: Transition,
802 iterations: Option<f32>,
803 ) -> Option<[f32; 4]> {
804 let now = self.now?;
805 let finite =
806 iterations.map(|n| (n, cycle_start(&mut self.starts, key, now, self.frame_no)));
807 let (v, running) = sample_track(track, transition, Some(now), finite)?;
808 if running {
809 self.owes_cycle = true;
810 }
811 Some(v)
812 }
813}
814
815fn sample_track(
824 track: &Track,
825 transition: Transition,
826 now: Option<f64>,
827 finite: Option<(f32, f64)>,
828) -> Option<([f32; 4], bool)> {
829 let dur = transition.duration_ms as f64 / 1000.0;
830 let (Some(now), true, Some(&(_, first))) = (now, dur > 0.0, track.first()) else {
831 return None;
832 };
833 let delay = transition.delay_ms as f64 / 1000.0;
834 let (p, running) = match finite {
835 None => (transition.repeat.progress((now - delay) / dur), true),
836 Some((n, start)) => {
837 let u = (now - start - delay) / dur;
838 if u <= 0.0 {
839 (transition.repeat.start(), true)
840 } else if u >= n as f64 {
841 (transition.repeat.end(n as f64), false)
842 } else {
843 (transition.repeat.progress(u), true)
844 }
845 }
846 };
847 let v = segment_at(track, transition, p, first);
848 Some((v, running))
849}
850
851fn segment_at(track: &Track, transition: Transition, p: f32, first: [f32; 4]) -> [f32; 4] {
855 let mut from = (0.0, first);
856 for &(at, value) in track {
857 if p < at {
858 let (a, va) = from;
859 let t = if at > a { (p - a) / (at - a) } else { 1.0 };
860 let e = transition.curve().apply(t);
861 let mut out = [0.0; 4];
862 for i in 0..4 {
863 out[i] = va[i] + (value[i] - va[i]) * e;
864 }
865 return out;
866 }
867 from = (at, value);
868 }
869 from.1
870}
871
872fn cycle_start(starts: &mut FxHashMap<Key, (f64, u64)>, key: Key, now: f64, frame_no: u64) -> f64 {
875 let e = starts.entry(key).or_insert((now, frame_no));
876 if e.1 + 1 < frame_no {
877 e.0 = now;
878 }
879 e.1 = frame_no;
880 e.0
881}
882
883pub(crate) struct NodeAnim<'a> {
887 slots: &'a mut [Option<Tween>; SLOTS],
888 key: Key,
890 starts: &'a mut FxHashMap<Key, (f64, u64)>,
891 now: Option<f64>,
893 frame_no: u64,
894 active: &'a mut bool,
896 cycling: &'a mut bool,
898}
899
900impl NodeAnim<'_> {
901 pub(crate) fn sample(
921 &mut self,
922 track: &Track,
923 transition: Transition,
924 iterations: Option<f32>,
925 ) -> Option<[f32; 4]> {
926 let now = self.now?;
927 let finite =
928 iterations.map(|n| (n, cycle_start(self.starts, self.key, now, self.frame_no)));
929 let (v, running) = sample_track(track, transition, Some(now), finite)?;
930 if running {
931 *self.cycling = true;
932 }
933 Some(v)
934 }
935
936 #[inline]
937 pub(crate) fn drive(
938 &mut self,
939 slot: Slot,
940 enter_from: Option<[f32; 4]>,
941 target: [f32; 4],
942 transition: Transition,
943 follow: bool,
944 ) -> [f32; 4] {
945 debug_assert!(slot != Slot::Transform);
948 drive_entry(
949 &mut self.slots[slot as usize],
950 self.now,
951 self.frame_no,
952 self.active,
953 enter_from,
954 target,
955 transition,
956 follow,
957 )
958 }
959}
960
961#[inline(never)]
971#[allow(clippy::too_many_arguments)]
972fn drive_entry(
973 entry: &mut Option<Tween>,
974 now: Option<f64>,
975 frame_no: u64,
976 active: &mut bool,
977 enter_from: Option<[f32; 4]>,
978 target: [f32; 4],
979 transition: Transition,
980 follow: bool,
981) -> [f32; 4] {
982 {
983 let stale = entry.is_none_or(|t| t.last_used + 1 < frame_no);
984 let Some(now) = now else {
985 *entry = Some(Tween::settled(target, 0.0, 0.0, transition, frame_no));
986 return target;
987 };
988 if stale {
989 match enter_from {
990 Some(from) if from != target => {
992 *entry = Some(Tween::leg(from, target, now, transition, frame_no));
993 }
994 _ => {
997 *entry = Some(Tween::settled(
998 target,
999 f64::NEG_INFINITY,
1000 now,
1001 transition,
1002 frame_no,
1003 ));
1004 return target;
1005 }
1006 }
1007 }
1008 let tw = entry.as_mut().expect("checked above");
1009 tw.last_used = frame_no;
1010 if !follow && tw.to != target && tw.value == tw.to && tw.velocity == [0.0; 4] {
1011 tw.from = target;
1013 tw.to = target;
1014 tw.value = target;
1015 tw.start = f64::NEG_INFINITY;
1016 tw.last_time = now;
1017 return target;
1018 }
1019 if let Some(zeta) = transition.damping() {
1020 tw.to = target;
1022 tw.duration_ms = transition.duration_ms;
1023 tw.easing = transition.curve();
1024 if tw.spring_step(now, zeta) {
1025 *active = true;
1026 }
1027 return tw.value;
1028 }
1029 if tw.to != target {
1030 tw.from = tw.eased_at(now);
1038 tw.to = target;
1039 tw.start = now;
1040 tw.duration_ms = transition.duration_ms;
1041 tw.easing = transition.curve();
1042 }
1043 let p = tw.progress_at(now);
1044 if p < 1.0 {
1045 *active = true;
1046 }
1047 tw.value = tw.at(p);
1048 tw.value
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 struct Frames(u64);
1059 impl Frames {
1060 fn next(&mut self) -> u64 {
1061 self.0 += 1;
1062 self.0
1063 }
1064 }
1065
1066 #[test]
1078 fn a_tween_stays_small() {
1079 const BOUND: usize = 96;
1080 let size = std::mem::size_of::<Option<Tween>>();
1081 assert!(
1082 size <= BOUND,
1083 "Option<Tween> is {size} bytes, over the {BOUND}-byte bound. \
1084 It is held per slot per node across frames; put what the node \
1085 owns on the node and what the cycle owns in the Transition."
1086 );
1087 assert_eq!(size, std::mem::size_of::<Tween>());
1090 }
1091
1092 fn one(v: f32) -> [f32; 4] {
1093 [v, 0.0, 0.0, 0.0]
1094 }
1095
1096 #[test]
1097 fn easings_hit_their_endpoints() {
1098 for e in [
1099 Easing::Linear,
1100 Easing::EaseOut,
1101 Easing::EaseIn,
1102 Easing::EaseInOut,
1103 Easing::Spring,
1104 Easing::Bouncy,
1105 Easing::Smooth,
1106 Easing::Snappy,
1107 ] {
1108 assert_eq!(e.apply(0.0), 0.0);
1109 assert_eq!(e.apply(1.0), 1.0);
1110 assert!(e.apply(0.5) > 0.0 && e.apply(0.5) < 1.0);
1111 }
1112 }
1113
1114 #[test]
1115 fn first_sight_snaps_then_retargets_ease() {
1116 let mut a = AnimStore::default();
1117 let mut frame = Frames(0);
1118 let k = Key::ROOT.str("x");
1119 let t = Transition::ms(100.0).easing(Easing::Linear);
1120 a.set_time(0.0);
1121 a.begin_frame(frame.next());
1122 assert_eq!(a.drive(k, Slot::Width, None, one(10.0), t, true)[0], 10.0);
1123 assert!(!a.animating());
1124
1125 a.set_time(0.0);
1126 a.begin_frame(frame.next());
1127 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 10.0);
1128 assert!(a.animating(), "mid-flight after retarget");
1129
1130 a.set_time(0.05);
1131 a.begin_frame(frame.next());
1132 assert!((a.drive(k, Slot::Width, None, one(20.0), t, true)[0] - 15.0).abs() < 1e-4);
1133
1134 a.set_time(0.2);
1135 a.begin_frame(frame.next());
1136 assert_eq!(a.drive(k, Slot::Width, None, one(20.0), t, true)[0], 20.0);
1137 assert!(!a.animating(), "settled");
1138 }
1139
1140 #[test]
1141 fn unchanged_targets_owe_no_frames() {
1142 let mut a = AnimStore::default();
1143 let mut frame = Frames(0);
1144 let k = Key::ROOT.str("x");
1145 let t = Transition::ms(100.0);
1146 for i in 0..3 {
1147 a.set_time(i as f64 * 0.001);
1148 a.begin_frame(frame.next());
1149 a.drive(k, Slot::Width, None, one(5.0), t, true);
1150 assert!(!a.animating(), "frame {i}: same value, nothing to animate");
1151 }
1152 }
1153
1154 #[test]
1155 fn retarget_mid_flight_starts_from_current_value() {
1156 let mut a = AnimStore::default();
1157 let mut frame = Frames(0);
1158 let k = Key::ROOT.str("x");
1159 let t = Transition::ms(100.0).easing(Easing::Linear);
1160 a.set_time(0.0);
1161 a.begin_frame(frame.next());
1162 a.drive(k, Slot::Width, None, one(0.0), t, true);
1163 a.set_time(0.0);
1164 a.begin_frame(frame.next());
1165 a.drive(k, Slot::Width, None, one(100.0), t, true);
1166 a.set_time(0.05);
1167 a.begin_frame(frame.next());
1168 assert!((a.drive(k, Slot::Width, None, one(100.0), t, true)[0] - 50.0).abs() < 1e-4);
1169 a.set_time(0.05);
1171 a.begin_frame(frame.next());
1172 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 50.0).abs() < 1e-4);
1173 a.set_time(0.10);
1174 a.begin_frame(frame.next());
1175 assert!((a.drive(k, Slot::Width, None, one(0.0), t, true)[0] - 25.0).abs() < 1e-4);
1176 }
1177
1178 #[test]
1179 fn springs_overshoot_settle_and_keep_momentum() {
1180 let mut a = AnimStore::default();
1181 let mut frame = Frames(0);
1182 let k = Key::ROOT.str("x");
1183 let t = Transition::ms(200.0).easing(Easing::Bouncy);
1184 a.set_time(0.0);
1185 a.begin_frame(frame.next());
1186 a.drive(k, Slot::Width, None, one(0.0), t, true);
1187 let mut max = 0.0f32;
1189 let mut settled_at = None;
1190 for i in 1..=180 {
1191 a.set_time(i as f64 / 60.0);
1192 a.begin_frame(frame.next());
1193 let v = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
1194 max = max.max(v);
1195 if !a.animating() && settled_at.is_none() {
1196 settled_at = Some(i);
1197 }
1198 }
1199 assert!(max > 101.0, "bouncy overshoots: peak {max}");
1200 let settled = settled_at.expect("settles within 3s");
1201 assert!(settled > 6, "not instant: {settled}");
1202 a.set_time(4.0);
1203 a.begin_frame(frame.next());
1204 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1205
1206 a.set_time(4.0);
1210 a.begin_frame(frame.next());
1211 a.drive(k, Slot::Width, None, one(200.0), t, true);
1212 let mut v_prev = 100.0;
1213 for i in 1..=2 {
1214 a.set_time(4.0 + i as f64 / 60.0);
1215 a.begin_frame(frame.next());
1216 v_prev = a.drive(k, Slot::Width, None, one(200.0), t, true)[0];
1217 }
1218 assert!(v_prev > 100.0);
1219 a.set_time(4.0 + 3.0 / 60.0);
1220 a.begin_frame(frame.next());
1221 let after = a.drive(k, Slot::Width, None, one(100.0), t, true)[0];
1222 assert!(
1223 after > v_prev,
1224 "momentum carries past the retarget: {v_prev} -> {after}"
1225 );
1226 }
1227
1228 fn peak(t: Transition) -> (f32, bool) {
1231 let mut a = AnimStore::default();
1232 let mut frame = Frames(0);
1233 let k = Key::ROOT.str("x");
1234 a.set_time(0.0);
1235 a.begin_frame(frame.next());
1236 a.drive(k, Slot::Width, None, one(0.0), t, true);
1237 let mut max = 0.0f32;
1238 for i in 1..=180 {
1239 a.set_time(i as f64 / 60.0);
1240 a.begin_frame(frame.next());
1241 max = max.max(a.drive(k, Slot::Width, None, one(100.0), t, true)[0]);
1242 }
1243 (max, !a.animating())
1244 }
1245
1246 #[test]
1251 fn a_spring_s_bounce_is_how_far_it_overshoots() {
1252 let t = Transition::ms(200.0);
1253 assert_eq!(t.easing(Easing::Spring).damping(), Some(0.75));
1254 assert_eq!(t.easing(Easing::Bouncy).damping(), Some(0.5));
1255 assert_eq!(t.damping(), None, "a timed curve is no spring");
1256
1257 let (smooth, settled) = peak(t.easing(Easing::Smooth));
1258 assert!(settled, "smooth comes to rest");
1259 assert!(smooth <= 100.0 + 1e-3, "smooth never overshoots: {smooth}");
1260 let mut last = smooth;
1261 for e in [Easing::Snappy, Easing::Spring, Easing::Bouncy] {
1262 let (p, settled) = peak(t.easing(e));
1263 assert!(settled, "{e:?} comes to rest");
1264 assert!(
1265 p > last,
1266 "{e:?} overshoots past the one before: {p} <= {last}"
1267 );
1268 last = p;
1269 }
1270 assert_eq!(
1272 peak(t.easing(Easing::Bouncy).bounce(0.0)).0,
1273 smooth,
1274 "bouncy with no bounce is smooth"
1275 );
1276 assert_eq!(
1277 peak(t.easing(Easing::Smooth).bounce(0.5)).0,
1278 last,
1279 "smooth with bouncy's bounce is bouncy"
1280 );
1281 }
1282
1283 #[test]
1287 fn a_bounce_on_a_timed_curve_makes_it_a_spring() {
1288 let t = Transition::ms(200.0).easing(Easing::Linear).bounce(0.5);
1289 assert_eq!(t.curve(), Easing::Spring);
1290 assert_eq!(t.damping(), Some(0.5));
1291 assert_eq!(
1292 peak(t).0,
1293 peak(Transition::ms(200.0).easing(Easing::Bouncy)).0
1294 );
1295 assert_eq!(Transition::ms(200.0).curve(), Easing::EaseOut);
1296 }
1297
1298 #[test]
1299 fn a_bounce_holds_its_range() {
1300 assert!((Bounce::new(0.3).get() - 0.3).abs() < 1e-4);
1301 assert_eq!(Bounce::new(0.0).get(), 0.0);
1302 assert_eq!(Bounce::new(-1.0).get(), 0.0);
1303 assert_eq!(Bounce::new(f32::NAN).get(), 0.0);
1304 assert_eq!(Bounce::new(1.0).get(), MAX_BOUNCE, "1 would never settle");
1305 let (_, settled) = peak(Transition::ms(100.0).easing(Easing::Spring).bounce(1.0));
1306 assert!(settled, "the most bounce there is still comes to rest");
1307 }
1308
1309 #[test]
1313 fn a_transition_carries_its_bounce_in_its_padding() {
1314 assert_eq!(std::mem::size_of::<Option<Bounce>>(), 2);
1315 assert_eq!(std::mem::size_of::<Option<Transition>>(), 12);
1316 }
1317
1318 #[test]
1319 fn an_entrance_starts_its_first_leg_from_the_declared_value() {
1320 let mut a = AnimStore::default();
1321 let mut frame = Frames(0);
1322 let k = Key::ROOT.str("x");
1323 let t = Transition::ms(100.0).easing(Easing::Linear);
1324 a.set_time(0.0);
1325 a.begin_frame(frame.next());
1326 assert_eq!(
1327 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1328 -100.0,
1329 "first sight starts at the entrance"
1330 );
1331 assert!(a.animating(), "and owes a frame");
1332 a.set_time(0.05);
1333 a.begin_frame(frame.next());
1334 assert!(
1335 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0] + 50.0).abs() < 1e-3
1336 );
1337 a.set_time(0.2);
1338 a.begin_frame(frame.next());
1339 assert_eq!(
1340 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1341 0.0
1342 );
1343 assert!(!a.animating());
1344 a.set_time(0.2);
1346 a.begin_frame(frame.next());
1347 assert_eq!(
1348 a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0],
1349 0.0
1350 );
1351 a.set_time(0.25);
1352 a.begin_frame(frame.next());
1353 assert!(
1354 (a.drive(k, Slot::Pos, Some(one(-100.0)), one(40.0), t, true)[0] - 20.0).abs() < 1e-3
1355 );
1356 }
1357
1358 #[test]
1359 fn an_entrance_equal_to_the_target_is_no_entrance() {
1360 let mut a = AnimStore::default();
1361 let mut frame = Frames(0);
1362 let k = Key::ROOT.str("x");
1363 let t = Transition::ms(100.0);
1364 a.set_time(0.0);
1365 a.begin_frame(frame.next());
1366 assert_eq!(
1367 a.drive(k, Slot::Bg, Some(one(5.0)), one(5.0), t, true)[0],
1368 5.0
1369 );
1370 assert!(!a.animating());
1371 }
1372
1373 #[test]
1374 fn a_spring_entrance_carries_no_velocity_in() {
1375 let mut a = AnimStore::default();
1376 let mut frame = Frames(0);
1377 let k = Key::ROOT.str("x");
1378 let t = Transition::ms(100.0).easing(Easing::Spring);
1379 a.set_time(0.0);
1380 a.begin_frame(frame.next());
1381 assert_eq!(
1382 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0],
1383 -100.0
1384 );
1385 a.set_time(0.016);
1386 a.begin_frame(frame.next());
1387 let first = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1388 assert!(
1389 first > -100.0 && first < 0.0,
1390 "leaves the entrance toward the target: {first}"
1391 );
1392 assert!(a.animating());
1393 let mut v = first;
1394 for i in 2..=90 {
1395 a.set_time(i as f64 * 0.016);
1396 a.begin_frame(frame.next());
1397 v = a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, true)[0];
1398 }
1399 assert!(v.abs() < 1.0, "settled on the target: {v}");
1400 assert!(!a.animating());
1401 }
1402
1403 #[test]
1404 fn off_the_leash_a_settled_slot_snaps_to_a_new_target() {
1405 let mut a = AnimStore::default();
1406 let mut frame = Frames(0);
1407 let k = Key::ROOT.str("x");
1408 let t = Transition::ms(100.0).easing(Easing::Linear);
1409 a.set_time(0.0);
1410 a.begin_frame(frame.next());
1411 a.drive(k, Slot::Pos, Some(one(-100.0)), one(0.0), t, false);
1412 a.set_time(0.05);
1414 a.begin_frame(frame.next());
1415 let at = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1416 a.set_time(0.1);
1417 a.begin_frame(frame.next());
1418 let mid = a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0];
1419 assert!(
1420 at < mid && mid < 20.0,
1421 "mid-flight retarget keeps easing: {at} -> {mid}"
1422 );
1423 a.set_time(0.3);
1424 a.begin_frame(frame.next());
1425 assert_eq!(
1426 a.drive(k, Slot::Pos, Some(one(-100.0)), one(20.0), t, false)[0],
1427 20.0
1428 );
1429 assert!(!a.animating());
1430 a.set_time(0.3);
1432 a.begin_frame(frame.next());
1433 assert_eq!(
1434 a.drive(k, Slot::Pos, Some(one(-100.0)), one(300.0), t, false)[0],
1435 300.0
1436 );
1437 assert!(!a.animating());
1438 }
1439
1440 #[test]
1446 fn a_target_that_moves_every_frame_still_advances() {
1447 let mut a = AnimStore::default();
1448 let mut frame = Frames(0);
1449 let k = Key::ROOT.str("card");
1450 let t = Transition::ms(160.0);
1451 a.set_time(0.0);
1452 a.begin_frame(frame.next());
1453 a.drive(k, Slot::Pos, None, one(0.0), t, true);
1454 let mut behind = Vec::new();
1456 let mut drawn = 0.0;
1457 for f in 1..=60 {
1458 let target = f as f32 * 9.6;
1459 a.set_time(f as f64 * 0.016);
1460 a.begin_frame(frame.next());
1461 drawn = a.drive(k, Slot::Pos, None, one(target), t, true)[0];
1462 behind.push(target - drawn);
1463 }
1464 let target = 60.0 * 9.6;
1465 assert!(
1466 drawn > target * 0.8,
1467 "the pan reaches the screen: drawn {drawn} of {target}"
1468 );
1469 let (early, late) = (behind[29], behind[59]);
1472 assert!(
1473 (early - late).abs() < 1.0,
1474 "the gap stops growing: {early} then {late}"
1475 );
1476 }
1477
1478 #[test]
1479 fn a_slot_skipped_for_a_frame_snaps() {
1480 let mut a = AnimStore::default();
1481 let mut frame = Frames(0);
1482 let k = Key::ROOT.str("x");
1483 let t = Transition::ms(100.0);
1484 a.set_time(0.0);
1485 a.begin_frame(frame.next());
1486 a.drive(k, Slot::Width, None, one(0.0), t, true);
1487 a.set_time(0.01);
1489 a.begin_frame(frame.next());
1490 a.set_time(0.02);
1491 a.begin_frame(frame.next());
1492 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1493 assert!(!a.animating());
1494 }
1495
1496 #[test]
1499 fn keyframes_cycle_in_every_direction() {
1500 let track = [(0.0, one(0.0)), (1.0, one(100.0))];
1501 let mut frame = Frames(0);
1502 let mut at = |a: &mut AnimStore, repeat: Repeat, now: f64| {
1503 let t = Transition::ms(1000.0).easing(Easing::Linear).repeat(repeat);
1504 a.set_time(now);
1505 a.begin_frame(frame.next());
1506 let v = a.node(Key::ROOT).sample(&track, t, None).unwrap()[0];
1507 assert!(a.animating(), "a keyframed slot always owes a frame");
1508 v
1509 };
1510 let mut a = AnimStore::default();
1511 for (repeat, expect) in [
1512 (Repeat::Normal, [0.0, 25.0, 50.0, 75.0, 0.0, 25.0]),
1513 (Repeat::Reverse, [100.0, 75.0, 50.0, 25.0, 100.0, 75.0]),
1514 (Repeat::Alternate, [0.0, 25.0, 50.0, 75.0, 100.0, 75.0]),
1515 (
1516 Repeat::AlternateReverse,
1517 [100.0, 75.0, 50.0, 25.0, 0.0, 25.0],
1518 ),
1519 ] {
1520 for (i, e) in expect.iter().enumerate() {
1521 let v = at(&mut a, repeat, i as f64 * 0.25);
1522 assert!((v - e).abs() < 1e-3, "{repeat:?} at {i}/4: {v} != {e}");
1523 }
1524 }
1525 }
1526
1527 #[test]
1528 fn delay_shifts_the_cycle_and_easing_shapes_each_segment() {
1529 let mut a = AnimStore::default();
1530 let mut frame = Frames(0);
1531 let track = [(0.0, one(0.0)), (0.5, one(10.0)), (1.0, one(0.0))];
1532 let t = Transition::ms(1000.0).easing(Easing::Linear);
1533 a.set_time(0.25);
1534 a.begin_frame(frame.next());
1535 assert!((a.node(Key::ROOT).sample(&track, t, None).unwrap()[0] - 5.0).abs() < 1e-3);
1536 a.set_time(0.25);
1538 a.begin_frame(frame.next());
1539 assert!(
1540 (a.node(Key::ROOT)
1541 .sample(&track, t.delay(250.0), None)
1542 .unwrap()[0])
1543 .abs()
1544 < 1e-3
1545 );
1546 a.set_time(0.125);
1549 a.begin_frame(frame.next());
1550 let v = a
1551 .node(Key::ROOT)
1552 .sample(&track, t.easing(Easing::EaseIn), None)
1553 .unwrap()[0];
1554 assert!(v > 0.0 && v < 2.0, "{v}");
1555 let late = [(0.5, one(3.0)), (1.0, one(9.0))];
1557 a.set_time(0.1);
1558 a.begin_frame(frame.next());
1559 assert_eq!(a.node(Key::ROOT).sample(&late, t, None).unwrap()[0], 3.0);
1560 }
1561
1562 #[test]
1563 fn keyframes_need_a_clock_and_a_duration() {
1564 let mut a = AnimStore::default();
1565 let mut frame = Frames(0);
1566 let track = [(0.0, one(0.0)), (1.0, one(1.0))];
1567 a.begin_frame(frame.next());
1568 assert!(
1569 a.node(Key::ROOT)
1570 .sample(&track, Transition::ms(100.0), None)
1571 .is_none()
1572 );
1573 assert!(!a.animating());
1574 a.set_time(1.0);
1575 a.begin_frame(frame.next());
1576 assert!(
1577 a.node(Key::ROOT)
1578 .sample(&track, Transition::ms(0.0), None)
1579 .is_none()
1580 );
1581 assert!(
1582 a.node(Key::ROOT)
1583 .sample(&[], Transition::ms(100.0), None)
1584 .is_none()
1585 );
1586 assert!(!a.animating());
1587 }
1588
1589 #[test]
1590 fn no_clock_means_no_animation() {
1591 let mut a = AnimStore::default();
1592 let mut frame = Frames(0);
1593 let k = Key::ROOT.str("x");
1594 let t = Transition::ms(100.0);
1595 a.begin_frame(frame.next());
1596 a.drive(k, Slot::Width, None, one(0.0), t, true);
1597 a.begin_frame(frame.next());
1598 assert_eq!(a.drive(k, Slot::Width, None, one(100.0), t, true)[0], 100.0);
1599 assert!(!a.animating());
1600 }
1601}