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 {}