Skip to main content

euv_engine/sprite/
impl.rs

1use super::*;
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        // Compose the TRS matrix in Rust and apply it with a single `set_transform`
97        // instead of save/translate/rotate/scale/restore (5 FFI calls + a canvas
98        // state-stack push/pop per sprite). Scale signs handle flipping.
99        let rotation: f64 = transform.get_rotation();
100        let cos: f64 = rotation.cos();
101        let sin: f64 = rotation.sin();
102        let scale_x: f64 = transform.get_scale().get_x();
103        let scale_y: f64 = transform.get_scale().get_y();
104        let _: Result<(), JsValue> = context.set_transform(
105            cos * scale_x,
106            sin * scale_x,
107            -sin * scale_y,
108            cos * scale_y,
109            transform.get_position().get_x(),
110            transform.get_position().get_y(),
111        );
112        let _: Result<(), JsValue> = context
113            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
114                self.get_image(),
115                source.get_x(),
116                source.get_y(),
117                source.get_width(),
118                source.get_height(),
119                -source.get_width() * 0.5,
120                -source.get_height() * 0.5,
121                source.get_width(),
122                source.get_height(),
123            );
124        let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
125    }
126}
127
128/// Implements playback control for `Animator`.
129impl Animator {
130    /// Creates a new idle animator with no animation.
131    ///
132    /// # Returns
133    ///
134    /// - `Animator` - The new animator.
135    pub fn create() -> Animator {
136        Animator::new(AnimationState::Paused, 1)
137    }
138
139    /// Plays the given animation from the beginning.
140    ///
141    /// # Arguments
142    ///
143    /// - `SpriteAnimation` - The animation to play.
144    pub fn play(&mut self, animation: SpriteAnimation) {
145        self.set_current_animation(Some(animation));
146        self.set_current_frame_index(0);
147        self.set_elapsed_time(0.0);
148        self.set_state(AnimationState::Playing);
149        self.set_direction(1);
150    }
151
152    /// Pauses the current animation.
153    pub fn pause(&mut self) {
154        if self.get_state() == AnimationState::Playing {
155            self.set_state(AnimationState::Paused);
156        }
157    }
158
159    /// Resumes the current animation from where it was paused.
160    pub fn resume(&mut self) {
161        if self.get_state() == AnimationState::Paused {
162            self.set_state(AnimationState::Playing);
163        }
164    }
165
166    /// Stops the current animation and resets to the first frame.
167    pub fn stop(&mut self) {
168        self.set_current_frame_index(0);
169        self.set_elapsed_time(0.0);
170        self.set_state(AnimationState::Paused);
171        self.set_direction(1);
172    }
173
174    /// Advances the animation by the given delta time.
175    ///
176    /// # Arguments
177    ///
178    /// - `f64` - The time elapsed since the last update, in seconds.
179    pub fn update(&mut self, delta_time: f64) {
180        if self.get_state() != AnimationState::Playing {
181            return;
182        }
183        let current_frame_index: usize = self.get_current_frame_index();
184        let (frame_count, current_duration, mode) = {
185            let Some(animation) = self.get_mut_current_animation().as_ref() else {
186                return;
187            };
188            if animation.get_frames().is_empty() {
189                return;
190            }
191            (
192                animation.get_frames().len(),
193                animation.get_frames()[current_frame_index].get_duration(),
194                animation.get_mode(),
195            )
196        };
197        *self.get_mut_elapsed_time() += delta_time;
198        if self.get_elapsed_time() < current_duration {
199            return;
200        }
201        self.set_elapsed_time(0.0);
202        self.advance_frame(frame_count, mode);
203    }
204
205    /// Returns the source rectangle of the current frame, or `None` if no animation is playing.
206    ///
207    /// # Returns
208    ///
209    /// - `Option<Rect>` - The current frame's source rectangle.
210    pub fn current_frame_source(&self) -> Option<Rect> {
211        let animation: Option<SpriteAnimation> = self.get_current_animation();
212        let animation: &SpriteAnimation = animation.as_ref()?;
213        let frame: &SpriteFrame = animation.get_frames().get(self.get_current_frame_index())?;
214        Some(frame.get_source())
215    }
216
217    /// Advances to the next frame according to the animation mode.
218    ///
219    /// # Arguments
220    ///
221    /// - `&SpriteAnimation` - The current animation.
222    fn advance_frame(&mut self, frame_count: usize, mode: AnimationMode) {
223        match mode {
224            AnimationMode::Loop => {
225                self.set_current_frame_index((self.get_current_frame_index() + 1) % frame_count);
226            }
227            AnimationMode::Once => {
228                if self.get_current_frame_index() + 1 < frame_count {
229                    *self.get_mut_current_frame_index() += 1;
230                } else {
231                    self.set_state(AnimationState::Finished);
232                }
233            }
234            AnimationMode::PingPong => {
235                if self.get_direction() > 0 {
236                    if self.get_current_frame_index() + 1 < frame_count {
237                        *self.get_mut_current_frame_index() += 1;
238                    } else {
239                        self.set_direction(-1);
240                        if self.get_current_frame_index() > 0 {
241                            *self.get_mut_current_frame_index() -= 1;
242                        }
243                    }
244                } else {
245                    if self.get_current_frame_index() > 0 {
246                        *self.get_mut_current_frame_index() -= 1;
247                    } else {
248                        self.set_direction(1);
249                        if self.get_current_frame_index() + 1 < frame_count {
250                            *self.get_mut_current_frame_index() += 1;
251                        }
252                    }
253                }
254            }
255        }
256    }
257}
258
259/// Forwards `Animator::update` through the [`Updatable`] trait so that
260/// collections of heterogeneous updateable objects (entities, animators,
261/// physics worlds, scene managers) can be driven by a single scheduler loop.
262///
263/// The inherent [`Animator::update`] method is the canonical implementation;
264/// this impl exists purely for trait dispatch. The inherent call resolves
265/// first when both are in scope, so there is no recursion.
266impl Updatable for Animator {
267    fn update(&mut self, delta_time: f64) {
268        Animator::update(self, delta_time);
269    }
270}
271
272/// Implements animated sprite rendering for `Animator`.
273impl Animator {
274    /// Draws the current frame of this animator onto the canvas.
275    ///
276    /// # Arguments
277    ///
278    /// - `&CanvasRenderingContext2d` - The canvas context.
279    /// - `&SpriteSheet` - The sprite sheet containing the image.
280    /// - `&Transform2D` - The world-space transform to apply.
281    pub fn draw(
282        &self,
283        context: &CanvasRenderingContext2d,
284        sheet: &SpriteSheet,
285        transform: &Transform2D,
286    ) {
287        let Some(source) = self.current_frame_source() else {
288            return;
289        };
290        // Compose the TRS matrix in Rust (flip signs folded into scale) and apply
291        // it with a single `set_transform` instead of save/translate/rotate/scale/restore.
292        let rotation: f64 = transform.get_rotation();
293        let cos: f64 = rotation.cos();
294        let sin: f64 = rotation.sin();
295        let scale_x: f64 = if self.get_flip_x() {
296            -transform.get_scale().get_x()
297        } else {
298            transform.get_scale().get_x()
299        };
300        let scale_y: f64 = if self.get_flip_y() {
301            -transform.get_scale().get_y()
302        } else {
303            transform.get_scale().get_y()
304        };
305        let _: Result<(), JsValue> = context.set_transform(
306            cos * scale_x,
307            sin * scale_x,
308            -sin * scale_y,
309            cos * scale_y,
310            transform.get_position().get_x(),
311            transform.get_position().get_y(),
312        );
313        let _: Result<(), JsValue> = context
314            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
315                sheet.get_image(),
316                source.get_x(),
317                source.get_y(),
318                source.get_width(),
319                source.get_height(),
320                -source.get_width() * 0.5,
321                -source.get_height() * 0.5,
322                source.get_width(),
323                source.get_height(),
324            );
325        let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
326    }
327}
328
329/// Implements `Default` for `Animator` as a new idle animator.
330impl Default for Animator {
331    fn default() -> Animator {
332        Animator::create()
333    }
334}