1use image::RgbaImage;
7use std::time::Duration;
8
9const MIN_FRAME_DELAY: Duration = Duration::from_millis(100);
14const MIN_HONORED_DELAY: Duration = Duration::from_millis(10);
15
16pub struct AnimationState {
17 frames: Vec<(RgbaImage, Duration)>,
18 loop_count: Option<u32>, current: usize,
20 elapsed_in_frame: Duration,
21 playing: bool,
22 loops_done: u32,
23 finished: bool,
24}
25
26impl AnimationState {
27 pub fn new(frames: Vec<(RgbaImage, Duration)>, loop_count: Option<u32>) -> Self {
29 Self {
30 frames, loop_count, current: 0, elapsed_in_frame: Duration::ZERO,
31 playing: true, loops_done: 0, finished: false,
32 }
33 }
34
35 pub fn current_frame(&self) -> &RgbaImage { &self.frames[self.current].0 }
36 pub fn frame_index(&self) -> usize { self.current }
37 pub fn frame_count(&self) -> usize { self.frames.len() }
38 pub fn is_playing(&self) -> bool { self.playing }
39 pub fn is_finished(&self) -> bool { self.finished }
40
41 fn frame_delay(&self, i: usize) -> Duration {
42 let d = self.frames[i].1;
43 if d < MIN_HONORED_DELAY { MIN_FRAME_DELAY } else { d }
44 }
45
46 pub fn advance(&mut self, dt: Duration) -> bool {
52 if !self.playing || self.finished || self.frames.len() <= 1 { return false; }
53 let start = self.current;
54 self.elapsed_in_frame += dt;
55 loop {
56 let delay = self.frame_delay(self.current);
57 if self.elapsed_in_frame < delay { break; }
58 self.elapsed_in_frame -= delay;
59 if self.current + 1 < self.frames.len() {
60 self.current += 1;
61 } else {
62 self.loops_done += 1;
63 if let Some(n) = self.loop_count {
64 if self.loops_done >= n {
65 self.finished = true;
66 self.playing = false;
67 self.current = self.frames.len() - 1;
68 self.elapsed_in_frame = Duration::ZERO;
69 break;
70 }
71 }
72 self.current = 0;
73 }
74 }
75 self.current != start
76 }
77
78 pub fn next_deadline(&self) -> Option<Duration> {
80 if !self.playing || self.finished || self.frames.len() <= 1 { return None; }
81 Some(self.frame_delay(self.current).saturating_sub(self.elapsed_in_frame))
82 }
83
84 pub fn toggle_pause(&mut self) {
87 if self.finished { self.restart(); return; }
88 self.playing = !self.playing;
89 }
90
91 pub fn step(&mut self, delta: i32) {
93 self.playing = false;
94 self.finished = false;
95 self.elapsed_in_frame = Duration::ZERO;
96 let n = self.frames.len() as i64;
97 if n == 0 { return; }
98 let next = ((self.current as i64 + delta as i64) % n + n) % n;
99 self.current = next as usize;
100 }
101
102 pub fn restart(&mut self) {
104 self.current = 0;
105 self.elapsed_in_frame = Duration::ZERO;
106 self.loops_done = 0;
107 self.finished = false;
108 self.playing = true;
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use image::{Rgba, RgbaImage};
116
117 fn frames(n: usize, delay_ms: u64) -> Vec<(RgbaImage, Duration)> {
118 (0..n).map(|i| (RgbaImage::from_pixel(1, 1, Rgba([i as u8, 0, 0, 255])),
119 Duration::from_millis(delay_ms))).collect()
120 }
121
122 #[test]
123 fn advances_at_delay_boundary() {
124 let mut a = AnimationState::new(frames(3, 100), None);
125 assert_eq!(a.frame_index(), 0);
126 assert!(!a.advance(Duration::from_millis(50)));
127 assert_eq!(a.frame_index(), 0);
128 assert!(a.advance(Duration::from_millis(60)));
129 assert_eq!(a.frame_index(), 1);
130 }
131
132 #[test]
133 fn large_dt_crosses_multiple_frames() {
134 let mut a = AnimationState::new(frames(4, 100), None);
135 assert!(a.advance(Duration::from_millis(250)));
136 assert_eq!(a.frame_index(), 2);
137 }
138
139 #[test]
140 fn infinite_loop_never_finishes() {
141 let mut a = AnimationState::new(frames(2, 100), None);
142 a.advance(Duration::from_millis(1000));
143 assert!(!a.is_finished());
144 assert!(a.is_playing());
145 }
146
147 #[test]
148 fn finite_loop_finishes_on_last_frame() {
149 let mut a = AnimationState::new(frames(3, 100), Some(1));
150 a.advance(Duration::from_millis(300));
151 assert!(a.is_finished());
152 assert!(!a.is_playing());
153 assert_eq!(a.frame_index(), 2);
154 assert_eq!(a.next_deadline(), None);
155 assert!(!a.advance(Duration::from_millis(500)));
156 }
157
158 #[test]
159 fn pause_blocks_advance_and_resume_restores() {
160 let mut a = AnimationState::new(frames(3, 100), None);
161 a.toggle_pause();
162 assert!(!a.is_playing());
163 assert!(!a.advance(Duration::from_millis(500)));
164 assert_eq!(a.frame_index(), 0);
165 a.toggle_pause();
166 assert!(a.is_playing());
167 }
168
169 #[test]
170 fn step_wraps_and_pauses() {
171 let mut a = AnimationState::new(frames(3, 100), None);
172 a.step(-1);
173 assert_eq!(a.frame_index(), 2);
174 assert!(!a.is_playing());
175 a.step(1);
176 assert_eq!(a.frame_index(), 0);
177 }
178
179 #[test]
180 fn restart_resets() {
181 let mut a = AnimationState::new(frames(3, 100), Some(1));
182 a.advance(Duration::from_millis(300));
183 a.restart();
184 assert_eq!(a.frame_index(), 0);
185 assert!(a.is_playing());
186 assert!(!a.is_finished());
187 }
188
189 #[test]
190 fn toggle_pause_revives_finished() {
191 let mut a = AnimationState::new(frames(2, 100), Some(1));
192 a.advance(Duration::from_millis(200));
193 assert!(a.is_finished());
194 a.toggle_pause();
195 assert!(a.is_playing() && !a.is_finished() && a.frame_index() == 0);
196 }
197
198 #[test]
199 fn zero_delay_floored_not_busy_loop() {
200 let mut a = AnimationState::new(frames(2, 0), None);
201 assert!(!a.advance(Duration::from_millis(50)));
202 assert!(a.advance(Duration::from_millis(60)));
203 }
204
205 #[test]
206 fn zero_delay_floor_applies_to_next_deadline() {
207 let a = AnimationState::new(frames(2, 0), None);
208 assert_eq!(a.next_deadline(), Some(Duration::from_millis(100)));
210 }
211
212 #[test]
213 fn per_frame_delays_are_respected() {
214 use image::{Rgba, RgbaImage};
215 let mk = |c: u8, ms: u64| (RgbaImage::from_pixel(1, 1, Rgba([c, 0, 0, 255])), Duration::from_millis(ms));
216 let mut a = AnimationState::new(vec![mk(0, 50), mk(1, 200), mk(2, 50)], None);
218 assert_eq!(a.next_deadline(), Some(Duration::from_millis(50)), "frame 0 delay is 50ms");
219 assert!(a.advance(Duration::from_millis(50))); assert_eq!(a.frame_index(), 1);
221 assert_eq!(a.next_deadline(), Some(Duration::from_millis(200)), "frame 1 delay is 200ms");
222 assert!(!a.advance(Duration::from_millis(100)), "100ms < frame 1's 200ms, no flip");
223 assert_eq!(a.frame_index(), 1);
224 assert!(a.advance(Duration::from_millis(120)), "now crosses frame 1's 200ms");
225 assert_eq!(a.frame_index(), 2);
226 }
227}