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 on_complete: Option<Box<dyn FnMut()>>,
308}
309
310impl Keyframes {
311 pub fn new(duration_ticks: u64) -> Self {
317 Self {
318 duration_ticks,
319 start_tick: 0,
320 stops: Vec::new(),
321 default_easing: ease_linear,
322 segment_easing: Vec::new(),
323 loop_mode: LoopMode::Once,
324 done: false,
325 on_complete: None,
326 }
327 }
328
329 pub fn stop(mut self, position: f64, value: f64) -> Self {
333 self.stops.push(KeyframeStop {
334 position: clamp01(position),
335 value,
336 });
337 if self.stops.len() >= 2 {
338 self.segment_easing.push(self.default_easing);
339 }
340 self.stops.sort_by(|a, b| a.position.total_cmp(&b.position));
341 self
342 }
343
344 pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
349 self.default_easing = f;
350 self.segment_easing.fill(f);
351 self
352 }
353
354 pub fn segment_easing(mut self, segment_index: usize, f: fn(f64) -> f64) -> Self {
362 debug_assert!(
363 segment_index < self.segment_easing.len(),
364 "Keyframes::segment_easing: index {} is out of range \
365 (only {} segments defined; call stop() first to add more stops)",
366 segment_index,
367 self.segment_easing.len(),
368 );
369 if let Some(slot) = self.segment_easing.get_mut(segment_index) {
370 *slot = f;
371 }
372 self
373 }
374
375 pub fn loop_mode(mut self, mode: LoopMode) -> Self {
377 self.loop_mode = mode;
378 self
379 }
380
381 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
383 self.on_complete = Some(Box::new(f));
384 self
385 }
386
387 pub fn value(&mut self, tick: u64) -> f64 {
389 if self.stops.is_empty() {
390 self.done = true;
391 if let Some(cb) = &mut self.on_complete {
392 cb();
393 }
394 return 0.0;
395 }
396 if self.stops.len() == 1 {
397 self.done = true;
398 if let Some(cb) = &mut self.on_complete {
399 cb();
400 }
401 return self.stops[0].value;
402 }
403
404 let stops = &self.stops;
405
406 let end_value = stops.last().map_or(0.0, |s| s.value);
407 let loop_tick = match map_loop_tick(
408 tick,
409 self.start_tick,
410 self.duration_ticks,
411 self.loop_mode,
412 &mut self.done,
413 ) {
414 Some(v) => v,
415 None => {
416 if let Some(cb) = &mut self.on_complete {
417 cb();
418 }
419 return end_value;
420 }
421 };
422
423 if self.duration_ticks == 0 {
424 return stops.last().map_or(0.0, |s| s.value);
425 }
426 let progress = loop_tick as f64 / self.duration_ticks as f64;
427
428 if progress <= stops[0].position {
429 return stops[0].value;
430 }
431 if progress >= 1.0 {
432 return end_value;
433 }
434
435 for i in 0..(stops.len() - 1) {
436 let a = stops[i];
437 let b = stops[i + 1];
438 if progress <= b.position {
439 let span = b.position - a.position;
440 if span <= f64::EPSILON {
441 return b.value;
442 }
443 let local = clamp01((progress - a.position) / span);
444 let easing = self
445 .segment_easing
446 .get(i)
447 .copied()
448 .unwrap_or(self.default_easing);
449 let eased = easing(local);
450 return lerp(a.value, b.value, eased);
451 }
452 }
453
454 end_value
455 }
456
457 pub fn is_done(&self) -> bool {
459 self.done
460 }
461
462 pub fn reset(&mut self, tick: u64) {
464 self.start_tick = tick;
465 self.done = false;
466 }
467}
468
469#[derive(Clone, Copy)]
470struct SequenceSegment {
471 from: f64,
472 to: f64,
473 duration_ticks: u64,
474 easing: fn(f64) -> f64,
475}
476
477pub struct Sequence {
496 segments: Vec<SequenceSegment>,
497 loop_mode: LoopMode,
498 start_tick: u64,
499 done: bool,
500 on_complete: Option<Box<dyn FnMut()>>,
501}
502
503impl Default for Sequence {
504 fn default() -> Self {
505 Self::new()
506 }
507}
508
509impl Sequence {
510 pub fn new() -> Self {
515 Self {
516 segments: Vec::new(),
517 loop_mode: LoopMode::Once,
518 start_tick: 0,
519 done: false,
520 on_complete: None,
521 }
522 }
523
524 pub fn then(mut self, from: f64, to: f64, duration_ticks: u64, easing: fn(f64) -> f64) -> Self {
526 self.segments.push(SequenceSegment {
527 from,
528 to,
529 duration_ticks,
530 easing,
531 });
532 self
533 }
534
535 pub fn loop_mode(mut self, mode: LoopMode) -> Self {
537 self.loop_mode = mode;
538 self
539 }
540
541 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
543 self.on_complete = Some(Box::new(f));
544 self
545 }
546
547 pub fn value(&mut self, tick: u64) -> f64 {
549 if self.segments.is_empty() {
550 self.done = true;
551 if let Some(cb) = &mut self.on_complete {
552 cb();
553 }
554 return 0.0;
555 }
556
557 let total_duration = self
558 .segments
559 .iter()
560 .fold(0_u64, |acc, s| acc.saturating_add(s.duration_ticks));
561 let end_value = self.segments.last().map_or(0.0, |s| s.to);
562
563 let loop_tick = match map_loop_tick(
564 tick,
565 self.start_tick,
566 total_duration,
567 self.loop_mode,
568 &mut self.done,
569 ) {
570 Some(v) => v,
571 None => {
572 if let Some(cb) = &mut self.on_complete {
573 cb();
574 }
575 return end_value;
576 }
577 };
578
579 let mut remaining = loop_tick;
580 for segment in &self.segments {
581 if segment.duration_ticks == 0 {
582 continue;
583 }
584 if remaining < segment.duration_ticks {
585 let progress = remaining as f64 / segment.duration_ticks as f64;
586 let eased = (segment.easing)(clamp01(progress));
587 return lerp(segment.from, segment.to, eased);
588 }
589 remaining -= segment.duration_ticks;
590 }
591
592 end_value
593 }
594
595 pub fn is_done(&self) -> bool {
597 self.done
598 }
599
600 pub fn reset(&mut self, tick: u64) {
602 self.start_tick = tick;
603 self.done = false;
604 }
605}
606
607pub struct Stagger {
631 from: f64,
632 to: f64,
633 duration_ticks: u64,
634 start_tick: u64,
635 delay_ticks: u64,
636 easing: fn(f64) -> f64,
637 loop_mode: LoopMode,
638 item_count: usize,
639 done: bool,
640 on_complete: Option<Box<dyn FnMut()>>,
641}
642
643impl Stagger {
644 pub fn new(from: f64, to: f64, duration_ticks: u64) -> Self {
648 Self {
649 from,
650 to,
651 duration_ticks,
652 start_tick: 0,
653 delay_ticks: 0,
654 easing: ease_linear,
655 loop_mode: LoopMode::Once,
656 item_count: 0,
657 done: false,
658 on_complete: None,
659 }
660 }
661
662 pub fn easing(mut self, f: fn(f64) -> f64) -> Self {
664 self.easing = f;
665 self
666 }
667
668 pub fn delay(mut self, ticks: u64) -> Self {
670 self.delay_ticks = ticks;
671 self
672 }
673
674 pub fn loop_mode(mut self, mode: LoopMode) -> Self {
677 self.loop_mode = mode;
678 self
679 }
680
681 pub fn on_complete(mut self, f: impl FnMut() + 'static) -> Self {
683 self.on_complete = Some(Box::new(f));
684 self
685 }
686
687 pub fn items(mut self, count: usize) -> Self {
693 self.item_count = count;
694 self
695 }
696
697 pub fn value(&mut self, tick: u64, item_index: usize) -> f64 {
699 if item_index >= self.item_count {
700 self.item_count = item_index + 1;
701 }
702
703 let total_cycle = self.total_cycle_ticks();
704
705 let effective_tick = if self.loop_mode == LoopMode::Once {
706 tick
707 } else {
708 let elapsed = tick.wrapping_sub(self.start_tick);
709 let mapped = match self.loop_mode {
710 LoopMode::Repeat => {
711 if total_cycle == 0 {
712 0
713 } else {
714 elapsed % total_cycle
715 }
716 }
717 LoopMode::PingPong => {
718 if total_cycle == 0 {
719 0
720 } else {
721 let full = total_cycle.saturating_mul(2);
722 let phase = elapsed % full;
723 if phase < total_cycle {
724 phase
725 } else {
726 full - phase
727 }
728 }
729 }
730 LoopMode::Once => unreachable!(),
731 };
732 self.start_tick.wrapping_add(mapped)
733 };
734
735 let delay = self.delay_ticks.wrapping_mul(item_index as u64);
736 let item_start = self.start_tick.wrapping_add(delay);
737
738 if effective_tick < item_start {
739 self.done = false;
740 return self.from;
741 }
742
743 if self.duration_ticks == 0 {
744 self.done = true;
745 if let Some(cb) = &mut self.on_complete {
746 cb();
747 }
748 return self.to;
749 }
750
751 let elapsed = effective_tick - item_start;
752 if elapsed >= self.duration_ticks {
753 self.done = true;
754 if let Some(cb) = &mut self.on_complete {
755 cb();
756 }
757 return self.to;
758 }
759
760 self.done = false;
761 let progress = elapsed as f64 / self.duration_ticks as f64;
762 let eased = (self.easing)(clamp01(progress));
763 lerp(self.from, self.to, eased)
764 }
765
766 fn total_cycle_ticks(&self) -> u64 {
767 let max_delay = self
768 .delay_ticks
769 .wrapping_mul(self.item_count.saturating_sub(1) as u64);
770 self.duration_ticks.saturating_add(max_delay)
771 }
772
773 pub fn is_done(&self) -> bool {
781 self.done
782 }
783
784 pub fn is_all_done(&self, tick: u64, item_count: usize) -> bool {
801 if item_count == 0 {
802 return true;
803 }
804 let last_start = self.start_tick.saturating_add(
805 self.delay_ticks
806 .saturating_mul(item_count.saturating_sub(1) as u64),
807 );
808 tick >= last_start.saturating_add(self.duration_ticks)
809 }
810
811 pub fn reset(&mut self, tick: u64) {
813 self.start_tick = tick;
814 self.done = false;
815 }
816}
817
818fn map_loop_tick(
819 tick: u64,
820 start_tick: u64,
821 duration_ticks: u64,
822 loop_mode: LoopMode,
823 done: &mut bool,
824) -> Option<u64> {
825 if duration_ticks == 0 {
826 *done = true;
827 return None;
828 }
829
830 let elapsed = tick.wrapping_sub(start_tick);
831 match loop_mode {
832 LoopMode::Once => {
833 if elapsed >= duration_ticks {
834 *done = true;
835 None
836 } else {
837 *done = false;
838 Some(elapsed)
839 }
840 }
841 LoopMode::Repeat => {
842 *done = false;
843 Some(elapsed % duration_ticks)
844 }
845 LoopMode::PingPong => {
846 *done = false;
847 let cycle = duration_ticks.saturating_mul(2);
848 if cycle == 0 {
849 return Some(0);
850 }
851 let phase = elapsed % cycle;
852 if phase < duration_ticks {
853 Some(phase)
854 } else {
855 Some(cycle - phase)
856 }
857 }
858 }
859}
860
861pub struct Spring {
893 value: f64,
894 target: f64,
895 velocity: f64,
896 stiffness: f64,
897 damping: f64,
898 settled: bool,
899 on_settle: Option<Box<dyn FnMut()>>,
900}
901
902impl Spring {
903 pub fn new(initial: f64, stiffness: f64, damping: f64) -> Self {
908 debug_assert!(
909 damping > 0.0 && damping < 1.0,
910 "Spring::new: damping must be in (0, 1), got {damping}. \
911 Values >= 1.0 conserve or amplify energy and never settle."
912 );
913 Self {
914 value: initial,
915 target: initial,
916 velocity: 0.0,
917 stiffness,
918 damping,
919 settled: true,
920 on_settle: None,
921 }
922 }
923
924 pub fn on_settle(mut self, f: impl FnMut() + 'static) -> Self {
926 self.on_settle = Some(Box::new(f));
927 self
928 }
929
930 pub fn set_target(&mut self, target: f64) {
932 self.target = target;
933 self.settled = self.is_settled();
934 }
935
936 pub fn tick(&mut self) {
940 let displacement = self.target - self.value;
941 let spring_force = displacement * self.stiffness;
942 self.velocity = (self.velocity + spring_force) * self.damping;
943 self.value += self.velocity;
944
945 let is_settled = self.is_settled();
946 if !self.settled && is_settled {
947 self.settled = true;
948 if let Some(cb) = &mut self.on_settle {
949 cb();
950 }
951 }
952 }
953
954 pub fn value(&self) -> f64 {
956 self.value
957 }
958
959 pub fn is_settled(&self) -> bool {
964 (self.target - self.value).abs() < 0.01 && self.velocity.abs() < 0.01
965 }
966}
967
968fn clamp01(t: f64) -> f64 {
969 t.clamp(0.0, 1.0)
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975 use std::cell::Cell;
976 use std::rc::Rc;
977
978 fn assert_endpoints(f: fn(f64) -> f64) {
979 assert_eq!(f(0.0), 0.0);
980 assert_eq!(f(1.0), 1.0);
981 }
982
983 #[test]
984 fn easing_functions_have_expected_endpoints() {
985 let easing_functions: [fn(f64) -> f64; 9] = [
986 ease_linear,
987 ease_in_quad,
988 ease_out_quad,
989 ease_in_out_quad,
990 ease_in_cubic,
991 ease_out_cubic,
992 ease_in_out_cubic,
993 ease_out_elastic,
994 ease_out_bounce,
995 ];
996
997 for easing in easing_functions {
998 assert_endpoints(easing);
999 }
1000 }
1001
1002 #[test]
1003 fn tween_returns_start_middle_end_values() {
1004 let mut tween = Tween::new(0.0, 10.0, 10);
1005 tween.reset(100);
1006
1007 assert_eq!(tween.value(100), 0.0);
1008 assert_eq!(tween.value(105), 5.0);
1009 assert_eq!(tween.value(110), 10.0);
1010 assert!(tween.is_done());
1011 }
1012
1013 #[test]
1014 fn tween_reset_restarts_animation() {
1015 let mut tween = Tween::new(0.0, 1.0, 10);
1016 tween.reset(0);
1017 let _ = tween.value(10);
1018 assert!(tween.is_done());
1019
1020 tween.reset(20);
1021 assert!(!tween.is_done());
1022 assert_eq!(tween.value(20), 0.0);
1023 assert_eq!(tween.value(30), 1.0);
1024 assert!(tween.is_done());
1025 }
1026
1027 #[test]
1028 fn tween_on_complete_fires_once() {
1029 let count = Rc::new(Cell::new(0));
1030 let callback_count = Rc::clone(&count);
1031 let mut tween = Tween::new(0.0, 10.0, 10).on_complete(move || {
1032 callback_count.set(callback_count.get() + 1);
1033 });
1034
1035 tween.reset(0);
1036 assert_eq!(count.get(), 0);
1037
1038 assert_eq!(tween.value(5), 5.0);
1039 assert_eq!(count.get(), 0);
1040
1041 assert_eq!(tween.value(10), 10.0);
1042 assert_eq!(count.get(), 1);
1043
1044 assert_eq!(tween.value(11), 10.0);
1045 assert_eq!(count.get(), 1);
1046 }
1047
1048 #[test]
1049 fn spring_settles_to_target() {
1050 let mut spring = Spring::new(0.0, 0.2, 0.85);
1051 spring.set_target(10.0);
1052
1053 for _ in 0..300 {
1054 spring.tick();
1055 if spring.is_settled() {
1056 break;
1057 }
1058 }
1059
1060 assert!(spring.is_settled());
1061 assert!((spring.value() - 10.0).abs() < 0.01);
1062 }
1063
1064 #[test]
1065 fn spring_on_settle_fires_once() {
1066 let count = Rc::new(Cell::new(0));
1067 let callback_count = Rc::clone(&count);
1068 let mut spring = Spring::new(0.0, 0.2, 0.85).on_settle(move || {
1069 callback_count.set(callback_count.get() + 1);
1070 });
1071 spring.set_target(10.0);
1072
1073 for _ in 0..500 {
1074 spring.tick();
1075 if spring.is_settled() {
1076 break;
1077 }
1078 }
1079
1080 assert!(spring.is_settled());
1081 assert_eq!(count.get(), 1);
1082
1083 for _ in 0..50 {
1084 spring.tick();
1085 }
1086
1087 assert_eq!(count.get(), 1);
1088 }
1089
1090 #[cfg(debug_assertions)]
1091 #[test]
1092 #[should_panic(expected = "damping must be in (0, 1)")]
1093 fn spring_damping_one_panics_in_debug() {
1094 let _ = Spring::new(0.0, 0.5, 1.0);
1095 }
1096
1097 #[cfg(debug_assertions)]
1098 #[test]
1099 #[should_panic(expected = "damping must be in (0, 1)")]
1100 fn spring_damping_gt_one_panics_in_debug() {
1101 let _ = Spring::new(0.0, 0.5, 2.0);
1102 }
1103
1104 #[test]
1105 fn spring_valid_damping_settles() {
1106 for &d in &[0.5_f64, 0.7, 0.85, 0.95] {
1107 let mut s = Spring::new(0.0, 0.2, d);
1108 s.set_target(100.0);
1109 for _ in 0..1000 {
1110 s.tick();
1111 if s.is_settled() {
1112 break;
1113 }
1114 }
1115 assert!(s.is_settled(), "damping={d} should settle");
1116 assert!((s.value() - 100.0).abs() < 0.01, "damping={d} value off");
1117 }
1118 }
1119
1120 #[test]
1121 fn lerp_interpolates_values() {
1122 assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
1123 assert_eq!(lerp(0.0, 10.0, 0.5), 5.0);
1124 assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
1125 }
1126
1127 #[test]
1128 fn keyframes_interpolates_across_multiple_stops() {
1129 let mut keyframes = Keyframes::new(100)
1130 .stop(0.0, 0.0)
1131 .stop(0.3, 100.0)
1132 .stop(0.7, 50.0)
1133 .stop(1.0, 80.0)
1134 .easing(ease_linear);
1135
1136 keyframes.reset(0);
1137 assert_eq!(keyframes.value(0), 0.0);
1138 assert_eq!(keyframes.value(15), 50.0);
1139 assert_eq!(keyframes.value(30), 100.0);
1140 assert_eq!(keyframes.value(50), 75.0);
1141 assert_eq!(keyframes.value(70), 50.0);
1142 assert_eq!(keyframes.value(85), 65.0);
1143 assert_eq!(keyframes.value(100), 80.0);
1144 assert!(keyframes.is_done());
1145 }
1146
1147 #[test]
1148 fn keyframes_repeat_loop_restarts() {
1149 let mut keyframes = Keyframes::new(10)
1150 .stop(0.0, 0.0)
1151 .stop(1.0, 10.0)
1152 .loop_mode(LoopMode::Repeat);
1153
1154 keyframes.reset(0);
1155 assert_eq!(keyframes.value(5), 5.0);
1156 assert_eq!(keyframes.value(10), 0.0);
1157 assert_eq!(keyframes.value(12), 2.0);
1158 assert!(!keyframes.is_done());
1159 }
1160
1161 #[test]
1162 fn keyframes_pingpong_reverses_direction() {
1163 let mut keyframes = Keyframes::new(10)
1164 .stop(0.0, 0.0)
1165 .stop(1.0, 10.0)
1166 .loop_mode(LoopMode::PingPong);
1167
1168 keyframes.reset(0);
1169 assert_eq!(keyframes.value(8), 8.0);
1170 assert_eq!(keyframes.value(10), 10.0);
1171 assert_eq!(keyframes.value(12), 8.0);
1172 assert_eq!(keyframes.value(15), 5.0);
1173 assert!(!keyframes.is_done());
1174 }
1175
1176 #[test]
1177 fn sequence_chains_segments_in_order() {
1178 let mut sequence = Sequence::new()
1179 .then(0.0, 100.0, 30, ease_linear)
1180 .then(100.0, 50.0, 20, ease_linear)
1181 .then(50.0, 200.0, 40, ease_linear);
1182
1183 sequence.reset(0);
1184 assert_eq!(sequence.value(15), 50.0);
1185 assert_eq!(sequence.value(30), 100.0);
1186 assert_eq!(sequence.value(40), 75.0);
1187 assert_eq!(sequence.value(50), 50.0);
1188 assert_eq!(sequence.value(70), 125.0);
1189 assert_eq!(sequence.value(90), 200.0);
1190 assert!(sequence.is_done());
1191 }
1192
1193 #[test]
1194 fn sequence_loop_modes_repeat_and_pingpong_work() {
1195 let mut repeat = Sequence::new()
1196 .then(0.0, 10.0, 10, ease_linear)
1197 .loop_mode(LoopMode::Repeat);
1198 repeat.reset(0);
1199 assert_eq!(repeat.value(12), 2.0);
1200 assert!(!repeat.is_done());
1201
1202 let mut pingpong = Sequence::new()
1203 .then(0.0, 10.0, 10, ease_linear)
1204 .loop_mode(LoopMode::PingPong);
1205 pingpong.reset(0);
1206 assert_eq!(pingpong.value(12), 8.0);
1207 assert!(!pingpong.is_done());
1208 }
1209
1210 #[test]
1211 fn stagger_applies_per_item_delay() {
1212 let mut stagger = Stagger::new(0.0, 100.0, 20).easing(ease_linear).delay(5);
1213
1214 stagger.reset(0);
1215 assert_eq!(stagger.value(4, 3), 0.0);
1216 assert_eq!(stagger.value(15, 3), 0.0);
1217 assert_eq!(stagger.value(20, 3), 25.0);
1218 assert_eq!(stagger.value(35, 3), 100.0);
1219 assert!(stagger.is_done());
1220 }
1221
1222 #[test]
1225 fn stagger_is_all_done_returns_false_mid_animation() {
1226 let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1227 assert!(!stagger.is_all_done(15, 5), "items still in progress");
1229 }
1230
1231 #[test]
1233 fn stagger_is_all_done_returns_true_after_last_item() {
1234 let stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1235 assert!(stagger.is_all_done(31, 5), "all items done by tick 31");
1236 }
1237
1238 #[test]
1240 fn stagger_is_done_reflects_last_sampled_item_only() {
1241 let mut stagger = Stagger::new(0.0, 100.0, 10).delay(5);
1242 stagger.value(100, 4); assert!(stagger.is_done());
1244 stagger.value(15, 2);
1246 assert!(!stagger.is_done(), "is_done reflects last sampled item");
1247 }
1248
1249 #[test]
1251 fn stagger_is_all_done_zero_items() {
1252 let stagger = Stagger::new(0.0, 100.0, 10);
1253 assert!(stagger.is_all_done(0, 0));
1254 }
1255
1256 #[cfg(debug_assertions)]
1258 #[test]
1259 #[should_panic(expected = "out of range")]
1260 fn keyframes_segment_easing_oob_panics_in_debug() {
1261 let _ = Keyframes::new(60)
1263 .stop(0.0, 0.0)
1264 .stop(1.0, 100.0)
1265 .segment_easing(5, ease_linear);
1266 }
1267
1268 #[test]
1270 fn keyframes_segment_easing_valid_index() {
1271 let kf = Keyframes::new(60)
1272 .stop(0.0, 0.0)
1273 .stop(0.5, 50.0)
1274 .stop(1.0, 100.0)
1275 .segment_easing(0, ease_in_quad)
1276 .segment_easing(1, ease_out_quad);
1277 let _ = kf;
1278 }
1279}