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!(
58 fps.is_finite() && fps > 0.0,
59 "FrameAnimation: fps must be finite and positive, got {fps}"
60 );
61 Self {
62 order,
63 frame_duration: 1.0 / fps,
64 loop_mode,
65 position: 0,
66 timer: 0.0,
67 finished: false,
68 }
69 }
70
71 #[must_use]
74 pub fn start_at(mut self, position: usize) -> Self {
75 self.position = position.min(self.order.len() - 1);
76 self
77 }
78
79 pub fn tick(&mut self, dt: f32) -> bool {
87 if self.finished || !dt.is_finite() || dt <= 0.0 {
88 return false;
89 }
90 self.timer += dt;
91 while self.timer >= self.frame_duration {
92 let rest = self.timer - self.frame_duration;
93 if rest >= self.timer && self.loop_mode == LoopMode::Repeat {
94 self.timer = 0.0;
99 return false;
100 }
101 self.timer = rest;
102 if self.position + 1 < self.order.len() {
103 self.position += 1;
104 } else {
105 match self.loop_mode {
106 LoopMode::Repeat => self.position = 0,
107 LoopMode::Once => {
108 self.finished = true;
110 self.timer = 0.0;
111 return true;
112 }
113 }
114 }
115 }
116 false
117 }
118
119 #[must_use]
121 pub fn current_frame(&self) -> usize {
122 self.order[self.position]
123 }
124
125 #[must_use]
127 pub fn position(&self) -> usize {
128 self.position
129 }
130
131 #[must_use]
133 pub fn is_finished(&self) -> bool {
134 self.finished
135 }
136
137 pub fn reset(&mut self) {
139 self.position = 0;
140 self.timer = 0.0;
141 self.finished = false;
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use wasm_bindgen_test::wasm_bindgen_test;
149
150 #[wasm_bindgen_test(unsupported = test)]
153 fn start_at_positions_the_cursor() {
154 let anim = FrameAnimation::new(vec![5, 6, 7, 8], 10.0, LoopMode::Repeat).start_at(2);
155 assert_eq!(anim.position(), 2);
156 assert_eq!(anim.current_frame(), 7);
157 let mut anim = anim;
158 anim.tick(0.1);
159 assert_eq!(anim.position(), 3);
160 let clamped = FrameAnimation::new(vec![5, 6], 10.0, LoopMode::Repeat).start_at(9);
161 assert_eq!(clamped.position(), 1);
162 }
163
164 #[wasm_bindgen_test(unsupported = test)]
167 fn repeat_order_cycles_at_exact_fps_boundaries() {
168 let mut a = FrameAnimation::new([0, 1, 2, 1], 10.0, LoopMode::Repeat);
169 let mut seen = vec![a.current_frame()];
170 for _ in 0..8 {
171 assert!(!a.tick(0.1), "Repeat never reports finished");
172 seen.push(a.current_frame());
173 }
174 assert_eq!(seen, vec![0, 1, 2, 1, 0, 1, 2, 1, 0]);
175 }
176
177 #[wasm_bindgen_test(unsupported = test)]
180 fn sub_boundary_tick_does_not_advance() {
181 let mut a = FrameAnimation::new([0, 1], 10.0, LoopMode::Repeat);
182 a.tick(0.099);
183 assert_eq!(a.current_frame(), 0, "0.099 s < 0.1 s: still frame 0");
184 a.tick(0.001);
185 assert_eq!(a.current_frame(), 1, "accumulated exactly 0.1 s: frame 1");
186 }
187
188 #[wasm_bindgen_test(unsupported = test)]
190 fn large_dt_steps_multiple_frames() {
191 let mut a = FrameAnimation::new([0, 1, 2, 1], 10.0, LoopMode::Repeat);
192 a.tick(0.25); assert_eq!(a.current_frame(), 2);
194 a.tick(0.05); assert_eq!(a.current_frame(), 1);
196 }
197
198 #[wasm_bindgen_test(unsupported = test)]
201 fn once_finishes_on_the_edge_and_holds() {
202 let mut a = FrameAnimation::new([0, 1, 2], 5.0, LoopMode::Once);
203 assert!(!a.tick(0.2));
204 assert_eq!(a.current_frame(), 1);
205 assert!(!a.tick(0.2));
206 assert_eq!(a.current_frame(), 2);
207 assert!(a.tick(0.2), "completion edge");
209 assert!(a.is_finished());
210 assert_eq!(a.current_frame(), 2, "holds the last frame");
211 assert!(!a.tick(1.0));
213 assert_eq!(a.current_frame(), 2);
214 }
215
216 #[wasm_bindgen_test(unsupported = test)]
218 fn chicken_starts_at_frame_two_and_follows_its_order() {
219 let mut a = FrameAnimation::new([0, 2, 1, 2], 20.0, LoopMode::Repeat).start_at(1);
220 let mut seen = vec![a.current_frame()];
221 for _ in 0..5 {
222 a.tick(0.05);
223 seen.push(a.current_frame());
224 }
225 assert_eq!(seen, vec![2, 1, 2, 0, 2, 1]);
226 }
227
228 #[wasm_bindgen_test(unsupported = test)]
231 fn a_non_finite_dt_is_ignored() {
232 let mut a = FrameAnimation::new([0, 1], 10.0, LoopMode::Repeat);
233 for dt in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
234 assert!(!a.tick(dt));
235 assert_eq!(a.current_frame(), 0);
236 }
237 a.tick(0.1);
238 assert_eq!(a.current_frame(), 1);
239 }
240
241 #[wasm_bindgen_test(unsupported = test)]
244 fn nonpositive_dt_does_not_build_time_debt() {
245 for loop_mode in [LoopMode::Once, LoopMode::Repeat] {
246 let mut anim = FrameAnimation::new([3, 4, 5], 10.0, loop_mode);
247 assert!(!anim.tick(-1.0));
248 assert!(!anim.tick(0.1));
249 assert_eq!(anim.current_frame(), 4);
250 assert!(!anim.tick(0.05));
251 for dt in [-1.0, 0.0, -0.0] {
252 assert!(!anim.tick(dt));
253 }
254 assert!(!anim.tick(0.05));
255 assert_eq!(anim.current_frame(), 5);
256 assert_eq!(anim.tick(0.1), loop_mode == LoopMode::Once);
257 assert_eq!(anim.is_finished(), loop_mode == LoopMode::Once);
258 }
259 }
260
261 #[wasm_bindgen_test(unsupported = test)]
265 fn a_frame_duration_below_the_timer_precision_drops_the_remainder() {
266 let fps = 2f32.powi(30);
267 let mut a = FrameAnimation::new([0, 1], fps, LoopMode::Repeat);
268 a.tick(1.0);
269 assert_eq!(a.current_frame(), 0);
270 a.tick(1.0 / fps);
271 assert_eq!(a.current_frame(), 1);
272 }
273
274 #[wasm_bindgen_test(unsupported = test)]
277 fn a_tick_past_a_whole_pass_lands_on_the_spec_frame() {
278 let mut a = FrameAnimation::new([0, 1, 2], 10.0, LoopMode::Repeat);
279 a.tick(0.4);
280 assert_eq!(a.current_frame(), 1);
281 }
282
283 #[cfg(not(target_arch = "wasm32"))]
285 #[test]
286 #[should_panic(expected = "fps must be finite and positive")]
287 fn infinite_fps_is_rejected() {
288 let _ = FrameAnimation::new([0], f32::INFINITY, LoopMode::Repeat);
289 }
290
291 #[wasm_bindgen_test(unsupported = test)]
293 fn reset_restarts_a_finished_animation() {
294 let mut a = FrameAnimation::new([3, 4], 10.0, LoopMode::Once);
295 a.tick(1.0);
296 assert!(a.is_finished());
297 a.reset();
298 assert!(!a.is_finished());
299 assert_eq!(a.current_frame(), 3);
300 assert!(!a.tick(0.1));
301 assert_eq!(a.current_frame(), 4);
302 }
303}