1mod sequence;
16
17pub use sequence::{AnimationSequence, SequenceBuilder, SequenceError, SequenceLoop, SequenceStep};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum LoopMode {
22 Once,
24 Repeat,
26}
27
28#[derive(Debug, Clone)]
34pub struct FrameAnimation {
35 order: Vec<usize>,
38 frame_duration: f32,
40 loop_mode: LoopMode,
41 position: usize,
43 timer: f32,
45 finished: bool,
46}
47
48impl FrameAnimation {
49 pub fn new(order: impl Into<Vec<usize>>, fps: f32, loop_mode: LoopMode) -> Self {
55 let order = order.into();
56 assert!(!order.is_empty(), "FrameAnimation: empty frame order");
57 assert!(fps > 0.0, "FrameAnimation: fps must be positive, got {fps}");
58 Self {
59 order,
60 frame_duration: 1.0 / fps,
61 loop_mode,
62 position: 0,
63 timer: 0.0,
64 finished: false,
65 }
66 }
67
68 #[must_use]
71 pub fn start_at(mut self, position: usize) -> Self {
72 self.position = position.min(self.order.len() - 1);
73 self
74 }
75
76 pub fn tick(&mut self, dt: f32) -> bool {
82 if self.finished {
83 return false;
84 }
85 self.timer += dt;
86 while self.timer >= self.frame_duration {
87 self.timer -= self.frame_duration;
88 if self.position + 1 < self.order.len() {
89 self.position += 1;
90 } else {
91 match self.loop_mode {
92 LoopMode::Repeat => self.position = 0,
93 LoopMode::Once => {
94 self.finished = true;
96 self.timer = 0.0;
97 return true;
98 }
99 }
100 }
101 }
102 false
103 }
104
105 #[must_use]
107 pub fn current_frame(&self) -> usize {
108 self.order[self.position]
109 }
110
111 #[must_use]
113 pub fn position(&self) -> usize {
114 self.position
115 }
116
117 #[must_use]
119 pub fn is_finished(&self) -> bool {
120 self.finished
121 }
122
123 pub fn reset(&mut self) {
125 self.position = 0;
126 self.timer = 0.0;
127 self.finished = false;
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use wasm_bindgen_test::wasm_bindgen_test;
135
136 #[wasm_bindgen_test(unsupported = test)]
139 fn start_at_positions_the_cursor() {
140 let anim = FrameAnimation::new(vec![5, 6, 7, 8], 10.0, LoopMode::Repeat).start_at(2);
141 assert_eq!(anim.position(), 2);
142 assert_eq!(anim.current_frame(), 7);
143 let mut anim = anim;
144 anim.tick(0.1);
145 assert_eq!(anim.position(), 3);
146 let clamped = FrameAnimation::new(vec![5, 6], 10.0, LoopMode::Repeat).start_at(9);
147 assert_eq!(clamped.position(), 1);
148 }
149
150 #[wasm_bindgen_test(unsupported = test)]
153 fn repeat_order_cycles_at_exact_fps_boundaries() {
154 let mut a = FrameAnimation::new([0, 1, 2, 1], 10.0, LoopMode::Repeat);
155 let mut seen = vec![a.current_frame()];
156 for _ in 0..8 {
157 assert!(!a.tick(0.1), "Repeat never reports finished");
158 seen.push(a.current_frame());
159 }
160 assert_eq!(seen, vec![0, 1, 2, 1, 0, 1, 2, 1, 0]);
161 }
162
163 #[wasm_bindgen_test(unsupported = test)]
166 fn sub_boundary_tick_does_not_advance() {
167 let mut a = FrameAnimation::new([0, 1], 10.0, LoopMode::Repeat);
168 a.tick(0.099);
169 assert_eq!(a.current_frame(), 0, "0.099 s < 0.1 s: still frame 0");
170 a.tick(0.001);
171 assert_eq!(a.current_frame(), 1, "accumulated exactly 0.1 s: frame 1");
172 }
173
174 #[wasm_bindgen_test(unsupported = test)]
176 fn large_dt_steps_multiple_frames() {
177 let mut a = FrameAnimation::new([0, 1, 2, 1], 10.0, LoopMode::Repeat);
178 a.tick(0.25); assert_eq!(a.current_frame(), 2);
180 a.tick(0.05); assert_eq!(a.current_frame(), 1);
182 }
183
184 #[wasm_bindgen_test(unsupported = test)]
187 fn once_finishes_on_the_edge_and_holds() {
188 let mut a = FrameAnimation::new([0, 1, 2], 5.0, LoopMode::Once);
189 assert!(!a.tick(0.2));
190 assert_eq!(a.current_frame(), 1);
191 assert!(!a.tick(0.2));
192 assert_eq!(a.current_frame(), 2);
193 assert!(a.tick(0.2), "completion edge");
195 assert!(a.is_finished());
196 assert_eq!(a.current_frame(), 2, "holds the last frame");
197 assert!(!a.tick(1.0));
199 assert_eq!(a.current_frame(), 2);
200 }
201
202 #[wasm_bindgen_test(unsupported = test)]
204 fn chicken_starts_at_frame_two_and_follows_its_order() {
205 let mut a = FrameAnimation::new([0, 2, 1, 2], 20.0, LoopMode::Repeat).start_at(1);
206 let mut seen = vec![a.current_frame()];
207 for _ in 0..5 {
208 a.tick(0.05);
209 seen.push(a.current_frame());
210 }
211 assert_eq!(seen, vec![2, 1, 2, 0, 2, 1]);
212 }
213
214 #[wasm_bindgen_test(unsupported = test)]
216 fn reset_restarts_a_finished_animation() {
217 let mut a = FrameAnimation::new([3, 4], 10.0, LoopMode::Once);
218 a.tick(1.0);
219 assert!(a.is_finished());
220 a.reset();
221 assert!(!a.is_finished());
222 assert_eq!(a.current_frame(), 3);
223 assert!(!a.tick(0.1));
224 assert_eq!(a.current_frame(), 4);
225 }
226}