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 ///
14 /// # Returns
15 ///
16 /// - `SpriteSheet` - The new sprite sheet.
17 pub fn from_image(image: HtmlImageElement, frame_width: f64, frame_height: f64) -> SpriteSheet {
18 let total_width: f64 = image.width() as f64;
19 let total_height: f64 = image.height() as f64;
20 let columns: u32 = (total_width / frame_width).max(1.0) as u32;
21 let rows: u32 = (total_height / frame_height).max(1.0) as u32;
22 SpriteSheet::new(image, frame_width, frame_height, columns, rows)
23 }
24
25 /// Returns the source rectangle for the frame at the given grid index.
26 ///
27 /// # Arguments
28 ///
29 /// - `u32` - The frame index (left-to-right, top-to-bottom).
30 ///
31 /// # Returns
32 ///
33 /// - `Rect` - The source rectangle.
34 pub(crate) fn frame_source(&self, index: u32) -> Rect {
35 let column: u32 = index % self.get_columns();
36 let row: u32 = index / self.get_columns();
37 Rect::new(
38 column as f64 * self.get_frame_width(),
39 row as f64 * self.get_frame_height(),
40 self.get_frame_width(),
41 self.get_frame_height(),
42 )
43 }
44
45 /// Creates a frame at the given grid index with a default duration.
46 ///
47 /// # Arguments
48 ///
49 /// - `u32` - The frame index.
50 ///
51 /// # Returns
52 ///
53 /// - `SpriteFrame` - The new frame.
54 pub fn frame(&self, index: u32) -> SpriteFrame {
55 SpriteFrame::new(self.frame_source(index), SPRITE_DEFAULT_FRAME_DURATION)
56 }
57
58 /// Creates an animation from a range of frame indices.
59 ///
60 /// # Arguments
61 ///
62 /// - `&str` - The animation name.
63 /// - `u32` - The starting frame index (inclusive).
64 /// - `AnimationMode` - The playback mode.
65 ///
66 /// # Returns
67 ///
68 /// - `SpriteAnimation` - The new animation.
69 pub fn animation(
70 &self,
71 name: &str,
72 start: u32,
73 end: u32,
74 mode: AnimationMode,
75 ) -> SpriteAnimation {
76 let frames: Vec<SpriteFrame> = (start..end).map(|index: u32| self.frame(index)).collect();
77 SpriteAnimation::new(name.to_string(), frames, mode)
78 }
79
80 /// Draws a static (non-animated) sprite frame onto the canvas.
81 ///
82 /// # Arguments
83 ///
84 /// - `&CanvasRenderingContext2d` - The canvas context.
85 /// - `u32` - The frame index to draw.
86 /// - `&Transform2D` - The world-space transform to apply.
87 pub fn draw_frame(
88 &self,
89 context: &CanvasRenderingContext2d,
90 frame_index: u32,
91 transform: &Transform2D,
92 ) {
93 let source: Rect = self.frame_source(frame_index);
94 // Compose the TRS matrix in Rust and apply it with a single `set_transform`
95 // instead of save/translate/rotate/scale/restore (5 FFI calls + a canvas
96 // state-stack push/pop per sprite). Scale signs handle flipping.
97 let rotation: f64 = transform.get_rotation();
98 let cos: f64 = rotation.cos();
99 let sin: f64 = rotation.sin();
100 let scale_x: f64 = transform.get_scale().get_x();
101 let scale_y: f64 = transform.get_scale().get_y();
102 let _: Result<(), JsValue> = context.set_transform(
103 cos * scale_x,
104 sin * scale_x,
105 -sin * scale_y,
106 cos * scale_y,
107 transform.get_position().get_x(),
108 transform.get_position().get_y(),
109 );
110 let _: Result<(), JsValue> = context
111 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
112 self.get_image(),
113 source.get_x(),
114 source.get_y(),
115 source.get_width(),
116 source.get_height(),
117 -source.get_width() * 0.5,
118 -source.get_height() * 0.5,
119 source.get_width(),
120 source.get_height(),
121 );
122 let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
123 }
124}
125
126/// Implements playback control for `Animator`.
127impl Animator {
128 /// Creates a new idle animator with no animation.
129 ///
130 /// # Returns
131 ///
132 /// - `Animator` - The new animator.
133 pub fn create() -> Animator {
134 Animator::new(AnimationState::Paused, 1)
135 }
136
137 /// Plays the given animation from the beginning.
138 ///
139 /// # Arguments
140 ///
141 /// - `SpriteAnimation` - The animation to play.
142 pub fn play(&mut self, animation: SpriteAnimation) {
143 self.set_current_animation(Some(animation));
144 self.set_current_frame_index(0);
145 self.set_elapsed_time(0.0);
146 self.set_state(AnimationState::Playing);
147 self.set_direction(1);
148 }
149
150 /// Pauses the current animation.
151 pub fn pause(&mut self) {
152 if self.get_state() == AnimationState::Playing {
153 self.set_state(AnimationState::Paused);
154 }
155 }
156
157 /// Resumes the current animation from where it was paused.
158 pub fn resume(&mut self) {
159 if self.get_state() == AnimationState::Paused {
160 self.set_state(AnimationState::Playing);
161 }
162 }
163
164 /// Stops the current animation and resets to the first frame.
165 pub fn stop(&mut self) {
166 self.set_current_frame_index(0);
167 self.set_elapsed_time(0.0);
168 self.set_state(AnimationState::Paused);
169 self.set_direction(1);
170 }
171
172 /// Advances the animation by the given delta time.
173 ///
174 /// # Arguments
175 ///
176 /// - `f64` - The time elapsed since the last update, in seconds.
177 pub fn update(&mut self, delta_time: f64) {
178 if self.get_state() != AnimationState::Playing {
179 return;
180 }
181 let current_frame_index: usize = self.get_current_frame_index();
182 let (frame_count, current_duration, mode) = {
183 let Some(animation) = self.get_mut_current_animation().as_ref() else {
184 return;
185 };
186 if animation.get_frames().is_empty() {
187 return;
188 }
189 (
190 animation.get_frames().len(),
191 animation.get_frames()[current_frame_index].get_duration(),
192 animation.get_mode(),
193 )
194 };
195 *self.get_mut_elapsed_time() += delta_time;
196 if self.get_elapsed_time() < current_duration {
197 return;
198 }
199 self.set_elapsed_time(0.0);
200 self.advance_frame(frame_count, mode);
201 }
202
203 /// Returns the source rectangle of the current frame, or `None` if no animation is playing.
204 ///
205 /// # Returns
206 ///
207 /// - `Option<Rect>` - The current frame's source rectangle.
208 pub fn current_frame_source(&self) -> Option<Rect> {
209 let animation: Option<SpriteAnimation> = self.get_current_animation();
210 let animation: &SpriteAnimation = animation.as_ref()?;
211 let frame: &SpriteFrame = animation.get_frames().get(self.get_current_frame_index())?;
212 Some(frame.get_source())
213 }
214
215 /// Advances to the next frame according to the animation mode.
216 ///
217 /// # Arguments
218 ///
219 /// - `usize` - The total number of frames in the current animation.
220 /// - `AnimationMode` - The playback mode driving the advance.
221 fn advance_frame(&mut self, frame_count: usize, mode: AnimationMode) {
222 match mode {
223 AnimationMode::Loop => {
224 self.set_current_frame_index((self.get_current_frame_index() + 1) % frame_count);
225 }
226 AnimationMode::Once => {
227 if self.get_current_frame_index() + 1 < frame_count {
228 *self.get_mut_current_frame_index() += 1;
229 } else {
230 self.set_state(AnimationState::Finished);
231 }
232 }
233 AnimationMode::PingPong => {
234 if self.get_direction() > 0 {
235 if self.get_current_frame_index() + 1 < frame_count {
236 *self.get_mut_current_frame_index() += 1;
237 } else {
238 self.set_direction(-1);
239 if self.get_current_frame_index() > 0 {
240 *self.get_mut_current_frame_index() -= 1;
241 }
242 }
243 } else if self.get_current_frame_index() > 0 {
244 *self.get_mut_current_frame_index() -= 1;
245 } else {
246 self.set_direction(1);
247 if self.get_current_frame_index() + 1 < frame_count {
248 *self.get_mut_current_frame_index() += 1;
249 }
250 }
251 }
252 }
253 }
254}
255
256/// Forwards `Animator::update` through the [`Updatable`] trait so that
257/// collections of heterogeneous updateable objects (entities, animators,
258/// physics worlds, scene managers) can be driven by a single scheduler loop.
259///
260/// The inherent [`Animator::update`] method is the canonical implementation;
261/// this impl exists purely for trait dispatch. The inherent call resolves
262/// first when both are in scope, so there is no recursion.
263impl Updatable for Animator {
264 /// Advances the simulation by `delta_time` seconds.
265 ///
266 /// # Arguments
267 ///
268 /// - `f64` - Seconds elapsed since the previous update.
269 fn update(&mut self, delta_time: f64) {
270 Animator::update(self, delta_time);
271 }
272}
273
274/// Implements animated sprite rendering for `Animator`.
275impl Animator {
276 /// Draws the current frame of this animator onto the canvas.
277 ///
278 /// # Arguments
279 ///
280 /// - `&CanvasRenderingContext2d` - The canvas context.
281 /// - `&SpriteSheet` - The sprite sheet containing the image.
282 /// - `&Transform2D` - The world-space transform to apply.
283 pub fn draw(
284 &self,
285 context: &CanvasRenderingContext2d,
286 sheet: &SpriteSheet,
287 transform: &Transform2D,
288 ) {
289 let Some(source) = self.current_frame_source() else {
290 return;
291 };
292 // Compose the TRS matrix in Rust (flip signs folded into scale) and apply
293 // it with a single `set_transform` instead of save/translate/rotate/scale/restore.
294 let rotation: f64 = transform.get_rotation();
295 let cos: f64 = rotation.cos();
296 let sin: f64 = rotation.sin();
297 let scale_x: f64 = if self.get_flip_x() {
298 -transform.get_scale().get_x()
299 } else {
300 transform.get_scale().get_x()
301 };
302 let scale_y: f64 = if self.get_flip_y() {
303 -transform.get_scale().get_y()
304 } else {
305 transform.get_scale().get_y()
306 };
307 let _: Result<(), JsValue> = context.set_transform(
308 cos * scale_x,
309 sin * scale_x,
310 -sin * scale_y,
311 cos * scale_y,
312 transform.get_position().get_x(),
313 transform.get_position().get_y(),
314 );
315 let _: Result<(), JsValue> = context
316 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
317 sheet.get_image(),
318 source.get_x(),
319 source.get_y(),
320 source.get_width(),
321 source.get_height(),
322 -source.get_width() * 0.5,
323 -source.get_height() * 0.5,
324 source.get_width(),
325 source.get_height(),
326 );
327 let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
328 }
329}
330
331/// Implements `Default` for `Animator` as a new idle animator.
332impl Default for Animator {
333 /// Constructs a default [`Animator`] value.
334 ///
335 /// # Returns
336 ///
337 /// - `Animator` - A default-constructed instance with the documented initial state.
338 fn default() -> Animator {
339 Animator::create()
340 }
341}
342
343/// Implements named access over the three-by-three nine-slice grid.
344///
345/// The index constants live in this module's `const.rs`; each accessor
346/// maps a grid position to a self-documenting name so call sites and
347/// assert messages read as geometry rather than as `(row, column)` pairs.
348impl NineSliceRects {
349 /// Returns the top-left corner sub-rectangle.
350 ///
351 /// # Returns
352 ///
353 /// - `Rect` - The top-left corner patch.
354 pub fn get_top_left(&self) -> Rect {
355 self.get_grid()[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_LEFT]
356 }
357
358 /// Returns the top edge sub-rectangle.
359 ///
360 /// # Returns
361 ///
362 /// - `Rect` - The top edge patch.
363 pub fn get_top(&self) -> Rect {
364 self.get_grid()[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_CENTER]
365 }
366
367 /// Returns the top-right corner sub-rectangle.
368 ///
369 /// # Returns
370 ///
371 /// - `Rect` - The top-right corner patch.
372 pub fn get_top_right(&self) -> Rect {
373 self.get_grid()[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_RIGHT]
374 }
375
376 /// Returns the left edge sub-rectangle.
377 ///
378 /// # Returns
379 ///
380 /// - `Rect` - The left edge patch.
381 pub fn get_left(&self) -> Rect {
382 self.get_grid()[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_LEFT]
383 }
384
385 /// Returns the stretchable center sub-rectangle.
386 ///
387 /// # Returns
388 ///
389 /// - `Rect` - The center patch.
390 pub fn get_center(&self) -> Rect {
391 self.get_grid()[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_CENTER]
392 }
393
394 /// Returns the right edge sub-rectangle.
395 ///
396 /// # Returns
397 ///
398 /// - `Rect` - The right edge patch.
399 pub fn get_right(&self) -> Rect {
400 self.get_grid()[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_RIGHT]
401 }
402
403 /// Returns the bottom-left corner sub-rectangle.
404 ///
405 /// # Returns
406 ///
407 /// - `Rect` - The bottom-left corner patch.
408 pub fn get_bottom_left(&self) -> Rect {
409 self.get_grid()[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_LEFT]
410 }
411
412 /// Returns the bottom edge sub-rectangle.
413 ///
414 /// # Returns
415 ///
416 /// - `Rect` - The bottom edge patch.
417 pub fn get_bottom(&self) -> Rect {
418 self.get_grid()[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_CENTER]
419 }
420
421 /// Returns the bottom-right corner sub-rectangle.
422 ///
423 /// # Returns
424 ///
425 /// - `Rect` - The bottom-right corner patch.
426 pub fn get_bottom_right(&self) -> Rect {
427 self.get_grid()[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_RIGHT]
428 }
429
430 /// Returns the nine patches as a flat slice in reading order.
431 ///
432 /// # Returns
433 ///
434 /// - `Vec<Rect>` - The nine patches, top row first then middle then bottom.
435 pub fn to_vec(&self) -> Vec<Rect> {
436 let grid: [[Rect; 3]; 3] = self.get_grid();
437 vec![
438 grid[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_LEFT],
439 grid[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_CENTER],
440 grid[NINE_SLICE_ROW_TOP][NINE_SLICE_COL_RIGHT],
441 grid[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_LEFT],
442 grid[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_CENTER],
443 grid[NINE_SLICE_ROW_MIDDLE][NINE_SLICE_COL_RIGHT],
444 grid[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_LEFT],
445 grid[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_CENTER],
446 grid[NINE_SLICE_ROW_BOTTOM][NINE_SLICE_COL_RIGHT],
447 ]
448 }
449}
450
451/// Implements the nine-slice geometry math for `NineSliceInsets`.
452///
453/// All methods here are pure functions of the insets and the rectangle
454/// being split: no canvas context, no image handle, no DOM. That is what
455/// makes the split verifiable on the host, and it is the same math
456/// [`NineSlice::draw_into`] and [`NineSlice::record`] reuse.
457impl NineSliceInsets {
458 /// Splits a source rectangle into its nine sub-rectangles.
459 ///
460 /// The insets are clamped against the source size so the nine results
461 /// always tile the source exactly: the two horizontal insets share the
462 /// available width and the two vertical insets share the available
463 /// height, with the center taking whatever remains.
464 ///
465 /// # Arguments
466 ///
467 /// - `Rect` - The full source rectangle to split.
468 ///
469 /// # Returns
470 ///
471 /// - `NineSliceRects` - The nine source sub-rectangles in reading order.
472 pub fn source_rects(&self, source: Rect) -> NineSliceRects {
473 let width: f64 = Numeric::clamp(source.get_width(), 0.0, f64::MAX);
474 let height: f64 = Numeric::clamp(source.get_height(), 0.0, f64::MAX);
475 let left: f64 = Numeric::clamp(self.get_left(), 0.0, width);
476 let right: f64 = Numeric::clamp(self.get_right(), 0.0, width - left);
477 let top: f64 = Numeric::clamp(self.get_top(), 0.0, height);
478 let bottom: f64 = Numeric::clamp(self.get_bottom(), 0.0, height - top);
479 let center_x: f64 = source.get_x() + left;
480 let center_y: f64 = source.get_y() + top;
481 let center_width: f64 = width - left - right;
482 let center_height: f64 = height - top - bottom;
483 let right_x: f64 = center_x + center_width;
484 let bottom_y: f64 = center_y + center_height;
485 NineSliceRects::new([
486 [
487 Rect::new(source.get_x(), source.get_y(), left, top),
488 Rect::new(center_x, source.get_y(), center_width, top),
489 Rect::new(right_x, source.get_y(), right, top),
490 ],
491 [
492 Rect::new(source.get_x(), center_y, left, center_height),
493 Rect::new(center_x, center_y, center_width, center_height),
494 Rect::new(right_x, center_y, right, center_height),
495 ],
496 [
497 Rect::new(source.get_x(), bottom_y, left, bottom),
498 Rect::new(center_x, bottom_y, center_width, bottom),
499 Rect::new(right_x, bottom_y, right, bottom),
500 ],
501 ])
502 }
503
504 /// Splits a destination rectangle into its nine sub-rectangles.
505 ///
506 /// Corners and edges keep their natural pixel size taken from the
507 /// insets, and the center absorbs all remaining destination space, so
508 /// the nine results tile the destination exactly. When the
509 /// destination is smaller than the combined borders the center
510 /// collapses to zero width and/or height instead of going negative.
511 ///
512 /// # Arguments
513 ///
514 /// - `Rect` - The full destination rectangle to split.
515 ///
516 /// # Returns
517 ///
518 /// - `NineSliceRects` - The nine destination sub-rectangles in reading order.
519 pub fn dest_rects(&self, dest: Rect) -> NineSliceRects {
520 let left: f64 = Numeric::clamp(self.get_left(), 0.0, f64::MAX);
521 let right: f64 = Numeric::clamp(self.get_right(), 0.0, f64::MAX);
522 let top: f64 = Numeric::clamp(self.get_top(), 0.0, f64::MAX);
523 let bottom: f64 = Numeric::clamp(self.get_bottom(), 0.0, f64::MAX);
524 let left_edge: f64 = Numeric::clamp(left, 0.0, dest.get_width());
525 let right_edge: f64 = Numeric::clamp(right, 0.0, dest.get_width() - left_edge);
526 let top_edge: f64 = Numeric::clamp(top, 0.0, dest.get_height());
527 let bottom_edge: f64 = Numeric::clamp(bottom, 0.0, dest.get_height() - top_edge);
528 let center_x: f64 = dest.get_x() + left_edge;
529 let center_y: f64 = dest.get_y() + top_edge;
530 let center_width: f64 = dest.get_width() - left_edge - right_edge;
531 let center_height: f64 = dest.get_height() - top_edge - bottom_edge;
532 let right_x: f64 = center_x + center_width;
533 let bottom_y: f64 = center_y + center_height;
534 NineSliceRects::new([
535 [
536 Rect::new(dest.get_x(), dest.get_y(), left_edge, top_edge),
537 Rect::new(center_x, dest.get_y(), center_width, top_edge),
538 Rect::new(right_x, dest.get_y(), right_edge, top_edge),
539 ],
540 [
541 Rect::new(dest.get_x(), center_y, left_edge, center_height),
542 Rect::new(center_x, center_y, center_width, center_height),
543 Rect::new(right_x, center_y, right_edge, center_height),
544 ],
545 [
546 Rect::new(dest.get_x(), bottom_y, left_edge, bottom_edge),
547 Rect::new(center_x, bottom_y, center_width, bottom_edge),
548 Rect::new(right_x, bottom_y, right_edge, bottom_edge),
549 ],
550 ])
551 }
552}
553
554/// Implements image-backed nine-slice drawing and deferred recording.
555impl NineSlice {
556 /// Creates a nine-slice from an image and uniform border insets.
557 ///
558 /// # Arguments
559 ///
560 /// - `HtmlImageElement` - The source image containing the nine patches.
561 /// - `f64` - The border inset applied to all four edges, in source pixels.
562 ///
563 /// # Returns
564 ///
565 /// - `NineSlice` - The new nine-slice.
566 pub fn from_image(image: HtmlImageElement, border: f64) -> NineSlice {
567 NineSlice::new(image, NineSliceInsets::new(border, border, border, border))
568 }
569
570 /// Returns the nine source sub-rectangles for the whole source image.
571 ///
572 /// # Returns
573 ///
574 /// - `NineSliceRects` - The nine source sub-rectangles in reading order.
575 pub fn source_rects(&self) -> NineSliceRects {
576 let image: &HtmlImageElement = &self.get_image();
577 let source: Rect = Rect::new(0.0, 0.0, image.width() as f64, image.height() as f64);
578 self.get_insets().source_rects(source)
579 }
580
581 /// Returns the nine destination sub-rectangles for a destination rect.
582 ///
583 /// # Arguments
584 ///
585 /// - `Rect` - The destination rectangle to split.
586 ///
587 /// # Returns
588 ///
589 /// - `NineSliceRects` - The nine destination sub-rectangles in reading order.
590 pub fn dest_rects(&self, dest: Rect) -> NineSliceRects {
591 self.get_insets().dest_rects(dest)
592 }
593
594 /// Draws the nine-slice into a destination rectangle immediately.
595 ///
596 /// Issues nine `drawImage` calls against the canvas context, pairing
597 /// each source patch with its destination sub-rectangle. Uses the
598 /// canvas transform already in effect, so the caller controls world
599 /// positioning, rotation and scale.
600 ///
601 /// # Arguments
602 ///
603 /// - `&CanvasRenderingContext2d` - The canvas context.
604 /// - `Rect` - The destination rectangle in current canvas space.
605 pub fn draw_into(&self, context: &CanvasRenderingContext2d, dest: Rect) {
606 let sources: Vec<Rect> = self.source_rects().to_vec();
607 let dests: Vec<Rect> = self.dest_rects(dest).to_vec();
608 let image: &HtmlImageElement = &self.get_image();
609 for index in 0..sources.len() {
610 let source: Rect = sources[index];
611 let target: Rect = dests[index];
612 let _: Result<(), JsValue> = context
613 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
614 image,
615 source.get_x(),
616 source.get_y(),
617 source.get_width(),
618 source.get_height(),
619 target.get_x(),
620 target.get_y(),
621 target.get_width(),
622 target.get_height(),
623 );
624 }
625 }
626
627 /// Records the nine-slice into a deferred draw list.
628 ///
629 /// The command carries the image plus the insets rather than the nine
630 /// resolved sub-rectangles, so the split is recomputed at replay time
631 /// against whatever destination size the command was recorded with.
632 ///
633 /// # Arguments
634 ///
635 /// - `&mut DrawList` - The draw list to record into.
636 /// - `Vector2D` - The destination top-left position in world space.
637 /// - `f64` - The destination width in pixels.
638 pub fn record(
639 &self,
640 list: &mut DrawList,
641 dest_position: Vector2D,
642 dest_width: f64,
643 dest_height: f64,
644 ) {
645 list.get_mut_commands().push(DrawCommand::DrawNineSlice {
646 image: self.get_image(),
647 insets: self.get_insets(),
648 dest_position,
649 dest_width,
650 dest_height,
651 });
652 }
653}
654
655/// Implements the pure name-to-rectangle index for `AtlasRegions`.
656impl AtlasRegions {
657 /// Inserts or replaces the source rectangle stored under a name.
658 ///
659 /// # Arguments
660 ///
661 /// - `&str` - The sprite name.
662 /// - `Rect` - The source rectangle in atlas pixels.
663 pub fn insert(&mut self, name: &str, region: Rect) {
664 self.get_mut_regions().insert(name.to_string(), region);
665 }
666
667 /// Returns the source rectangle stored under a name.
668 ///
669 /// # Arguments
670 ///
671 /// - `&str` - The sprite name.
672 ///
673 /// # Returns
674 ///
675 /// - `Option<Rect>` - The source rectangle, or `None` if the name is unknown.
676 pub fn get(&self, name: &str) -> Option<Rect> {
677 self.get_regions().get(name).copied()
678 }
679
680 /// Returns whether a name is present in the index.
681 ///
682 /// # Arguments
683 ///
684 /// - `&str` - The sprite name.
685 ///
686 /// # Returns
687 ///
688 /// - `bool` - `true` when the name has a stored rectangle.
689 pub fn contains(&self, name: &str) -> bool {
690 self.get_regions().contains_key(name)
691 }
692
693 /// Returns the number of named regions in the index.
694 ///
695 /// # Returns
696 ///
697 /// - `usize` - The region count.
698 pub fn len(&self) -> usize {
699 self.get_regions().len()
700 }
701
702 /// Returns whether the index holds no regions.
703 ///
704 /// # Returns
705 ///
706 /// - `bool` - `true` when no region has been inserted.
707 pub fn is_empty(&self) -> bool {
708 self.get_regions().is_empty()
709 }
710
711 /// Returns the stored names in unspecified order.
712 ///
713 /// # Returns
714 ///
715 /// - `Vec<String>` - Every stored sprite name.
716 pub fn names(&self) -> Vec<String> {
717 self.get_regions().keys().cloned().collect()
718 }
719}
720
721/// Implements image-backed drawing, UV conversion, and recording for `SpriteAtlas`.
722impl SpriteAtlas {
723 /// Creates an empty atlas backed by the given image.
724 ///
725 /// # Arguments
726 ///
727 /// - `HtmlImageElement` - The shared image holding every packed sprite.
728 ///
729 /// # Returns
730 ///
731 /// - `SpriteAtlas` - The new empty atlas.
732 pub fn create(image: HtmlImageElement) -> SpriteAtlas {
733 SpriteAtlas::new(image, AtlasRegions::default())
734 }
735
736 /// Inserts or replaces the source rectangle stored under a name.
737 ///
738 /// # Arguments
739 ///
740 /// - `&str` - The sprite name.
741 /// - `Rect` - The source rectangle in atlas pixels.
742 pub fn insert(&mut self, name: &str, region: Rect) {
743 self.get_mut_regions().insert(name, region);
744 }
745
746 /// Returns the source rectangle stored under a name.
747 ///
748 /// # Arguments
749 ///
750 /// - `&str` - The sprite name.
751 ///
752 /// # Returns
753 ///
754 /// - `Option<Rect>` - The source rectangle, or `None` if the name is unknown.
755 pub fn get(&self, name: &str) -> Option<Rect> {
756 self.get_regions().get(name)
757 }
758
759 /// Returns whether a name is present in the atlas.
760 ///
761 /// # Arguments
762 ///
763 /// - `&str` - The sprite name.
764 ///
765 /// # Returns
766 ///
767 /// - `bool` - `true` when the name has a stored rectangle.
768 pub fn contains(&self, name: &str) -> bool {
769 self.get_regions().contains(name)
770 }
771
772 /// Returns the number of named regions in the atlas.
773 ///
774 /// # Returns
775 ///
776 /// - `usize` - The region count.
777 pub fn len(&self) -> usize {
778 self.get_regions().len()
779 }
780
781 /// Returns whether the atlas holds no regions.
782 ///
783 /// # Returns
784 ///
785 /// - `bool` - `true` when no region has been inserted.
786 pub fn is_empty(&self) -> bool {
787 self.get_regions().is_empty()
788 }
789
790 /// Returns the normalized texture coordinates for one atlas region.
791 ///
792 /// Normalizes against the image's intrinsic (`naturalWidth` /
793 /// `naturalHeight`) size, which is the atlas's true texture size
794 /// regardless of any CSS size applied to the element. A zero-sized
795 /// atlas — not yet loaded, or decoded with no intrinsic size — yields
796 /// an all-zero `UvRect` rather than dividing by zero, so the result is
797 /// never `NaN` or infinite.
798 ///
799 /// # Arguments
800 ///
801 /// - `Rect` - The source rectangle in atlas pixels.
802 ///
803 /// # Returns
804 ///
805 /// - `UvRect` - The normalized `(u0, v0, u1, v1)` coordinates.
806 pub fn uv(&self, region: Rect) -> UvRect {
807 let image: &HtmlImageElement = &self.get_image();
808 let width: f64 = image.natural_width() as f64;
809 let height: f64 = image.natural_height() as f64;
810 Self::normalize_uv(region, width, height)
811 }
812
813 /// Normalizes a pixel rectangle against an explicit atlas size.
814 ///
815 /// The pure half of [`SpriteAtlas::uv`], separated so the arithmetic
816 /// can be exercised without an image element. A non-positive width
817 /// or height yields an all-zero `UvRect` instead of dividing by zero.
818 ///
819 /// # Arguments
820 ///
821 /// - `Rect` - The source rectangle in atlas pixels.
822 /// - `f64` - The atlas width in pixels.
823 ///
824 /// # Returns
825 ///
826 /// - `UvRect` - The normalized `(u0, v0, u1, v1)` coordinates.
827 pub fn normalize_uv(region: Rect, width: f64, height: f64) -> UvRect {
828 if width <= 0.0 || height <= 0.0 {
829 return UvRect::new(0.0, 0.0, 0.0, 0.0);
830 }
831 UvRect::new(
832 region.get_x() / width,
833 region.get_y() / height,
834 (region.get_x() + region.get_width()) / width,
835 (region.get_y() + region.get_height()) / height,
836 )
837 }
838
839 /// Blits one named atlas region into a destination rectangle.
840 ///
841 /// Unknown names are a no-op. Uses the canvas transform already in
842 /// effect, so the caller controls world positioning.
843 ///
844 /// # Arguments
845 ///
846 /// - `&CanvasRenderingContext2d` - The canvas context.
847 /// - `&str` - The sprite name to blit.
848 /// - `Rect` - The destination rectangle in current canvas space.
849 pub fn draw(&self, context: &CanvasRenderingContext2d, name: &str, dest: Rect) {
850 let Some(source) = self.get(name) else {
851 return;
852 };
853 let _: Result<(), JsValue> = context
854 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
855 &self.get_image(),
856 source.get_x(),
857 source.get_y(),
858 source.get_width(),
859 source.get_height(),
860 dest.get_x(),
861 dest.get_y(),
862 dest.get_width(),
863 dest.get_height(),
864 );
865 }
866
867 /// Records one named atlas region into a deferred draw list.
868 ///
869 /// Unknown names are a no-op, so callers do not need to probe with
870 /// [`SpriteAtlas::get`] first.
871 ///
872 /// # Arguments
873 ///
874 /// - `&mut DrawList` - The draw list to record into.
875 /// - `&str` - The sprite name to blit.
876 /// - `Vector2D` - The destination top-left position in world space.
877 /// - `f64` - The destination width in pixels.
878 pub fn record(
879 &self,
880 list: &mut DrawList,
881 name: &str,
882 dest_position: Vector2D,
883 dest_width: f64,
884 dest_height: f64,
885 ) {
886 let Some(source) = self.get(name) else {
887 return;
888 };
889 list.get_mut_commands().push(DrawCommand::DrawAtlasRegion {
890 image: self.get_image(),
891 source,
892 dest_position,
893 dest_width,
894 dest_height,
895 });
896 }
897}