use core::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "serde")]
mod dto;
#[cfg_attr(
feature = "serde",
doc = "
# Deserialization format
```yaml
# The mode can be one of: 'Once', 'Repeat', 'PingPong'
# or '!RepeatFrom: n' (where 'n' is the frame-index to repeat from)
# The default is 'Repeat'
mode: PingPong
frames:
- index: 0 # index in the sprite sheet for that frame
duration: 100 # duration of the frame in milliseconds
- index: 1
duration: 100
- index: 2
duration: 120
```
There is also a short-hand notation if all frames have the same duration:
```yaml
fps: 12 # may be substitued by 'frame_duration' of 'total_duration'
frames: [0, 1, 2] # sequence of frame indices
```
"
)]
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(try_from = "dto::AnimationDto", into = "dto::AnimationDto")
)]
pub struct Animation {
pub(crate) frames: Vec<Frame>,
pub(crate) mode: Mode,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Frame {
pub(crate) index: usize,
pub(crate) duration: Duration,
}
impl Animation {
#[must_use]
pub fn from_frames(frames: impl IntoIterator<Item = Frame>) -> Self {
Self {
frames: frames.into_iter().collect(),
mode: Mode::default(),
}
}
pub fn from_indices(indices: impl IntoIterator<Item = usize>, frame_rate: FrameRate) -> Self {
let mut anim: Self = indices
.into_iter()
.map(|index| Frame::new(index, frame_rate.frame_duration))
.collect();
if frame_rate.is_total_duration {
#[allow(clippy::cast_precision_loss)]
let actual_duration = frame_rate.frame_duration.div_f64(anim.frames.len() as f64);
for frame in &mut anim.frames {
frame.duration = actual_duration;
}
}
anim
}
#[must_use]
pub fn once(mut self) -> Self {
self.mode = Mode::Once;
self
}
#[must_use]
pub fn repeat(mut self) -> Self {
self.mode = Mode::RepeatFrom(0);
self
}
#[must_use]
pub fn repeat_from(mut self, frame_index: usize) -> Self {
self.mode = Mode::RepeatFrom(frame_index);
self
}
#[must_use]
pub fn ping_pong(mut self) -> Self {
self.mode = Mode::PingPong;
self
}
pub(crate) fn has_frames(&self) -> bool {
!self.frames.is_empty()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) enum Mode {
Once,
RepeatFrom(usize),
PingPong,
}
impl FromIterator<Frame> for Animation {
fn from_iter<T: IntoIterator<Item = Frame>>(iter: T) -> Self {
Self::from_frames(iter)
}
}
impl Extend<Frame> for Animation {
fn extend<T: IntoIterator<Item = Frame>>(&mut self, iter: T) {
self.frames.extend(iter);
}
}
impl Default for Mode {
#[inline]
fn default() -> Self {
Self::RepeatFrom(0)
}
}
impl Frame {
#[inline]
#[must_use]
pub fn new(index: usize, duration: Duration) -> Self {
assert!(
!duration.is_zero(),
"zero-duration is invalid for animation frame"
);
Self { index, duration }
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[must_use]
pub struct FrameRate {
frame_duration: Duration,
is_total_duration: bool,
}
impl FrameRate {
pub fn from_fps(fps: f64) -> Self {
assert!(fps.is_finite() && fps > 0.0, "Invalid FPS: ${fps}");
Self {
frame_duration: Duration::from_secs(1).div_f64(fps),
is_total_duration: false,
}
}
pub fn from_frame_duration(duration: Duration) -> Self {
Self {
frame_duration: duration,
is_total_duration: false,
}
}
pub fn from_total_duration(duration: Duration) -> Self {
Self {
frame_duration: duration,
is_total_duration: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest]
#[should_panic]
fn invalid_frame_rate_panics(#[values(0.0, -1.0, f64::NAN, f64::INFINITY)] fps: f64) {
let _ = FrameRate::from_fps(fps);
}
#[test]
#[should_panic]
fn panics_for_zero_duration() {
let _ = Frame::new(0, Duration::ZERO);
}
#[test]
fn extends() {
let mut anim = Animation::from_indices(
0..=0,
FrameRate::from_frame_duration(Duration::from_secs(1)),
);
anim.extend([Frame::new(2, Duration::from_secs(2))]);
assert_eq!(
anim,
Animation::from_frames(vec![
Frame::new(0, Duration::from_secs(1)),
Frame::new(2, Duration::from_secs(2))
])
);
}
#[test]
fn fps_frame_duration_equivalence() {
assert_eq!(
Animation::from_indices(1..=3, FrameRate::from_fps(10.0)),
Animation::from_indices(
1..=3,
FrameRate::from_frame_duration(Duration::from_millis(100))
),
);
}
#[test]
fn total_duration() {
assert_eq!(
Animation::from_indices(
0..10,
FrameRate::from_total_duration(Duration::from_secs(1))
),
Animation::from_indices(
0..10,
FrameRate::from_frame_duration(Duration::from_millis(100))
),
);
}
}