1use std::f64::consts::PI;
8
9pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
14 a + (b - a) * t
15}
16
17pub fn ease_linear(t: f64) -> f64 {
19 clamp01(t)
20}
21
22pub fn ease_in_quad(t: f64) -> f64 {
24 let t = clamp01(t);
25 t * t
26}
27
28pub fn ease_out_quad(t: f64) -> f64 {
30 let t = clamp01(t);
31 1.0 - (1.0 - t) * (1.0 - t)
32}
33
34pub fn ease_in_out_quad(t: f64) -> f64 {
36 let t = clamp01(t);
37 if t < 0.5 {
38 2.0 * t * t
39 } else {
40 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
41 }
42}
43
44pub fn ease_in_cubic(t: f64) -> f64 {
46 let t = clamp01(t);
47 t * t * t
48}
49
50pub fn ease_out_cubic(t: f64) -> f64 {
52 let t = clamp01(t);
53 1.0 - (1.0 - t).powi(3)
54}
55
56pub fn ease_in_out_cubic(t: f64) -> f64 {
58 let t = clamp01(t);
59 if t < 0.5 {
60 4.0 * t * t * t
61 } else {
62 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
63 }
64}
65
66pub fn ease_out_elastic(t: f64) -> f64 {
68 let t = clamp01(t);
69 if t == 0.0 {
70 0.0
71 } else if t == 1.0 {
72 1.0
73 } else {
74 let c4 = (2.0 * PI) / 3.0;
75 2f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
76 }
77}
78
79pub fn ease_out_bounce(t: f64) -> f64 {
81 let t = clamp01(t);
82 let n1 = 7.5625;
83 let d1 = 2.75;
84
85 if t < 1.0 / d1 {
86 n1 * t * t
87 } else if t < 2.0 / d1 {
88 let t = t - 1.5 / d1;
89 n1 * t * t + 0.75
90 } else if t < 2.5 / d1 {
91 let t = t - 2.25 / d1;
92 n1 * t * t + 0.9375
93 } else {
94 let t = t - 2.625 / d1;
95 n1 * t * t + 0.984_375
96 }
97}
98
99pub struct Tween {
119 from: f64,
120 to: f64,
121 duration_ticks: u64,
122 start_tick: u64,
123 easing: fn(f64) -> f64,
124 done: bool,
125 on_complete: Option<Box<dyn FnMut()>>,
126}
127
128impl Tween {
129 pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
135 Self {
136 from,
137 to,
138 duration_ticks,
139 start_tick: 0,
140 easing: ease_linear,
141 done: false,
142 on_complete: None,
143 }
144 }
145
146 pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
151 self.easing = f;
152 self
153 }
154
155 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
157 self.on_complete = Some(Box::new(f));
158 self
159 }
160
161 pub fn value(&mut self, tick: u64) -> f64 {
166 if self.done {
167 return self.to;
168 }
169
170 if self.duration_ticks == 0 {
171 self.done = true;
172 if let Some(cb) = &mut self.on_complete {
173 cb();
174 }
175 return self.to;
176 }
177
178 let elapsed = tick.wrapping_sub(self.start_tick);
179 if elapsed >= self.duration_ticks {
180 self.done = true;
181 if let Some(cb) = &mut self.on_complete {
182 cb();
183 }
184 return self.to;
185 }
186
187 let progress = elapsed as f64 / self.duration_ticks as f64;
188 let eased = (self.easing)(clamp01(progress));
189 lerp(self.from, self.to, eased)
190 }
191
192 pub fn is_done(&self) -> bool {
194 self.done
195 }
196
197 pub fn reset(&mut self, tick: u64) {
199 self.start_tick = tick;
200 self.done = false;
201 }
202}
203
204pub const DEFAULT_ANIMATE_TICKS: u64 = 12;
212
213pub(crate) struct AnimState {
222 pub(crate) tween: Tween,
223 pub(crate) last_target: f64,
224}
225
226impl AnimState {
227 pub(crate) fn new(target: f64, tick: u64) -> Self {
230 let mut tween = Tween::new(target, target, 0);
231 tween.reset(tick);
232 Self {
233 tween,
234 last_target: target,
235 }
236 }
237
238 pub(crate) fn sample(&mut self, target: f64, duration_ticks: u64, tick: u64) -> f64 {
244 if self.last_target.to_bits() != target.to_bits() {
247 let current = self.tween.value(tick);
248 self.tween = Tween::new(current, target, duration_ticks);
249 self.tween.reset(tick);
250 self.last_target = target;
251 }
252 self.tween.value(tick)
253 }
254}
255
256#[non_exhaustive]
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum LoopMode {
260 Once,
262 Repeat,
264 PingPong,
266}
267
268#[derive(Clone, Copy)]
269struct KeyframeStop {
270 position: f64,
271 value: f64,
272}
273
274pub struct Keyframes {
300 duration_ticks: u64,
301 start_tick: u64,
302 stops: Vec<KeyframeStop>,
303 default_easing: fn(f64) -> f64,
304 segment_easing: Vec<fn(f64) -> f64>,
305 loop_mode: LoopMode,
306 done: bool,
307 completion_fired: bool,
308 on_complete: Option<Box<dyn FnMut()>>,
309}
310
311impl Keyframes {
312 pub fn new(duration_ticks: u64) -> Self {
318 Self {
319 duration_ticks,
320 start_tick: 0,
321 stops: Vec::new(),
322 default_easing: ease_linear,
323 segment_easing: Vec::new(),
324 loop_mode: LoopMode::Once,
325 done: false,
326 completion_fired: false,
327 on_complete: None,
328 }
329 }
330
331 pub fn stop(mut self, position: f64, value: f64) -> Self {
335 self.stops.push(KeyframeStop {
336 position: clamp01(position),
337 value,
338 });
339 if self.stops.len() >= 2 {
340 self.segment_easing.push(self.default_easing);
341 }
342 self.stops.sort_by(|a, b| a.position.total_cmp(&b.position));
343 self
344 }
345
346 pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
351 self.default_easing = f;
352 self.segment_easing.fill(f);
353 self
354 }
355
356 pub fn segment_easing(mut self, segment_index: usize, f: fn(f64) -> f64) -> Self {
364 debug_assert!(
365 segment_index < self.segment_easing.len(),
366 "Keyframes::segment_easing: index {} is out of range \
367 (only {} segments defined; call stop() first to add more stops)",
368 segment_index,
369 self.segment_easing.len(),
370 );
371 if let Some(slot) = self.segment_easing.get_mut(segment_index) {
372 *slot = f;
373 }
374 self
375 }
376
377 pub fn loop_mode(mut self, mode: LoopMode) -> Self {
379 self.loop_mode = mode;
380 self
381 }
382
383 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
385 self.on_complete = Some(Box::new(f));
386 self
387 }
388
389 pub fn value(&mut self, tick: u64) -> f64 {
391 if self.stops.is_empty() {
392 self.complete_once();
393 return 0.0;
394 }
395 if self.stops.len() == 1 {
396 self.complete_once();
397 return self.stops[0].value;
398 }
399
400 let stops = &self.stops;
401
402 let end_value = stops.last().map_or(0.0, |s| s.value);
403 let loop_tick = match map_loop_tick(
404 tick,
405 self.start_tick,
406 self.duration_ticks,
407 self.loop_mode,
408 &mut self.done,
409 ) {
410 Some(v) => v,
411 None => {
412 self.complete_once();
413 return end_value;
414 }
415 };
416
417 if self.duration_ticks == 0 {
418 return stops.last().map_or(0.0, |s| s.value);
419 }
420 let progress = loop_tick as f64 / self.duration_ticks as f64;
421
422 if progress <= stops[0].position {
423 return stops[0].value;
424 }
425 if progress >= 1.0 {
426 return end_value;
427 }
428
429 for i in 0..(stops.len() - 1) {
430 let a = stops[i];
431 let b = stops[i + 1];
432 if progress <= b.position {
433 let span = b.position - a.position;
434 if span <= f64::EPSILON {
435 return b.value;
436 }
437 let local = clamp01((progress - a.position) / span);
438 let easing = self
439 .segment_easing
440 .get(i)
441 .copied()
442 .unwrap_or(self.default_easing);
443 let eased = easing(local);
444 return lerp(a.value, b.value, eased);
445 }
446 }
447
448 end_value
449 }
450
451 pub fn is_done(&self) -> bool {
453 self.done
454 }
455
456 pub fn reset(&mut self, tick: u64) {
458 self.start_tick = tick;
459 self.done = false;
460 self.completion_fired = false;
461 }
462
463 fn complete_once(&mut self) {
464 self.done = true;
465 if self.loop_mode == LoopMode::Once && !self.completion_fired {
466 self.completion_fired = true;
467 if let Some(cb) = &mut self.on_complete {
468 cb();
469 }
470 }
471 }
472}
473
474#[derive(Clone, Copy)]
475struct SequenceSegment {
476 from: f64,
477 to: f64,
478 duration_ticks: u64,
479 easing: fn(f64) -> f64,
480}
481
482pub struct Sequence {
501 segments: Vec<SequenceSegment>,
502 loop_mode: LoopMode,
503 start_tick: u64,
504 done: bool,
505 completion_fired: bool,
506 on_complete: Option<Box<dyn FnMut()>>,
507}
508
509impl Default for Sequence {
510 fn default() -> Self {
511 Self::new()
512 }
513}
514
515impl Sequence {
516 pub fn new() -> Self {
521 Self {
522 segments: Vec::new(),
523 loop_mode: LoopMode::Once,
524 start_tick: 0,
525 done: false,
526 completion_fired: false,
527 on_complete: None,
528 }
529 }
530
531 pub fn then(mut self, from: f64, to: f64, duration_ticks: u64, easing: fn(f64) -> f64) -> Self {
533 self.segments.push(SequenceSegment {
534 from,
535 to,
536 duration_ticks,
537 easing,
538 });
539 self
540 }
541
542 pub fn loop_mode(mut self, mode: LoopMode) -> Self {
544 self.loop_mode = mode;
545 self
546 }
547
548 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
550 self.on_complete = Some(Box::new(f));
551 self
552 }
553
554 pub fn value(&mut self, tick: u64) -> f64 {
556 if self.segments.is_empty() {
557 self.complete_once();
558 return 0.0;
559 }
560
561 let total_duration = self
562 .segments
563 .iter()
564 .fold(0_u64, |acc, s| acc.saturating_add(s.duration_ticks));
565 let end_value = self.segments.last().map_or(0.0, |s| s.to);
566
567 let loop_tick = match map_loop_tick(
568 tick,
569 self.start_tick,
570 total_duration,
571 self.loop_mode,
572 &mut self.done,
573 ) {
574 Some(v) => v,
575 None => {
576 self.complete_once();
577 return end_value;
578 }
579 };
580
581 let mut remaining = loop_tick;
582 for segment in &self.segments {
583 if segment.duration_ticks == 0 {
584 continue;
585 }
586 if remaining < segment.duration_ticks {
587 let progress = remaining as f64 / segment.duration_ticks as f64;
588 let eased = (segment.easing)(clamp01(progress));
589 return lerp(segment.from, segment.to, eased);
590 }
591 remaining -= segment.duration_ticks;
592 }
593
594 end_value
595 }
596
597 pub fn is_done(&self) -> bool {
599 self.done
600 }
601
602 pub fn reset(&mut self, tick: u64) {
604 self.start_tick = tick;
605 self.done = false;
606 self.completion_fired = false;
607 }
608
609 fn complete_once(&mut self) {
610 self.done = true;
611 if self.loop_mode == LoopMode::Once && !self.completion_fired {
612 self.completion_fired = true;
613 if let Some(cb) = &mut self.on_complete {
614 cb();
615 }
616 }
617 }
618}
619
620pub struct Stagger {
644 from: f64,
645 to: f64,
646 duration_ticks: u64,
647 start_tick: u64,
648 delay_ticks: u64,
649 easing: fn(f64) -> f64,
650 loop_mode: LoopMode,
651 item_count: usize,
652 done: bool,
653 completion_fired: bool,
654 on_complete: Option<Box<dyn FnMut()>>,
655}
656
657impl Stagger {
658 pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
662 Self {
663 from,
664 to,
665 duration_ticks,
666 start_tick: 0,
667 delay_ticks: 0,
668 easing: ease_linear,
669 loop_mode: LoopMode::Once,
670 item_count: 0,
671 done: false,
672 completion_fired: false,
673 on_complete: None,
674 }
675 }
676
677 pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
679 self.easing = f;
680 self
681 }
682
683 pub fn delay(mut self, ticks: u64) -> Self {
685 self.delay_ticks = ticks;
686 self
687 }
688
689 pub fn loop_mode(mut self, mode: LoopMode) -> Self {
692 self.loop_mode = mode;
693 self
694 }
695
696 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
698 self.on_complete = Some(Box::new(f));
699 self
700 }
701
702 pub fn items(mut self, count: usize) -> Self {
708 self.item_count = count;
709 self
710 }
711
712 pub fn value(&mut self, tick: u64, item_index: usize) -> f64 {
714 if item_index >= self.item_count {
715 self.item_count = item_index + 1;
716 }
717
718 let total_cycle = self.total_cycle_ticks();
719
720 let effective_tick = if self.loop_mode == LoopMode::Once {
721 tick
722 } else {
723 let elapsed = tick.wrapping_sub(self.start_tick);
724 let mapped = match self.loop_mode {
725 LoopMode::Repeat => {
726 if total_cycle == 0 {
727 0
728 } else {
729 elapsed % total_cycle
730 }
731 }
732 LoopMode::PingPong => {
733 if total_cycle == 0 {
734 0
735 } else {
736 let full = total_cycle.saturating_mul(2);
737 let phase = elapsed % full;
738 if phase < total_cycle {
739 phase
740 } else {
741 full - phase
742 }
743 }
744 }
745 LoopMode::Once => unreachable!(),
746 };
747 self.start_tick.wrapping_add(mapped)
748 };
749
750 let delay = self.delay_ticks.wrapping_mul(item_index as u64);
751 let item_start = self.start_tick.wrapping_add(delay);
752
753 if effective_tick < item_start {
754 self.done = false;
755 return self.from;
756 }
757
758 if self.duration_ticks == 0 {
759 self.done = true;
760 self.complete_once();
761 return self.to;
762 }
763
764 let elapsed = effective_tick - item_start;
765 if elapsed >= self.duration_ticks {
766 self.done = true;
767 self.complete_once();
768 return self.to;
769 }
770
771 self.done = false;
772 let progress = elapsed as f64 / self.duration_ticks as f64;
773 let eased = (self.easing)(clamp01(progress));
774 lerp(self.from, self.to, eased)
775 }
776
777 fn total_cycle_ticks(&self) -> u64 {
778 let max_delay = self
779 .delay_ticks
780 .wrapping_mul(self.item_count.saturating_sub(1) as u64);
781 self.duration_ticks.saturating_add(max_delay)
782 }
783
784 pub fn is_done(&self) -> bool {
792 self.done
793 }
794
795 pub fn is_all_done(&self, tick: u64, item_count: usize) -> bool {
812 if item_count == 0 {
813 return true;
814 }
815 let last_start = self.start_tick.saturating_add(
816 self.delay_ticks
817 .saturating_mul(item_count.saturating_sub(1) as u64),
818 );
819 tick >= last_start.saturating_add(self.duration_ticks)
820 }
821
822 pub fn reset(&mut self, tick: u64) {
824 self.start_tick = tick;
825 self.done = false;
826 self.completion_fired = false;
827 }
828
829 fn complete_once(&mut self) {
830 if self.loop_mode == LoopMode::Once && !self.completion_fired {
831 self.completion_fired = true;
832 if let Some(cb) = &mut self.on_complete {
833 cb();
834 }
835 }
836 }
837}
838
839fn map_loop_tick(
840 tick: u64,
841 start_tick: u64,
842 duration_ticks: u64,
843 loop_mode: LoopMode,
844 done: &mut bool,
845) -> Option<u64> {
846 if duration_ticks == 0 {
847 *done = true;
848 return None;
849 }
850
851 let elapsed = tick.wrapping_sub(start_tick);
852 match loop_mode {
853 LoopMode::Once => {
854 if elapsed >= duration_ticks {
855 *done = true;
856 None
857 } else {
858 *done = false;
859 Some(elapsed)
860 }
861 }
862 LoopMode::Repeat => {
863 *done = false;
864 Some(elapsed % duration_ticks)
865 }
866 LoopMode::PingPong => {
867 *done = false;
868 let cycle = duration_ticks.saturating_mul(2);
869 if cycle == 0 {
870 return Some(0);
871 }
872 let phase = elapsed % cycle;
873 if phase < duration_ticks {
874 Some(phase)
875 } else {
876 Some(cycle - phase)
877 }
878 }
879 }
880}
881
882pub struct Spring {
914 value: f64,
915 target: f64,
916 velocity: f64,
917 stiffness: f64,
918 damping: f64,
919 settled: bool,
920 on_settle: Option<Box<dyn FnMut()>>,
921}
922
923impl Spring {
924 pub fn new(initial: f64, stiffness: f64, damping: f64) -> Self {
929 debug_assert!(
930 damping > 0.0 && damping < 1.0,
931 "Spring::new: damping must be in (0, 1), got {damping}. \
932 Values >= 1.0 conserve or amplify energy and never settle."
933 );
934 Self {
935 value: initial,
936 target: initial,
937 velocity: 0.0,
938 stiffness,
939 damping,
940 settled: true,
941 on_settle: None,
942 }
943 }
944
945 pub fn on_settle(mut self, f: impl FnMut() + 'static) -> Self {
947 self.on_settle = Some(Box::new(f));
948 self
949 }
950
951 pub fn set_target(&mut self, target: f64) {
953 self.target = target;
954 self.settled = self.is_settled();
955 }
956
957 pub fn tick(&mut self) {
961 let displacement = self.target - self.value;
962 let spring_force = displacement * self.stiffness;
963 self.velocity = (self.velocity + spring_force) * self.damping;
964 self.value += self.velocity;
965
966 let is_settled = self.is_settled();
967 if !self.settled && is_settled {
968 self.settled = true;
969 if let Some(cb) = &mut self.on_settle {
970 cb();
971 }
972 }
973 }
974
975 pub fn value(&self) -> f64 {
977 self.value
978 }
979
980 pub fn is_settled(&self) -> bool {
985 (self.target - self.value).abs() < 0.01 && self.velocity.abs() < 0.01
986 }
987}
988
989fn clamp01(t: f64) -> f64 {
990 t.clamp(0.0, 1.0)
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use std::cell::Cell;
997 use std::rc::Rc;
998
999 fn assert_endpoints(f: fn(f64) -> f64) {
1000 assert_eq!(f(0.0), 0.0);
1001 assert_eq!(f(1.0), 1.0);
1002 }
1003
1004 #[test]
1005 fn easing_functions_have_expected_endpoints() {
1006 let easing_functions: [fn(f64) -> f64; 9] = [
1007 ease_linear,
1008 ease_in_quad,
1009 ease_out_quad,
1010 ease_in_out_quad,
1011 ease_in_cubic,
1012 ease_out_cubic,
1013 ease_in_out_cubic,
1014 ease_out_elastic,
1015 ease_out_bounce,
1016 ];
1017
1018 for easing in easing_functions {
1019 assert_endpoints(easing);
1020 }
1021 }
1022
1023 #[test]
1024 fn tween_returns_start_middle_end_values() {
1025 let mut tween = Tween::new(0.0, 10.0, 10);
1026 tween.reset(100);
1027
1028 assert_eq!(tween.value(100), 0.0);
1029 assert_eq!(tween.value(105), 5.0);
1030 assert_eq!(tween.value(110), 10.0);
1031 assert!(tween.is_done());
1032 }
1033
1034 #[test]
1035 fn tween_reset_restarts_animation() {
1036 let mut tween = Tween::new(0.0, 1.0, 10);
1037 tween.reset(0);
1038 let _ = tween.value(10);
1039 assert!(tween.is_done());
1040
1041 tween.reset(20);
1042 assert!(!tween.is_done());
1043 assert_eq!(tween.value(20), 0.0);
1044 assert_eq!(tween.value(30), 1.0);
1045 assert!(tween.is_done());
1046 }
1047
1048 #[test]
1049 fn tween_on_complete_fires_once() {
1050 let count = Rc::new(Cell::new(0));
1051 let callback_count = Rc::clone(&count);
1052 let mut tween = Tween::new(0.0, 10.0, 10).on_complete(move || {
1053 callback_count.set(callback_count.get() + 1);
1054 });
1055
1056 tween.reset(0);
1057 assert_eq!(count.get(), 0);
1058
1059 assert_eq!(tween.value(5), 5.0);
1060 assert_eq!(count.get(), 0);
1061
1062 assert_eq!(tween.value(10), 10.0);
1063 assert_eq!(count.get(), 1);
1064
1065 assert_eq!(tween.value(11), 10.0);
1066 assert_eq!(count.get(), 1);
1067 }
1068
1069 #[test]
1070 fn completion_callbacks_fire_once_per_reset() {
1071 let keyframe_count = Rc::new(Cell::new(0));
1072 let mut keyframes = Keyframes::new(10)
1073 .stop(0.0, 0.0)
1074 .stop(1.0, 1.0)
1075 .on_complete({
1076 let count = Rc::clone(&keyframe_count);
1077 move || count.set(count.get() + 1)
1078 });
1079 keyframes.reset(0);
1080 assert_eq!(keyframes.value(10), 1.0);
1081 assert_eq!(keyframes.value(11), 1.0);
1082 assert_eq!(keyframe_count.get(), 1);
1083 keyframes.reset(20);
1084 assert_eq!(keyframes.value(30), 1.0);
1085 assert_eq!(keyframe_count.get(), 2);
1086
1087 let sequence_count = Rc::new(Cell::new(0));
1088 let mut sequence = Sequence::new()
1089 .then(0.0, 1.0, 10, ease_linear)
1090 .on_complete({
1091 let count = Rc::clone(&sequence_count);
1092 move || count.set(count.get() + 1)
1093 });
1094 sequence.reset(0);
1095 assert_eq!(sequence.value(10), 1.0);
1096 assert_eq!(sequence.value(100), 1.0);
1097 assert_eq!(sequence_count.get(), 1);
1098 sequence.reset(200);
1099 assert_eq!(sequence.value(210), 1.0);
1100 assert_eq!(sequence_count.get(), 2);
1101
1102 let stagger_count = Rc::new(Cell::new(0));
1103 let mut stagger = Stagger::new(0.0, 1.0, 10).on_complete({
1104 let count = Rc::clone(&stagger_count);
1105 move || count.set(count.get() + 1)
1106 });
1107 stagger.reset(0);
1108 assert_eq!(stagger.value(10, 0), 1.0);
1109 assert_eq!(stagger.value(11, 0), 1.0);
1110 assert_eq!(stagger_count.get(), 1);
1111 stagger.reset(20);
1112 assert_eq!(stagger.value(30, 0), 1.0);
1113 assert_eq!(stagger_count.get(), 2);
1114 }
1115
1116 #[test]
1117 fn degenerate_animations_complete_once() {
1118 let empty_keyframe_count = Rc::new(Cell::new(0));
1119 let mut empty_keyframes = Keyframes::new(10).on_complete({
1120 let count = Rc::clone(&empty_keyframe_count);
1121 move || count.set(count.get() + 1)
1122 });
1123 assert_eq!(empty_keyframes.value(0), 0.0);
1124 assert_eq!(empty_keyframes.value(1), 0.0);
1125 assert_eq!(empty_keyframe_count.get(), 1);
1126
1127 let single_keyframe_count = Rc::new(Cell::new(0));
1128 let mut single_keyframe = Keyframes::new(10).stop(0.5, 7.0).on_complete({
1129 let count = Rc::clone(&single_keyframe_count);
1130 move || count.set(count.get() + 1)
1131 });
1132 assert_eq!(single_keyframe.value(0), 7.0);
1133 assert_eq!(single_keyframe.value(100), 7.0);
1134 assert_eq!(single_keyframe_count.get(), 1);
1135
1136 let zero_sequence_count = Rc::new(Cell::new(0));
1137 let mut zero_sequence = Sequence::new().then(1.0, 2.0, 0, ease_linear).on_complete({
1138 let count = Rc::clone(&zero_sequence_count);
1139 move || count.set(count.get() + 1)
1140 });
1141 assert_eq!(zero_sequence.value(0), 2.0);
1142 assert_eq!(zero_sequence.value(1), 2.0);
1143 assert_eq!(zero_sequence_count.get(), 1);
1144 }
1145
1146 #[test]
1147 fn looping_animations_do_not_report_terminal_completion() {
1148 let keyframe_count = Rc::new(Cell::new(0));
1149 let mut keyframes = Keyframes::new(10)
1150 .stop(0.0, 0.0)
1151 .stop(1.0, 1.0)
1152 .loop_mode(LoopMode::Repeat)
1153 .on_complete({
1154 let count = Rc::clone(&keyframe_count);
1155 move || count.set(count.get() + 1)
1156 });
1157 for tick in [10, 20, 100] {
1158 let _ = keyframes.value(tick);
1159 }
1160 assert_eq!(keyframe_count.get(), 0);
1161 assert!(!keyframes.is_done());
1162
1163 let sequence_count = Rc::new(Cell::new(0));
1164 let mut sequence = Sequence::new()
1165 .then(0.0, 1.0, 10, ease_linear)
1166 .loop_mode(LoopMode::PingPong)
1167 .on_complete({
1168 let count = Rc::clone(&sequence_count);
1169 move || count.set(count.get() + 1)
1170 });
1171 for tick in [10, 20, 100] {
1172 let _ = sequence.value(tick);
1173 }
1174 assert_eq!(sequence_count.get(), 0);
1175 assert!(!sequence.is_done());
1176
1177 let stagger_count = Rc::new(Cell::new(0));
1178 let mut stagger = Stagger::new(0.0, 1.0, 10)
1179 .loop_mode(LoopMode::Repeat)
1180 .on_complete({
1181 let count = Rc::clone(&stagger_count);
1182 move || count.set(count.get() + 1)
1183 });
1184 for tick in [10, 20, 100] {
1185 let _ = stagger.value(tick, 0);
1186 }
1187 assert_eq!(stagger_count.get(), 0);
1188 }
1189
1190 #[test]
1191 fn spring_settles_to_target() {
1192 let mut spring = Spring::new(0.0, 0.2, 0.85);
1193 spring.set_target(10.0);
1194
1195 for _ in 0..300 {
1196 spring.tick();
1197 if spring.is_settled() {
1198 break;
1199 }
1200 }
1201
1202 assert!(spring.is_settled());
1203 assert!((spring.value() - 10.0).abs() < 0.01);
1204 }
1205
1206 #[test]
1207 fn spring_on_settle_fires_once() {
1208 let count = Rc::new(Cell::new(0));
1209 let callback_count = Rc::clone(&count);
1210 let mut spring = Spring::new(0.0, 0.2, 0.85).on_settle(move || {
1211 callback_count.set(callback_count.get() + 1);
1212 });
1213 spring.set_target(10.0);
1214
1215 for _ in 0..500 {
1216 spring.tick();
1217 if spring.is_settled() {
1218 break;
1219 }
1220 }
1221
1222 assert!(spring.is_settled());
1223 assert_eq!(count.get(), 1);
1224
1225 for _ in 0..50 {
1226 spring.tick();
1227 }
1228
1229 assert_eq!(count.get(), 1);
1230 }
1231
1232 #[cfg(debug_assertions)]
1233 #[test]
1234 #[should_panic(expected = "damping must be in (0, 1)")]
1235 fn spring_damping_one_panics_in_debug() {
1236 let _ = Spring::new(0.0, 0.5, 1.0);
1237 }
1238
1239 #[cfg(debug_assertions)]
1240 #[test]
1241 #[should_panic(expected = "damping must be in (0, 1)")]
1242 fn spring_damping_gt_one_panics_in_debug() {
1243 let _ = Spring::new(0.0, 0.5, 2.0);
1244 }
1245
1246 #[test]
1247 fn spring_valid_damping_settles() {
1248 for &d in &[0.5_f64, 0.7, 0.85, 0.95] {
1249 let mut s = Spring::new(0.0, 0.2, d);
1250 s.set_target(100.0);
1251 for _ in 0..1000 {
1252 s.tick();
1253 if s.is_settled() {
1254 break;
1255 }
1256 }
1257 assert!(s.is_settled(), "damping={d} should settle");
1258 assert!((s.value() - 100.0).abs() < 0.01, "damping={d} value off");
1259 }
1260 }
1261
1262 #[test]
1263 fn lerp_interpolates_values() {
1264 assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
1265 assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
1266 assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
1267 }
1268
1269 #[test]
1270 fn keyframes_interpolates_across_multiple_stops() {
1271 let mut keyframes = Keyframes::new(100)
1272 .stop(0.0, 0.0)
1273 .stop(0.3, 100.0)
1274 .stop(0.7, 50.0)
1275 .stop(1.0, 80.0)
1276 .easing(ease_linear);
1277
1278 keyframes.reset(0);
1279 assert_eq!(keyframes.value(0), 0.0);
1280 assert_eq!(keyframes.value(15), 50.0);
1281 assert_eq!(keyframes.value(30), 100.0);
1282 assert_eq!(keyframes.value(50), 75.0);
1283 assert_eq!(keyframes.value(70), 50.0);
1284 assert_eq!(keyframes.value(85), 65.0);
1285 assert_eq!(keyframes.value(100), 80.0);
1286 assert!(keyframes.is_done());
1287 }
1288
1289 #[test]
1290 fn keyframes_repeat_loop_restarts() {
1291 let mut keyframes = Keyframes::new(10)
1292 .stop(0.0, 0.0)
1293 .stop(1.0, 10.0)
1294 .loop_mode(LoopMode::Repeat);
1295
1296 keyframes.reset(0);
1297 assert_eq!(keyframes.value(5), 5.0);
1298 assert_eq!(keyframes.value(10), 0.0);
1299 assert_eq!(keyframes.value(12), 2.0);
1300 assert!(!keyframes.is_done());
1301 }
1302
1303 #[test]
1304 fn keyframes_pingpong_reverses_direction() {
1305 let mut keyframes = Keyframes::new(10)
1306 .stop(0.0, 0.0)
1307 .stop(1.0, 10.0)
1308 .loop_mode(LoopMode::PingPong);
1309
1310 keyframes.reset(0);
1311 assert_eq!(keyframes.value(8), 8.0);
1312 assert_eq!(keyframes.value(10), 10.0);
1313 assert_eq!(keyframes.value(12), 8.0);
1314 assert_eq!(keyframes.value(15), 5.0);
1315 assert!(!keyframes.is_done());
1316 }
1317
1318 #[test]
1319 fn sequence_chains_segments_in_order() {
1320 let mut sequence = Sequence::new()
1321 .then(0.0, 100.0, 30, ease_linear)
1322 .then(100.0, 50.0, 20, ease_linear)
1323 .then(50.0, 200.0, 40, ease_linear);
1324
1325 sequence.reset(0);
1326 assert_eq!(sequence.value(15), 50.0);
1327 assert_eq!(sequence.value(30), 100.0);
1328 assert_eq!(sequence.value(40), 75.0);
1329 assert_eq!(sequence.value(50), 50.0);
1330 assert_eq!(sequence.value(70), 125.0);
1331 assert_eq!(sequence.value(90), 200.0);
1332 assert!(sequence.is_done());
1333 }
1334
1335 #[test]
1336 fn sequence_loop_modes_repeat_and_pingpong_work() {
1337 let mut repeat = Sequence::new()
1338 .then(0.0, 10.0, 10, ease_linear)
1339 .loop_mode(LoopMode::Repeat);
1340 repeat.reset(0);
1341 assert_eq!(repeat.value(12), 2.0);
1342 assert!(!repeat.is_done());
1343
1344 let mut pingpong = Sequence::new()
1345 .then(0.0, 10.0, 10, ease_linear)
1346 .loop_mode(LoopMode::PingPong);
1347 pingpong.reset(0);
1348 assert_eq!(pingpong.value(12), 8.0);
1349 assert!(!pingpong.is_done());
1350 }
1351
1352 #[test]
1353 fn stagger_applies_per_item_delay() {
1354 let mut stagger = Stagger::new(0.0, 100.0, 20).easing(ease_linear).delay(5);
1355
1356 stagger.reset(0);
1357 assert_eq!(stagger.value(4, 3), 0.0);
1358 assert_eq!(stagger.value(15, 3), 0.0);
1359 assert_eq!(stagger.value(20, 3), 25.0);
1360 assert_eq!(stagger.value(35, 3), 100.0);
1361 assert!(stagger.is_done());
1362 }
1363
1364 #[test]
1367 fn stagger_is_all_done_returns_false_mid_animation() {
1368 let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1369 assert!(!stagger.is_all_done(15, 5), "items still in progress");
1371 }
1372
1373 #[test]
1375 fn stagger_is_all_done_returns_true_after_last_item() {
1376 let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1377 assert!(stagger.is_all_done(31, 5), "all items done by tick 31");
1378 }
1379
1380 #[test]
1382 fn stagger_is_done_reflects_last_sampled_item_only() {
1383 let mut stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1384 stagger.value(100, 4); assert!(stagger.is_done());
1386 stagger.value(15, 2);
1388 assert!(!stagger.is_done(), "is_done reflects last sampled item");
1389 }
1390
1391 #[test]
1393 fn stagger_is_all_done_zero_items() {
1394 let stagger = Stagger::new(0.0, 100.0, 10);
1395 assert!(stagger.is_all_done(0, 0));
1396 }
1397
1398 #[cfg(debug_assertions)]
1400 #[test]
1401 #[should_panic(expected = "out of range")]
1402 fn keyframes_segment_easing_oob_panics_in_debug() {
1403 let _ = Keyframes::new(60)
1405 .stop(0.0, 0.0)
1406 .stop(1.0, 100.0)
1407 .segment_easing(5, ease_linear);
1408 }
1409
1410 #[test]
1412 fn keyframes_segment_easing_valid_index() {
1413 let kf = Keyframes::new(60)
1414 .stop(0.0, 0.0)
1415 .stop(0.5, 50.0)
1416 .stop(1.0, 100.0)
1417 .segment_easing(0, ease_in_quad)
1418 .segment_easing(1, ease_out_quad);
1419 let _ = kf;
1420 }
1421}