Skip to main content

euv_engine/sprite/
impl.rs

1use crate::*;
2
3/// Implements frame extraction, rendering, and animation creation for `SpriteSheet`.
4impl SpriteSheet {
5    /// Creates a new sprite sheet from an image element and uniform frame dimensions.
6    ///
7    /// Computes the grid columns and rows from the image dimensions and frame size.
8    ///
9    /// # Arguments
10    ///
11    /// - `HtmlImageElement` - The sprite sheet image.
12    /// - `f64` - The width of each frame.
13    /// - `f64` - The height of each frame.
14    ///
15    /// # Returns
16    ///
17    /// - `SpriteSheet` - The new sprite sheet.
18    pub fn from_image(image: HtmlImageElement, frame_width: f64, frame_height: f64) -> SpriteSheet {
19        let total_width: f64 = image.width() as f64;
20        let total_height: f64 = image.height() as f64;
21        let columns: u32 = (total_width / frame_width).max(1.0) as u32;
22        let rows: u32 = (total_height / frame_height).max(1.0) as u32;
23        SpriteSheet::new(image, frame_width, frame_height, columns, rows)
24    }
25
26    /// Returns the source rectangle for the frame at the given grid index.
27    ///
28    /// # Arguments
29    ///
30    /// - `u32` - The frame index (left-to-right, top-to-bottom).
31    ///
32    /// # Returns
33    ///
34    /// - `Rect` - The source rectangle.
35    pub fn frame_source(&self, index: u32) -> Rect {
36        let column: u32 = index % self.get_columns();
37        let row: u32 = index / self.get_columns();
38        Rect::new(
39            column as f64 * self.get_frame_width(),
40            row as f64 * self.get_frame_height(),
41            self.get_frame_width(),
42            self.get_frame_height(),
43        )
44    }
45
46    /// Creates a frame at the given grid index with a default duration.
47    ///
48    /// # Arguments
49    ///
50    /// - `u32` - The frame index.
51    ///
52    /// # Returns
53    ///
54    /// - `SpriteFrame` - The new frame.
55    pub fn frame(&self, index: u32) -> SpriteFrame {
56        SpriteFrame::new(self.frame_source(index), SPRITE_DEFAULT_FRAME_DURATION)
57    }
58
59    /// Creates an animation from a range of frame indices.
60    ///
61    /// # Arguments
62    ///
63    /// - `&str` - The animation name.
64    /// - `u32` - The starting frame index (inclusive).
65    /// - `u32` - The ending frame index (exclusive).
66    /// - `AnimationMode` - The playback mode.
67    ///
68    /// # Returns
69    ///
70    /// - `SpriteAnimation` - The new animation.
71    pub fn animation(
72        &self,
73        name: &str,
74        start: u32,
75        end: u32,
76        mode: AnimationMode,
77    ) -> SpriteAnimation {
78        let frames: Vec<SpriteFrame> = (start..end).map(|index: u32| self.frame(index)).collect();
79        SpriteAnimation::new(name.to_string(), frames, mode)
80    }
81
82    /// Draws a static (non-animated) sprite frame onto the canvas.
83    ///
84    /// # Arguments
85    ///
86    /// - `&CanvasRenderingContext2d` - The canvas context.
87    /// - `u32` - The frame index to draw.
88    /// - `&Transform2D` - The world-space transform to apply.
89    pub fn draw_frame(
90        &self,
91        context: &CanvasRenderingContext2d,
92        frame_index: u32,
93        transform: &Transform2D,
94    ) {
95        let source: Rect = self.frame_source(frame_index);
96        context.save();
97        let _ = context.translate(
98            transform.get_position().get_x(),
99            transform.get_position().get_y(),
100        );
101        let _ = context.rotate(transform.get_rotation());
102        let _ = context.scale(transform.get_scale().get_x(), transform.get_scale().get_y());
103        let _ = context
104            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
105                self.get_image(),
106                source.get_x(),
107                source.get_y(),
108                source.get_width(),
109                source.get_height(),
110                -source.get_width() * 0.5,
111                -source.get_height() * 0.5,
112                source.get_width(),
113                source.get_height(),
114            );
115        context.restore();
116    }
117}
118
119/// Implements playback control for `Animator`.
120impl Animator {
121    /// Creates a new idle animator with no animation.
122    ///
123    /// # Returns
124    ///
125    /// - `Animator` - The new animator.
126    pub fn create() -> Animator {
127        Animator::new(AnimationState::Paused, 1)
128    }
129
130    /// Plays the given animation from the beginning.
131    ///
132    /// # Arguments
133    ///
134    /// - `SpriteAnimation` - The animation to play.
135    pub fn play(&mut self, animation: SpriteAnimation) {
136        self.set_current_animation(Some(animation));
137        self.set_current_frame_index(0);
138        self.set_elapsed_time(0.0);
139        self.set_state(AnimationState::Playing);
140        self.set_direction(1);
141    }
142
143    /// Pauses the current animation.
144    pub fn pause(&mut self) {
145        if self.get_state() == AnimationState::Playing {
146            self.set_state(AnimationState::Paused);
147        }
148    }
149
150    /// Resumes the current animation from where it was paused.
151    pub fn resume(&mut self) {
152        if self.get_state() == AnimationState::Paused {
153            self.set_state(AnimationState::Playing);
154        }
155    }
156
157    /// Stops the current animation and resets to the first frame.
158    pub fn stop(&mut self) {
159        self.set_current_frame_index(0);
160        self.set_elapsed_time(0.0);
161        self.set_state(AnimationState::Paused);
162        self.set_direction(1);
163    }
164
165    /// Advances the animation by the given delta time.
166    ///
167    /// # Arguments
168    ///
169    /// - `f64` - The time elapsed since the last update, in seconds.
170    pub fn update(&mut self, delta_time: f64) {
171        if self.get_state() != AnimationState::Playing {
172            return;
173        }
174        let current_frame_index: usize = self.get_current_frame_index();
175        let (frame_count, current_duration, mode) = {
176            let Some(animation) = self.get_mut_current_animation().as_ref() else {
177                return;
178            };
179            if animation.get_frames().is_empty() {
180                return;
181            }
182            (
183                animation.get_frames().len(),
184                animation.get_frames()[current_frame_index].get_duration(),
185                animation.get_mode(),
186            )
187        };
188        *self.get_mut_elapsed_time() += delta_time;
189        if self.get_elapsed_time() < current_duration {
190            return;
191        }
192        self.set_elapsed_time(0.0);
193        self.advance_frame(frame_count, mode);
194    }
195
196    /// Returns the source rectangle of the current frame, or `None` if no animation is playing.
197    ///
198    /// # Returns
199    ///
200    /// - `Option<Rect>` - The current frame's source rectangle.
201    pub fn current_frame_source(&self) -> Option<Rect> {
202        let animation: Option<SpriteAnimation> = self.get_current_animation();
203        let animation: &SpriteAnimation = animation.as_ref()?;
204        let frame: &SpriteFrame = animation.get_frames().get(self.get_current_frame_index())?;
205        Some(frame.get_source())
206    }
207
208    /// Advances to the next frame according to the animation mode.
209    ///
210    /// # Arguments
211    ///
212    /// - `&SpriteAnimation` - The current animation.
213    fn advance_frame(&mut self, frame_count: usize, mode: AnimationMode) {
214        match mode {
215            AnimationMode::Loop => {
216                self.set_current_frame_index((self.get_current_frame_index() + 1) % frame_count);
217            }
218            AnimationMode::Once => {
219                if self.get_current_frame_index() + 1 < frame_count {
220                    *self.get_mut_current_frame_index() += 1;
221                } else {
222                    self.set_state(AnimationState::Finished);
223                }
224            }
225            AnimationMode::PingPong => {
226                if self.get_direction() > 0 {
227                    if self.get_current_frame_index() + 1 < frame_count {
228                        *self.get_mut_current_frame_index() += 1;
229                    } else {
230                        self.set_direction(-1);
231                        if self.get_current_frame_index() > 0 {
232                            *self.get_mut_current_frame_index() -= 1;
233                        }
234                    }
235                } else {
236                    if self.get_current_frame_index() > 0 {
237                        *self.get_mut_current_frame_index() -= 1;
238                    } else {
239                        self.set_direction(1);
240                        if self.get_current_frame_index() + 1 < frame_count {
241                            *self.get_mut_current_frame_index() += 1;
242                        }
243                    }
244                }
245            }
246        }
247    }
248}
249
250/// Implements animated sprite rendering for `Animator`.
251impl Animator {
252    /// Draws the current frame of this animator onto the canvas.
253    ///
254    /// # Arguments
255    ///
256    /// - `&CanvasRenderingContext2d` - The canvas context.
257    /// - `&SpriteSheet` - The sprite sheet containing the image.
258    /// - `&Transform2D` - The world-space transform to apply.
259    pub fn draw(
260        &self,
261        context: &CanvasRenderingContext2d,
262        sheet: &SpriteSheet,
263        transform: &Transform2D,
264    ) {
265        let Some(source) = self.current_frame_source() else {
266            return;
267        };
268        context.save();
269        let _ = context.translate(
270            transform.get_position().get_x(),
271            transform.get_position().get_y(),
272        );
273        let _ = context.rotate(transform.get_rotation());
274        let _ = context.scale(
275            if self.get_flip_x() {
276                -transform.get_scale().get_x()
277            } else {
278                transform.get_scale().get_x()
279            },
280            if self.get_flip_y() {
281                -transform.get_scale().get_y()
282            } else {
283                transform.get_scale().get_y()
284            },
285        );
286        let _ = context
287            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
288                sheet.get_image(),
289                source.get_x(),
290                source.get_y(),
291                source.get_width(),
292                source.get_height(),
293                -source.get_width() * 0.5,
294                -source.get_height() * 0.5,
295                source.get_width(),
296                source.get_height(),
297            );
298        context.restore();
299    }
300}
301
302/// Implements `Default` for `Animator` as a new idle animator.
303impl Default for Animator {
304    fn default() -> Animator {
305        Animator::create()
306    }
307}