Skip to main content

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}