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