1use crate::random::Rng;
27
28#[derive(Debug, Clone, PartialEq)]
30pub enum SequenceStep {
31 PlayFrames {
33 frames: Vec<usize>,
35 frame_duration: f32,
37 },
38 Pause(f32),
40 RandomPause {
44 min: f32,
46 max: f32,
48 },
49 Action(u32),
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum SequenceLoop {
57 Once,
59 Repeat,
61 PingPongRepeat,
63 PingPongOnce,
65}
66
67#[derive(Debug, Clone)]
75pub struct AnimationSequence {
76 steps: Vec<SequenceStep>,
78 cursors: Vec<usize>,
80 held_frame: Option<usize>,
82 loop_mode: SequenceLoop,
83 step: usize,
85 timer: f32,
87 step_duration: Option<f32>,
90 forward: bool,
92 actions: std::collections::VecDeque<u32>,
94 finished: bool,
95 just_completed: bool,
96 cycle_positions: usize,
100}
101
102impl AnimationSequence {
103 #[must_use]
105 pub fn builder() -> SequenceBuilder {
106 SequenceBuilder::new()
107 }
108
109 pub fn tick(&mut self, dt: f32, rng: &mut Rng) -> bool {
127 self.just_completed = false;
128 self.actions.clear();
129 if self.finished || !dt.is_finite() || dt <= 0.0 {
130 return false;
131 }
132 self.timer += dt;
133
134 let repeating = matches!(
135 self.loop_mode,
136 SequenceLoop::Repeat | SequenceLoop::PingPongRepeat
137 );
138 let mut idle_advances = 0;
139 loop {
140 let duration = self.resolved_duration(rng);
141 if self.timer < duration {
142 return false;
143 }
144 let before = self.timer;
145 self.timer -= duration;
146 if self.timer < before {
147 idle_advances = 0;
148 } else if repeating {
149 idle_advances += 1;
150 if idle_advances > self.cycle_positions {
151 self.timer = 0.0;
152 return false;
153 }
154 }
155 if self.advance() {
156 self.finished = true;
157 self.just_completed = true;
158 self.timer = 0.0;
159 return true;
160 }
161 }
162 }
163
164 #[must_use]
169 pub fn current_frame(&self) -> Option<usize> {
170 match self.steps.get(self.step) {
171 Some(SequenceStep::PlayFrames { frames, .. }) => {
172 frames.get(self.cursors[self.step]).copied()
173 }
174 _ => self.held_frame,
175 }
176 }
177
178 #[must_use]
181 pub fn just_completed(&self) -> bool {
182 self.just_completed
183 }
184
185 #[must_use]
187 pub fn is_finished(&self) -> bool {
188 self.finished
189 }
190
191 pub fn take_action(&mut self) -> Option<u32> {
196 self.actions.pop_front()
197 }
198
199 pub fn reset(&mut self) {
202 self.step = 0;
203 self.timer = 0.0;
204 self.step_duration = None;
205 self.forward = true;
206 self.finished = false;
207 self.just_completed = false;
208 self.actions.clear();
209 self.cursors.fill(0);
210 self.held_frame = None;
211 }
212
213 fn resolved_duration(&mut self, rng: &mut Rng) -> f32 {
216 if let Some(duration) = self.step_duration {
217 return duration;
218 }
219 let duration = match &self.steps[self.step] {
220 SequenceStep::PlayFrames { frame_duration, .. } => *frame_duration,
221 SequenceStep::Pause(duration) => *duration,
222 SequenceStep::RandomPause { min, max } => min + rng.f32() * (max - min),
223 SequenceStep::Action(_) => 0.0,
224 };
225 self.step_duration = Some(duration);
226 duration
227 }
228
229 fn advance(&mut self) -> bool {
232 let cursor = self.cursors[self.step];
233 let next_frame = match &self.steps[self.step] {
234 SequenceStep::PlayFrames { frames, .. } => {
235 self.held_frame = Some(frames[cursor]);
236 if self.forward {
237 (cursor + 1 < frames.len()).then_some(cursor + 1)
238 } else {
239 cursor.checked_sub(1)
240 }
241 }
242 _ => None,
243 };
244 if let Some(cursor) = next_frame {
245 self.cursors[self.step] = cursor;
246 self.step_duration = None;
247 return false;
248 }
249 if let SequenceStep::Action(id) = &self.steps[self.step] {
250 self.actions.push_back(*id);
251 }
252 self.next_step()
253 }
254
255 fn next_step(&mut self) -> bool {
258 self.step_duration = None;
259 match self.loop_mode {
260 SequenceLoop::Once => {
261 self.step += 1;
262 self.step >= self.steps.len()
263 }
264 SequenceLoop::Repeat => {
265 self.step += 1;
266 if self.step >= self.steps.len() {
267 self.step = 0;
268 self.cursors.fill(0);
269 }
270 false
271 }
272 SequenceLoop::PingPongRepeat | SequenceLoop::PingPongOnce => {
273 if self.forward {
274 if self.step + 1 < self.steps.len() {
275 self.step += 1;
276 self.cursors[self.step] = 0;
277 false
278 } else {
279 self.turn_back()
280 }
281 } else if self.step > 0 {
282 self.step -= 1;
283 self.cursors[self.step] = self.element_count(self.step) - 1;
284 false
285 } else {
286 self.restart_forward()
287 }
288 }
289 }
290 }
291
292 fn turn_back(&mut self) -> bool {
299 let (first, last) = self.frame_ends();
300 self.forward = false;
301 let resume = if last > first {
302 last - 1
303 } else if self.element_count(last) > 1 {
304 self.step = last;
305 self.cursors[last] = self.element_count(last) - 2;
306 return false;
307 } else if last > 0 {
308 last - 1
310 } else {
311 return self.restart_forward();
312 };
313 self.step = resume;
314 self.cursors[resume] = self.element_count(resume) - 1;
315 false
316 }
317
318 fn restart_forward(&mut self) -> bool {
324 if self.loop_mode == SequenceLoop::PingPongOnce {
325 return true;
326 }
327 let (first, last) = self.frame_ends();
328 self.forward = true;
329 self.cursors.fill(0);
330 if last > first {
331 self.step = first + 1;
332 } else {
333 self.step = first;
334 self.cursors[first] = usize::from(self.element_count(first) > 1);
335 }
336 false
337 }
338
339 fn frame_ends(&self) -> (usize, usize) {
342 let plays = |step: &SequenceStep| matches!(step, SequenceStep::PlayFrames { .. });
343 match (
344 self.steps.iter().position(plays),
345 self.steps.iter().rposition(plays),
346 ) {
347 (Some(first), Some(last)) => (first, last),
348 _ => (0, self.steps.len() - 1),
349 }
350 }
351
352 fn element_count(&self, step: usize) -> usize {
354 match &self.steps[step] {
355 SequenceStep::PlayFrames { frames, .. } => frames.len(),
356 _ => 1,
357 }
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq)]
363pub enum SequenceError {
364 NoSteps,
366 EmptyFrames { step: usize },
368 BadDuration { step: usize },
370 BadRandomRange { step: usize },
373}
374
375impl std::fmt::Display for SequenceError {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 match self {
378 Self::NoSteps => write!(f, "animation sequence has no steps"),
379 Self::EmptyFrames { step } => {
380 write!(f, "animation sequence step {step} plays no frames")
381 }
382 Self::BadDuration { step } => write!(
383 f,
384 "animation sequence step {step} needs a finite, non-negative duration"
385 ),
386 Self::BadRandomRange { step } => write!(
387 f,
388 "animation sequence step {step} needs a finite random pause range with min <= max"
389 ),
390 }
391 }
392}
393
394impl std::error::Error for SequenceError {}
395
396#[derive(Debug, Clone)]
399pub struct SequenceBuilder {
400 steps: Vec<SequenceStep>,
401 loop_mode: SequenceLoop,
402}
403
404impl SequenceBuilder {
405 fn new() -> Self {
406 Self {
407 steps: Vec::new(),
408 loop_mode: SequenceLoop::Once,
409 }
410 }
411
412 #[must_use]
414 pub fn loop_mode(mut self, loop_mode: SequenceLoop) -> Self {
415 self.loop_mode = loop_mode;
416 self
417 }
418
419 #[must_use]
422 pub fn play_frames(mut self, frames: impl Into<Vec<usize>>, frame_duration: f32) -> Self {
423 self.steps.push(SequenceStep::PlayFrames {
424 frames: frames.into(),
425 frame_duration,
426 });
427 self
428 }
429
430 #[must_use]
432 pub fn play_frame_range(self, first: usize, last: usize, frame_duration: f32) -> Self {
433 let frames: Vec<usize> = if first <= last {
434 (first..=last).collect()
435 } else {
436 Vec::new()
437 };
438 self.play_frames(frames, frame_duration)
439 }
440
441 #[must_use]
443 pub fn pause(mut self, duration: f32) -> Self {
444 self.steps.push(SequenceStep::Pause(duration));
445 self
446 }
447
448 #[must_use]
451 pub fn random_pause(mut self, min: f32, max: f32) -> Self {
452 self.steps.push(SequenceStep::RandomPause { min, max });
453 self
454 }
455
456 #[must_use]
458 pub fn action(mut self, id: u32) -> Self {
459 self.steps.push(SequenceStep::Action(id));
460 self
461 }
462
463 pub fn build(self) -> Result<AnimationSequence, SequenceError> {
466 if self.steps.is_empty() {
467 return Err(SequenceError::NoSteps);
468 }
469 for (step, item) in self.steps.iter().enumerate() {
470 match item {
471 SequenceStep::PlayFrames {
472 frames,
473 frame_duration,
474 } => {
475 if frames.is_empty() {
476 return Err(SequenceError::EmptyFrames { step });
477 }
478 if !frame_duration.is_finite() || *frame_duration < 0.0 {
479 return Err(SequenceError::BadDuration { step });
480 }
481 }
482 SequenceStep::Pause(duration) => {
483 if !duration.is_finite() || *duration < 0.0 {
484 return Err(SequenceError::BadDuration { step });
485 }
486 }
487 SequenceStep::RandomPause { min, max } => {
488 if !min.is_finite() || !max.is_finite() || *min < 0.0 || max < min {
489 return Err(SequenceError::BadRandomRange { step });
490 }
491 }
492 SequenceStep::Action(_) => {}
493 }
494 }
495 let positions: usize = self
496 .steps
497 .iter()
498 .map(|step| match step {
499 SequenceStep::PlayFrames { frames, .. } => frames.len(),
500 _ => 1,
501 })
502 .sum();
503 let directions = match self.loop_mode {
504 SequenceLoop::PingPongRepeat | SequenceLoop::PingPongOnce => 2,
505 SequenceLoop::Repeat | SequenceLoop::Once => 1,
506 };
507 Ok(AnimationSequence {
508 cycle_positions: positions * directions,
509 cursors: vec![0; self.steps.len()],
510 held_frame: None,
511 steps: self.steps,
512 loop_mode: self.loop_mode,
513 step: 0,
514 timer: 0.0,
515 step_duration: None,
516 forward: true,
517 actions: std::collections::VecDeque::new(),
518 finished: false,
519 just_completed: false,
520 })
521 }
522}
523
524impl Default for SequenceBuilder {
525 fn default() -> Self {
526 Self::new()
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use wasm_bindgen_test::wasm_bindgen_test;
534
535 #[wasm_bindgen_test(unsupported = test)]
539 fn a_tick_over_zero_duration_steps_makes_one_full_pass() {
540 let mut sequence = AnimationSequence::builder()
541 .loop_mode(SequenceLoop::Repeat)
542 .action(1)
543 .action(2)
544 .build()
545 .unwrap();
546 let mut rng = rng();
547 for _ in 0..2 {
548 sequence.tick(0.5, &mut rng);
549 let mut fired = Vec::new();
550 while let Some(action) = sequence.take_action() {
551 fired.push(action);
552 }
553 assert_eq!(fired.len(), 2, "one action per step per tick: {fired:?}");
554 }
555 }
556
557 #[wasm_bindgen_test(unsupported = test)]
561 fn two_step_pingpong_repeat_restarts_one_step_in() {
562 let mut sequence = AnimationSequence::builder()
563 .loop_mode(SequenceLoop::PingPongRepeat)
564 .play_frames(vec![0, 1], 0.1)
565 .play_frames(vec![2, 3], 0.1)
566 .build()
567 .unwrap();
568 let mut rng = rng();
569 let mut seen = vec![sequence.current_frame()];
570 for _ in 0..9 {
571 sequence.tick(0.1, &mut rng);
572 seen.push(sequence.current_frame());
573 }
574 assert_eq!(seen, [0, 1, 2, 3, 1, 0, 2, 3, 1, 0].map(Some).to_vec());
575 }
576
577 #[wasm_bindgen_test(unsupported = test)]
580 fn a_remainder_carries_into_the_next_tick() {
581 let mut sequence = AnimationSequence::builder()
582 .loop_mode(SequenceLoop::Repeat)
583 .play_frames(vec![0, 1, 2, 3, 4], 0.1)
584 .build()
585 .unwrap();
586 let mut rng = rng();
587 sequence.tick(0.25, &mut rng);
588 assert_eq!(sequence.current_frame(), Some(2));
589 sequence.tick(0.05, &mut rng);
590 assert_eq!(
591 sequence.current_frame(),
592 Some(3),
593 "0.05 s remainder carried"
594 );
595 }
596
597 fn play_with_actions(
600 seq: &mut AnimationSequence,
601 dt: f32,
602 ticks: usize,
603 ) -> (Vec<Option<usize>>, Vec<u32>) {
604 let mut rng = rng();
605 let mut seen = vec![seq.current_frame()];
606 let mut fired = Vec::new();
607 for _ in 0..ticks {
608 let completed = seq.tick(dt, &mut rng);
609 while let Some(action) = seq.take_action() {
610 fired.push(action);
611 }
612 if completed {
613 break;
614 }
615 seen.push(seq.current_frame());
616 }
617 (seen, fired)
618 }
619
620 #[wasm_bindgen_test(unsupported = test)]
625 fn a_trailing_action_does_not_repeat_the_end_frame() {
626 let build = |loop_mode| {
627 AnimationSequence::builder()
628 .loop_mode(loop_mode)
629 .play_frames(vec![0, 1, 2], 0.1)
630 .action(5)
631 .build()
632 .unwrap()
633 };
634 let (seen, fired) = play_with_actions(&mut build(SequenceLoop::PingPongOnce), 0.1, 10);
635 assert_eq!(seen, [0, 1, 2, 1, 0].map(Some).to_vec());
636 assert_eq!(fired, [5]);
637
638 let (seen, fired) = play_with_actions(&mut build(SequenceLoop::PingPongRepeat), 0.1, 8);
639 assert_eq!(seen, [0, 1, 2, 1, 0, 1, 2, 1, 0].map(Some).to_vec());
640 assert_eq!(fired, [5, 5]);
641 }
642
643 #[wasm_bindgen_test(unsupported = test)]
647 fn a_leading_action_does_not_move_the_start() {
648 let build = |loop_mode| {
649 AnimationSequence::builder()
650 .loop_mode(loop_mode)
651 .action(9)
652 .play_frames(vec![0, 1, 2], 0.1)
653 .build()
654 .unwrap()
655 };
656 let (seen, fired) = play_with_actions(&mut build(SequenceLoop::PingPongRepeat), 0.1, 8);
657 assert_eq!(
658 seen,
659 [
660 None,
661 Some(1),
662 Some(2),
663 Some(1),
664 Some(0),
665 Some(1),
666 Some(2),
667 Some(1),
668 Some(0)
669 ]
670 );
671 assert_eq!(fired, [9, 9]);
672
673 let mut once = build(SequenceLoop::PingPongOnce);
674 let (seen, fired) = play_with_actions(&mut once, 0.1, 10);
675 assert_eq!(
676 seen,
677 [None, Some(1), Some(2), Some(1), Some(0)],
678 "completes on tick 5"
679 );
680 assert_eq!(fired, [9, 9]);
681 assert!(once.is_finished());
682 }
683
684 #[wasm_bindgen_test(unsupported = test)]
687 fn a_trailing_action_keeps_multi_step_pingpong_endpoints() {
688 let mut seq = AnimationSequence::builder()
689 .loop_mode(SequenceLoop::PingPongOnce)
690 .play_frames(vec![0, 1], 0.1)
691 .play_frames(vec![2, 3], 0.1)
692 .action(5)
693 .build()
694 .unwrap();
695 let (seen, fired) = play_with_actions(&mut seq, 0.1, 10);
696 assert_eq!(seen, [0, 1, 2, 3, 1, 0].map(Some).to_vec());
697 assert_eq!(fired, [5]);
698 }
699
700 #[wasm_bindgen_test(unsupported = test)]
703 fn two_step_pingpong_enters_reversed_steps_at_their_last_frame() {
704 let mut sequence = AnimationSequence::builder()
705 .loop_mode(SequenceLoop::PingPongOnce)
706 .play_frames(vec![0, 1], 0.1)
707 .play_frames(vec![2, 3], 0.1)
708 .build()
709 .unwrap();
710 let mut rng = rng();
711 let mut seen = vec![sequence.current_frame()];
712 for _ in 0..6 {
713 sequence.tick(0.1, &mut rng);
714 seen.push(sequence.current_frame());
715 }
716 assert_eq!(
718 seen,
719 vec![
720 Some(0),
721 Some(1),
722 Some(2),
723 Some(3),
724 Some(1),
725 Some(0),
726 Some(0)
727 ]
728 );
729 assert!(sequence.is_finished());
730 }
731
732 #[wasm_bindgen_test(unsupported = test)]
735 fn a_single_one_frame_step_pingpongs_without_panicking() {
736 let mut sequence = AnimationSequence::builder()
737 .loop_mode(SequenceLoop::PingPongOnce)
738 .play_frames(vec![7], 0.1)
739 .build()
740 .unwrap();
741 let mut rng = rng();
742 for _ in 0..4 {
743 sequence.tick(0.1, &mut rng);
744 assert!(matches!(sequence.current_frame(), Some(7) | None));
745 }
746 assert!(sequence.is_finished());
747 }
748
749 #[wasm_bindgen_test(unsupported = test)]
752 fn a_one_frame_pingpong_once_completes_after_one_frame_duration() {
753 let mut sequence = AnimationSequence::builder()
754 .loop_mode(SequenceLoop::PingPongOnce)
755 .play_frames(vec![0], 0.25)
756 .build()
757 .unwrap();
758 let mut rng = rng();
759 assert!(sequence.tick(0.25, &mut rng), "completes after 0.25 s");
760 assert_eq!(sequence.current_frame(), Some(0));
761 assert!(!sequence.tick(0.25, &mut rng), "completion fires once");
762 }
763
764 #[wasm_bindgen_test(unsupported = test)]
766 fn a_lone_action_pingpong_once_fires_once() {
767 let mut sequence = AnimationSequence::builder()
768 .loop_mode(SequenceLoop::PingPongOnce)
769 .action(5)
770 .build()
771 .unwrap();
772 assert!(sequence.tick(0.1, &mut rng()));
773 assert_eq!(sequence.take_action(), Some(5));
774 assert_eq!(sequence.take_action(), None);
775 }
776
777 #[wasm_bindgen_test(unsupported = test)]
780 fn the_builder_accepts_zero_durations_and_equal_random_bounds() {
781 assert!(
782 AnimationSequence::builder()
783 .play_frames(vec![0], 0.0)
784 .random_pause(0.5, 0.5)
785 .build()
786 .is_ok()
787 );
788 assert_eq!(
789 AnimationSequence::builder()
790 .random_pause(0.6, 0.5)
791 .build()
792 .err(),
793 Some(SequenceError::BadRandomRange { step: 0 })
794 );
795 }
796
797 fn rng() -> Rng {
798 Rng::from_seed(1)
799 }
800
801 fn play(seq: &mut AnimationSequence, dt: f32, ticks: usize) -> Vec<Option<usize>> {
804 let mut rng = rng();
805 let mut seen = vec![seq.current_frame()];
806 for _ in 0..ticks {
807 if seq.tick(dt, &mut rng) {
808 break;
809 }
810 seen.push(seq.current_frame());
811 }
812 seen
813 }
814
815 fn single(frames: [usize; 3], loop_mode: SequenceLoop) -> AnimationSequence {
816 AnimationSequence::builder()
817 .loop_mode(loop_mode)
818 .play_frames(frames, 0.1)
819 .build()
820 .expect("valid sequence")
821 }
822
823 #[wasm_bindgen_test(unsupported = test)]
826 fn single_step_pingpong_once_plays_up_and_back() {
827 let mut seq = single([0, 1, 2], SequenceLoop::PingPongOnce);
828 assert_eq!(
829 play(&mut seq, 0.1, 10),
830 vec![Some(0), Some(1), Some(2), Some(1), Some(0)]
831 );
832 assert!(seq.is_finished() && seq.just_completed());
833 let mut rng = rng();
834 assert!(!seq.tick(0.1, &mut rng), "the completion edge fires once");
835 assert!(!seq.just_completed());
836 }
837
838 #[wasm_bindgen_test(unsupported = test)]
840 fn single_step_pingpong_repeat_bounces_forever() {
841 let mut seq = single([0, 1, 2], SequenceLoop::PingPongRepeat);
842 assert_eq!(
843 play(&mut seq, 0.1, 8),
844 vec![
845 Some(0),
846 Some(1),
847 Some(2),
848 Some(1),
849 Some(0),
850 Some(1),
851 Some(2),
852 Some(1),
853 Some(0),
854 ]
855 );
856 assert!(!seq.is_finished());
857 }
858
859 #[wasm_bindgen_test(unsupported = test)]
862 fn multi_step_pingpong_reverses_step_order() {
863 let mut seq = AnimationSequence::builder()
864 .loop_mode(SequenceLoop::PingPongOnce)
865 .play_frames([0, 1], 0.1)
866 .play_frames([2, 3], 0.1)
867 .play_frames([4, 5], 0.1)
868 .build()
869 .expect("valid sequence");
870 assert_eq!(
874 play(&mut seq, 0.1, 20),
875 [0, 1, 2, 3, 4, 5, 3, 2, 1, 0].map(Some).to_vec()
876 );
877 assert!(seq.just_completed());
878 }
879
880 #[wasm_bindgen_test(unsupported = test)]
883 fn repeat_rewinds_every_step_on_wrap() {
884 let mut seq = AnimationSequence::builder()
885 .loop_mode(SequenceLoop::Repeat)
886 .play_frames([0, 1], 0.1)
887 .play_frames([7, 8], 0.1)
888 .build()
889 .expect("valid sequence");
890 assert_eq!(
891 play(&mut seq, 0.1, 8),
892 [0, 1, 7, 8, 0, 1, 7, 8, 0].map(Some).to_vec()
893 );
894 assert!(!seq.is_finished());
895 }
896
897 #[wasm_bindgen_test(unsupported = test)]
900 fn large_dt_steps_several_frames_and_carries_the_remainder() {
901 let mut seq = AnimationSequence::builder()
902 .loop_mode(SequenceLoop::Repeat)
903 .play_frames([0, 1, 2, 3], 0.1)
904 .build()
905 .expect("valid sequence");
906 let mut rng = rng();
907 assert!(!seq.tick(0.35, &mut rng));
909 assert_eq!(seq.current_frame(), Some(3));
910 assert!(!seq.tick(0.05, &mut rng));
912 assert_eq!(seq.current_frame(), Some(0));
913 }
914
915 #[wasm_bindgen_test(unsupported = test)]
917 fn leftover_time_carries_across_steps() {
918 let mut seq = AnimationSequence::builder()
919 .play_frames([0], 0.1)
920 .pause(0.1)
921 .play_frames([5, 6], 0.1)
922 .build()
923 .expect("valid sequence");
924 let mut rng = rng();
925 assert!(!seq.tick(0.25, &mut rng));
927 assert_eq!(seq.current_frame(), Some(5));
928 assert!(!seq.tick(0.05, &mut rng));
929 assert_eq!(seq.current_frame(), Some(6));
930 }
931
932 #[wasm_bindgen_test(unsupported = test)]
935 fn large_ticks_hold_the_last_frame_through_pauses() {
936 let frames = AnimationSequence::builder().play_frames([0, 1], 0.125);
937 for builder in [frames.clone().pause(1.0), frames.random_pause(1.0, 2.0)] {
938 for schedule in [&[0.25][..], &[0.125, 0.125][..]] {
939 let mut seq = builder.clone().build().unwrap();
940 let mut rng = rng();
941 for dt in schedule {
942 assert!(!seq.tick(*dt, &mut rng));
943 }
944 assert_eq!(seq.current_frame(), Some(1), "entered the pause");
945 assert!(!seq.tick(0.5, &mut rng));
946 assert_eq!(seq.current_frame(), Some(1), "still in the pause");
947 }
948 }
949 }
950
951 #[wasm_bindgen_test(unsupported = test)]
954 fn completing_tick_holds_the_last_traversed_frame() {
955 for (loop_mode, endpoint) in [(SequenceLoop::Once, 5), (SequenceLoop::PingPongOnce, 3)] {
956 let mut seq = AnimationSequence::builder()
957 .loop_mode(loop_mode)
958 .play_frames([3, 4, 5], 0.125)
959 .build()
960 .unwrap();
961 let mut rng = rng();
962 assert!(seq.tick(1.0, &mut rng));
963 assert_eq!(seq.current_frame(), Some(endpoint));
964 assert!(!seq.tick(1.0, &mut rng));
965 assert_eq!(seq.current_frame(), Some(endpoint));
966 }
967 }
968
969 #[wasm_bindgen_test(unsupported = test)]
972 fn reversed_pause_holds_the_frame_at_the_turnaround() {
973 let mut seq = AnimationSequence::builder()
974 .loop_mode(SequenceLoop::PingPongOnce)
975 .play_frames([0, 1], 0.125)
976 .pause(0.5)
977 .play_frames([2, 3], 0.125)
978 .build()
979 .unwrap();
980 let mut rng = rng();
981 assert!(!seq.tick(1.0, &mut rng));
982 assert_eq!(seq.current_frame(), Some(3), "reverse pause at 1.0 s");
983 assert!(seq.tick(0.75, &mut rng));
984 assert_eq!(seq.current_frame(), Some(0), "reverse run ends at 1.75 s");
985 }
986
987 #[wasm_bindgen_test(unsupported = test)]
990 fn reset_clears_the_held_frame_before_a_leading_pause() {
991 let mut seq = AnimationSequence::builder()
992 .pause(0.25)
993 .play_frames([4, 7], 0.125)
994 .build()
995 .unwrap();
996 let mut rng = rng();
997 assert!(seq.tick(0.5, &mut rng));
998 assert_eq!(seq.current_frame(), Some(7));
999 seq.reset();
1000 assert!(!seq.tick(0.125, &mut rng));
1001 assert_eq!(seq.current_frame(), None, "no frame reached after reset");
1002 assert!(!seq.tick(0.125, &mut rng));
1003 assert_eq!(seq.current_frame(), Some(4), "first frame reached again");
1004 }
1005
1006 #[wasm_bindgen_test(unsupported = test)]
1009 fn zero_duration_steps_cannot_spin_within_a_tick() {
1010 let mut seq = AnimationSequence::builder()
1011 .loop_mode(SequenceLoop::Repeat)
1012 .action(3)
1013 .pause(0.0)
1014 .random_pause(0.0, 0.0)
1015 .build()
1016 .expect("valid sequence");
1017 let mut rng = rng();
1018 assert!(!seq.tick(60.0, &mut rng), "Repeat never completes");
1019 assert_eq!(seq.take_action(), Some(3));
1020 assert_eq!(seq.take_action(), None, "one pass fires one action");
1021 assert!(!seq.tick(60.0, &mut rng));
1022 }
1023
1024 #[wasm_bindgen_test(unsupported = test)]
1027 fn actions_queue_in_order_and_drain_once() {
1028 let mut seq = AnimationSequence::builder()
1029 .play_frames([0], 0.1)
1030 .action(7)
1031 .action(9)
1032 .play_frames([1], 0.1)
1033 .build()
1034 .expect("valid sequence");
1035 let mut rng = rng();
1036 assert_eq!(
1037 seq.take_action(),
1038 None,
1039 "nothing fires before the first tick"
1040 );
1041 assert!(!seq.tick(0.1, &mut rng));
1042 assert_eq!(seq.current_frame(), Some(1), "both actions were crossed");
1043 assert_eq!(seq.take_action(), Some(7));
1044 assert_eq!(seq.take_action(), Some(9));
1045 assert_eq!(seq.take_action(), None);
1046 assert!(seq.tick(0.1, &mut rng), "the last frame completes the run");
1047 assert_eq!(seq.take_action(), None, "actions do not re-fire");
1048 }
1049
1050 #[wasm_bindgen_test(unsupported = test)]
1053 fn an_exact_random_pause_lasts_exactly_min() {
1054 let mut seq = AnimationSequence::builder()
1055 .play_frames([4], 0.125)
1056 .random_pause(0.5, 0.5)
1057 .build()
1058 .expect("valid sequence");
1059 let mut rng = rng();
1060 assert!(!seq.tick(0.125, &mut rng), "off the frame, onto the pause");
1061 assert_eq!(
1062 seq.current_frame(),
1063 Some(4),
1064 "a pause holds its prior frame"
1065 );
1066 for tick in 1..4 {
1067 assert!(
1068 !seq.tick(0.125, &mut rng),
1069 "still pausing after {tick} ticks"
1070 );
1071 }
1072 assert!(seq.tick(0.125, &mut rng), "0.5 s of pause has elapsed");
1073 }
1074
1075 #[wasm_bindgen_test(unsupported = test)]
1079 fn random_pauses_are_bounded_and_seed_reproducible() {
1080 let build = || {
1081 AnimationSequence::builder()
1082 .random_pause(0.4, 0.8)
1083 .build()
1084 .expect("valid sequence")
1085 };
1086 let ticks_to_finish = |seed: u64| {
1088 let mut seq = build();
1089 let mut rng = Rng::from_seed(seed);
1090 (1..=100)
1091 .find(|_| seq.tick(0.05, &mut rng))
1092 .expect("the pause ends")
1093 };
1094 for seed in 0..32 {
1095 let ticks = ticks_to_finish(seed);
1096 assert!(
1097 (8..=16).contains(&ticks),
1098 "seed {seed} paused {ticks} ticks"
1099 );
1100 assert_eq!(ticks, ticks_to_finish(seed), "seed {seed} is reproducible");
1101 }
1102 }
1103
1104 #[wasm_bindgen_test(unsupported = test)]
1107 fn steps_without_randomness_do_not_draw_from_the_rng() {
1108 let mut seq = AnimationSequence::builder()
1109 .loop_mode(SequenceLoop::Repeat)
1110 .play_frames([0, 1], 0.1)
1111 .pause(0.2)
1112 .action(1)
1113 .build()
1114 .expect("valid sequence");
1115 let mut used = Rng::from_seed(5);
1116 for _ in 0..50 {
1117 seq.tick(0.1, &mut used);
1118 }
1119 let mut untouched = Rng::from_seed(5);
1120 assert_eq!(used.u32(0..u32::MAX), untouched.u32(0..u32::MAX));
1121 }
1122
1123 #[wasm_bindgen_test(unsupported = test)]
1125 fn reset_restarts_a_finished_sequence() {
1126 let mut seq = single([3, 4, 5], SequenceLoop::Once);
1127 let mut rng = rng();
1128 while !seq.is_finished() {
1129 seq.tick(0.1, &mut rng);
1130 }
1131 assert_eq!(seq.current_frame(), Some(5), "Once holds its last frame");
1132 seq.reset();
1133 assert!(!seq.is_finished() && !seq.just_completed());
1134 assert_eq!(seq.current_frame(), Some(3));
1135 assert!(!seq.tick(0.1, &mut rng));
1136 assert_eq!(seq.current_frame(), Some(4));
1137 }
1138
1139 #[wasm_bindgen_test(unsupported = test)]
1142 fn the_builder_rejects_authoring_errors() {
1143 let err = |builder: SequenceBuilder| builder.build().expect_err("authoring error");
1144 assert_eq!(err(AnimationSequence::builder()), SequenceError::NoSteps);
1145 assert_eq!(
1146 err(AnimationSequence::builder().play_frames(Vec::new(), 0.1)),
1147 SequenceError::EmptyFrames { step: 0 }
1148 );
1149 assert_eq!(
1150 err(AnimationSequence::builder().play_frame_range(4, 2, 0.1)),
1151 SequenceError::EmptyFrames { step: 0 },
1152 "a backwards range plays nothing"
1153 );
1154 assert_eq!(
1155 err(AnimationSequence::builder().play_frames([0], -0.1)),
1156 SequenceError::BadDuration { step: 0 }
1157 );
1158 assert_eq!(
1159 err(AnimationSequence::builder()
1160 .play_frames([0], 0.1)
1161 .pause(f32::NAN)),
1162 SequenceError::BadDuration { step: 1 }
1163 );
1164 assert_eq!(
1165 err(AnimationSequence::builder().pause(f32::INFINITY)),
1166 SequenceError::BadDuration { step: 0 }
1167 );
1168 assert_eq!(
1169 err(AnimationSequence::builder().random_pause(2.0, 1.0)),
1170 SequenceError::BadRandomRange { step: 0 }
1171 );
1172 assert_eq!(
1173 err(AnimationSequence::builder().random_pause(f32::NAN, 1.0)),
1174 SequenceError::BadRandomRange { step: 0 }
1175 );
1176 assert!(
1177 AnimationSequence::builder()
1178 .random_pause(0.0, 0.0)
1179 .build()
1180 .is_ok(),
1181 "a zero-length random pause is legitimate"
1182 );
1183 }
1184
1185 #[wasm_bindgen_test(unsupported = test)]
1188 fn a_non_finite_dt_is_ignored() {
1189 let mut seq = AnimationSequence::builder()
1190 .loop_mode(SequenceLoop::PingPongRepeat)
1191 .play_frames(vec![0, 1, 2], 0.1)
1192 .build()
1193 .expect("valid sequence");
1194 let mut rng = rng();
1195 for dt in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1196 assert!(!seq.tick(dt, &mut rng));
1197 assert_eq!(seq.current_frame(), Some(0));
1198 }
1199 seq.tick(0.1, &mut rng);
1200 assert_eq!(seq.current_frame(), Some(1));
1201 }
1202
1203 #[wasm_bindgen_test(unsupported = test)]
1206 fn nonpositive_dt_does_not_build_time_debt() {
1207 for loop_mode in [SequenceLoop::Once, SequenceLoop::Repeat] {
1208 let mut seq = single([3, 4, 5], loop_mode);
1209 let mut rng = rng();
1210 assert!(!seq.tick(-1.0, &mut rng));
1211 assert!(!seq.tick(0.1, &mut rng));
1212 assert_eq!(seq.current_frame(), Some(4));
1213 assert!(!seq.tick(0.05, &mut rng));
1214 for dt in [-1.0, 0.0, -0.0] {
1215 assert!(!seq.tick(dt, &mut rng));
1216 }
1217 assert!(!seq.tick(0.05, &mut rng));
1218 assert_eq!(seq.current_frame(), Some(5));
1219 assert_eq!(seq.tick(0.1, &mut rng), loop_mode == SequenceLoop::Once);
1220 assert_eq!(seq.is_finished(), loop_mode == SequenceLoop::Once);
1221 assert_eq!(seq.just_completed(), loop_mode == SequenceLoop::Once);
1222 assert!(!seq.tick(-1.0, &mut rng));
1223 assert!(!seq.just_completed(), "the previous edge is cleared");
1224 }
1225 }
1226
1227 #[wasm_bindgen_test(unsupported = test)]
1230 fn ignored_dt_does_not_fire_actions_or_draw_randomness() {
1231 let mut seq = AnimationSequence::builder()
1232 .action(7)
1233 .random_pause(0.0, 0.0)
1234 .play_frames([4, 5], 0.1)
1235 .build()
1236 .expect("valid sequence");
1237 let mut used = Rng::from_seed(5);
1238 for dt in [-1.0, 0.0, -0.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
1239 assert!(!seq.tick(dt, &mut used));
1240 assert_eq!(seq.take_action(), None);
1241 assert_eq!(seq.current_frame(), None);
1242 }
1243 let mut untouched = Rng::from_seed(5);
1244 assert_eq!(used.u32(0..u32::MAX), untouched.u32(0..u32::MAX));
1245 assert!(!seq.tick(0.1, &mut used));
1246 assert_eq!(seq.current_frame(), Some(5));
1247 assert_eq!(
1248 seq.take_action(),
1249 Some(7),
1250 "the action waits for elapsed time"
1251 );
1252 }
1253
1254 #[wasm_bindgen_test(unsupported = test)]
1258 fn a_once_sequence_with_vanishing_durations_completes_in_one_tick() {
1259 let mut seq = AnimationSequence::builder()
1260 .play_frames(vec![0, 1, 2, 3], 1e-30)
1261 .build()
1262 .expect("valid sequence");
1263 assert!(seq.tick(0.016, &mut rng()), "completion edge");
1264 assert_eq!(seq.current_frame(), Some(3));
1265 }
1266
1267 #[wasm_bindgen_test(unsupported = test)]
1273 fn zero_duration_frames_do_not_delay_a_following_action() {
1274 for loop_mode in [SequenceLoop::Once, SequenceLoop::Repeat] {
1275 let mut seq = AnimationSequence::builder()
1276 .loop_mode(loop_mode)
1277 .play_frames([0, 1, 2, 3], 0.0)
1278 .action(7)
1279 .pause(1.0)
1280 .build()
1281 .expect("valid sequence");
1282 let mut rng = rng();
1283 assert!(!seq.tick(0.5, &mut rng));
1284 assert_eq!(seq.take_action(), Some(7), "{loop_mode:?}");
1285 assert_eq!(seq.current_frame(), Some(3));
1286 let completed = seq.tick(0.5, &mut rng);
1287 assert_eq!(completed, loop_mode == SequenceLoop::Once, "{loop_mode:?}");
1288 }
1289 }
1290
1291 #[wasm_bindgen_test(unsupported = test)]
1294 fn a_zero_time_pingpong_cycle_reaches_its_action() {
1295 let mut seq = AnimationSequence::builder()
1296 .loop_mode(SequenceLoop::PingPongRepeat)
1297 .play_frames([0, 1, 2], 0.0)
1298 .action(4)
1299 .build()
1300 .expect("valid sequence");
1301 assert!(!seq.tick(1.0, &mut rng()));
1302 assert_eq!(seq.take_action(), Some(4));
1303 }
1304
1305 #[wasm_bindgen_test(unsupported = test)]
1308 fn a_vanishing_frame_duration_does_not_hang_a_repeating_sequence() {
1309 for loop_mode in [SequenceLoop::Repeat, SequenceLoop::PingPongRepeat] {
1310 let mut seq = AnimationSequence::builder()
1311 .loop_mode(loop_mode)
1312 .play_frames(vec![0], 1e-30)
1313 .build()
1314 .expect("valid sequence");
1315 assert!(!seq.tick(0.016, &mut rng()));
1316 assert_eq!(seq.current_frame(), Some(0));
1317 }
1318 }
1319
1320 #[wasm_bindgen_test(unsupported = test)]
1322 fn play_frame_range_is_inclusive() {
1323 let mut seq = AnimationSequence::builder()
1324 .loop_mode(SequenceLoop::Once)
1325 .play_frame_range(8, 11, 0.1)
1326 .build()
1327 .expect("valid sequence");
1328 assert_eq!(play(&mut seq, 0.1, 10), [8, 9, 10, 11].map(Some).to_vec());
1329 }
1330}