benimator/animation/
mod.rs1use core::time::Duration;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "serde")]
7mod dto;
8
9#[cfg_attr(
11 feature = "serde",
12 doc = "
13
14# Deserialization format
15
16```yaml
17# The mode can be one of: 'Once', 'Repeat', 'PingPong'
18# or '!RepeatFrom: n' (where 'n' is the frame-index to repeat from)
19# The default is 'Repeat'
20mode: PingPong
21frames:
22 - index: 0 # index in the sprite sheet for that frame
23 duration: 100 # duration of the frame in milliseconds
24 - index: 1
25 duration: 100
26 - index: 2
27 duration: 120
28```
29
30There is also a short-hand notation if all frames have the same duration:
31
32```yaml
33fps: 12 # may be substitued by 'frame_duration' of 'total_duration'
34frames: [0, 1, 2] # sequence of frame indices
35```
36"
37)]
38#[derive(Debug, Clone, Eq, PartialEq)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40#[cfg_attr(
41 feature = "serde",
42 serde(try_from = "dto::AnimationDto", into = "dto::AnimationDto")
43)]
44pub struct Animation {
45 pub(crate) frames: Vec<Frame>,
47 pub(crate) mode: Mode,
49}
50
51#[derive(Debug, Copy, Clone, PartialEq, Eq)]
53pub struct Frame {
54 pub(crate) index: usize,
56 pub(crate) duration: Duration,
58}
59
60impl Animation {
61 #[must_use]
63 pub fn from_frames(frames: impl IntoIterator<Item = Frame>) -> Self {
64 Self {
65 frames: frames.into_iter().collect(),
66 mode: Mode::default(),
67 }
68 }
69
70 pub fn from_indices(indices: impl IntoIterator<Item = usize>, frame_rate: FrameRate) -> Self {
98 let mut anim: Self = indices
99 .into_iter()
100 .map(|index| Frame::new(index, frame_rate.frame_duration))
101 .collect();
102
103 if frame_rate.is_total_duration {
104 #[allow(clippy::cast_precision_loss)]
105 let actual_duration = frame_rate.frame_duration.div_f64(anim.frames.len() as f64);
106 for frame in &mut anim.frames {
107 frame.duration = actual_duration;
108 }
109 }
110
111 anim
112 }
113
114 #[must_use]
116 pub fn once(mut self) -> Self {
117 self.mode = Mode::Once;
118 self
119 }
120
121 #[must_use]
123 pub fn repeat(mut self) -> Self {
124 self.mode = Mode::RepeatFrom(0);
125 self
126 }
127
128 #[must_use]
131 pub fn repeat_from(mut self, frame_index: usize) -> Self {
132 self.mode = Mode::RepeatFrom(frame_index);
133 self
134 }
135
136 #[must_use]
138 pub fn ping_pong(mut self) -> Self {
139 self.mode = Mode::PingPong;
140 self
141 }
142
143 pub(crate) fn has_frames(&self) -> bool {
144 !self.frames.is_empty()
145 }
146}
147
148#[derive(Debug, Copy, Clone, Eq, PartialEq)]
149pub(crate) enum Mode {
150 Once,
151 RepeatFrom(usize),
152 PingPong,
153}
154
155impl FromIterator<Frame> for Animation {
156 fn from_iter<T: IntoIterator<Item = Frame>>(iter: T) -> Self {
157 Self::from_frames(iter)
158 }
159}
160
161impl Extend<Frame> for Animation {
162 fn extend<T: IntoIterator<Item = Frame>>(&mut self, iter: T) {
163 self.frames.extend(iter);
164 }
165}
166
167impl Default for Mode {
168 #[inline]
169 fn default() -> Self {
170 Self::RepeatFrom(0)
171 }
172}
173
174impl Frame {
175 #[inline]
183 #[must_use]
184 pub fn new(index: usize, duration: Duration) -> Self {
185 assert!(
186 !duration.is_zero(),
187 "zero-duration is invalid for animation frame"
188 );
189 Self { index, duration }
190 }
191}
192
193#[derive(Debug, Copy, Clone, Eq, PartialEq)]
195#[must_use]
196pub struct FrameRate {
197 frame_duration: Duration,
198 is_total_duration: bool,
199}
200
201impl FrameRate {
202 pub fn from_fps(fps: f64) -> Self {
208 assert!(fps.is_finite() && fps > 0.0, "Invalid FPS: ${fps}");
209 Self {
210 frame_duration: Duration::from_secs(1).div_f64(fps),
211 is_total_duration: false,
212 }
213 }
214
215 pub fn from_frame_duration(duration: Duration) -> Self {
217 Self {
218 frame_duration: duration,
219 is_total_duration: false,
220 }
221 }
222
223 pub fn from_total_duration(duration: Duration) -> Self {
227 Self {
228 frame_duration: duration,
229 is_total_duration: true,
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[rstest]
239 #[should_panic]
240 fn invalid_frame_rate_panics(#[values(0.0, -1.0, f64::NAN, f64::INFINITY)] fps: f64) {
241 let _ = FrameRate::from_fps(fps);
242 }
243
244 #[test]
245 #[should_panic]
246 fn panics_for_zero_duration() {
247 let _ = Frame::new(0, Duration::ZERO);
248 }
249
250 #[test]
251 fn extends() {
252 let mut anim = Animation::from_indices(
253 0..=0,
254 FrameRate::from_frame_duration(Duration::from_secs(1)),
255 );
256 anim.extend([Frame::new(2, Duration::from_secs(2))]);
257 assert_eq!(
258 anim,
259 Animation::from_frames(vec![
260 Frame::new(0, Duration::from_secs(1)),
261 Frame::new(2, Duration::from_secs(2))
262 ])
263 );
264 }
265
266 #[test]
267 fn fps_frame_duration_equivalence() {
268 assert_eq!(
269 Animation::from_indices(1..=3, FrameRate::from_fps(10.0)),
270 Animation::from_indices(
271 1..=3,
272 FrameRate::from_frame_duration(Duration::from_millis(100))
273 ),
274 );
275 }
276
277 #[test]
278 fn total_duration() {
279 assert_eq!(
280 Animation::from_indices(
281 0..10,
282 FrameRate::from_total_duration(Duration::from_secs(1))
283 ),
284 Animation::from_indices(
285 0..10,
286 FrameRate::from_frame_duration(Duration::from_millis(100))
287 ),
288 );
289 }
290}