1use std::{
2 num::NonZeroU32,
3 ops::{Deref, DerefMut, Sub},
4 rc::Rc,
5 time::Duration,
6};
7
8use crate::spring::DEFAULT_SPRING_EPSILON;
9use crate::{SpringAnimation, SpringConfig, SpringState, SpringTarget};
10
11pub const fn secs(seconds: u64) -> Duration {
25 Duration::from_secs(seconds)
26}
27
28pub const fn millis(milliseconds: u64) -> Duration {
43 Duration::from_millis(milliseconds)
44}
45
46#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
63pub struct Progress(f32);
64
65impl Progress {
66 pub const START: Self = Self(0.0);
68
69 pub const END: Self = Self(1.0);
71
72 pub fn clamped(value: f32) -> Self {
78 assert!(!value.is_nan(), "progress must not be NaN");
79 Self(value.clamp(Self::START.0, Self::END.0))
80 }
81
82 pub const fn get(self) -> f32 {
85 self.0
86 }
87
88 pub const fn is_at_end(self) -> bool {
92 self.0 == Self::END.0
93 }
94
95 pub(crate) fn reversed(self) -> Self {
96 Self::eased(Self::END.0 - self.0)
97 }
98
99 fn eased(value: f32) -> Self {
100 assert!(value.is_finite(), "easing must return a finite value");
101 Self(value)
102 }
103}
104
105pub trait MotionDurationExt {
122 fn with_easing(self, easing: impl Fn(f32) -> f32 + 'static) -> Motion;
124
125 fn with_spring(self, config: SpringConfig) -> Motion;
128}
129
130impl MotionDurationExt for Duration {
131 fn with_easing(self, easing: impl Fn(f32) -> f32 + 'static) -> Motion {
132 Motion::new(self).with_easing(easing)
133 }
134
135 fn with_spring(self, config: SpringConfig) -> Motion {
136 Motion::new(self).with_spring(config)
137 }
138}
139
140pub use MotionDurationExt as DurationWithEasing;
142
143#[derive(Clone)]
158pub struct Easing(Rc<dyn Fn(f32) -> f32>);
159
160impl Easing {
161 pub fn new(easing: impl Fn(f32) -> f32 + 'static) -> Self {
165 Self(Rc::new(easing))
166 }
167
168 pub fn sample(&self, progress: Progress) -> Progress {
174 Progress::eased((self.0)(progress.get()))
175 }
176}
177
178impl Default for Easing {
179 fn default() -> Self {
180 Self::new(crate::linear)
181 }
182}
183
184#[derive(Clone)]
201pub struct MotionPass {
202 duration: Duration,
203 easing: Easing,
204}
205
206impl MotionPass {
207 pub fn new(duration: Duration) -> Self {
209 Self {
210 duration,
211 easing: Easing::default(),
212 }
213 }
214
215 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
217 self.easing = Easing::new(easing);
218 self
219 }
220
221 pub fn duration(&self) -> Duration {
225 self.duration
226 }
227
228 pub fn easing(&self) -> &Easing {
230 &self.easing
231 }
232
233 fn duration_nanos(&self) -> u128 {
234 self.duration.as_nanos()
235 }
236
237 fn local_time(&self, elapsed_nanos: u128) -> Progress {
238 let duration_nanos = self.duration_nanos();
239 debug_assert!(duration_nanos > 0);
240 debug_assert!(elapsed_nanos < duration_nanos);
241
242 let linear = elapsed_nanos as f64 / duration_nanos as f64;
243 Progress::clamped(linear as f32)
244 }
245}
246
247impl From<Duration> for MotionPass {
248 fn from(duration: Duration) -> Self {
249 Self::new(duration)
250 }
251}
252
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
259pub enum Iterations {
260 Finite(NonZeroU32),
262 Forever,
264}
265
266impl Default for Iterations {
268 fn default() -> Self {
269 Self::Finite(NonZeroU32::MIN)
270 }
271}
272
273#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
278pub enum Direction {
279 #[default]
281 Forward,
282 Reverse,
284 Alternate,
286 AlternateReverse,
288}
289
290#[derive(Clone, Copy, Debug, Eq, PartialEq)]
292pub enum PassDirection {
293 Forward,
295 Reverse,
297}
298
299#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
303pub struct Playback {
304 iterations: Iterations,
305 direction: Direction,
306}
307
308impl Playback {
309 pub const fn new(iterations: Iterations, direction: Direction) -> Self {
311 Self {
312 iterations,
313 direction,
314 }
315 }
316
317 pub const fn iterations(self) -> Iterations {
319 self.iterations
320 }
321
322 pub const fn direction(self) -> Direction {
324 self.direction
325 }
326}
327
328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
332pub enum MotionExtent {
333 Finite(Duration),
335 Infinite,
337}
338
339#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341pub enum MotionPhase {
342 Delayed,
344 Playing,
346 Completed,
348 Inactive,
351}
352
353impl Direction {
354 fn pass(self, iteration: u128) -> PassDirection {
355 match self {
356 Self::Forward => PassDirection::Forward,
357 Self::Reverse => PassDirection::Reverse,
358 Self::Alternate => {
359 if iteration.is_multiple_of(2) {
360 PassDirection::Forward
361 } else {
362 PassDirection::Reverse
363 }
364 }
365 Self::AlternateReverse => {
366 if iteration.is_multiple_of(2) {
367 PassDirection::Reverse
368 } else {
369 PassDirection::Forward
370 }
371 }
372 }
373 }
374}
375
376#[derive(Clone, Copy, Debug, PartialEq)]
381pub struct MotionSample {
382 pub progress: Progress,
384
385 pub local_time: Progress,
388
389 pub iteration: u128,
392
393 pub direction: PassDirection,
396
397 pub phase: MotionPhase,
399
400 pub is_active: bool,
404}
405
406impl MotionSample {
407 pub const fn is_complete(self) -> bool {
412 matches!(self.phase, MotionPhase::Completed)
413 }
414
415 pub const fn is_at_end(self) -> bool {
417 self.progress.is_at_end()
418 }
419}
420
421#[derive(Clone)]
423pub struct DurationDescription {
424 forward: MotionPass,
425 reverse: Option<MotionPass>,
426 delay: Duration,
427 playback: Playback,
428}
429
430#[derive(Clone, Copy, Debug, PartialEq)]
432pub struct SpringDescription {
433 pub(crate) config: SpringConfig,
435
436 pub(crate) epsilon: f32,
438
439 settle_after: Duration,
440}
441
442#[derive(Clone, Debug)]
507pub struct Motion<Description = DurationDescription> {
508 description: Description,
509}
510
511impl<Description> Deref for Motion<Description> {
512 type Target = Description;
513
514 fn deref(&self) -> &Self::Target {
515 &self.description
516 }
517}
518
519impl<Description> DerefMut for Motion<Description> {
520 fn deref_mut(&mut self) -> &mut Self::Target {
521 &mut self.description
522 }
523}
524
525impl Motion<DurationDescription> {
526 pub fn new(duration: Duration) -> Self {
528 Self {
529 description: DurationDescription {
530 forward: MotionPass::new(duration),
531 reverse: None,
532 delay: Duration::ZERO,
533 playback: Playback::default(),
534 },
535 }
536 }
537
538 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
543 self.description.forward.easing = Easing::new(easing);
544 self
545 }
546
547 pub fn with_spring(self, config: SpringConfig) -> Self {
553 let duration = self.forward.duration.as_secs_f32();
554 let initial_state = SpringState {
555 position: 0.0,
556 velocity: 0.0,
557 };
558
559 self.with_easing(move |progress| {
560 if progress <= 0.0 {
561 0.0
562 } else if progress >= 1.0 {
563 1.0
564 } else {
565 config
566 .step(initial_state, 1.0, progress * duration)
567 .position
568 }
569 })
570 }
571
572 pub fn with_forward_pass(mut self, pass: MotionPass) -> Self {
574 self.forward = pass;
575 self
576 }
577
578 pub fn with_reverse_pass(mut self, pass: MotionPass) -> Self {
588 self.reverse = Some(pass);
589 self
590 }
591
592 pub fn with_delay(mut self, delay: Duration) -> Self {
597 self.delay = delay;
598 self
599 }
600
601 pub fn iterations(mut self, iterations: u32) -> Self {
612 self.playback.iterations = Iterations::Finite(
613 NonZeroU32::new(iterations).expect("motion iterations must be at least 1"),
614 );
615 self
616 }
617
618 pub fn repeat_forever(mut self) -> Self {
624 self.playback.iterations = Iterations::Forever;
625 self
626 }
627
628 pub fn alternate(mut self) -> Self {
634 self.playback.direction = Direction::Alternate;
635 self
636 }
637
638 pub fn direction(mut self, direction: Direction) -> Self {
640 self.playback.direction = direction;
641 self
642 }
643
644 pub fn with_playback(mut self, playback: Playback) -> Self {
646 self.playback = playback;
647 self
648 }
649
650 pub fn forward_pass(&self) -> &MotionPass {
652 &self.forward
653 }
654
655 pub fn reverse_pass(&self) -> Option<&MotionPass> {
657 self.reverse.as_ref()
658 }
659
660 pub fn delay(&self) -> Duration {
662 self.delay
663 }
664
665 pub const fn playback(&self) -> Playback {
667 self.description.playback
668 }
669
670 fn reverse_pass_or_forward(&self) -> &MotionPass {
671 self.reverse.as_ref().unwrap_or(&self.forward)
672 }
673
674 fn finite_duration_nanos(&self, iterations: NonZeroU32) -> Option<u128> {
675 let iterations = u128::from(iterations.get());
676 let forward = self.forward.duration_nanos();
677 let reverse = self.reverse_pass_or_forward().duration_nanos();
678 match self.playback.direction {
679 Direction::Forward => forward.checked_mul(iterations),
680 Direction::Reverse => reverse.checked_mul(iterations),
681 Direction::Alternate => (forward.checked_add(reverse)?)
682 .checked_mul(iterations / 2)?
683 .checked_add(forward.checked_mul(iterations % 2)?),
684 Direction::AlternateReverse => (forward.checked_add(reverse)?)
685 .checked_mul(iterations / 2)?
686 .checked_add(reverse.checked_mul(iterations % 2)?),
687 }
688 }
689
690 pub fn checked_extent(&self) -> Option<MotionExtent> {
704 let Iterations::Finite(iterations) = self.playback.iterations else {
705 return Some(MotionExtent::Infinite);
706 };
707 let nanos = self
708 .delay
709 .as_nanos()
710 .checked_add(self.finite_duration_nanos(iterations)?)?;
711 let seconds = u64::try_from(nanos / 1_000_000_000).ok()?;
712 let subsec_nanos = (nanos % 1_000_000_000) as u32;
713 Some(MotionExtent::Finite(Duration::new(seconds, subsec_nanos)))
714 }
715
716 fn pass(&self, direction: PassDirection) -> &MotionPass {
717 match direction {
718 PassDirection::Forward => &self.forward,
719 PassDirection::Reverse => self.reverse_pass_or_forward(),
720 }
721 }
722
723 fn active_pass(&self, elapsed_nanos: u128) -> Option<(&MotionPass, u128, PassDirection, u128)> {
724 let forward_duration = self.forward.duration_nanos();
725 let reverse_duration = self.reverse_pass_or_forward().duration_nanos();
726 match self.playback.direction {
727 Direction::Forward | Direction::Reverse => {
728 let direction = self.playback.direction.pass(0);
729 let pass = self.pass(direction);
730 let duration = pass.duration_nanos();
731 if duration == 0 {
732 return None;
733 }
734 let iteration = elapsed_nanos / duration;
735 let local = elapsed_nanos % duration;
736 Some((pass, local, direction, iteration))
737 }
738 Direction::Alternate | Direction::AlternateReverse => {
739 let cycle = forward_duration + reverse_duration;
740 if cycle == 0 {
741 return None;
742 }
743 let first = self.playback.direction.pass(0);
744 let first_duration = self.pass(first).duration_nanos();
745 let cycle_index = elapsed_nanos / cycle;
746 let in_cycle = elapsed_nanos % cycle;
747 let iteration = cycle_index * 2;
748 if in_cycle < first_duration {
749 Some((self.pass(first), in_cycle, first, iteration))
750 } else {
751 let second = self.playback.direction.pass(1);
752 Some((
753 self.pass(second),
754 in_cycle - first_duration,
755 second,
756 iteration + 1,
757 ))
758 }
759 }
760 }
761 }
762
763 fn start_progress(&self) -> Progress {
764 match self.playback.direction.pass(0) {
765 PassDirection::Forward => Progress::START,
766 PassDirection::Reverse => Progress::END,
767 }
768 }
769
770 fn terminal_sample(
771 &self,
772 direction: PassDirection,
773 iteration: u128,
774 phase: MotionPhase,
775 zero_duration: bool,
776 ) -> MotionSample {
777 let progress = if zero_duration {
778 match direction {
779 PassDirection::Forward => Progress::END,
780 PassDirection::Reverse => Progress::START,
781 }
782 } else {
783 self.presentation(self.pass(direction), Progress::END, direction)
784 };
785 MotionSample {
786 progress,
787 local_time: Progress::END,
788 iteration,
789 direction,
790 phase,
791 is_active: false,
792 }
793 }
794
795 fn presentation(
796 &self,
797 pass: &MotionPass,
798 local_time: Progress,
799 direction: PassDirection,
800 ) -> Progress {
801 let eased = pass.easing.sample(local_time);
802 if direction == PassDirection::Reverse {
803 eased.reversed()
804 } else {
805 eased
806 }
807 }
808
809 pub fn sample(&self, elapsed: Duration) -> MotionSample {
816 if elapsed < self.delay {
817 return MotionSample {
818 progress: self.start_progress(),
819 local_time: Progress::START,
820 iteration: 0,
821 direction: self.playback.direction.pass(0),
822 phase: MotionPhase::Delayed,
823 is_active: true,
824 };
825 }
826
827 let elapsed_nanos = (elapsed - self.delay).as_nanos();
828 if let Iterations::Finite(iterations) = self.playback.iterations {
829 let total_duration = self
830 .finite_duration_nanos(iterations)
831 .expect("finite Motion extent fits in u128 nanoseconds");
832 if elapsed_nanos >= total_duration {
833 let last = u128::from(iterations.get() - 1);
834 return self.terminal_sample(
835 self.playback.direction.pass(last),
836 last,
837 MotionPhase::Completed,
838 total_duration == 0,
839 );
840 }
841 }
842
843 let Some((pass, elapsed_in_pass, direction, iteration)) = self.active_pass(elapsed_nanos)
844 else {
845 let direction = self.playback.direction.pass(0);
846 return MotionSample {
847 progress: self.start_progress(),
848 local_time: Progress::START,
849 iteration: 0,
850 direction,
851 phase: MotionPhase::Inactive,
852 is_active: false,
853 };
854 };
855
856 if matches!(
859 self.playback.direction,
860 Direction::Forward | Direction::Reverse
861 ) && elapsed_nanos > 0
862 && elapsed_in_pass == 0
863 {
864 return MotionSample {
865 progress: self.presentation(pass, Progress::END, direction),
866 local_time: Progress::END,
867 iteration: iteration - 1,
868 direction,
869 phase: MotionPhase::Playing,
870 is_active: true,
871 };
872 }
873
874 let local_time = pass.local_time(elapsed_in_pass);
875
876 MotionSample {
877 progress: self.presentation(pass, local_time, direction),
878 local_time,
879 iteration,
880 direction,
881 phase: MotionPhase::Playing,
882 is_active: true,
883 }
884 }
885
886 pub fn sample_at<Time>(&self, started_at: Time, now: Time) -> MotionSample
891 where
892 Time: Sub<Time, Output = Duration>,
893 {
894 self.sample(now - started_at)
895 }
896
897 pub(crate) fn resting_progress(&self) -> Progress {
898 match self.playback.iterations {
899 Iterations::Finite(iterations) => {
900 let last = u128::from(iterations.get() - 1);
901 match self.playback.direction.pass(last) {
902 PassDirection::Forward => Progress::END,
903 PassDirection::Reverse => Progress::START,
904 }
905 }
906 Iterations::Forever => self.start_progress(),
907 }
908 }
909}
910
911impl Default for Motion<DurationDescription> {
912 fn default() -> Self {
913 Self::new(Duration::ZERO)
914 }
915}
916
917impl From<Duration> for Motion<DurationDescription> {
918 fn from(duration: Duration) -> Self {
919 Self::new(duration)
920 }
921}
922
923#[deprecated(note = "use Motion")]
925pub type MotionInfo = Motion;
926
927impl Motion<SpringDescription> {
928 pub fn spring(config: SpringConfig) -> Self {
933 let epsilon = DEFAULT_SPRING_EPSILON;
934
935 Self {
936 description: SpringDescription {
937 config,
938 epsilon,
939 settle_after: config.settle_time(SpringState::default(), 1.0, epsilon),
940 },
941 }
942 }
943
944 pub fn config(&self) -> SpringConfig {
946 self.config
947 }
948
949 pub fn epsilon(&self) -> f32 {
951 self.epsilon
952 }
953
954 pub fn with_epsilon(mut self, epsilon: f32) -> Self {
960 self.description.epsilon = epsilon;
961 self.description.settle_after =
962 self.config
963 .settle_time(SpringState::default(), 1.0, epsilon);
964 self
965 }
966
967 pub fn sample(&self, elapsed: Duration) -> MotionSample {
985 if elapsed >= self.settle_after {
986 return MotionSample {
987 progress: Progress::END,
988 local_time: Progress::END,
989 iteration: 0,
990 direction: PassDirection::Forward,
991 phase: MotionPhase::Completed,
992 is_active: false,
993 };
994 }
995
996 let state = self
997 .config
998 .step(SpringState::default(), 1.0, elapsed.as_secs_f32());
999
1000 MotionSample {
1001 progress: Progress::eased(state.position),
1002 local_time: Progress::clamped(
1003 (elapsed.as_secs_f64() / self.settle_after.as_secs_f64()) as f32,
1004 ),
1005 iteration: 0,
1006 direction: PassDirection::Forward,
1007 phase: MotionPhase::Playing,
1008 is_active: true,
1009 }
1010 }
1011
1012 pub fn to<T: SpringTarget>(self, target: T) -> SpringAnimation<T> {
1016 SpringAnimation {
1017 motion: self,
1018 target,
1019 initial: None,
1020 playback: crate::SpringPlayback::Running,
1021 }
1022 }
1023}
1024
1025impl From<SpringConfig> for Motion<SpringDescription> {
1026 fn from(config: SpringConfig) -> Self {
1027 Self::spring(config)
1028 }
1029}
1030
1031#[derive(Clone)]
1035pub enum AnyMotion {
1036 Duration(Motion<DurationDescription>),
1038
1039 Spring(Motion<SpringDescription>),
1041}
1042
1043impl AnyMotion {
1044 pub(crate) fn starts_in_reverse(&self) -> bool {
1045 matches!(self, Self::Duration(motion) if motion.playback.direction.pass(0) == PassDirection::Reverse)
1046 }
1047
1048 pub fn sample(&self, elapsed: Duration) -> MotionSample {
1050 match self {
1051 Self::Duration(motion) => motion.sample(elapsed),
1052 Self::Spring(motion) => motion.sample(elapsed),
1053 }
1054 }
1055
1056 pub fn sample_at<Time>(&self, started_at: Time, now: Time) -> MotionSample
1059 where
1060 Time: Sub<Time, Output = Duration>,
1061 {
1062 self.sample(now - started_at)
1063 }
1064}
1065
1066impl From<Motion<DurationDescription>> for AnyMotion {
1067 fn from(motion: Motion<DurationDescription>) -> Self {
1068 Self::Duration(motion)
1069 }
1070}
1071
1072impl From<Motion<SpringDescription>> for AnyMotion {
1073 fn from(motion: Motion<SpringDescription>) -> Self {
1074 Self::Spring(motion)
1075 }
1076}
1077
1078impl From<Duration> for AnyMotion {
1079 fn from(duration: Duration) -> Self {
1080 Self::Duration(duration.into())
1081 }
1082}
1083
1084impl From<SpringConfig> for AnyMotion {
1085 fn from(config: SpringConfig) -> Self {
1086 Self::Spring(config.into())
1087 }
1088}
1089
1090impl From<&Motion<DurationDescription>> for AnyMotion {
1091 fn from(motion: &Motion<DurationDescription>) -> Self {
1092 Self::Duration(motion.clone())
1093 }
1094}
1095
1096impl From<&Motion<SpringDescription>> for AnyMotion {
1097 fn from(motion: &Motion<SpringDescription>) -> Self {
1098 Self::Spring(motion.clone())
1099 }
1100}
1101
1102impl From<&AnyMotion> for AnyMotion {
1103 fn from(motion: &AnyMotion) -> Self {
1104 motion.clone()
1105 }
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use super::*;
1111
1112 fn progress(value: f32) -> Progress {
1113 Progress::clamped(value)
1114 }
1115
1116 fn assert_sample(motion: &Motion, elapsed: Duration, value: f32, is_active: bool) {
1117 let sample = motion.sample(elapsed);
1118 assert_eq!(sample.progress, progress(value));
1119 assert_eq!(sample.is_active, is_active);
1120 }
1121
1122 #[test]
1123 fn creates_durations() {
1124 assert_eq!(secs(2), Duration::from_secs(2));
1125 assert_eq!(millis(250), Duration::from_millis(250));
1126 }
1127
1128 #[test]
1129 fn samples_default_one_shot_and_custom_easing() {
1130 let motion = Duration::from_secs(2).with_easing(|progress| progress * progress);
1131 assert_eq!(motion.forward_pass().duration(), Duration::from_secs(2));
1132 assert!(motion.reverse_pass().is_none());
1133 assert_eq!(motion.delay(), Duration::ZERO);
1134 assert_eq!(motion.playback(), Playback::default());
1135
1136 assert_sample(&motion, Duration::ZERO, 0.0, true);
1137 assert_sample(&motion, Duration::from_secs(1), 0.25, true);
1138 assert_sample(&motion, Duration::from_secs(3), 1.0, false);
1139 assert_eq!(
1140 motion
1141 .sample_at(Duration::from_secs(3), Duration::from_secs(5))
1142 .progress,
1143 Progress::END
1144 );
1145
1146 assert_eq!(Progress::clamped(-1.0), Progress::START);
1147 assert_eq!(Progress::clamped(2.0), Progress::END);
1148 }
1149
1150 #[test]
1151 fn delay_applies_once_at_the_start_of_a_run() {
1152 let motion = Motion::new(Duration::from_secs(1))
1153 .with_delay(Duration::from_millis(100))
1154 .iterations(3);
1155
1156 assert_sample(&motion, Duration::ZERO, 0.0, true);
1157 assert_sample(&motion, Duration::from_millis(99), 0.0, true);
1158 assert_sample(&motion, Duration::from_millis(100), 0.0, true);
1159 assert_sample(&motion, Duration::from_millis(600), 0.5, true);
1160 assert_sample(&motion, Duration::from_millis(1_100), 1.0, true);
1161 assert_sample(&motion, Duration::from_millis(3_100), 1.0, false);
1162 }
1163
1164 #[test]
1165 fn restarting_iterations_have_exact_boundaries_and_stable_completion() {
1166 let motion = Motion::new(Duration::from_secs(1)).iterations(3);
1167
1168 for (milliseconds, value, active) in [
1169 (0, 0.0, true),
1170 (500, 0.5, true),
1171 (1_000, 1.0, true),
1172 (2_000, 1.0, true),
1173 (2_750, 0.75, true),
1174 (3_000, 1.0, false),
1175 (30_000, 1.0, false),
1176 ] {
1177 assert_sample(&motion, Duration::from_millis(milliseconds), value, active);
1178 }
1179 }
1180
1181 #[test]
1182 fn alternating_iterations_reverse_and_settle_by_parity() {
1183 for (iterations, expected_final) in [(1, 1.0), (2, 0.0), (3, 1.0), (4, 0.0)] {
1184 let motion = Motion::new(Duration::from_secs(1))
1185 .iterations(iterations)
1186 .alternate();
1187 assert_sample(
1188 &motion,
1189 Duration::from_secs(u64::from(iterations)),
1190 expected_final,
1191 false,
1192 );
1193 assert_sample(&motion, Duration::from_secs(100), expected_final, false);
1194 }
1195
1196 let motion = Motion::new(Duration::from_secs(1))
1197 .iterations(4)
1198 .alternate();
1199 for (milliseconds, value) in [
1200 (0, 0.0),
1201 (500, 0.5),
1202 (1_000, 1.0),
1203 (1_500, 0.5),
1204 (2_000, 0.0),
1205 (3_000, 1.0),
1206 ] {
1207 assert_sample(&motion, Duration::from_millis(milliseconds), value, true);
1208 }
1209 }
1210
1211 #[test]
1212 fn asymmetric_alternating_passes_use_local_easing() {
1213 let motion = Motion::new(Duration::from_secs(1))
1214 .with_easing(|value| value)
1215 .with_reverse_pass(
1216 MotionPass::new(Duration::from_secs(2)).with_easing(|value| value * value),
1217 )
1218 .iterations(5)
1219 .alternate();
1220
1221 assert_eq!(
1222 motion.reverse_pass().unwrap().duration(),
1223 Duration::from_secs(2)
1224 );
1225 assert_sample(&motion, Duration::from_millis(500), 0.5, true);
1226 assert_sample(&motion, Duration::from_secs(1), 1.0, true);
1227 assert_sample(&motion, Duration::from_secs(2), 0.75, true);
1228 assert_sample(&motion, Duration::from_secs(3), 0.0, true);
1229 assert_sample(&motion, Duration::from_millis(6_500), 0.5, true);
1230 assert_sample(&motion, Duration::from_secs(7), 1.0, false);
1231 }
1232
1233 #[test]
1234 fn infinite_motion_restarts_or_alternates_without_completing() {
1235 let restarting = Motion::new(Duration::from_secs(1)).repeat_forever();
1236 assert_sample(&restarting, Duration::from_millis(250), 0.25, true);
1237 assert_sample(&restarting, Duration::from_secs(1), 1.0, true);
1238 assert_sample(&restarting, Duration::from_millis(2_500), 0.5, true);
1239
1240 let alternating = Motion::new(Duration::from_secs(1))
1241 .repeat_forever()
1242 .alternate();
1243 assert_sample(&alternating, Duration::from_millis(1_250), 0.75, true);
1244 assert_sample(&alternating, Duration::from_secs(2), 0.0, true);
1245 }
1246
1247 #[test]
1248 fn zero_duration_motion_resolves_after_delay_without_spinning() {
1249 for (iterations, alternate, expected) in [
1250 (1, false, 1.0),
1251 (2, false, 1.0),
1252 (2, true, 0.0),
1253 (3, true, 1.0),
1254 ] {
1255 let mut motion = Motion::new(Duration::ZERO)
1256 .with_delay(Duration::from_millis(10))
1257 .iterations(iterations);
1258 if alternate {
1259 motion = motion.alternate();
1260 }
1261 assert_sample(&motion, Duration::from_millis(9), 0.0, true);
1262 assert_sample(&motion, Duration::from_millis(10), expected, false);
1263 }
1264
1265 let forever = Motion::new(Duration::ZERO).repeat_forever().alternate();
1266 assert_sample(&forever, Duration::ZERO, 0.0, false);
1267 assert_sample(&forever, Duration::MAX, 0.0, false);
1268 }
1269
1270 #[test]
1271 fn zero_length_passes_are_skipped_without_iteration() {
1272 let zero_forward = Motion::new(Duration::ZERO)
1273 .with_reverse_pass(MotionPass::new(Duration::from_secs(2)))
1274 .iterations(2)
1275 .alternate();
1276 assert_sample(&zero_forward, Duration::ZERO, 1.0, true);
1277 assert_sample(&zero_forward, Duration::from_secs(1), 0.5, true);
1278 assert_sample(&zero_forward, Duration::from_secs(2), 0.0, false);
1279
1280 let zero_reverse = Motion::new(Duration::from_secs(1))
1281 .with_reverse_pass(MotionPass::new(Duration::ZERO))
1282 .iterations(3)
1283 .alternate();
1284 assert_sample(&zero_reverse, Duration::from_secs(1), 0.0, true);
1285 assert_sample(&zero_reverse, Duration::from_secs(2), 1.0, false);
1286 }
1287
1288 #[test]
1289 fn nanosecond_and_large_duration_sampling_keep_exact_pass_selection() {
1290 let nanos = Motion::new(Duration::from_nanos(3))
1291 .iterations(3)
1292 .alternate();
1293 assert_sample(&nanos, Duration::from_nanos(3), 1.0, true);
1294 assert_sample(&nanos, Duration::from_nanos(6), 0.0, true);
1295 assert_sample(&nanos, Duration::from_nanos(9), 1.0, false);
1296
1297 let enormous = Motion::new(Duration::MAX).iterations(u32::MAX);
1298 assert_sample(&enormous, Duration::MAX, 1.0, true);
1299 }
1300
1301 #[test]
1302 fn playback_model_and_sample_distinguish_end_from_completion() {
1303 assert_eq!(Iterations::default(), Iterations::Finite(NonZeroU32::MIN));
1304 assert_eq!(Playback::default().direction(), Direction::Forward);
1305 let motion = Motion::new(secs(1)).iterations(3);
1306 assert_eq!(
1307 motion.playback().iterations(),
1308 Iterations::Finite(NonZeroU32::new(3).unwrap())
1309 );
1310 let boundary = motion.sample(secs(1));
1311 assert_eq!(boundary.progress, Progress::END);
1312 assert_eq!(boundary.local_time, Progress::END);
1313 assert_eq!(boundary.iteration, 0);
1314 assert!(boundary.is_at_end());
1315 assert!(!boundary.is_complete());
1316 let next = motion.sample(secs(1) + Duration::from_nanos(1));
1317 assert_eq!(next.iteration, 1);
1318 assert_eq!(next.phase, MotionPhase::Playing);
1319
1320 let alternating = Motion::new(secs(1)).iterations(2).alternate();
1321 let end = alternating.sample(secs(2));
1322 assert_eq!(end.progress, Progress::START);
1323 assert!(!end.is_at_end());
1324 assert!(end.is_complete());
1325 assert_eq!(end.iteration, 1);
1326 }
1327
1328 #[test]
1329 fn overshoot_is_not_the_presentation_endpoint() {
1330 let motion =
1331 Motion::new(secs(1)).with_easing(|progress| if progress < 1.0 { 1.2 } else { 1.0 });
1332 let overshoot = motion.sample(millis(500));
1333 assert_eq!(overshoot.progress.get(), 1.2);
1334 assert!(!overshoot.progress.is_at_end());
1335 assert!(!overshoot.is_at_end());
1336 assert!(!overshoot.is_complete());
1337 assert!(motion.sample(secs(1)).is_at_end());
1338 }
1339
1340 #[test]
1341 fn all_directions_resolve_passes_and_reverse_first_delay() {
1342 for (configured, first, second) in [
1343 (
1344 Direction::Forward,
1345 PassDirection::Forward,
1346 PassDirection::Forward,
1347 ),
1348 (
1349 Direction::Reverse,
1350 PassDirection::Reverse,
1351 PassDirection::Reverse,
1352 ),
1353 (
1354 Direction::Alternate,
1355 PassDirection::Forward,
1356 PassDirection::Reverse,
1357 ),
1358 (
1359 Direction::AlternateReverse,
1360 PassDirection::Reverse,
1361 PassDirection::Forward,
1362 ),
1363 ] {
1364 let motion = Motion::new(secs(1))
1365 .with_delay(millis(100))
1366 .iterations(2)
1367 .direction(configured);
1368 let delayed = motion.sample(Duration::ZERO);
1369 assert_eq!(delayed.phase, MotionPhase::Delayed);
1370 assert_eq!(delayed.direction, first);
1371 assert_eq!(
1372 delayed.progress,
1373 if first == PassDirection::Reverse {
1374 Progress::END
1375 } else {
1376 Progress::START
1377 }
1378 );
1379 let active = motion.sample(millis(600));
1380 assert_eq!(active.direction, first);
1381 assert_eq!(active.iteration, 0);
1382 assert_eq!(active.local_time, progress(0.5));
1383 let boundary = motion.sample(millis(1_100));
1384 assert_eq!(boundary.phase, MotionPhase::Playing);
1385 assert_eq!(
1386 boundary.progress,
1387 if first == PassDirection::Forward {
1388 Progress::END
1389 } else {
1390 Progress::START
1391 }
1392 );
1393 let later = motion.sample(millis(1_600));
1394 assert_eq!(later.direction, second);
1395 assert_eq!(later.iteration, 1);
1396 assert_eq!(later.local_time, progress(0.5));
1397 let completed = motion.sample(millis(2_100));
1398 assert_eq!(completed.direction, second);
1399 assert!(completed.is_complete());
1400 }
1401 }
1402
1403 #[test]
1404 fn reverse_pass_easing_is_distinct_from_uneased_local_time() {
1405 let motion = Motion::new(secs(1))
1406 .with_reverse_pass(MotionPass::new(secs(2)).with_easing(|t| t * t))
1407 .direction(Direction::Reverse);
1408 let sample = motion.sample(secs(1));
1409 assert_eq!(sample.local_time, progress(0.5));
1410 assert_eq!(sample.progress, progress(0.75));
1411 assert_eq!(sample.direction, PassDirection::Reverse);
1412 }
1413
1414 #[test]
1415 fn checked_extent_counts_delay_and_asymmetric_passes() {
1416 let base = Motion::new(millis(300))
1417 .with_reverse_pass(MotionPass::new(millis(700)))
1418 .with_delay(millis(200))
1419 .iterations(3);
1420 for (direction, expected) in [
1421 (Direction::Forward, 1_100),
1422 (Direction::Reverse, 2_300),
1423 (Direction::Alternate, 1_500),
1424 (Direction::AlternateReverse, 1_900),
1425 ] {
1426 assert_eq!(
1427 base.clone().direction(direction).checked_extent(),
1428 Some(MotionExtent::Finite(millis(expected)))
1429 );
1430 }
1431 for direction in [Direction::Alternate, Direction::AlternateReverse] {
1432 assert_eq!(
1433 base.clone()
1434 .iterations(2)
1435 .direction(direction)
1436 .checked_extent(),
1437 Some(MotionExtent::Finite(millis(1_200)))
1438 );
1439 }
1440 assert_eq!(
1441 base.repeat_forever().checked_extent(),
1442 Some(MotionExtent::Infinite)
1443 );
1444 assert_eq!(
1445 Motion::new(Duration::MAX).iterations(2).checked_extent(),
1446 None
1447 );
1448 assert_eq!(
1449 Motion::new(Duration::MAX)
1450 .with_delay(Duration::from_nanos(1))
1451 .checked_extent(),
1452 None
1453 );
1454 }
1455
1456 #[test]
1457 fn zero_length_passes_work_in_both_reverse_first_modes() {
1458 let finite = Motion::new(Duration::ZERO)
1459 .with_reverse_pass(MotionPass::new(secs(1)))
1460 .iterations(2)
1461 .direction(Direction::AlternateReverse);
1462 assert_eq!(
1463 finite.sample(Duration::ZERO).direction,
1464 PassDirection::Reverse
1465 );
1466 assert_eq!(finite.sample(Duration::ZERO).progress, Progress::END);
1467 assert_eq!(finite.sample(secs(1)).phase, MotionPhase::Completed);
1468 let forever = Motion::new(Duration::ZERO)
1469 .with_reverse_pass(MotionPass::new(Duration::ZERO))
1470 .repeat_forever()
1471 .direction(Direction::AlternateReverse);
1472 assert_eq!(forever.sample(Duration::MAX).phase, MotionPhase::Inactive);
1473 assert_eq!(forever.sample(Duration::MAX).progress, Progress::END);
1474 }
1475
1476 #[test]
1477 fn zero_duration_combinations_terminate_or_stay_active_as_configured() {
1478 for direction in [
1479 Direction::Forward,
1480 Direction::Reverse,
1481 Direction::Alternate,
1482 Direction::AlternateReverse,
1483 ] {
1484 for (forward, reverse) in [
1485 (Duration::ZERO, Duration::ZERO),
1486 (Duration::ZERO, secs(1)),
1487 (secs(1), Duration::ZERO),
1488 ] {
1489 let motion = Motion::new(forward)
1490 .with_reverse_pass(MotionPass::new(reverse))
1491 .iterations(3)
1492 .direction(direction);
1493 let MotionExtent::Finite(extent) = motion.checked_extent().unwrap() else {
1494 panic!("finite playback must have finite extent");
1495 };
1496 assert_eq!(motion.sample(extent).phase, MotionPhase::Completed);
1497 assert!(!motion.sample(extent).is_active);
1498
1499 let forever = motion.repeat_forever();
1500 let sample = forever.sample(Duration::MAX);
1501 let applicable_duration = match direction {
1502 Direction::Forward => forward,
1503 Direction::Reverse => reverse,
1504 Direction::Alternate | Direction::AlternateReverse => forward + reverse,
1505 };
1506 assert_eq!(
1507 sample.phase == MotionPhase::Inactive,
1508 applicable_duration.is_zero()
1509 );
1510 }
1511 }
1512
1513 let fallback = Motion::new(secs(1)).direction(Direction::Reverse);
1514 assert_eq!(fallback.sample(millis(500)).progress, progress(0.5));
1515 assert_eq!(
1516 fallback.checked_extent(),
1517 Some(MotionExtent::Finite(secs(1)))
1518 );
1519 }
1520
1521 #[test]
1522 #[should_panic(expected = "motion iterations must be at least 1")]
1523 fn zero_iterations_are_invalid() {
1524 let _ = Motion::new(Duration::from_secs(1)).iterations(0);
1525 }
1526 #[test]
1527 fn spring_motion_preserves_overshoot_and_rich_sample_metadata() {
1528 let config = SpringConfig::new(100.0, 6.0, 1.0);
1529 let sampled = Motion::new(secs(1)).with_spring(config);
1530 assert!((1..100).any(|step| { sampled.sample(millis(step * 10)).progress.get() > 1.0 }));
1531
1532 let native: AnyMotion = config.into();
1533 let loose = Motion::spring(config).with_epsilon(0.1);
1534 let cutoff = loose.settle_after;
1535 assert!(native.sample(cutoff).is_active);
1536 let completed = AnyMotion::from(loose).sample_at(Duration::ZERO, cutoff);
1537 assert!(completed.is_complete());
1538 assert_eq!(completed.progress, Progress::END);
1539 assert_eq!(completed.local_time, Progress::END);
1540 assert_eq!(completed.direction, PassDirection::Forward);
1541 }
1542
1543 #[test]
1544 fn resting_progress_uses_the_configured_endpoint() {
1545 let forward = Motion::new(secs(1)).with_easing(|_| 0.5);
1546 assert_eq!(forward.resting_progress(), Progress::END);
1547 assert_eq!(
1548 forward
1549 .clone()
1550 .direction(Direction::Reverse)
1551 .resting_progress(),
1552 Progress::START
1553 );
1554 assert_eq!(
1555 forward
1556 .repeat_forever()
1557 .direction(Direction::Reverse)
1558 .resting_progress(),
1559 Progress::END
1560 );
1561 }
1562}