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}