Skip to main content

game_toolkit_platformer/anim/
clip.rs

1//! [`Clip`]: a sequence of frames with durations, a playback mode and named markers.
2
3use std::sync::Arc;
4
5/// How a clip continues past its last frame.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Playback {
8    /// Start over.
9    Loop,
10    /// Hold the last frame and report the clip finished.
11    Once,
12    /// Play backwards to the start, then forwards again (end frames are not doubled).
13    PingPong,
14}
15
16/// One frame: which sprite to show, and for how long at rate 1.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct Frame {
19    /// Sprite index; what it means is up to the renderer (an atlas cell, a pose).
20    pub sprite: u16,
21    /// Seconds.
22    pub secs: f32,
23}
24
25/// An animation clip.
26#[derive(Clone, Debug, PartialEq)]
27pub struct Clip {
28    /// The frames in order. Never empty.
29    pub frames: Vec<Frame>,
30    /// What happens after the last frame.
31    pub playback: Playback,
32    /// Named markers, reported when their frame starts: `(frame index, name)`.
33    pub markers: Vec<(usize, Arc<str>)>,
34}
35
36impl Clip {
37    /// A clip of `(sprite, seconds)` frames.
38    pub fn new(playback: Playback, frames: &[(u16, f32)]) -> Self {
39        assert!(!frames.is_empty(), "a clip needs at least one frame");
40        Self {
41            frames: frames
42                .iter()
43                .map(|&(sprite, secs)| Frame { sprite, secs })
44                .collect(),
45            playback,
46            markers: Vec::new(),
47        }
48    }
49
50    /// A clip of `sprites`, each shown for `secs`.
51    pub fn uniform(playback: Playback, sprites: &[u16], secs: f32) -> Self {
52        let frames: Vec<_> = sprites.iter().map(|&s| (s, secs)).collect();
53        Self::new(playback, &frames)
54    }
55
56    /// The same clip with a marker `name` on frame `frame`.
57    pub fn marker(mut self, frame: usize, name: &str) -> Self {
58        self.markers.push((frame, name.into()));
59        self
60    }
61
62    /// Seconds for one pass through the frames.
63    pub fn duration(&self) -> f32 {
64        self.frames.iter().map(|f| f.secs).sum()
65    }
66
67    /// The frame index shown at clip time `t`, and whether a [`Playback::Once`] clip has
68    /// finished.
69    pub fn frame_at(&self, t: f32) -> (usize, bool) {
70        let n = self.frames.len();
71        let total = self.duration();
72        if n == 1 || total <= 0.0 {
73            return (0, self.playback == Playback::Once && t >= total);
74        }
75        match self.playback {
76            Playback::Once if t >= total => (n - 1, true),
77            Playback::Once => (self.index_in(&(0..n).collect::<Vec<_>>(), t), false),
78            Playback::Loop => (self.index_in(&(0..n).collect::<Vec<_>>(), t % total), false),
79            Playback::PingPong => {
80                let order: Vec<usize> = (0..n).chain((1..n - 1).rev()).collect();
81                let cycle: f32 = order.iter().map(|&i| self.frames[i].secs).sum();
82                (self.index_in(&order, t % cycle), false)
83            }
84        }
85    }
86
87    /// The frame of `order` (frame indices, played in turn) shown `t` seconds in.
88    fn index_in(&self, order: &[usize], mut t: f32) -> usize {
89        for &i in order {
90            t -= self.frames[i].secs;
91            if t < 0.0 {
92                return i;
93            }
94        }
95        order[order.len() - 1]
96    }
97
98    /// The sprite of frame `i`.
99    pub fn sprite(&self, i: usize) -> u16 {
100        self.frames[i.min(self.frames.len() - 1)].sprite
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn once_holds_the_last_frame_and_finishes() {
110        let c = Clip::uniform(Playback::Once, &[1, 2, 3], 0.1);
111        assert_eq!(c.frame_at(0.15), (1, false));
112        assert_eq!(c.frame_at(0.31), (2, true));
113    }
114
115    #[test]
116    fn loop_wraps() {
117        let c = Clip::uniform(Playback::Loop, &[1, 2], 0.1);
118        assert_eq!(c.frame_at(0.25).0, 0);
119    }
120
121    #[test]
122    fn ping_pong_turns_without_doubling_the_ends() {
123        let c = Clip::uniform(Playback::PingPong, &[1, 2, 3], 0.1);
124        let seq: Vec<usize> = (0..6)
125            .map(|k| c.frame_at(k as f32 * 0.1 + 0.05).0)
126            .collect();
127        assert_eq!(seq, [0, 1, 2, 1, 0, 1]);
128    }
129}