euv_engine/renderer/canvas/impl.rs
1use super::*;
2
3/// Implements camera transformation methods for `Camera2D`.
4impl Camera2D {
5 /// Creates a new camera centered at the origin with default zoom and no rotation.
6 ///
7 /// # Arguments
8 ///
9 /// - `f64` - The viewport width in pixels.
10 /// - `f64` - The viewport height in pixels.
11 ///
12 /// # Returns
13 ///
14 /// - `Camera2D` - The new camera.
15 pub fn create(viewport_width: f64, viewport_height: f64) -> Camera2D {
16 Camera2D::new(
17 Vector2D::zero(),
18 RENDERER_DEFAULT_CAMERA_ZOOM,
19 RENDERER_DEFAULT_CAMERA_ROTATION,
20 viewport_width,
21 viewport_height,
22 )
23 }
24
25 /// Converts a world-space point to screen-space coordinates.
26 ///
27 /// # Arguments
28 ///
29 /// - `Vector2D` - The world-space point.
30 ///
31 /// # Returns
32 ///
33 /// - `Vector2D` - The screen-space point.
34 pub fn world_to_screen(&self, world: Vector2D) -> Vector2D {
35 let relative: Vector2D = world - self.get_position();
36 let rotated: Vector2D = relative.rotated(-self.get_rotation());
37 Vector2D::new(
38 rotated.get_x() * self.get_zoom() + self.get_viewport_width() * 0.5,
39 rotated.get_y() * self.get_zoom() + self.get_viewport_height() * 0.5,
40 )
41 }
42
43 /// Converts a screen-space point to world-space coordinates.
44 ///
45 /// # Arguments
46 ///
47 /// - `Vector2D` - The screen-space point.
48 ///
49 /// # Returns
50 ///
51 /// - `Vector2D` - The world-space point.
52 pub fn screen_to_world(&self, screen: Vector2D) -> Vector2D {
53 let relative: Vector2D = Vector2D::new(
54 (screen.get_x() - self.get_viewport_width() * 0.5) / self.get_zoom(),
55 (screen.get_y() - self.get_viewport_height() * 0.5) / self.get_zoom(),
56 );
57 let rotated: Vector2D = relative.rotated(self.get_rotation());
58 rotated + self.get_position()
59 }
60
61 /// Moves the camera position by the given offset.
62 ///
63 /// # Arguments
64 ///
65 /// - `Vector2D` - The translation offset in world space.
66 pub fn translate(&mut self, offset: Vector2D) {
67 self.set_position(self.get_position() + offset);
68 }
69
70 /// Adjusts the zoom by the given factor, clamped to a minimum of `EPSILON`.
71 ///
72 /// # Arguments
73 ///
74 /// - `f64` - The zoom multiplier.
75 pub fn zoom_by(&mut self, factor: f64) {
76 self.set_zoom((self.get_zoom() * factor).max(EPSILON));
77 }
78}
79
80/// Implements `Default` for `Camera2D` as a camera at the origin with 800x600 viewport.
81impl Default for Camera2D {
82 /// Constructs a default [`Camera2D`] value.
83 ///
84 /// # Returns
85 ///
86 /// - `Camera2D` - A default-constructed instance with the documented initial state.
87 fn default() -> Camera2D {
88 Camera2D::create(800.0, 600.0)
89 }
90}
91
92/// Implements static font and color utility methods for `CanvasRenderer`.
93impl CanvasRenderer {
94 /// Builds a CSS font string from font size and family.
95 ///
96 /// # Arguments
97 ///
98 /// - `f64` - The font size in pixels.
99 /// - `F` - The font family name.
100 ///
101 /// # Returns
102 ///
103 /// - `String` - The CSS font string (e.g., `"16px sans-serif"`).
104 pub fn font<F>(size: f64, family: F) -> String
105 where
106 F: AsRef<str>,
107 {
108 let family: &str = family.as_ref();
109 format!("{size}px {family}")
110 }
111
112 /// Creates a default font string using the default font size and family.
113 ///
114 /// # Returns
115 ///
116 /// - `String` - The default CSS font string.
117 pub fn default_font() -> String {
118 Self::font(RENDERER_DEFAULT_FONT_SIZE, RENDERER_DEFAULT_FONT_FAMILY)
119 }
120
121 /// Enables high-quality anti-aliasing on an arbitrary canvas 2D context.
122 ///
123 /// Applies the `High` rendering quality preset via `apply_quality`,
124 /// which sets `imageSmoothingEnabled`, `imageSmoothingQuality = "high"`,
125 /// and `textRendering = "geometricPrecision"` on the given context.
126 ///
127 /// Use this static helper when you manage your own `CanvasRenderingContext2d`
128 /// and don't hold a `CanvasRenderer` instance. For instances, call
129 /// `renderer.enable_smoothing()` instead.
130 ///
131 /// # Arguments
132 ///
133 /// - `&CanvasRenderingContext2d` - The canvas context to configure.
134 pub fn enable_smoothing_on(context: &CanvasRenderingContext2d) {
135 Self::apply_quality(context, RenderQuality::High);
136 }
137
138 /// Detects the host device pixel ratio (HiDPI scale factor) via reflection.
139 ///
140 /// Reads `window.devicePixelRatio` using `Reflect::get` because the
141 /// `web-sys` `Window` features currently in use do not expose a native
142 /// getter for this property. Falls back to
143 /// `RENDERER_DEFAULT_DEVICE_PIXEL_RATIO` (1.0) when the global window or
144 /// the value is missing, not a finite number, or below 1.0.
145 ///
146 /// # Returns
147 ///
148 /// - `f64` - The detected device pixel ratio (clamped to `>= 1.0`).
149 pub fn detect_dpr() -> f64 {
150 let Some(window_value) = window() else {
151 return RENDERER_DEFAULT_DEVICE_PIXEL_RATIO;
152 };
153 let raw: Option<f64> = Reflect::get(
154 window_value.as_ref(),
155 &JsValue::from_str(RENDERER_PROPERTY_DEVICE_PIXEL_RATIO),
156 )
157 .ok()
158 .and_then(|value: JsValue| value.as_f64());
159 raw.filter(|value: &f64| value.is_finite() && *value >= 1.0)
160 .unwrap_or(RENDERER_DEFAULT_DEVICE_PIXEL_RATIO)
161 }
162
163 /// Applies the given `RenderQuality` preset to an arbitrary canvas context.
164 ///
165 /// Sets `imageSmoothingEnabled`, `imageSmoothingQuality`, and
166 /// `textRendering` according to the supplied quality. `Low` disables
167 /// smoothing (intended for use with CSS `image-rendering: pixelated`),
168 /// `Medium` and `High` enable it with the matching quality level.
169 ///
170 /// # Arguments
171 ///
172 /// - `&CanvasRenderingContext2d` - The target context.
173 /// - `RenderQuality` - The quality preset to apply.
174 pub(crate) fn apply_quality(context: &CanvasRenderingContext2d, quality: RenderQuality) {
175 let smoothing_enabled: bool = !matches!(quality, RenderQuality::Low);
176 context.set_image_smoothing_enabled(smoothing_enabled);
177 let quality_value: &str = match quality {
178 RenderQuality::Low => RENDERER_IMAGE_SMOOTHING_QUALITY_LOW,
179 RenderQuality::Medium => RENDERER_IMAGE_SMOOTHING_QUALITY_MEDIUM,
180 RenderQuality::High => RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH,
181 };
182 let _: Result<bool, JsValue> = Reflect::set(
183 context,
184 &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
185 &JsValue::from_str(quality_value),
186 );
187 let _: Result<bool, JsValue> = Reflect::set(
188 context,
189 &JsValue::from_str(RENDERER_PROPERTY_TEXT_RENDERING),
190 &JsValue::from_str(RENDERER_TEXT_RENDERING_GEOMETRIC_PRECISION),
191 );
192 }
193}
194
195/// Implements static CSS conversion for `Color`.
196impl Color {
197 /// Converts a `Color` to a CSS `rgba()` string suitable for canvas fill or stroke styles.
198 ///
199 /// # Arguments
200 ///
201 /// - `&Color` - The color to convert.
202 ///
203 /// # Returns
204 ///
205 /// - `String` - The CSS `rgba()` color string.
206 pub fn to_css(color: &Color) -> String {
207 color.to_css_rgba()
208 }
209}
210
211/// Implements drawing and camera management methods for `CanvasRenderer`.
212/// Implements recording and replay for `DrawList`.
213impl DrawList {
214 /// Creates an empty draw list.
215 ///
216 /// # Returns
217 ///
218 /// - `DrawList` - The new empty draw list.
219 pub fn create() -> DrawList {
220 DrawList::new(Vec::new())
221 }
222
223 /// Returns whether the list contains no commands.
224 ///
225 /// # Returns
226 ///
227 /// - `bool` - `true` if there are no recorded commands.
228 pub fn is_empty(&self) -> bool {
229 self.get_commands().is_empty()
230 }
231
232 /// Returns the number of recorded commands.
233 ///
234 /// # Returns
235 ///
236 /// - `usize` - The command count.
237 pub fn len(&self) -> usize {
238 self.get_commands().len()
239 }
240
241 /// Returns the recorded commands as a slice for replay iteration.
242 ///
243 /// # Returns
244 ///
245 /// - `&[DrawCommand]` - The commands in the order they were recorded.
246 pub fn commands(&self) -> &[DrawCommand] {
247 self.get_commands().as_slice()
248 }
249
250 /// Removes all recorded commands, keeping the allocated capacity for reuse
251 /// on the next frame.
252 pub fn clear(&mut self) {
253 self.get_mut_commands().clear();
254 }
255
256 /// Records a fill-rectangle command.
257 ///
258 /// # Arguments
259 ///
260 /// - `Vector2D` - 2D vector (`Vector2D`).
261 /// - `f64` - A 64-bit float (`f64`).
262 /// - `f64` - A 64-bit float (`f64`).
263 /// - `Color` - A `Color` parameter.
264 pub fn fill_rect(&mut self, position: Vector2D, width: f64, height: f64, color: Color) {
265 self.get_mut_commands().push(DrawCommand::FillRect {
266 position,
267 width,
268 height,
269 color,
270 });
271 }
272
273 /// Records a stroke-rectangle command.
274 ///
275 /// # Arguments
276 ///
277 /// - `Vector2D` - 2D vector (`Vector2D`).
278 /// - `f64` - A 64-bit float (`f64`).
279 /// - `f64` - A 64-bit float (`f64`).
280 /// - `Color` - A `Color` parameter.
281 /// - `f64` - A 64-bit float (`f64`).
282 pub fn stroke_rect(
283 &mut self,
284 position: Vector2D,
285 width: f64,
286 height: f64,
287 color: Color,
288 line_width: f64,
289 ) {
290 self.get_mut_commands().push(DrawCommand::StrokeRect {
291 position,
292 width,
293 height,
294 color,
295 line_width,
296 });
297 }
298
299 /// Records a fill-circle command.
300 ///
301 /// # Arguments
302 ///
303 /// - `Vector2D` - 2D vector (`Vector2D`).
304 /// - `f64` - A 64-bit float (`f64`).
305 /// - `Color` - A `Color` parameter.
306 pub fn fill_circle(&mut self, center: Vector2D, radius: f64, color: Color) {
307 self.get_mut_commands().push(DrawCommand::FillCircle {
308 center,
309 radius,
310 color,
311 });
312 }
313
314 /// Records a stroke-circle command.
315 ///
316 /// # Arguments
317 ///
318 /// - `Vector2D` - 2D vector (`Vector2D`).
319 /// - `f64` - A 64-bit float (`f64`).
320 /// - `Color` - A `Color` parameter.
321 /// - `f64` - A 64-bit float (`f64`).
322 pub fn stroke_circle(&mut self, center: Vector2D, radius: f64, color: Color, line_width: f64) {
323 self.get_mut_commands().push(DrawCommand::StrokeCircle {
324 center,
325 radius,
326 color,
327 line_width,
328 });
329 }
330
331 /// Records a line-segment command.
332 ///
333 /// # Arguments
334 ///
335 /// - `Vector2D` - 2D vector (`Vector2D`).
336 /// - `Vector2D` - 2D vector (`Vector2D`).
337 /// - `Color` - A `Color` parameter.
338 /// - `f64` - A 64-bit float (`f64`).
339 pub fn draw_line(&mut self, start: Vector2D, end: Vector2D, color: Color, line_width: f64) {
340 self.get_mut_commands().push(DrawCommand::Line {
341 start,
342 end,
343 color,
344 line_width,
345 });
346 }
347
348 /// Records a fill-text command.
349 ///
350 /// # Arguments
351 ///
352 /// - `T` - A generic type parameter.
353 /// - `Vector2D` - 2D vector (`Vector2D`).
354 /// - `Color` - A `Color` parameter.
355 /// - `F` - A generic type parameter.
356 pub fn fill_text<T, F>(&mut self, text: T, position: Vector2D, color: Color, font: F)
357 where
358 T: AsRef<str>,
359 F: AsRef<str>,
360 {
361 self.get_mut_commands().push(DrawCommand::FillText {
362 text: text.as_ref().to_string(),
363 position,
364 color,
365 font: font.as_ref().to_string(),
366 });
367 }
368
369 /// Records a transformed sprite draw command.
370 ///
371 /// # Arguments
372 ///
373 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
374 /// - `Rect` - A `Rect` parameter.
375 /// - `Transform2D` - A `Transform2D` parameter.
376 pub fn draw_sprite(&mut self, image: &HtmlImageElement, source: Rect, transform: Transform2D) {
377 self.get_mut_commands().push(DrawCommand::DrawSprite {
378 image: image.clone(),
379 source,
380 transform,
381 });
382 }
383
384 /// Records an image sub-region draw command (no rotation).
385 ///
386 /// # Arguments
387 ///
388 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
389 /// - `Rect` - A `Rect` parameter.
390 /// - `Vector2D` - 2D vector (`Vector2D`).
391 /// - `f64` - A 64-bit float (`f64`).
392 /// - `f64` - A 64-bit float (`f64`).
393 pub fn draw_image_rect(
394 &mut self,
395 image: &HtmlImageElement,
396 source: Rect,
397 dest_position: Vector2D,
398 dest_width: f64,
399 dest_height: f64,
400 ) {
401 self.get_mut_commands().push(DrawCommand::DrawImageRect {
402 image: image.clone(),
403 source,
404 dest_position,
405 dest_width,
406 dest_height,
407 });
408 }
409
410 /// Records a global-alpha state change.
411 ///
412 /// # Arguments
413 ///
414 /// - `f64` - A 64-bit float (`f64`).
415 pub fn set_global_alpha(&mut self, alpha: f64) {
416 self.get_mut_commands()
417 .push(DrawCommand::SetGlobalAlpha { alpha });
418 }
419
420 /// Records a blend-mode state change.
421 ///
422 /// # Arguments
423 ///
424 /// - `BlendMode` - A `BlendMode` parameter.
425 pub fn set_blend_mode(&mut self, mode: BlendMode) {
426 self.get_mut_commands()
427 .push(DrawCommand::SetBlendMode { mode });
428 }
429}
430
431/// Inherent implementation of [`CanvasRenderer`].
432impl CanvasRenderer {
433 /// Creates a new renderer from a canvas element selector and viewport dimensions.
434 ///
435 /// # Arguments
436 ///
437 /// - `S` - The CSS selector for the canvas element.
438 /// - `f64` - The viewport width.
439 /// - `f64` - The viewport height.
440 ///
441 /// # Returns
442 ///
443 /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas was not found.
444 pub fn from_selector<S>(
445 canvas_selector: S,
446 viewport_width: f64,
447 viewport_height: f64,
448 ) -> Option<CanvasRenderer>
449 where
450 S: AsRef<str>,
451 {
452 let window_value: Window = window()?;
453 let document_value: Document = window_value.document()?;
454 let element: Element = document_value
455 .query_selector(canvas_selector.as_ref())
456 .ok()
457 .flatten()?;
458 let canvas_element: HtmlCanvasElement = element.unchecked_into();
459 let context_object: Object = canvas_element
460 .get_context(RENDERER_CONTEXT_TYPE_2D)
461 .ok()
462 .flatten()?;
463 let context: CanvasRenderingContext2d = context_object.unchecked_into();
464 let renderer: CanvasRenderer = CanvasRenderer::new(
465 context,
466 Camera2D::create(viewport_width, viewport_height),
467 RenderQuality::default(),
468 );
469 renderer.enable_smoothing();
470 Some(renderer)
471 }
472
473 /// Enables high-quality anti-aliasing on the canvas context by setting
474 /// `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`.
475 ///
476 /// Applies the active `quality` preset via the shared `apply_quality`
477 /// helper so that all smoothing-related settings are kept in sync.
478 pub fn enable_smoothing(&self) {
479 Self::apply_quality(self.get_context(), self.get_quality());
480 }
481
482 /// Clears the entire canvas viewport.
483 pub fn clear(&self) {
484 self.get_context().clear_rect(
485 0.0,
486 0.0,
487 self.get_camera().get_viewport_width(),
488 self.get_camera().get_viewport_height(),
489 );
490 }
491
492 /// Clears the canvas and fills it with the given CSS color string.
493 ///
494 /// # Arguments
495 ///
496 /// - `C` - The CSS color string (e.g., `"#000000"`).
497 pub fn clear_color<C>(&self, color: C)
498 where
499 C: AsRef<str>,
500 {
501 self.get_context().set_fill_style_str(color.as_ref());
502 self.get_context().fill_rect(
503 0.0,
504 0.0,
505 self.get_camera().get_viewport_width(),
506 self.get_camera().get_viewport_height(),
507 );
508 }
509
510 /// Saves the current canvas state (transform, styles) onto the state stack.
511 pub fn save(&self) {
512 self.get_context().save();
513 }
514
515 /// Restores the most recently saved canvas state.
516 pub fn restore(&self) {
517 self.get_context().restore();
518 }
519
520 /// Replays a recorded `DrawList` onto this renderer's canvas.
521 ///
522 /// Convenience wrapper around `replay_context` using this renderer's context.
523 ///
524 /// # Arguments
525 ///
526 /// - `&DrawList` - The recorded commands to replay.
527 pub fn replay(&self, list: &DrawList) {
528 Self::replay_context(self.get_context(), list);
529 }
530
531 /// Replays a recorded `DrawList` onto an arbitrary canvas 2D context in a
532 /// single batched pass.
533 ///
534 /// Consecutive same-style shapes are merged into one path (one `begin_path`
535 /// plus one `fill`/`stroke` per style run), fill/stroke colors and line
536 /// widths are only re-applied when they change, and sprites are drawn with a
537 /// single `set_transform` rather than a save/restore pair. This collapses
538 /// the per-shape canvas state churn of immediate-mode drawing.
539 ///
540 /// The canvas transform and global alpha are reset to identity / 1.0 when
541 /// replay finishes, so callers can sandwich the call between
542 /// `save()`/`apply_camera()` and `restore()` without leaking state.
543 ///
544 /// # Arguments
545 ///
546 /// - `&CanvasRenderingContext2d` - The target canvas 2D context.
547 /// - `&DrawList` - The recorded commands to replay.
548 pub fn replay_context(context: &CanvasRenderingContext2d, list: &DrawList) {
549 let mut current_fill: Option<Color> = None;
550 let mut current_stroke: Option<Color> = None;
551 let mut current_line_width: f64 = f64::NAN;
552 // Reused scratch for `write_css_rgba` — avoids one `Color::to_css`
553 // String allocation per style change during replay.
554 let mut css_buf: String = String::new();
555 // Whether a same-style path run is currently open.
556 let mut run_open: bool = false;
557 let mut run_is_fill: bool = true;
558 let mut run_key: Option<(u8, Color, f64)> = None;
559 /// Computes the batching key for a [`DrawCommand`].
560 ///
561 /// # Arguments
562 ///
563 /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
564 ///
565 /// # Returns
566 ///
567 /// - `Option<(u8, Color, f64)>` - `Some(...)` on success, `None` otherwise.
568 fn batch_key(command: &DrawCommand) -> Option<(u8, Color, f64)> {
569 match command {
570 DrawCommand::FillRect { color, .. } | DrawCommand::FillCircle { color, .. } => {
571 Some((0, *color, 0.0))
572 }
573 DrawCommand::StrokeRect {
574 color, line_width, ..
575 }
576 | DrawCommand::StrokeCircle {
577 color, line_width, ..
578 }
579 | DrawCommand::Line {
580 color, line_width, ..
581 } => Some((1, *color, *line_width)),
582 _ => None,
583 }
584 }
585 /// Emits the geometry for the supplied [`DrawCommand`] into the canvas context.
586 ///
587 /// # Arguments
588 ///
589 /// - `&CanvasRenderingContext2d` - Shared reference to a `CanvasRenderingContext2d`.
590 /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
591 fn emit_geometry(context: &CanvasRenderingContext2d, command: &DrawCommand) {
592 match command {
593 DrawCommand::FillRect {
594 position,
595 width,
596 height,
597 ..
598 }
599 | DrawCommand::StrokeRect {
600 position,
601 width,
602 height,
603 ..
604 } => {
605 context.rect(position.get_x(), position.get_y(), *width, *height);
606 }
607 DrawCommand::FillCircle { center, radius, .. }
608 | DrawCommand::StrokeCircle { center, radius, .. } => {
609 context.move_to(center.get_x() + radius, center.get_y());
610 let _: Result<(), JsValue> =
611 context.arc(center.get_x(), center.get_y(), *radius, 0.0, TWO_PI);
612 }
613 DrawCommand::Line { start, end, .. } => {
614 context.move_to(start.get_x(), start.get_y());
615 context.line_to(end.get_x(), end.get_y());
616 }
617 _ => {}
618 }
619 }
620 for command in list.commands() {
621 let key: Option<(u8, Color, f64)> = batch_key(command);
622 // Close the open run if this command breaks it or starts a new style.
623 if run_open && key != run_key {
624 if run_is_fill {
625 context.fill();
626 } else {
627 context.stroke();
628 }
629 run_open = false;
630 }
631 if let Some(current_key) = key {
632 // Begin (or continue) a same-style path run.
633 if !run_open {
634 let (kind, color, line_width) = current_key;
635 if kind == 0 {
636 if current_fill != Some(color) {
637 css_buf.clear();
638 color.write_css_rgba(&mut css_buf);
639 context.set_fill_style_str(&css_buf);
640 current_fill = Some(color);
641 }
642 run_is_fill = true;
643 } else {
644 if current_stroke != Some(color) {
645 css_buf.clear();
646 color.write_css_rgba(&mut css_buf);
647 context.set_stroke_style_str(&css_buf);
648 current_stroke = Some(color);
649 }
650 if current_line_width != line_width {
651 context.set_line_width(line_width);
652 current_line_width = line_width;
653 }
654 run_is_fill = false;
655 }
656 context.begin_path();
657 run_open = true;
658 run_key = Some(current_key);
659 }
660 emit_geometry(context, command);
661 continue;
662 }
663 // Non-batchable command: draw it immediately.
664 match command {
665 DrawCommand::FillText {
666 text,
667 position,
668 color,
669 font,
670 } => {
671 if current_fill != Some(*color) {
672 css_buf.clear();
673 color.write_css_rgba(&mut css_buf);
674 context.set_fill_style_str(&css_buf);
675 current_fill = Some(*color);
676 }
677 context.set_font(font);
678 let _: Result<(), JsValue> =
679 context.fill_text(text, position.get_x(), position.get_y());
680 }
681 DrawCommand::DrawSprite {
682 image,
683 source,
684 transform,
685 } => {
686 draw_sprite_immediate(context, image, source, transform);
687 }
688 DrawCommand::DrawImageRect {
689 image,
690 source,
691 dest_position,
692 dest_width,
693 dest_height,
694 } => {
695 let _: Result<(), JsValue> = context
696 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
697 image,
698 source.get_x(),
699 source.get_y(),
700 source.get_width(),
701 source.get_height(),
702 dest_position.get_x(),
703 dest_position.get_y(),
704 *dest_width,
705 *dest_height,
706 );
707 }
708 DrawCommand::SetGlobalAlpha { alpha } => {
709 context.set_global_alpha(Numeric::clamp(*alpha, 0.0, 1.0));
710 }
711 DrawCommand::SetBlendMode { mode } => {
712 let _: Result<(), JsValue> =
713 context.set_global_composite_operation(mode.to_css());
714 }
715 _ => {}
716 }
717 }
718 // Flush any trailing open run.
719 if run_open {
720 if run_is_fill {
721 context.fill();
722 } else {
723 context.stroke();
724 }
725 }
726 let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
727 context.set_global_alpha(1.0);
728 }
729
730 /// Applies the camera transform to the canvas context.
731 ///
732 /// Translates to the screen center, applies zoom and rotation,
733 /// then offsets by the negative camera position.
734 pub fn apply_camera(&self) {
735 let camera: Camera2D = self.get_camera();
736 let _: Result<(), JsValue> = self.get_context().translate(
737 camera.get_viewport_width() * 0.5,
738 camera.get_viewport_height() * 0.5,
739 );
740 let _: Result<(), JsValue> = self
741 .get_context()
742 .scale(camera.get_zoom(), camera.get_zoom());
743 let _: Result<(), JsValue> = self.get_context().rotate(camera.get_rotation());
744 let _: Result<(), JsValue> = self.get_context().translate(
745 -camera.get_position().get_x(),
746 -camera.get_position().get_y(),
747 );
748 }
749
750 /// Sets the fill color for subsequent fill operations.
751 ///
752 /// # Arguments
753 ///
754 /// - `C` - The CSS color string.
755 pub fn set_fill_color<C>(&self, color: C)
756 where
757 C: AsRef<str>,
758 {
759 self.get_context().set_fill_style_str(color.as_ref());
760 }
761
762 /// Sets the stroke color for subsequent stroke operations.
763 ///
764 /// # Arguments
765 ///
766 /// - `C` - The CSS color string.
767 pub fn set_stroke_color<C>(&self, color: C)
768 where
769 C: AsRef<str>,
770 {
771 self.get_context().set_stroke_style_str(color.as_ref());
772 }
773
774 /// Sets the line width for subsequent stroke operations.
775 ///
776 /// # Arguments
777 ///
778 /// - `f64` - The line width in pixels.
779 pub fn set_line_width(&self, width: f64) {
780 self.get_context().set_line_width(width);
781 }
782
783 /// Sets the global alpha (opacity) for all subsequent drawing operations.
784 ///
785 /// # Arguments
786 ///
787 /// - `f64` - The alpha value in the range 0.0 to 1.0.
788 pub fn set_global_alpha(&self, alpha: f64) {
789 self.get_context()
790 .set_global_alpha(Numeric::clamp(alpha, 0.0, 1.0));
791 }
792
793 /// Fills a rectangle at the given world-space position and dimensions.
794 ///
795 /// # Arguments
796 ///
797 /// - `Vector2D` - The top-left position in world space.
798 /// - `f64` - The width.
799 /// - `f64` - The height.
800 pub fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
801 self.get_context()
802 .fill_rect(position.get_x(), position.get_y(), width, height);
803 }
804
805 /// Strokes the outline of a rectangle at the given world-space position and dimensions.
806 ///
807 /// # Arguments
808 ///
809 /// - `Vector2D` - The top-left position in world space.
810 /// - `f64` - The width.
811 /// - `f64` - The height.
812 pub fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
813 self.get_context()
814 .stroke_rect(position.get_x(), position.get_y(), width, height);
815 }
816
817 /// Fills a circle at the given world-space center with the specified radius.
818 ///
819 /// # Arguments
820 ///
821 /// - `Vector2D` - The center in world space.
822 /// - `f64` - The radius.
823 pub fn fill_circle(&self, center: Vector2D, radius: f64) {
824 self.get_context().begin_path();
825 self.get_context()
826 .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
827 .unwrap_or(());
828 self.get_context().fill();
829 }
830
831 /// Strokes the outline of a circle at the given world-space center.
832 ///
833 /// # Arguments
834 ///
835 /// - `Vector2D` - The center in world space.
836 /// - `f64` - The radius.
837 pub fn stroke_circle(&self, center: Vector2D, radius: f64) {
838 self.get_context().begin_path();
839 self.get_context()
840 .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
841 .unwrap_or(());
842 self.get_context().stroke();
843 }
844
845 /// Draws a line segment between two world-space points.
846 ///
847 /// # Arguments
848 ///
849 /// - `Vector2D` - The start point.
850 /// - `Vector2D` - The end point.
851 pub fn draw_line(&self, start: Vector2D, end: Vector2D) {
852 self.get_context().begin_path();
853 self.get_context().move_to(start.get_x(), start.get_y());
854 self.get_context().line_to(end.get_x(), end.get_y());
855 self.get_context().stroke();
856 }
857
858 /// Fills text at the given world-space position.
859 ///
860 /// # Arguments
861 ///
862 /// - `T` - The text to draw.
863 /// - `Vector2D` - The position in world space.
864 pub fn fill_text<T>(&self, text: T, position: Vector2D)
865 where
866 T: AsRef<str>,
867 {
868 self.get_context()
869 .fill_text(text.as_ref(), position.get_x(), position.get_y())
870 .unwrap_or(());
871 }
872
873 /// Sets the font for subsequent text rendering.
874 ///
875 /// # Arguments
876 ///
877 /// - `F` - The CSS font string (e.g., `"16px sans-serif"`).
878 pub fn set_font<F>(&self, font: F)
879 where
880 F: AsRef<str>,
881 {
882 self.get_context().set_font(font.as_ref());
883 }
884
885 /// Draws an image element at the given world-space position and dimensions.
886 ///
887 /// # Arguments
888 ///
889 /// - `&HtmlImageElement` - The image element to draw.
890 /// - `Vector2D` - The top-left position in world space.
891 /// - `f64` - The destination width.
892 /// - `f64` - The destination height.
893 pub fn draw_image(
894 &self,
895 image: &HtmlImageElement,
896 position: Vector2D,
897 width: f64,
898 height: f64,
899 ) {
900 let _: Result<(), JsValue> = self
901 .get_context()
902 .draw_image_with_html_image_element_and_dw_and_dh(
903 image,
904 position.get_x(),
905 position.get_y(),
906 width,
907 height,
908 );
909 }
910
911 /// Draws a sub-region of an image element at the given world-space position.
912 ///
913 /// # Arguments
914 ///
915 /// - `&HtmlImageElement` - The image element to draw.
916 /// - `Rect` - The source rectangle within the image.
917 /// - `Vector2D` - The destination top-left position in world space.
918 /// - `f64` - The destination width.
919 /// - `f64` - The destination height.
920 pub fn draw_image_rect(
921 &self,
922 image: &HtmlImageElement,
923 source: Rect,
924 dest_position: Vector2D,
925 dest_width: f64,
926 dest_height: f64,
927 ) {
928 let _: Result<(), JsValue> = self
929 .get_context()
930 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
931 image,
932 source.get_x(),
933 source.get_y(),
934 source.get_width(),
935 source.get_height(),
936 dest_position.get_x(),
937 dest_position.get_y(),
938 dest_width,
939 dest_height,
940 );
941 }
942}
943
944/// Implements 3D camera transformation and projection methods for `Camera3D`.
945impl Camera3D {
946 /// Creates a new 3D camera at the given position looking at the target.
947 ///
948 /// # Arguments
949 ///
950 /// - `Vector3D` - The eye position.
951 /// - `Vector3D` - The target position to look at.
952 /// - `f64` - The viewport width.
953 /// - `f64` - The viewport height.
954 ///
955 /// # Returns
956 ///
957 /// - `Camera3D` - The new camera.
958 pub fn create(
959 position: Vector3D,
960 target: Vector3D,
961 viewport_width: f64,
962 viewport_height: f64,
963 ) -> Camera3D {
964 let mut camera: Camera3D = Camera3D::new(position, target, viewport_width, viewport_height);
965 camera.set_up(Vector3D::up());
966 camera.set_fov(DEFAULT_CAMERA_FOV);
967 camera.set_near(DEFAULT_CAMERA_NEAR);
968 camera.set_far(DEFAULT_CAMERA_FAR);
969 camera
970 }
971
972 /// Returns the aspect ratio (width / height).
973 ///
974 /// # Returns
975 ///
976 /// - `f64` - The aspect ratio.
977 pub fn aspect(&self) -> f64 {
978 if self.get_viewport_height() < EPSILON {
979 return 1.0;
980 }
981 self.get_viewport_width() / self.get_viewport_height()
982 }
983
984 /// Returns the forward direction (from position to target, normalized).
985 ///
986 /// # Returns
987 ///
988 /// - `Vector3D` - The forward direction.
989 pub fn forward(&self) -> Vector3D {
990 (self.get_target() - self.get_position()).normalized()
991 }
992
993 /// Returns the right direction (cross product of forward and up).
994 ///
995 /// # Returns
996 ///
997 /// - `Vector3D` - The right direction.
998 pub fn right(&self) -> Vector3D {
999 self.forward().cross(self.get_up()).normalized()
1000 }
1001
1002 /// Returns the view matrix for this camera.
1003 ///
1004 /// # Returns
1005 ///
1006 /// - `Matrix4x4` - The view matrix.
1007 pub fn view_matrix(&self) -> Matrix4x4 {
1008 Matrix4x4::look_at(self.get_position(), self.get_target(), self.get_up())
1009 }
1010
1011 /// Returns the perspective projection matrix for this camera.
1012 ///
1013 /// # Returns
1014 ///
1015 /// - `Matrix4x4` - The projection matrix.
1016 pub fn projection_matrix(&self) -> Matrix4x4 {
1017 Matrix4x4::perspective(
1018 self.get_fov(),
1019 self.aspect(),
1020 self.get_near(),
1021 self.get_far(),
1022 )
1023 }
1024
1025 /// Returns the combined view-projection matrix.
1026 ///
1027 /// # Returns
1028 ///
1029 /// - `Matrix4x4` - The view-projection matrix.
1030 pub fn view_proj_matrix(&self) -> Matrix4x4 {
1031 self.projection_matrix().multiply(self.view_matrix())
1032 }
1033
1034 /// Converts a 3D world-space point to screen-space (NDC) coordinates.
1035 ///
1036 /// # Arguments
1037 ///
1038 /// - `Vector3D` - The world-space point.
1039 ///
1040 /// # Returns
1041 ///
1042 /// - `Vector3D` - The screen-space point where x and y are in [0, 1] and z is the depth.
1043 pub fn world_to_screen(&self, world: Vector3D) -> Vector3D {
1044 let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1045 Vector3D::new(
1046 (clip.get_x() + 1.0) * 0.5 * self.get_viewport_width(),
1047 (1.0 - clip.get_y()) * 0.5 * self.get_viewport_height(),
1048 clip.get_z(),
1049 )
1050 }
1051
1052 /// Projects a world-space point and returns whether it is within the camera frustum.
1053 ///
1054 /// # Arguments
1055 ///
1056 /// - `Vector3D` - The world-space point.
1057 ///
1058 /// # Returns
1059 ///
1060 /// - `bool` - True if the point is within the frustum.
1061 pub fn in_frustum(&self, world: Vector3D) -> bool {
1062 let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1063 clip.get_x() >= -1.0
1064 && clip.get_x() <= 1.0
1065 && clip.get_y() >= -1.0
1066 && clip.get_y() <= 1.0
1067 && clip.get_z() >= -1.0
1068 && clip.get_z() <= 1.0
1069 }
1070
1071 /// Moves the camera position by the given offset, keeping the target offset by the same amount.
1072 ///
1073 /// # Arguments
1074 ///
1075 /// - `Vector3D` - The translation offset.
1076 pub fn translate(&mut self, offset: Vector3D) {
1077 self.set_position(self.get_position() + offset);
1078 self.set_target(self.get_target() + offset);
1079 }
1080
1081 /// Moves the camera position towards the target by the given distance.
1082 ///
1083 /// # Arguments
1084 ///
1085 /// - `f64` - The distance to zoom in (positive) or out (negative).
1086 pub fn zoom(&mut self, distance: f64) {
1087 let direction: Vector3D = self.forward();
1088 self.set_position(self.get_position() + direction.scaled(distance));
1089 }
1090
1091 /// Orbits the camera around the target by the given yaw and pitch angles.
1092 ///
1093 /// # Arguments
1094 ///
1095 /// - `f64` - The yaw delta in radians (horizontal rotation).
1096 /// - `f64` - The pitch delta in radians (vertical rotation).
1097 pub fn orbit(&mut self, yaw_delta: f64, pitch_delta: f64) {
1098 let offset: Vector3D = self.get_position() - self.get_target();
1099 let current_distance: f64 = offset.magnitude();
1100 let current_yaw: f64 = offset.get_x().atan2(offset.get_z());
1101 let horizontal_dist: f64 =
1102 (offset.get_x() * offset.get_x() + offset.get_z() * offset.get_z()).sqrt();
1103 let current_pitch: f64 = (offset.get_y() / horizontal_dist.max(EPSILON)).asin();
1104 let new_yaw: f64 = current_yaw + yaw_delta;
1105 let new_pitch: f64 = Numeric::clamp(
1106 current_pitch + pitch_delta,
1107 -HALF_PI + EPSILON,
1108 HALF_PI - EPSILON,
1109 );
1110 let cos_pitch: f64 = new_pitch.cos();
1111 self.set_position(
1112 self.get_target()
1113 + Vector3D::new(
1114 new_yaw.sin() * cos_pitch * current_distance,
1115 new_pitch.sin() * current_distance,
1116 new_yaw.cos() * cos_pitch * current_distance,
1117 ),
1118 );
1119 }
1120}
1121
1122/// Implements `Default` for `Camera3D` as a camera at (0, 0, 5) looking at the origin.
1123impl Default for Camera3D {
1124 /// Constructs a default [`Camera3D`] value.
1125 ///
1126 /// # Returns
1127 ///
1128 /// - `Camera3D` - A default-constructed instance with the documented initial state.
1129 fn default() -> Camera3D {
1130 Camera3D::create(Vector3D::new(0.0, 0.0, 5.0), Vector3D::zero(), 800.0, 600.0)
1131 }
1132}
1133
1134/// Implements construction, presentation, and anti-aliasing methods for `SsaaCanvas`.
1135impl SsaaCanvas {
1136 /// Creates an `SsaaCanvas` from a CSS selector using the default scale factor.
1137 ///
1138 /// # Arguments
1139 ///
1140 /// - `S` - The CSS selector for the display canvas element.
1141 /// - `f64` - The logical display width in CSS pixels.
1142 /// - `f64` - The logical display height in CSS pixels.
1143 ///
1144 /// # Returns
1145 ///
1146 /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1147 pub fn from_selector<S>(canvas_selector: S, width: f64, height: f64) -> Option<SsaaCanvas>
1148 where
1149 S: AsRef<str>,
1150 {
1151 Self::from_selector_with_scale(
1152 canvas_selector,
1153 width,
1154 height,
1155 RENDERER_DEFAULT_SSAA_SCALE_FACTOR,
1156 )
1157 }
1158
1159 /// Creates an `SsaaCanvas` from a CSS selector with a custom SSAA scale factor.
1160 ///
1161 /// The offscreen canvas is created at `width * scale_factor` by `height * scale_factor`
1162 /// pixels, and its context is pre-scaled so that drawing code uses logical coordinates.
1163 ///
1164 /// # Arguments
1165 ///
1166 /// - `S` - The CSS selector for the display canvas element.
1167 /// - `f64` - The logical display width in CSS pixels.
1168 /// - `f64` - The logical display height in CSS pixels.
1169 /// - `f64` - The supersampling scale factor (e.g., 2.0 for 4x SSAA).
1170 ///
1171 /// # Returns
1172 ///
1173 /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1174 pub fn from_selector_with_scale<S>(
1175 canvas_selector: S,
1176 width: f64,
1177 height: f64,
1178 scale_factor: f64,
1179 ) -> Option<SsaaCanvas>
1180 where
1181 S: AsRef<str>,
1182 {
1183 let window_value: Window = window()?;
1184 let document_value: Document = window_value.document()?;
1185 let element: Element = document_value
1186 .query_selector(canvas_selector.as_ref())
1187 .ok()
1188 .flatten()?;
1189 let display_canvas: HtmlCanvasElement = element.unchecked_into();
1190 let device_pixel_ratio: f64 = CanvasRenderer::detect_dpr();
1191 let physical_width: u32 = (width * device_pixel_ratio).round() as u32;
1192 let physical_height: u32 = (height * device_pixel_ratio).round() as u32;
1193 display_canvas.set_width(physical_width);
1194 display_canvas.set_height(physical_height);
1195 let display_context_object: Object = display_canvas
1196 .get_context(RENDERER_CONTEXT_TYPE_2D)
1197 .ok()
1198 .flatten()?;
1199 let display_context: CanvasRenderingContext2d = display_context_object.unchecked_into();
1200 let _: Result<(), JsValue> = display_context.scale(device_pixel_ratio, device_pixel_ratio);
1201 let offscreen_canvas: HtmlCanvasElement = document_value
1202 .create_element(RENDERER_ELEMENT_CANVAS)
1203 .ok()?
1204 .unchecked_into();
1205 let scaled_width: u32 = (width * scale_factor * device_pixel_ratio).round() as u32;
1206 let scaled_height: u32 = (height * scale_factor * device_pixel_ratio).round() as u32;
1207 offscreen_canvas.set_width(scaled_width);
1208 offscreen_canvas.set_height(scaled_height);
1209 let offscreen_context_object: Object = offscreen_canvas
1210 .get_context(RENDERER_CONTEXT_TYPE_2D)
1211 .ok()
1212 .flatten()?;
1213 let offscreen_context: CanvasRenderingContext2d = offscreen_context_object.unchecked_into();
1214 let _: Result<(), JsValue> = offscreen_context.scale(
1215 scale_factor * device_pixel_ratio,
1216 scale_factor * device_pixel_ratio,
1217 );
1218 let ssaa_canvas: SsaaCanvas = SsaaCanvas::new(
1219 display_canvas,
1220 display_context,
1221 offscreen_canvas,
1222 offscreen_context,
1223 scale_factor,
1224 width,
1225 height,
1226 );
1227 ssaa_canvas.enable_smoothing();
1228 Some(ssaa_canvas)
1229 }
1230
1231 /// Presents the offscreen buffer onto the display canvas with high-quality downscaling.
1232 ///
1233 /// Clears the display canvas, then draws the offscreen canvas scaled
1234 /// down to the logical display size. The active `quality` preset is
1235 /// applied once at construction via `enable_smoothing` — there is
1236 /// no need to re-apply it every frame, since the preset never
1237 /// changes mid-render.
1238 ///
1239 /// #32: the previous version called `apply_quality` on every
1240 /// `present()`, costing `set_image_smoothing_enabled` + 2×Reflect
1241 /// + 4×from_str ≈ 7 JS crossings per frame for a value that was
1242 /// invariant across the entire session.
1243 pub fn present(&self) {
1244 self.get_display_context()
1245 .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1246 let _: Result<(), JsValue> = self
1247 .get_display_context()
1248 .draw_image_with_html_canvas_element_and_dw_and_dh(
1249 self.get_offscreen_canvas(),
1250 0.0,
1251 0.0,
1252 self.get_width(),
1253 self.get_height(),
1254 );
1255 }
1256
1257 /// Clears the offscreen buffer to transparent.
1258 pub fn clear(&self) {
1259 self.get_offscreen_context()
1260 .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1261 }
1262
1263 /// Clears the offscreen buffer and fills it with the given CSS color.
1264 ///
1265 /// # Arguments
1266 ///
1267 /// - `C` - The CSS color string.
1268 pub fn clear_color<C>(&self, color: C)
1269 where
1270 C: AsRef<str>,
1271 {
1272 self.get_offscreen_context()
1273 .set_fill_style_str(color.as_ref());
1274 self.get_offscreen_context()
1275 .fill_rect(0.0, 0.0, self.get_width(), self.get_height());
1276 }
1277
1278 /// Enables high-quality anti-aliasing on both the display and offscreen contexts.
1279 ///
1280 /// Applies the active `quality` preset to both contexts via the shared
1281 /// `apply_quality` helper.
1282 pub fn enable_smoothing(&self) {
1283 let quality: RenderQuality = self.get_quality();
1284 CanvasRenderer::apply_quality(self.get_display_context(), quality);
1285 CanvasRenderer::apply_quality(self.get_offscreen_context(), quality);
1286 }
1287}
1288
1289/// Implements construction and canvas gradient creation for `LinearGradient`.
1290impl LinearGradient {
1291 /// Creates a new linear gradient from two points and a list of color stops.
1292 ///
1293 /// # Arguments
1294 ///
1295 /// - `Vector2D` - The start point.
1296 /// - `Vector2D` - The end point.
1297 /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1298 ///
1299 /// # Returns
1300 ///
1301 /// - `LinearGradient` - The new gradient.
1302 pub fn create(start: Vector2D, end: Vector2D, stops: Vec<(f64, String)>) -> LinearGradient {
1303 LinearGradient::new(start, end, stops)
1304 }
1305
1306 /// Creates a `CanvasGradient` from this gradient definition on the given context.
1307 ///
1308 /// # Arguments
1309 ///
1310 /// - `&CanvasRenderingContext2d` - The canvas context.
1311 ///
1312 /// # Returns
1313 ///
1314 /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1315 pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1316 let canvas_gradient: CanvasGradient = context.create_linear_gradient(
1317 self.get_start().get_x(),
1318 self.get_start().get_y(),
1319 self.get_end().get_x(),
1320 self.get_end().get_y(),
1321 );
1322 for (position, color) in self.get_stops() {
1323 let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1324 }
1325 Some(canvas_gradient)
1326 }
1327}
1328
1329/// Implements construction and canvas gradient creation for `RadialGradient`.
1330impl RadialGradient {
1331 /// Creates a new radial gradient from inner and outer circles and color stops.
1332 ///
1333 /// # Arguments
1334 ///
1335 /// - `Vector2D` - The inner circle center.
1336 /// - `f64` - The inner circle radius.
1337 /// - `Vector2D` - The outer circle center.
1338 /// - `f64` - The outer circle radius.
1339 /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1340 ///
1341 /// # Returns
1342 ///
1343 /// - `RadialGradient` - The new gradient.
1344 pub fn create(
1345 inner_center: Vector2D,
1346 inner_radius: f64,
1347 outer_center: Vector2D,
1348 outer_radius: f64,
1349 stops: Vec<(f64, String)>,
1350 ) -> RadialGradient {
1351 RadialGradient::new(
1352 inner_center,
1353 inner_radius,
1354 outer_center,
1355 outer_radius,
1356 stops,
1357 )
1358 }
1359
1360 /// Creates a `CanvasGradient` from this gradient definition on the given context.
1361 ///
1362 /// # Arguments
1363 ///
1364 /// - `&CanvasRenderingContext2d` - The canvas context.
1365 ///
1366 /// # Returns
1367 ///
1368 /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1369 pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1370 let canvas_gradient: CanvasGradient = context
1371 .create_radial_gradient(
1372 self.get_inner_center().get_x(),
1373 self.get_inner_center().get_y(),
1374 self.get_inner_radius(),
1375 self.get_outer_center().get_x(),
1376 self.get_outer_center().get_y(),
1377 self.get_outer_radius(),
1378 )
1379 .ok()?;
1380 for (position, color) in self.get_stops() {
1381 let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1382 }
1383 Some(canvas_gradient)
1384 }
1385}
1386
1387/// Implements construction methods for `ShadowConfig`.
1388impl ShadowConfig {
1389 /// Creates a shadow configuration with default values.
1390 ///
1391 /// # Returns
1392 ///
1393 /// - `ShadowConfig` - The default shadow configuration.
1394 pub fn create() -> ShadowConfig {
1395 ShadowConfig::new(
1396 RENDERER_DEFAULT_SHADOW_COLOR.to_string(),
1397 RENDERER_DEFAULT_SHADOW_BLUR,
1398 0.0,
1399 0.0,
1400 )
1401 }
1402}
1403
1404/// Implements `Default` for `ShadowConfig` with default shadow values.
1405impl Default for ShadowConfig {
1406 /// Constructs a default [`ShadowConfig`] value.
1407 ///
1408 /// # Returns
1409 ///
1410 /// - `ShadowConfig` - A default-constructed instance with the documented initial state.
1411 fn default() -> ShadowConfig {
1412 ShadowConfig::create()
1413 }
1414}
1415
1416/// Implements construction methods for `RenderLayer`.
1417impl RenderLayer {
1418 /// Creates a render layer with the given z-index and visibility.
1419 ///
1420 /// # Arguments
1421 ///
1422 /// - `i32` - The z-index determining draw order.
1423 /// - `bool` - Whether the layer is visible.
1424 ///
1425 /// # Returns
1426 ///
1427 /// - `RenderLayer` - The new render layer.
1428 pub fn create(z_index: i32, visible: bool) -> RenderLayer {
1429 RenderLayer::new(z_index, visible)
1430 }
1431
1432 /// Creates a background render layer with z-index 0 and visibility enabled.
1433 ///
1434 /// # Returns
1435 ///
1436 /// - `RenderLayer` - The background layer.
1437 pub fn background() -> RenderLayer {
1438 RenderLayer::new(RENDERER_LAYER_BACKGROUND, true)
1439 }
1440
1441 /// Creates a foreground render layer with a high z-index and visibility enabled.
1442 ///
1443 /// # Returns
1444 ///
1445 /// - `RenderLayer` - The foreground layer.
1446 pub fn foreground() -> RenderLayer {
1447 RenderLayer::new(RENDERER_LAYER_FOREGROUND, true)
1448 }
1449
1450 /// Creates a UI overlay render layer with the highest z-index and visibility enabled.
1451 ///
1452 /// # Returns
1453 ///
1454 /// - `RenderLayer` - The UI overlay layer.
1455 pub fn ui() -> RenderLayer {
1456 RenderLayer::new(RENDERER_LAYER_UI, true)
1457 }
1458}
1459
1460/// Implements blend mode, shadow, and gradient rendering methods for `CanvasRenderer`.
1461impl CanvasRenderer {
1462 /// Sets the blend mode for compositing subsequent draw operations.
1463 ///
1464 /// # Arguments
1465 ///
1466 /// - `BlendMode` - The blend mode to apply.
1467 pub fn set_blend_mode(&self, mode: BlendMode) {
1468 let _: Result<(), JsValue> = self
1469 .get_context()
1470 .set_global_composite_operation(mode.to_css());
1471 }
1472
1473 /// Applies a shadow configuration for subsequent draw operations.
1474 ///
1475 /// # Arguments
1476 ///
1477 /// - `&ShadowConfig` - The shadow configuration to apply.
1478 pub fn set_shadow(&self, config: &ShadowConfig) {
1479 self.get_context()
1480 .set_shadow_color(config.get_color().as_str());
1481 self.get_context().set_shadow_blur(config.get_blur());
1482 self.get_context()
1483 .set_shadow_offset_x(config.get_offset_x());
1484 self.get_context()
1485 .set_shadow_offset_y(config.get_offset_y());
1486 }
1487
1488 /// Clears any previously applied shadow, disabling shadow rendering.
1489 pub fn clear_shadow(&self) {
1490 self.get_context()
1491 .set_shadow_color(RENDERER_TRANSPARENT_SHADOW_COLOR);
1492 self.get_context().set_shadow_blur(0.0);
1493 self.get_context().set_shadow_offset_x(0.0);
1494 self.get_context().set_shadow_offset_y(0.0);
1495 }
1496
1497 /// Applies a linear gradient as the fill style for subsequent operations.
1498 ///
1499 /// # Arguments
1500 ///
1501 /// - `&LinearGradient` - The linear gradient to use as fill style.
1502 pub fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1503 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1504 self.get_context()
1505 .set_fill_style_canvas_gradient(&canvas_gradient);
1506 }
1507 }
1508
1509 /// Applies a radial gradient as the fill style for subsequent operations.
1510 ///
1511 /// # Arguments
1512 ///
1513 /// - `&RadialGradient` - The radial gradient to use as fill style.
1514 pub fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1515 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1516 self.get_context()
1517 .set_fill_style_canvas_gradient(&canvas_gradient);
1518 }
1519 }
1520
1521 /// Applies a linear gradient as the stroke style for subsequent operations.
1522 ///
1523 /// # Arguments
1524 ///
1525 /// - `&LinearGradient` - The linear gradient to use as stroke style.
1526 pub fn set_linear_gradient_stroke(&self, gradient: &LinearGradient) {
1527 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1528 self.get_context()
1529 .set_stroke_style_canvas_gradient(&canvas_gradient);
1530 }
1531 }
1532
1533 /// Applies a radial gradient as the stroke style for subsequent operations.
1534 ///
1535 /// # Arguments
1536 ///
1537 /// - `&RadialGradient` - The radial gradient to use as stroke style.
1538 pub fn set_radial_gradient_stroke(&self, gradient: &RadialGradient) {
1539 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1540 self.get_context()
1541 .set_stroke_style_canvas_gradient(&canvas_gradient);
1542 }
1543 }
1544}
1545
1546/// Implements the `RenderBackend` trait for `CanvasRenderer`, providing
1547/// a backend-agnostic rendering interface.
1548///
1549/// Each method forwards to the inherent `CanvasRenderer` method of the
1550/// same name, so the per-call documentation lives on the trait definition
1551/// in `engine::renderer::trait` — the inherent method is the source of
1552/// truth, this impl is the trait bridge.
1553impl RenderBackend for CanvasRenderer {
1554 /// Forwards to [`CanvasRenderer::clear`].
1555 fn clear(&self) {
1556 self.clear();
1557 }
1558
1559 /// Forwards to [`CanvasRenderer::clear_color`].
1560 ///
1561 /// # Arguments
1562 ///
1563 /// - `C` - A generic type parameter.
1564 fn clear_color<C>(&self, color: C)
1565 where
1566 C: AsRef<str>,
1567 {
1568 self.clear_color(color);
1569 }
1570
1571 /// Forwards to [`CanvasRenderer::save`].
1572 fn save(&self) {
1573 self.save();
1574 }
1575
1576 /// Forwards to [`CanvasRenderer::restore`].
1577 fn restore(&self) {
1578 self.restore();
1579 }
1580
1581 /// Forwards to [`CanvasRenderer::set_fill_color`].
1582 ///
1583 /// # Arguments
1584 ///
1585 /// - `&str` - Shared reference to a `str`.
1586 fn set_fill_color(&self, color: &str) {
1587 self.set_fill_color(color);
1588 }
1589
1590 /// Forwards to [`CanvasRenderer::set_stroke_color`].
1591 ///
1592 /// # Arguments
1593 ///
1594 /// - `&str` - Shared reference to a `str`.
1595 fn set_stroke_color(&self, color: &str) {
1596 self.set_stroke_color(color);
1597 }
1598
1599 /// Forwards to [`CanvasRenderer::set_line_width`].
1600 ///
1601 /// # Arguments
1602 ///
1603 /// - `f64` - A 64-bit float (`f64`).
1604 fn set_line_width(&self, width: f64) {
1605 self.set_line_width(width);
1606 }
1607
1608 /// Forwards to [`CanvasRenderer::set_global_alpha`].
1609 ///
1610 /// # Arguments
1611 ///
1612 /// - `f64` - A 64-bit float (`f64`).
1613 fn set_global_alpha(&self, alpha: f64) {
1614 self.set_global_alpha(alpha);
1615 }
1616
1617 /// Forwards to [`CanvasRenderer::set_blend_mode`].
1618 ///
1619 /// # Arguments
1620 ///
1621 /// - `BlendMode` - A `BlendMode` parameter.
1622 fn set_blend_mode(&self, mode: BlendMode) {
1623 self.set_blend_mode(mode);
1624 }
1625
1626 /// Forwards to [`CanvasRenderer::set_shadow`].
1627 ///
1628 /// # Arguments
1629 ///
1630 /// - `&ShadowConfig` - Shared reference to a `ShadowConfig`.
1631 fn set_shadow(&self, config: &ShadowConfig) {
1632 self.set_shadow(config);
1633 }
1634
1635 /// Forwards to [`CanvasRenderer::clear_shadow`].
1636 fn clear_shadow(&self) {
1637 self.clear_shadow();
1638 }
1639
1640 /// Forwards to [`CanvasRenderer::fill_rect`].
1641 ///
1642 /// # Arguments
1643 ///
1644 /// - `Vector2D` - 2D vector (`Vector2D`).
1645 /// - `f64` - A 64-bit float (`f64`).
1646 /// - `f64` - A 64-bit float (`f64`).
1647 fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1648 self.fill_rect(position, width, height);
1649 }
1650
1651 /// Forwards to [`CanvasRenderer::stroke_rect`].
1652 ///
1653 /// # Arguments
1654 ///
1655 /// - `Vector2D` - 2D vector (`Vector2D`).
1656 /// - `f64` - A 64-bit float (`f64`).
1657 /// - `f64` - A 64-bit float (`f64`).
1658 fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1659 self.stroke_rect(position, width, height);
1660 }
1661
1662 /// Forwards to [`CanvasRenderer::fill_circle`].
1663 ///
1664 /// # Arguments
1665 ///
1666 /// - `Vector2D` - 2D vector (`Vector2D`).
1667 /// - `f64` - A 64-bit float (`f64`).
1668 fn fill_circle(&self, center: Vector2D, radius: f64) {
1669 self.fill_circle(center, radius);
1670 }
1671
1672 /// Forwards to [`CanvasRenderer::stroke_circle`].
1673 ///
1674 /// # Arguments
1675 ///
1676 /// - `Vector2D` - 2D vector (`Vector2D`).
1677 /// - `f64` - A 64-bit float (`f64`).
1678 fn stroke_circle(&self, center: Vector2D, radius: f64) {
1679 self.stroke_circle(center, radius);
1680 }
1681
1682 /// Forwards to [`CanvasRenderer::draw_line`].
1683 ///
1684 /// # Arguments
1685 ///
1686 /// - `Vector2D` - 2D vector (`Vector2D`).
1687 /// - `Vector2D` - 2D vector (`Vector2D`).
1688 fn draw_line(&self, start: Vector2D, end: Vector2D) {
1689 self.draw_line(start, end);
1690 }
1691
1692 /// Forwards to [`CanvasRenderer::fill_text`].
1693 ///
1694 /// # Arguments
1695 ///
1696 /// - `&str` - Shared reference to a `str`.
1697 /// - `Vector2D` - 2D vector (`Vector2D`).
1698 fn fill_text(&self, text: &str, position: Vector2D) {
1699 self.fill_text(text, position);
1700 }
1701
1702 /// Forwards to [`CanvasRenderer::set_font`].
1703 ///
1704 /// # Arguments
1705 ///
1706 /// - `&str` - Shared reference to a `str`.
1707 fn set_font(&self, font: &str) {
1708 self.set_font(font);
1709 }
1710
1711 /// Forwards to [`CanvasRenderer::draw_image`].
1712 ///
1713 /// # Arguments
1714 ///
1715 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
1716 /// - `Vector2D` - 2D vector (`Vector2D`).
1717 /// - `f64` - A 64-bit float (`f64`).
1718 /// - `f64` - A 64-bit float (`f64`).
1719 fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1720 self.draw_image(image, position, width, height);
1721 }
1722
1723 /// Forwards to [`CanvasRenderer::set_linear_gradient_fill`].
1724 ///
1725 /// # Arguments
1726 ///
1727 /// - `&LinearGradient` - Shared reference to a `LinearGradient`.
1728 fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1729 self.set_linear_gradient_fill(gradient);
1730 }
1731
1732 /// Forwards to [`CanvasRenderer::set_radial_gradient_fill`].
1733 ///
1734 /// # Arguments
1735 ///
1736 /// - `&RadialGradient` - Shared reference to a `RadialGradient`.
1737 fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1738 self.set_radial_gradient_fill(gradient);
1739 }
1740}