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