Skip to main content

goud_engine/ecs/components/sprite_animator/
component.rs

1//! Sprite animation component and supporting types.
2
3use crate::core::math::Rect;
4use crate::ecs::Component;
5
6// =============================================================================
7// PlaybackMode
8// =============================================================================
9
10/// Controls how an animation behaves when it reaches the last frame.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub enum PlaybackMode {
13    /// Restart from frame 0 after the last frame.
14    Loop,
15    /// Stop at the last frame and mark the animation as finished.
16    OneShot,
17}
18
19// =============================================================================
20// AnimationClip
21// =============================================================================
22
23/// Defines the frame sequence and timing for a sprite animation.
24///
25/// `AnimationClip` is a plain data struct, not a component. It is stored
26/// inside a [`SpriteAnimator`] component.
27///
28/// # Example
29///
30/// ```
31/// use goud_engine::ecs::components::sprite_animator::{AnimationClip, PlaybackMode};
32/// use goud_engine::core::math::Rect;
33///
34/// let frames = vec![
35///     Rect::new(0.0, 0.0, 32.0, 32.0),
36///     Rect::new(32.0, 0.0, 32.0, 32.0),
37///     Rect::new(64.0, 0.0, 32.0, 32.0),
38/// ];
39///
40/// let clip = AnimationClip::new(frames, 0.1);
41/// assert_eq!(clip.mode, PlaybackMode::Loop);
42/// ```
43#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
44pub struct AnimationClip {
45    /// Source rectangles for each frame (pixel coordinates).
46    pub frames: Vec<Rect>,
47    /// Seconds per frame.
48    pub frame_duration: f32,
49    /// Playback mode (Loop or OneShot).
50    pub mode: PlaybackMode,
51}
52
53impl AnimationClip {
54    /// Creates a new animation clip with the given frames and frame duration.
55    ///
56    /// Defaults to `PlaybackMode::Loop`.
57    #[inline]
58    pub fn new(frames: Vec<Rect>, frame_duration: f32) -> Self {
59        Self {
60            frames,
61            frame_duration,
62            mode: PlaybackMode::Loop,
63        }
64    }
65
66    /// Sets the playback mode for this clip (builder pattern).
67    #[inline]
68    pub fn with_mode(mut self, mode: PlaybackMode) -> Self {
69        self.mode = mode;
70        self
71    }
72
73    /// Creates a looping animation clip.
74    #[inline]
75    pub fn looping(frames: Vec<Rect>, frame_duration: f32) -> Self {
76        Self::new(frames, frame_duration).with_mode(PlaybackMode::Loop)
77    }
78
79    /// Creates a one-shot animation clip.
80    #[inline]
81    pub fn one_shot(frames: Vec<Rect>, frame_duration: f32) -> Self {
82        Self::new(frames, frame_duration).with_mode(PlaybackMode::OneShot)
83    }
84}
85
86// =============================================================================
87// SpriteAnimator
88// =============================================================================
89
90/// ECS component that drives sprite sheet animation.
91///
92/// Attach this to an entity alongside a [`Sprite`](crate::ecs::components::Sprite)
93/// to animate the sprite's `source_rect` through a sequence of frames.
94///
95/// # Example
96///
97/// ```
98/// use goud_engine::ecs::components::sprite_animator::{SpriteAnimator, AnimationClip};
99/// use goud_engine::core::math::Rect;
100///
101/// let clip = AnimationClip::new(
102///     vec![Rect::new(0.0, 0.0, 32.0, 32.0), Rect::new(32.0, 0.0, 32.0, 32.0)],
103///     0.1,
104/// );
105///
106/// let animator = SpriteAnimator::new(clip);
107/// assert!(animator.playing);
108/// assert!(!animator.finished);
109/// ```
110#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
111pub struct SpriteAnimator {
112    /// The animation clip driving this animator.
113    pub clip: AnimationClip,
114    /// Index of the current frame in `clip.frames`.
115    pub current_frame: usize,
116    /// Accumulated time since the last frame advance.
117    pub elapsed: f32,
118    /// Whether the animation is currently playing.
119    pub playing: bool,
120    /// Whether a OneShot animation has completed.
121    pub finished: bool,
122}
123
124impl SpriteAnimator {
125    /// Creates a new animator from the given clip, starting playback immediately.
126    #[inline]
127    pub fn new(clip: AnimationClip) -> Self {
128        Self {
129            clip,
130            current_frame: 0,
131            elapsed: 0.0,
132            playing: true,
133            finished: false,
134        }
135    }
136
137    /// Starts (or restarts) playback from frame 0.
138    #[inline]
139    pub fn play(&mut self) {
140        self.current_frame = 0;
141        self.elapsed = 0.0;
142        self.playing = true;
143        self.finished = false;
144    }
145
146    /// Pauses playback without resetting frame position.
147    #[inline]
148    pub fn pause(&mut self) {
149        self.playing = false;
150    }
151
152    /// Resumes playback from the current frame position.
153    #[inline]
154    pub fn resume(&mut self) {
155        if !self.finished {
156            self.playing = true;
157        }
158    }
159
160    /// Resets the animator to its initial state (frame 0, not playing).
161    #[inline]
162    pub fn reset(&mut self) {
163        self.current_frame = 0;
164        self.elapsed = 0.0;
165        self.playing = false;
166        self.finished = false;
167    }
168
169    /// Returns `true` if a OneShot animation has completed.
170    #[inline]
171    pub fn is_finished(&self) -> bool {
172        self.finished
173    }
174
175    /// Returns the source `Rect` for the current frame, or `None` if
176    /// the clip has no frames.
177    #[inline]
178    pub fn current_rect(&self) -> Option<Rect> {
179        self.clip.frames.get(self.current_frame).copied()
180    }
181}
182
183impl Component for SpriteAnimator {}