benimator/animation/
mod.rs

1use core::time::Duration;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "serde")]
7mod dto;
8
9/// Definition of an animation
10#[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    /// Frames
46    pub(crate) frames: Vec<Frame>,
47    /// Animation mode
48    pub(crate) mode: Mode,
49}
50
51/// A single animation frame
52#[derive(Debug, Copy, Clone, PartialEq, Eq)]
53pub struct Frame {
54    /// Index in the sprite atlas
55    pub(crate) index: usize,
56    /// How long should the frame be displayed
57    pub(crate) duration: Duration,
58}
59
60impl Animation {
61    /// Create a new animation from frames
62    #[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    /// Create a new animation from an index iterator, using the same frame duration for each frame.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// # use benimator::{Animation, FrameRate};
76    /// # use std::time::Duration;
77    /// // From an index range
78    /// let animation = Animation::from_indices(0..=5, FrameRate::from_fps(12.0));
79    ///
80    /// // From an index array
81    /// let animation = Animation::from_indices([1, 2, 3, 4], FrameRate::from_fps(12.0));
82    ///
83    /// // Reversed animation
84    /// let animation = Animation::from_indices((0..5).rev(), FrameRate::from_fps(12.0));
85    ///
86    /// // Chained ranges
87    /// let animation = Animation::from_indices((0..3).chain(10..15), FrameRate::from_fps(12.0));
88    /// ```
89    ///
90    /// Note, the [`FrameRate`] may be created from fps, frame-duration and animation-duration
91    ///
92    /// To use different non-uniform frame-duration, see [`from_frames`](Animation::from_frames)
93    ///
94    /// # Panics
95    ///
96    /// Panics if the duration is zero
97    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    /// Runs the animation once and then stop playing
115    #[must_use]
116    pub fn once(mut self) -> Self {
117        self.mode = Mode::Once;
118        self
119    }
120
121    /// Repeat the animation forever
122    #[must_use]
123    pub fn repeat(mut self) -> Self {
124        self.mode = Mode::RepeatFrom(0);
125        self
126    }
127
128    /// Repeat the animation forever, from a given frame index (loop back to it at the end of the
129    /// animation)
130    #[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    /// Repeat the animation forever, going back and forth between the first and last frame.
137    #[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    /// Create a new animation frame
176    ///
177    /// The duration must be > 0
178    ///
179    /// # Panics
180    ///
181    /// Panics if the duration is zero
182    #[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/// Frame-Rate definition
194#[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    /// Frame rate defined by the FPS (Frame-Per-Second)
203    ///
204    /// # Panics
205    ///
206    /// This function will panic if `fps` is negative, zero or not finite.
207    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    /// Frame rate defined by the duration of each frame
216    pub fn from_frame_duration(duration: Duration) -> Self {
217        Self {
218            frame_duration: duration,
219            is_total_duration: false,
220        }
221    }
222
223    /// Frame rate defined by the total duration of the animation
224    ///
225    /// The actual FPS will then depend on the number of frame
226    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}