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    /// - `usize` - The total number of frames in the current animation.
222    /// - `AnimationMode` - The playback mode driving the advance.
223    fn advance_frame(&mut self, frame_count: usize, mode: AnimationMode) {
224        match mode {
225            AnimationMode::Loop => {
226                self.set_current_frame_index((self.get_current_frame_index() + 1) % frame_count);
227            }
228            AnimationMode::Once => {
229                if self.get_current_frame_index() + 1 < frame_count {
230                    *self.get_mut_current_frame_index() += 1;
231                } else {
232                    self.set_state(AnimationState::Finished);
233                }
234            }
235            AnimationMode::PingPong => {
236                if self.get_direction() > 0 {
237                    if self.get_current_frame_index() + 1 < frame_count {
238                        *self.get_mut_current_frame_index() += 1;
239                    } else {
240                        self.set_direction(-1);
241                        if self.get_current_frame_index() > 0 {
242                            *self.get_mut_current_frame_index() -= 1;
243                        }
244                    }
245                } else 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/// Forwards `Animator::update` through the [`Updatable`] trait so that
259/// collections of heterogeneous updateable objects (entities, animators,
260/// physics worlds, scene managers) can be driven by a single scheduler loop.
261///
262/// The inherent [`Animator::update`] method is the canonical implementation;
263/// this impl exists purely for trait dispatch. The inherent call resolves
264/// first when both are in scope, so there is no recursion.
265impl Updatable for Animator {
266    /// Advances the simulation by `delta_time` seconds.
267    ///
268    /// # Arguments
269    ///
270    /// - `f64` - Seconds elapsed since the previous update.
271    fn update(&mut self, delta_time: f64) {
272        Animator::update(self, delta_time);
273    }
274}
275
276/// Implements animated sprite rendering for `Animator`.
277impl Animator {
278    /// Draws the current frame of this animator onto the canvas.
279    ///
280    /// # Arguments
281    ///
282    /// - `&CanvasRenderingContext2d` - The canvas context.
283    /// - `&SpriteSheet` - The sprite sheet containing the image.
284    /// - `&Transform2D` - The world-space transform to apply.
285    pub fn draw(
286        &self,
287        context: &CanvasRenderingContext2d,
288        sheet: &SpriteSheet,
289        transform: &Transform2D,
290    ) {
291        let Some(source) = self.current_frame_source() else {
292            return;
293        };
294        // Compose the TRS matrix in Rust (flip signs folded into scale) and apply
295        // it with a single `set_transform` instead of save/translate/rotate/scale/restore.
296        let rotation: f64 = transform.get_rotation();
297        let cos: f64 = rotation.cos();
298        let sin: f64 = rotation.sin();
299        let scale_x: f64 = if self.get_flip_x() {
300            -transform.get_scale().get_x()
301        } else {
302            transform.get_scale().get_x()
303        };
304        let scale_y: f64 = if self.get_flip_y() {
305            -transform.get_scale().get_y()
306        } else {
307            transform.get_scale().get_y()
308        };
309        let _: Result<(), JsValue> = context.set_transform(
310            cos * scale_x,
311            sin * scale_x,
312            -sin * scale_y,
313            cos * scale_y,
314            transform.get_position().get_x(),
315            transform.get_position().get_y(),
316        );
317        let _: Result<(), JsValue> = context
318            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
319                sheet.get_image(),
320                source.get_x(),
321                source.get_y(),
322                source.get_width(),
323                source.get_height(),
324                -source.get_width() * 0.5,
325                -source.get_height() * 0.5,
326                source.get_width(),
327                source.get_height(),
328            );
329        let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
330    }
331}
332
333/// Implements `Default` for `Animator` as a new idle animator.
334impl Default for Animator {
335    /// Constructs a default [`Animator`] value.
336    ///
337    /// # Returns
338    ///
339    /// - `Animator` - A default-constructed instance with the documented initial state.
340    fn default() -> Animator {
341        Animator::create()
342    }
343}
344
345/// Implements named access over the three-by-three nine-slice grid.
346///
347/// The index constants live in this module's `const.rs`; each accessor
348/// maps a grid position to a self-documenting name so call sites and
349/// assert messages read as geometry rather than as `(row, column)` pairs.
350impl NineSliceRects {
351    /// Returns the top-left corner sub-rectangle.
352    ///
353    /// # Returns
354    ///
355    /// - `Rect` - The top-left corner patch.
356    pub fn get_top_left(&self) -> Rect {
357        self.get_grid()[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_LEFT]
358    }
359
360    /// Returns the top edge sub-rectangle.
361    ///
362    /// # Returns
363    ///
364    /// - `Rect` - The top edge patch.
365    pub fn get_top(&self) -> Rect {
366        self.get_grid()[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_CENTER]
367    }
368
369    /// Returns the top-right corner sub-rectangle.
370    ///
371    /// # Returns
372    ///
373    /// - `Rect` - The top-right corner patch.
374    pub fn get_top_right(&self) -> Rect {
375        self.get_grid()[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_RIGHT]
376    }
377
378    /// Returns the left edge sub-rectangle.
379    ///
380    /// # Returns
381    ///
382    /// - `Rect` - The left edge patch.
383    pub fn get_left(&self) -> Rect {
384        self.get_grid()[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_LEFT]
385    }
386
387    /// Returns the stretchable center sub-rectangle.
388    ///
389    /// # Returns
390    ///
391    /// - `Rect` - The center patch.
392    pub fn get_center(&self) -> Rect {
393        self.get_grid()[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_CENTER]
394    }
395
396    /// Returns the right edge sub-rectangle.
397    ///
398    /// # Returns
399    ///
400    /// - `Rect` - The right edge patch.
401    pub fn get_right(&self) -> Rect {
402        self.get_grid()[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_RIGHT]
403    }
404
405    /// Returns the bottom-left corner sub-rectangle.
406    ///
407    /// # Returns
408    ///
409    /// - `Rect` - The bottom-left corner patch.
410    pub fn get_bottom_left(&self) -> Rect {
411        self.get_grid()[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_LEFT]
412    }
413
414    /// Returns the bottom edge sub-rectangle.
415    ///
416    /// # Returns
417    ///
418    /// - `Rect` - The bottom edge patch.
419    pub fn get_bottom(&self) -> Rect {
420        self.get_grid()[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_CENTER]
421    }
422
423    /// Returns the bottom-right corner sub-rectangle.
424    ///
425    /// # Returns
426    ///
427    /// - `Rect` - The bottom-right corner patch.
428    pub fn get_bottom_right(&self) -> Rect {
429        self.get_grid()[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_RIGHT]
430    }
431
432    /// Returns the nine patches as a flat slice in reading order.
433    ///
434    /// # Returns
435    ///
436    /// - `Vec<Rect>` - The nine patches, top row first then middle then bottom.
437    pub fn to_vec(&self) -> Vec<Rect> {
438        let grid: [[Rect; 3]; 3] = self.get_grid();
439        vec![
440            grid[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_LEFT],
441            grid[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_CENTER],
442            grid[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_RIGHT],
443            grid[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_LEFT],
444            grid[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_CENTER],
445            grid[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_RIGHT],
446            grid[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_LEFT],
447            grid[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_CENTER],
448            grid[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_RIGHT],
449        ]
450    }
451}
452
453/// Implements the nine-slice geometry math for `NineSliceInsets`.
454///
455/// All methods here are pure functions of the insets and the rectangle
456/// being split: no canvas context, no image handle, no DOM. That is what
457/// makes the split verifiable on the host, and it is the same math
458/// [`NineSlice::draw_into`] and [`NineSlice::record`] reuse.
459impl NineSliceInsets {
460    /// Splits a source rectangle into its nine sub-rectangles.
461    ///
462    /// The insets are clamped against the source size so the nine results
463    /// always tile the source exactly: the two horizontal insets share the
464    /// available width and the two vertical insets share the available
465    /// height, with the center taking whatever remains.
466    ///
467    /// # Arguments
468    ///
469    /// - `Rect` - The full source rectangle to split.
470    ///
471    /// # Returns
472    ///
473    /// - `NineSliceRects` - The nine source sub-rectangles in reading order.
474    pub fn source_rects(&self, source: Rect) -> NineSliceRects {
475        let width: f64 = Numeric::clamp(source.get_width(), 0.0, f64::MAX);
476        let height: f64 = Numeric::clamp(source.get_height(), 0.0, f64::MAX);
477        let left: f64 = Numeric::clamp(self.get_left(), 0.0, width);
478        let right: f64 = Numeric::clamp(self.get_right(), 0.0, width - left);
479        let top: f64 = Numeric::clamp(self.get_top(), 0.0, height);
480        let bottom: f64 = Numeric::clamp(self.get_bottom(), 0.0, height - top);
481        let center_x: f64 = source.get_x() + left;
482        let center_y: f64 = source.get_y() + top;
483        let center_width: f64 = width - left - right;
484        let center_height: f64 = height - top - bottom;
485        let right_x: f64 = center_x + center_width;
486        let bottom_y: f64 = center_y + center_height;
487        NineSliceRects::new([
488            [
489                Rect::new(source.get_x(), source.get_y(), left, top),
490                Rect::new(center_x, source.get_y(), center_width, top),
491                Rect::new(right_x, source.get_y(), right, top),
492            ],
493            [
494                Rect::new(source.get_x(), center_y, left, center_height),
495                Rect::new(center_x, center_y, center_width, center_height),
496                Rect::new(right_x, center_y, right, center_height),
497            ],
498            [
499                Rect::new(source.get_x(), bottom_y, left, bottom),
500                Rect::new(center_x, bottom_y, center_width, bottom),
501                Rect::new(right_x, bottom_y, right, bottom),
502            ],
503        ])
504    }
505
506    /// Splits a destination rectangle into its nine sub-rectangles.
507    ///
508    /// Corners and edges keep their natural pixel size taken from the
509    /// insets, and the center absorbs all remaining destination space, so
510    /// the nine results tile the destination exactly. When the
511    /// destination is smaller than the combined borders the center
512    /// collapses to zero width and/or height instead of going negative.
513    ///
514    /// # Arguments
515    ///
516    /// - `Rect` - The full destination rectangle to split.
517    ///
518    /// # Returns
519    ///
520    /// - `NineSliceRects` - The nine destination sub-rectangles in reading order.
521    pub fn dest_rects(&self, dest: Rect) -> NineSliceRects {
522        let left: f64 = Numeric::clamp(self.get_left(), 0.0, f64::MAX);
523        let right: f64 = Numeric::clamp(self.get_right(), 0.0, f64::MAX);
524        let top: f64 = Numeric::clamp(self.get_top(), 0.0, f64::MAX);
525        let bottom: f64 = Numeric::clamp(self.get_bottom(), 0.0, f64::MAX);
526        let left_edge: f64 = Numeric::clamp(left, 0.0, dest.get_width());
527        let right_edge: f64 = Numeric::clamp(right, 0.0, dest.get_width() - left_edge);
528        let top_edge: f64 = Numeric::clamp(top, 0.0, dest.get_height());
529        let bottom_edge: f64 = Numeric::clamp(bottom, 0.0, dest.get_height() - top_edge);
530        let center_x: f64 = dest.get_x() + left_edge;
531        let center_y: f64 = dest.get_y() + top_edge;
532        let center_width: f64 = dest.get_width() - left_edge - right_edge;
533        let center_height: f64 = dest.get_height() - top_edge - bottom_edge;
534        let right_x: f64 = center_x + center_width;
535        let bottom_y: f64 = center_y + center_height;
536        NineSliceRects::new([
537            [
538                Rect::new(dest.get_x(), dest.get_y(), left_edge, top_edge),
539                Rect::new(center_x, dest.get_y(), center_width, top_edge),
540                Rect::new(right_x, dest.get_y(), right_edge, top_edge),
541            ],
542            [
543                Rect::new(dest.get_x(), center_y, left_edge, center_height),
544                Rect::new(center_x, center_y, center_width, center_height),
545                Rect::new(right_x, center_y, right_edge, center_height),
546            ],
547            [
548                Rect::new(dest.get_x(), bottom_y, left_edge, bottom_edge),
549                Rect::new(center_x, bottom_y, center_width, bottom_edge),
550                Rect::new(right_x, bottom_y, right_edge, bottom_edge),
551            ],
552        ])
553    }
554}
555
556/// Implements image-backed nine-slice drawing and deferred recording.
557impl NineSlice {
558    /// Creates a nine-slice from an image and uniform border insets.
559    ///
560    /// # Arguments
561    ///
562    /// - `HtmlImageElement` - The source image containing the nine patches.
563    /// - `f64` - The border inset applied to all four edges, in source pixels.
564    ///
565    /// # Returns
566    ///
567    /// - `NineSlice` - The new nine-slice.
568    pub fn from_image(image: HtmlImageElement, border: f64) -> NineSlice {
569        NineSlice::new(image, NineSliceInsets::new(border, border, border, border))
570    }
571
572    /// Returns the nine source sub-rectangles for the whole source image.
573    ///
574    /// # Returns
575    ///
576    /// - `NineSliceRects` - The nine source sub-rectangles in reading order.
577    pub fn source_rects(&self) -> NineSliceRects {
578        let image: &HtmlImageElement = &self.get_image();
579        let source: Rect = Rect::new(0.0, 0.0, image.width() as f64, image.height() as f64);
580        self.get_insets().source_rects(source)
581    }
582
583    /// Returns the nine destination sub-rectangles for a destination rect.
584    ///
585    /// # Arguments
586    ///
587    /// - `Rect` - The destination rectangle to split.
588    ///
589    /// # Returns
590    ///
591    /// - `NineSliceRects` - The nine destination sub-rectangles in reading order.
592    pub fn dest_rects(&self, dest: Rect) -> NineSliceRects {
593        self.get_insets().dest_rects(dest)
594    }
595
596    /// Draws the nine-slice into a destination rectangle immediately.
597    ///
598    /// Issues nine `drawImage` calls against the canvas context, pairing
599    /// each source patch with its destination sub-rectangle. Uses the
600    /// canvas transform already in effect, so the caller controls world
601    /// positioning, rotation and scale.
602    ///
603    /// # Arguments
604    ///
605    /// - `&CanvasRenderingContext2d` - The canvas context.
606    /// - `Rect` - The destination rectangle in current canvas space.
607    pub fn draw_into(&self, context: &CanvasRenderingContext2d, dest: Rect) {
608        let sources: Vec<Rect> = self.source_rects().to_vec();
609        let dests: Vec<Rect> = self.dest_rects(dest).to_vec();
610        let image: &HtmlImageElement = &self.get_image();
611        for index in 0..sources.len() {
612            let source: Rect = sources[index];
613            let target: Rect = dests[index];
614            let _: Result<(), JsValue> = context
615                .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
616                    image,
617                    source.get_x(),
618                    source.get_y(),
619                    source.get_width(),
620                    source.get_height(),
621                    target.get_x(),
622                    target.get_y(),
623                    target.get_width(),
624                    target.get_height(),
625                );
626        }
627    }
628
629    /// Records the nine-slice into a deferred draw list.
630    ///
631    /// The command carries the image plus the insets rather than the nine
632    /// resolved sub-rectangles, so the split is recomputed at replay time
633    /// against whatever destination size the command was recorded with.
634    ///
635    /// # Arguments
636    ///
637    /// - `&mut DrawList` - The draw list to record into.
638    /// - `Vector2D` - The destination top-left position in world space.
639    /// - `f64` - The destination width in pixels.
640    /// - `f64` - The destination height in pixels.
641    pub fn record(
642        &self,
643        list: &mut DrawList,
644        dest_position: Vector2D,
645        dest_width: f64,
646        dest_height: f64,
647    ) {
648        list.get_mut_commands().push(DrawCommand::DrawNineSlice {
649            image: self.get_image(),
650            insets: self.get_insets(),
651            dest_position,
652            dest_width,
653            dest_height,
654        });
655    }
656}
657
658/// Implements the pure name-to-rectangle index for `AtlasRegions`.
659impl AtlasRegions {
660    /// Inserts or replaces the source rectangle stored under a name.
661    ///
662    /// # Arguments
663    ///
664    /// - `&str` - The sprite name.
665    /// - `Rect` - The source rectangle in atlas pixels.
666    pub fn insert(&mut self, name: &str, region: Rect) {
667        self.get_mut_regions().insert(name.to_string(), region);
668    }
669
670    /// Returns the source rectangle stored under a name.
671    ///
672    /// # Arguments
673    ///
674    /// - `&str` - The sprite name.
675    ///
676    /// # Returns
677    ///
678    /// - `Option<Rect>` - The source rectangle, or `None` if the name is unknown.
679    pub fn get(&self, name: &str) -> Option<Rect> {
680        self.get_regions().get(name).copied()
681    }
682
683    /// Returns whether a name is present in the index.
684    ///
685    /// # Arguments
686    ///
687    /// - `&str` - The sprite name.
688    ///
689    /// # Returns
690    ///
691    /// - `bool` - `true` when the name has a stored rectangle.
692    pub fn contains(&self, name: &str) -> bool {
693        self.get_regions().contains_key(name)
694    }
695
696    /// Returns the number of named regions in the index.
697    ///
698    /// # Returns
699    ///
700    /// - `usize` - The region count.
701    pub fn len(&self) -> usize {
702        self.get_regions().len()
703    }
704
705    /// Returns whether the index holds no regions.
706    ///
707    /// # Returns
708    ///
709    /// - `bool` - `true` when no region has been inserted.
710    pub fn is_empty(&self) -> bool {
711        self.get_regions().is_empty()
712    }
713
714    /// Returns the stored names in unspecified order.
715    ///
716    /// # Returns
717    ///
718    /// - `Vec<String>` - Every stored sprite name.
719    pub fn names(&self) -> Vec<String> {
720        self.get_regions().keys().cloned().collect()
721    }
722}
723
724/// Implements image-backed drawing, UV conversion, and recording for `SpriteAtlas`.
725impl SpriteAtlas {
726    /// Creates an empty atlas backed by the given image.
727    ///
728    /// # Arguments
729    ///
730    /// - `HtmlImageElement` - The shared image holding every packed sprite.
731    ///
732    /// # Returns
733    ///
734    /// - `SpriteAtlas` - The new empty atlas.
735    pub fn create(image: HtmlImageElement) -> SpriteAtlas {
736        SpriteAtlas::new(image, AtlasRegions::default())
737    }
738
739    /// Inserts or replaces the source rectangle stored under a name.
740    ///
741    /// # Arguments
742    ///
743    /// - `&str` - The sprite name.
744    /// - `Rect` - The source rectangle in atlas pixels.
745    pub fn insert(&mut self, name: &str, region: Rect) {
746        self.get_mut_regions().insert(name, region);
747    }
748
749    /// Returns the source rectangle stored under a name.
750    ///
751    /// # Arguments
752    ///
753    /// - `&str` - The sprite name.
754    ///
755    /// # Returns
756    ///
757    /// - `Option<Rect>` - The source rectangle, or `None` if the name is unknown.
758    pub fn get(&self, name: &str) -> Option<Rect> {
759        self.get_regions().get(name)
760    }
761
762    /// Returns whether a name is present in the atlas.
763    ///
764    /// # Arguments
765    ///
766    /// - `&str` - The sprite name.
767    ///
768    /// # Returns
769    ///
770    /// - `bool` - `true` when the name has a stored rectangle.
771    pub fn contains(&self, name: &str) -> bool {
772        self.get_regions().contains(name)
773    }
774
775    /// Returns the number of named regions in the atlas.
776    ///
777    /// # Returns
778    ///
779    /// - `usize` - The region count.
780    pub fn len(&self) -> usize {
781        self.get_regions().len()
782    }
783
784    /// Returns whether the atlas holds no regions.
785    ///
786    /// # Returns
787    ///
788    /// - `bool` - `true` when no region has been inserted.
789    pub fn is_empty(&self) -> bool {
790        self.get_regions().is_empty()
791    }
792
793    /// Returns the normalized texture coordinates for one atlas region.
794    ///
795    /// Normalizes against the image's intrinsic (`naturalWidth` /
796    /// `naturalHeight`) size, which is the atlas's true texture size
797    /// regardless of any CSS size applied to the element. A zero-sized
798    /// atlas — not yet loaded, or decoded with no intrinsic size — yields
799    /// an all-zero `UvRect` rather than dividing by zero, so the result is
800    /// never `NaN` or infinite.
801    ///
802    /// # Arguments
803    ///
804    /// - `Rect` - The source rectangle in atlas pixels.
805    ///
806    /// # Returns
807    ///
808    /// - `UvRect` - The normalized `(u0, v0, u1, v1)` coordinates.
809    pub fn uv(&self, region: Rect) -> UvRect {
810        let image: &HtmlImageElement = &self.get_image();
811        let width: f64 = image.natural_width() as f64;
812        let height: f64 = image.natural_height() as f64;
813        Self::normalize_uv(region, width, height)
814    }
815
816    /// Normalizes a pixel rectangle against an explicit atlas size.
817    ///
818    /// The pure half of [`SpriteAtlas::uv`], separated so the arithmetic
819    /// can be exercised without an image element. A non-positive width
820    /// or height yields an all-zero `UvRect` instead of dividing by zero.
821    ///
822    /// # Arguments
823    ///
824    /// - `Rect` - The source rectangle in atlas pixels.
825    /// - `f64` - The atlas width in pixels.
826    /// - `f64` - The atlas height in pixels.
827    ///
828    /// # Returns
829    ///
830    /// - `UvRect` - The normalized `(u0, v0, u1, v1)` coordinates.
831    pub fn normalize_uv(region: Rect, width: f64, height: f64) -> UvRect {
832        if width <= 0.0 || height <= 0.0 {
833            return UvRect::new(0.0, 0.0, 0.0, 0.0);
834        }
835        UvRect::new(
836            region.get_x() / width,
837            region.get_y() / height,
838            (region.get_x() + region.get_width()) / width,
839            (region.get_y() + region.get_height()) / height,
840        )
841    }
842
843    /// Blits one named atlas region into a destination rectangle.
844    ///
845    /// Unknown names are a no-op. Uses the canvas transform already in
846    /// effect, so the caller controls world positioning.
847    ///
848    /// # Arguments
849    ///
850    /// - `&CanvasRenderingContext2d` - The canvas context.
851    /// - `&str` - The sprite name to blit.
852    /// - `Rect` - The destination rectangle in current canvas space.
853    pub fn draw(&self, context: &CanvasRenderingContext2d, name: &str, dest: Rect) {
854        let Some(source) = self.get(name) else {
855            return;
856        };
857        let _: Result<(), JsValue> = context
858            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
859                &self.get_image(),
860                source.get_x(),
861                source.get_y(),
862                source.get_width(),
863                source.get_height(),
864                dest.get_x(),
865                dest.get_y(),
866                dest.get_width(),
867                dest.get_height(),
868            );
869    }
870
871    /// Records one named atlas region into a deferred draw list.
872    ///
873    /// Unknown names are a no-op, so callers do not need to probe with
874    /// [`SpriteAtlas::get`] first.
875    ///
876    /// # Arguments
877    ///
878    /// - `&mut DrawList` - The draw list to record into.
879    /// - `&str` - The sprite name to blit.
880    /// - `Vector2D` - The destination top-left position in world space.
881    /// - `f64` - The destination width in pixels.
882    /// - `f64` - The destination height in pixels.
883    pub fn record(
884        &self,
885        list: &mut DrawList,
886        name: &str,
887        dest_position: Vector2D,
888        dest_width: f64,
889        dest_height: f64,
890    ) {
891        let Some(source) = self.get(name) else {
892            return;
893        };
894        list.get_mut_commands().push(DrawCommand::DrawAtlasRegion {
895            image: self.get_image(),
896            source,
897            dest_position,
898            dest_width,
899            dest_height,
900        });
901    }
902}