Skip to main content

euv_engine/renderer/
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    fn default() -> Camera2D {
83        Camera2D::create(800.0, 600.0)
84    }
85}
86
87/// Implements static font and color utility methods for `CanvasRenderer`.
88impl CanvasRenderer {
89    /// Builds a CSS font string from font size and family.
90    ///
91    /// # Arguments
92    ///
93    /// - `f64` - The font size in pixels.
94    /// - `F: AsRef<str>` - The font family name.
95    ///
96    /// # Returns
97    ///
98    /// - `String` - The CSS font string (e.g., `"16px sans-serif"`).
99    pub fn font<F>(size: f64, family: F) -> String
100    where
101        F: AsRef<str>,
102    {
103        let family: &str = family.as_ref();
104        format!("{size}px {family}")
105    }
106
107    /// Creates a default font string using the default font size and family.
108    ///
109    /// # Returns
110    ///
111    /// - `String` - The default CSS font string.
112    pub fn default_font() -> String {
113        Self::font(RENDERER_DEFAULT_FONT_SIZE, RENDERER_DEFAULT_FONT_FAMILY)
114    }
115
116    /// Enables high-quality anti-aliasing on an arbitrary canvas 2D context.
117    ///
118    /// Applies the `High` rendering quality preset via `apply_quality`,
119    /// which sets `imageSmoothingEnabled`, `imageSmoothingQuality = "high"`,
120    /// and `textRendering = "geometricPrecision"` on the given context.
121    ///
122    /// Use this static helper when you manage your own `CanvasRenderingContext2d`
123    /// and don't hold a `CanvasRenderer` instance. For instances, call
124    /// `renderer.enable_smoothing()` instead.
125    ///
126    /// # Arguments
127    ///
128    /// - `&CanvasRenderingContext2d` - The canvas context to configure.
129    pub fn enable_smoothing_on(context: &CanvasRenderingContext2d) {
130        Self::apply_quality(context, RenderQuality::High);
131    }
132
133    /// Detects the host device pixel ratio (HiDPI scale factor) via reflection.
134    ///
135    /// Reads `window.devicePixelRatio` using `Reflect::get` because the
136    /// `web-sys` `Window` features currently in use do not expose a native
137    /// getter for this property. Falls back to
138    /// `RENDERER_DEFAULT_DEVICE_PIXEL_RATIO` (1.0) when the value is missing,
139    /// not a finite number, or below 1.0.
140    ///
141    /// # Returns
142    ///
143    /// - `f64` - The detected device pixel ratio (clamped to `>= 1.0`).
144    pub fn detect_dpr() -> f64 {
145        let window_value: Window = window().expect("no global window exists");
146        let raw: Option<f64> = Reflect::get(
147            window_value.as_ref(),
148            &JsValue::from_str(RENDERER_PROPERTY_DEVICE_PIXEL_RATIO),
149        )
150        .ok()
151        .and_then(|value: JsValue| value.as_f64());
152        raw.filter(|value: &f64| value.is_finite() && *value >= 1.0)
153            .unwrap_or(RENDERER_DEFAULT_DEVICE_PIXEL_RATIO)
154    }
155
156    /// Applies the given `RenderQuality` preset to an arbitrary canvas context.
157    ///
158    /// Sets `imageSmoothingEnabled`, `imageSmoothingQuality`, and
159    /// `textRendering` according to the supplied quality. `Low` disables
160    /// smoothing (intended for use with CSS `image-rendering: pixelated`),
161    /// `Medium` and `High` enable it with the matching quality level.
162    ///
163    /// # Arguments
164    ///
165    /// - `&CanvasRenderingContext2d` - The target context.
166    /// - `RenderQuality` - The quality preset to apply.
167    pub(crate) fn apply_quality(context: &CanvasRenderingContext2d, quality: RenderQuality) {
168        let smoothing_enabled: bool = !matches!(quality, RenderQuality::Low);
169        context.set_image_smoothing_enabled(smoothing_enabled);
170        let quality_value: &str = match quality {
171            RenderQuality::Low => RENDERER_IMAGE_SMOOTHING_QUALITY_LOW,
172            RenderQuality::Medium => RENDERER_IMAGE_SMOOTHING_QUALITY_MEDIUM,
173            RenderQuality::High => RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH,
174        };
175        let _: Result<bool, JsValue> = Reflect::set(
176            context,
177            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
178            &JsValue::from_str(quality_value),
179        );
180        let _: Result<bool, JsValue> = Reflect::set(
181            context,
182            &JsValue::from_str(RENDERER_PROPERTY_TEXT_RENDERING),
183            &JsValue::from_str(RENDERER_TEXT_RENDERING_GEOMETRIC_PRECISION),
184        );
185    }
186}
187
188/// Implements static CSS conversion for `Color`.
189impl Color {
190    /// Converts a `Color` to a CSS `rgba()` string suitable for canvas fill or stroke styles.
191    ///
192    /// # Arguments
193    ///
194    /// - `&Color` - The color to convert.
195    ///
196    /// # Returns
197    ///
198    /// - `String` - The CSS `rgba()` color string.
199    pub fn to_css(color: &Color) -> String {
200        color.to_css_rgba()
201    }
202}
203
204/// Returns the command slice of a `DrawList` for replay iteration.
205fn self_commands(list: &DrawList) -> &[DrawCommand] {
206    list.get_commands().as_slice()
207}
208
209/// Draws a transformed sprite immediately with a single `set_transform`.
210///
211/// Mirrors the `SpriteSheet::draw_frame` fast path: the TRS matrix is composed
212/// in Rust (scale signs flip) and applied once, then reset to identity.
213fn draw_sprite_immediate(
214    context: &CanvasRenderingContext2d,
215    image: &HtmlImageElement,
216    source: &Rect,
217    transform: &Transform2D,
218) {
219    let rotation: f64 = transform.get_rotation();
220    let cos: f64 = rotation.cos();
221    let sin: f64 = rotation.sin();
222    let scale_x: f64 = transform.get_scale().get_x();
223    let scale_y: f64 = transform.get_scale().get_y();
224    let _: Result<(), JsValue> = context.set_transform(
225        cos * scale_x,
226        sin * scale_x,
227        -sin * scale_y,
228        cos * scale_y,
229        transform.get_position().get_x(),
230        transform.get_position().get_y(),
231    );
232    let _: Result<(), JsValue> = context
233        .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
234            image,
235            source.get_x(),
236            source.get_y(),
237            source.get_width(),
238            source.get_height(),
239            -source.get_width() * 0.5,
240            -source.get_height() * 0.5,
241            source.get_width(),
242            source.get_height(),
243        );
244    let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
245}
246
247/// Implements drawing and camera management methods for `CanvasRenderer`.
248/// Implements recording and replay for `DrawList`.
249impl DrawList {
250    /// Creates an empty draw list.
251    ///
252    /// # Returns
253    ///
254    /// - `DrawList` - The new empty draw list.
255    pub fn create() -> DrawList {
256        DrawList::new(Vec::new())
257    }
258
259    /// Returns whether the list contains no commands.
260    ///
261    /// # Returns
262    ///
263    /// - `bool` - `true` if there are no recorded commands.
264    pub fn is_empty(&self) -> bool {
265        self.get_commands().is_empty()
266    }
267
268    /// Returns the number of recorded commands.
269    ///
270    /// # Returns
271    ///
272    /// - `usize` - The command count.
273    pub fn len(&self) -> usize {
274        self.get_commands().len()
275    }
276
277    /// Removes all recorded commands, keeping the allocated capacity for reuse
278    /// on the next frame.
279    pub fn clear(&mut self) {
280        self.get_mut_commands().clear();
281    }
282
283    /// Records a fill-rectangle command.
284    pub fn fill_rect(&mut self, position: Vector2D, width: f64, height: f64, color: Color) {
285        self.get_mut_commands().push(DrawCommand::FillRect {
286            position,
287            width,
288            height,
289            color,
290        });
291    }
292
293    /// Records a stroke-rectangle command.
294    pub fn stroke_rect(
295        &mut self,
296        position: Vector2D,
297        width: f64,
298        height: f64,
299        color: Color,
300        line_width: f64,
301    ) {
302        self.get_mut_commands().push(DrawCommand::StrokeRect {
303            position,
304            width,
305            height,
306            color,
307            line_width,
308        });
309    }
310
311    /// Records a fill-circle command.
312    pub fn fill_circle(&mut self, center: Vector2D, radius: f64, color: Color) {
313        self.get_mut_commands().push(DrawCommand::FillCircle {
314            center,
315            radius,
316            color,
317        });
318    }
319
320    /// Records a stroke-circle command.
321    pub fn stroke_circle(&mut self, center: Vector2D, radius: f64, color: Color, line_width: f64) {
322        self.get_mut_commands().push(DrawCommand::StrokeCircle {
323            center,
324            radius,
325            color,
326            line_width,
327        });
328    }
329
330    /// Records a line-segment command.
331    pub fn draw_line(&mut self, start: Vector2D, end: Vector2D, color: Color, line_width: f64) {
332        self.get_mut_commands().push(DrawCommand::Line {
333            start,
334            end,
335            color,
336            line_width,
337        });
338    }
339
340    /// Records a fill-text command.
341    pub fn fill_text<T, F>(&mut self, text: T, position: Vector2D, color: Color, font: F)
342    where
343        T: AsRef<str>,
344        F: AsRef<str>,
345    {
346        self.get_mut_commands().push(DrawCommand::FillText {
347            text: text.as_ref().to_string(),
348            position,
349            color,
350            font: font.as_ref().to_string(),
351        });
352    }
353
354    /// Records a transformed sprite draw command.
355    pub fn draw_sprite(&mut self, image: &HtmlImageElement, source: Rect, transform: Transform2D) {
356        self.get_mut_commands().push(DrawCommand::DrawSprite {
357            image: image.clone(),
358            source,
359            transform,
360        });
361    }
362
363    /// Records an image sub-region draw command (no rotation).
364    pub fn draw_image_rect(
365        &mut self,
366        image: &HtmlImageElement,
367        source: Rect,
368        dest_position: Vector2D,
369        dest_width: f64,
370        dest_height: f64,
371    ) {
372        self.get_mut_commands().push(DrawCommand::DrawImageRect {
373            image: image.clone(),
374            source,
375            dest_position,
376            dest_width,
377            dest_height,
378        });
379    }
380
381    /// Records a global-alpha state change.
382    pub fn set_global_alpha(&mut self, alpha: f64) {
383        self.get_mut_commands()
384            .push(DrawCommand::SetGlobalAlpha { alpha });
385    }
386
387    /// Records a blend-mode state change.
388    pub fn set_blend_mode(&mut self, mode: BlendMode) {
389        self.get_mut_commands()
390            .push(DrawCommand::SetBlendMode { mode });
391    }
392}
393
394impl CanvasRenderer {
395    /// Creates a new renderer from a canvas element selector and viewport dimensions.
396    ///
397    /// # Arguments
398    ///
399    /// - `&str` - The CSS selector for the canvas element.
400    /// - `f64` - The viewport width.
401    /// - `f64` - The viewport height.
402    ///
403    /// # Returns
404    ///
405    /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas was not found.
406    pub fn from_selector<S>(
407        canvas_selector: S,
408        viewport_width: f64,
409        viewport_height: f64,
410    ) -> Option<CanvasRenderer>
411    where
412        S: AsRef<str>,
413    {
414        let window_value: Window = window().expect("no global window exists");
415        let document_value: Document = window_value.document().expect("should have a document");
416        let element: Element = document_value
417            .query_selector(canvas_selector.as_ref())
418            .ok()
419            .flatten()?;
420        let canvas_element: HtmlCanvasElement = element.unchecked_into();
421        let context_object: Object = canvas_element
422            .get_context(RENDERER_CONTEXT_TYPE_2D)
423            .ok()
424            .flatten()?;
425        let context: CanvasRenderingContext2d = context_object.unchecked_into();
426        let renderer: CanvasRenderer = CanvasRenderer::new(
427            context,
428            Camera2D::create(viewport_width, viewport_height),
429            RenderQuality::default(),
430        );
431        renderer.enable_smoothing();
432        Some(renderer)
433    }
434
435    /// Enables high-quality anti-aliasing on the canvas context by setting
436    /// `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`.
437    ///
438    /// Applies the active `quality` preset via the shared `apply_quality`
439    /// helper so that all smoothing-related settings are kept in sync.
440    pub fn enable_smoothing(&self) {
441        Self::apply_quality(self.get_context(), self.get_quality());
442    }
443
444    /// Clears the entire canvas viewport.
445    pub fn clear(&self) {
446        self.get_context().clear_rect(
447            0.0,
448            0.0,
449            self.get_camera().get_viewport_width(),
450            self.get_camera().get_viewport_height(),
451        );
452    }
453
454    /// Clears the canvas and fills it with the given CSS color string.
455    ///
456    /// # Arguments
457    ///
458    /// - `C: AsRef<str>` - The CSS color string (e.g., `"#000000"`).
459    pub fn clear_color<C>(&self, color: C)
460    where
461        C: AsRef<str>,
462    {
463        self.get_context().set_fill_style_str(color.as_ref());
464        self.get_context().fill_rect(
465            0.0,
466            0.0,
467            self.get_camera().get_viewport_width(),
468            self.get_camera().get_viewport_height(),
469        );
470    }
471
472    /// Saves the current canvas state (transform, styles) onto the state stack.
473    pub fn save(&self) {
474        self.get_context().save();
475    }
476
477    /// Restores the most recently saved canvas state.
478    pub fn restore(&self) {
479        self.get_context().restore();
480    }
481
482    /// Replays a recorded `DrawList` onto this renderer's canvas.
483    ///
484    /// Convenience wrapper around `replay_context` using this renderer's context.
485    ///
486    /// # Arguments
487    ///
488    /// - `&DrawList` - The recorded commands to replay.
489    pub fn replay(&self, list: &DrawList) {
490        Self::replay_context(self.get_context(), list);
491    }
492
493    /// Replays a recorded `DrawList` onto an arbitrary canvas 2D context in a
494    /// single batched pass.
495    ///
496    /// Consecutive same-style shapes are merged into one path (one `begin_path`
497    /// plus one `fill`/`stroke` per style run), fill/stroke colors and line
498    /// widths are only re-applied when they change, and sprites are drawn with a
499    /// single `set_transform` rather than a save/restore pair. This collapses
500    /// the per-shape canvas state churn of immediate-mode drawing.
501    ///
502    /// The canvas transform and global alpha are reset to identity / 1.0 when
503    /// replay finishes, so callers can sandwich the call between
504    /// `save()`/`apply_camera()` and `restore()` without leaking state.
505    ///
506    /// # Arguments
507    ///
508    /// - `&CanvasRenderingContext2d` - The target canvas 2D context.
509    /// - `&DrawList` - The recorded commands to replay.
510    pub fn replay_context(context: &CanvasRenderingContext2d, list: &DrawList) {
511        let mut current_fill: Option<Color> = None;
512        let mut current_stroke: Option<Color> = None;
513        let mut current_line_width: f64 = f64::NAN;
514        // Whether a same-style path run is currently open.
515        let mut run_open: bool = false;
516        let mut run_is_fill: bool = true;
517        let mut run_key: Option<(u8, Color, f64)> = None;
518
519        // Returns the style key for a path-batchable command, or `None` for
520        // commands that break a run (sprites, images, text, state changes).
521        fn batch_key(command: &DrawCommand) -> Option<(u8, Color, f64)> {
522            match command {
523                DrawCommand::FillRect { color, .. } | DrawCommand::FillCircle { color, .. } => {
524                    Some((0, *color, 0.0))
525                }
526                DrawCommand::StrokeRect {
527                    color, line_width, ..
528                }
529                | DrawCommand::StrokeCircle {
530                    color, line_width, ..
531                }
532                | DrawCommand::Line {
533                    color, line_width, ..
534                } => Some((1, *color, *line_width)),
535                _ => None,
536            }
537        }
538
539        // Emits a single path-batchable command's geometry into the open path.
540        fn emit_geometry(context: &CanvasRenderingContext2d, command: &DrawCommand) {
541            match command {
542                DrawCommand::FillRect {
543                    position,
544                    width,
545                    height,
546                    ..
547                }
548                | DrawCommand::StrokeRect {
549                    position,
550                    width,
551                    height,
552                    ..
553                } => {
554                    context.rect(position.get_x(), position.get_y(), *width, *height);
555                }
556                DrawCommand::FillCircle { center, radius, .. }
557                | DrawCommand::StrokeCircle { center, radius, .. } => {
558                    context.move_to(center.get_x() + radius, center.get_y());
559                    let _: Result<(), JsValue> =
560                        context.arc(center.get_x(), center.get_y(), *radius, 0.0, TWO_PI);
561                }
562                DrawCommand::Line { start, end, .. } => {
563                    context.move_to(start.get_x(), start.get_y());
564                    context.line_to(end.get_x(), end.get_y());
565                }
566                _ => {}
567            }
568        }
569
570        for command in self_commands(list) {
571            let key: Option<(u8, Color, f64)> = batch_key(command);
572            // Close the open run if this command breaks it or starts a new style.
573            if run_open && key != run_key {
574                if run_is_fill {
575                    context.fill();
576                } else {
577                    context.stroke();
578                }
579                run_open = false;
580            }
581            if let Some(current_key) = key {
582                // Begin (or continue) a same-style path run.
583                if !run_open {
584                    let (kind, color, line_width) = current_key;
585                    if kind == 0 {
586                        if current_fill != Some(color) {
587                            context.set_fill_style_str(&Color::to_css(&color));
588                            current_fill = Some(color);
589                        }
590                        run_is_fill = true;
591                    } else {
592                        if current_stroke != Some(color) {
593                            context.set_stroke_style_str(&Color::to_css(&color));
594                            current_stroke = Some(color);
595                        }
596                        if current_line_width != line_width {
597                            context.set_line_width(line_width);
598                            current_line_width = line_width;
599                        }
600                        run_is_fill = false;
601                    }
602                    context.begin_path();
603                    run_open = true;
604                    run_key = Some(current_key);
605                }
606                emit_geometry(context, command);
607                continue;
608            }
609            // Non-batchable command: draw it immediately.
610            match command {
611                DrawCommand::FillText {
612                    text,
613                    position,
614                    color,
615                    font,
616                } => {
617                    if current_fill != Some(*color) {
618                        context.set_fill_style_str(&Color::to_css(color));
619                        current_fill = Some(*color);
620                    }
621                    context.set_font(font);
622                    let _: Result<(), JsValue> =
623                        context.fill_text(text, position.get_x(), position.get_y());
624                }
625                DrawCommand::DrawSprite {
626                    image,
627                    source,
628                    transform,
629                } => {
630                    draw_sprite_immediate(context, image, source, transform);
631                }
632                DrawCommand::DrawImageRect {
633                    image,
634                    source,
635                    dest_position,
636                    dest_width,
637                    dest_height,
638                } => {
639                    let _: Result<(), JsValue> = context
640                        .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
641                            image,
642                            source.get_x(),
643                            source.get_y(),
644                            source.get_width(),
645                            source.get_height(),
646                            dest_position.get_x(),
647                            dest_position.get_y(),
648                            *dest_width,
649                            *dest_height,
650                        );
651                }
652                DrawCommand::SetGlobalAlpha { alpha } => {
653                    context.set_global_alpha(Numeric::clamp(*alpha, 0.0, 1.0));
654                }
655                DrawCommand::SetBlendMode { mode } => {
656                    let _: Result<(), JsValue> =
657                        context.set_global_composite_operation(mode.to_css());
658                }
659                _ => {}
660            }
661        }
662        // Flush any trailing open run.
663        if run_open {
664            if run_is_fill {
665                context.fill();
666            } else {
667                context.stroke();
668            }
669        }
670        let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
671        context.set_global_alpha(1.0);
672    }
673
674    /// Applies the camera transform to the canvas context.
675    ///
676    /// Translates to the screen center, applies zoom and rotation,
677    /// then offsets by the negative camera position.
678    pub fn apply_camera(&self) {
679        let camera: Camera2D = self.get_camera();
680        let _: Result<(), JsValue> = self.get_context().translate(
681            camera.get_viewport_width() * 0.5,
682            camera.get_viewport_height() * 0.5,
683        );
684        let _: Result<(), JsValue> = self
685            .get_context()
686            .scale(camera.get_zoom(), camera.get_zoom());
687        let _: Result<(), JsValue> = self.get_context().rotate(camera.get_rotation());
688        let _: Result<(), JsValue> = self.get_context().translate(
689            -camera.get_position().get_x(),
690            -camera.get_position().get_y(),
691        );
692    }
693
694    /// Sets the fill color for subsequent fill operations.
695    ///
696    /// # Arguments
697    ///
698    /// - `C: AsRef<str>` - The CSS color string.
699    pub fn set_fill_color<C>(&self, color: C)
700    where
701        C: AsRef<str>,
702    {
703        self.get_context().set_fill_style_str(color.as_ref());
704    }
705
706    /// Sets the stroke color for subsequent stroke operations.
707    ///
708    /// # Arguments
709    ///
710    /// - `C: AsRef<str>` - The CSS color string.
711    pub fn set_stroke_color<C>(&self, color: C)
712    where
713        C: AsRef<str>,
714    {
715        self.get_context().set_stroke_style_str(color.as_ref());
716    }
717
718    /// Sets the line width for subsequent stroke operations.
719    ///
720    /// # Arguments
721    ///
722    /// - `f64` - The line width in pixels.
723    pub fn set_line_width(&self, width: f64) {
724        self.get_context().set_line_width(width);
725    }
726
727    /// Sets the global alpha (opacity) for all subsequent drawing operations.
728    ///
729    /// # Arguments
730    ///
731    /// - `f64` - The alpha value in the range 0.0 to 1.0.
732    pub fn set_global_alpha(&self, alpha: f64) {
733        self.get_context()
734            .set_global_alpha(Numeric::clamp(alpha, 0.0, 1.0));
735    }
736
737    /// Fills a rectangle at the given world-space position and dimensions.
738    ///
739    /// # Arguments
740    ///
741    /// - `Vector2D` - The top-left position in world space.
742    /// - `f64` - The width.
743    /// - `f64` - The height.
744    pub fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
745        self.get_context()
746            .fill_rect(position.get_x(), position.get_y(), width, height);
747    }
748
749    /// Strokes the outline of a rectangle at the given world-space position and dimensions.
750    ///
751    /// # Arguments
752    ///
753    /// - `Vector2D` - The top-left position in world space.
754    /// - `f64` - The width.
755    /// - `f64` - The height.
756    pub fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
757        self.get_context()
758            .stroke_rect(position.get_x(), position.get_y(), width, height);
759    }
760
761    /// Fills a circle at the given world-space center with the specified radius.
762    ///
763    /// # Arguments
764    ///
765    /// - `Vector2D` - The center in world space.
766    /// - `f64` - The radius.
767    pub fn fill_circle(&self, center: Vector2D, radius: f64) {
768        self.get_context().begin_path();
769        self.get_context()
770            .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
771            .unwrap_or(());
772        self.get_context().fill();
773    }
774
775    /// Strokes the outline of a circle at the given world-space center.
776    ///
777    /// # Arguments
778    ///
779    /// - `Vector2D` - The center in world space.
780    /// - `f64` - The radius.
781    pub fn stroke_circle(&self, center: Vector2D, radius: f64) {
782        self.get_context().begin_path();
783        self.get_context()
784            .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
785            .unwrap_or(());
786        self.get_context().stroke();
787    }
788
789    /// Draws a line segment between two world-space points.
790    ///
791    /// # Arguments
792    ///
793    /// - `Vector2D` - The start point.
794    /// - `Vector2D` - The end point.
795    pub fn draw_line(&self, start: Vector2D, end: Vector2D) {
796        self.get_context().begin_path();
797        self.get_context().move_to(start.get_x(), start.get_y());
798        self.get_context().line_to(end.get_x(), end.get_y());
799        self.get_context().stroke();
800    }
801
802    /// Fills text at the given world-space position.
803    ///
804    /// # Arguments
805    ///
806    /// - `T: AsRef<str>` - The text to draw.
807    /// - `Vector2D` - The position in world space.
808    pub fn fill_text<T>(&self, text: T, position: Vector2D)
809    where
810        T: AsRef<str>,
811    {
812        self.get_context()
813            .fill_text(text.as_ref(), position.get_x(), position.get_y())
814            .unwrap_or(());
815    }
816
817    /// Sets the font for subsequent text rendering.
818    ///
819    /// # Arguments
820    ///
821    /// - `F: AsRef<str>` - The CSS font string (e.g., `"16px sans-serif"`).
822    pub fn set_font<F>(&self, font: F)
823    where
824        F: AsRef<str>,
825    {
826        self.get_context().set_font(font.as_ref());
827    }
828
829    /// Draws an image element at the given world-space position and dimensions.
830    ///
831    /// # Arguments
832    ///
833    /// - `&HtmlImageElement` - The image element to draw.
834    /// - `Vector2D` - The top-left position in world space.
835    /// - `f64` - The destination width.
836    /// - `f64` - The destination height.
837    pub fn draw_image(
838        &self,
839        image: &HtmlImageElement,
840        position: Vector2D,
841        width: f64,
842        height: f64,
843    ) {
844        let _: Result<(), JsValue> = self
845            .get_context()
846            .draw_image_with_html_image_element_and_dw_and_dh(
847                image,
848                position.get_x(),
849                position.get_y(),
850                width,
851                height,
852            );
853    }
854
855    /// Draws a sub-region of an image element at the given world-space position.
856    ///
857    /// # Arguments
858    ///
859    /// - `&HtmlImageElement` - The image element to draw.
860    /// - `Rect` - The source rectangle within the image.
861    /// - `Vector2D` - The destination top-left position in world space.
862    /// - `f64` - The destination width.
863    /// - `f64` - The destination height.
864    pub fn draw_image_rect(
865        &self,
866        image: &HtmlImageElement,
867        source: Rect,
868        dest_position: Vector2D,
869        dest_width: f64,
870        dest_height: f64,
871    ) {
872        let _: Result<(), JsValue> = self
873            .get_context()
874            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
875                image,
876                source.get_x(),
877                source.get_y(),
878                source.get_width(),
879                source.get_height(),
880                dest_position.get_x(),
881                dest_position.get_y(),
882                dest_width,
883                dest_height,
884            );
885    }
886}
887
888/// Implements 3D camera transformation and projection methods for `Camera3D`.
889impl Camera3D {
890    /// Creates a new 3D camera at the given position looking at the target.
891    ///
892    /// # Arguments
893    ///
894    /// - `Vector3D` - The eye position.
895    /// - `Vector3D` - The target position to look at.
896    /// - `f64` - The viewport width.
897    /// - `f64` - The viewport height.
898    ///
899    /// # Returns
900    ///
901    /// - `Camera3D` - The new camera.
902    pub fn create(
903        position: Vector3D,
904        target: Vector3D,
905        viewport_width: f64,
906        viewport_height: f64,
907    ) -> Camera3D {
908        let mut camera: Camera3D = Camera3D::new(position, target, viewport_width, viewport_height);
909        camera.set_up(Vector3D::up());
910        camera.set_fov(DEFAULT_CAMERA_FOV);
911        camera.set_near(DEFAULT_CAMERA_NEAR);
912        camera.set_far(DEFAULT_CAMERA_FAR);
913        camera
914    }
915
916    /// Returns the aspect ratio (width / height).
917    ///
918    /// # Returns
919    ///
920    /// - `f64` - The aspect ratio.
921    pub fn aspect(&self) -> f64 {
922        if self.get_viewport_height() < EPSILON {
923            return 1.0;
924        }
925        self.get_viewport_width() / self.get_viewport_height()
926    }
927
928    /// Returns the forward direction (from position to target, normalized).
929    ///
930    /// # Returns
931    ///
932    /// - `Vector3D` - The forward direction.
933    pub fn forward(&self) -> Vector3D {
934        (self.get_target() - self.get_position()).normalized()
935    }
936
937    /// Returns the right direction (cross product of forward and up).
938    ///
939    /// # Returns
940    ///
941    /// - `Vector3D` - The right direction.
942    pub fn right(&self) -> Vector3D {
943        self.forward().cross(self.get_up()).normalized()
944    }
945
946    /// Returns the view matrix for this camera.
947    ///
948    /// # Returns
949    ///
950    /// - `Matrix4x4` - The view matrix.
951    pub fn view_matrix(&self) -> Matrix4x4 {
952        Matrix4x4::look_at(self.get_position(), self.get_target(), self.get_up())
953    }
954
955    /// Returns the perspective projection matrix for this camera.
956    ///
957    /// # Returns
958    ///
959    /// - `Matrix4x4` - The projection matrix.
960    pub fn projection_matrix(&self) -> Matrix4x4 {
961        Matrix4x4::perspective(
962            self.get_fov(),
963            self.aspect(),
964            self.get_near(),
965            self.get_far(),
966        )
967    }
968
969    /// Returns the combined view-projection matrix.
970    ///
971    /// # Returns
972    ///
973    /// - `Matrix4x4` - The view-projection matrix.
974    pub fn view_proj_matrix(&self) -> Matrix4x4 {
975        self.projection_matrix().multiply(self.view_matrix())
976    }
977
978    /// Converts a 3D world-space point to screen-space (NDC) coordinates.
979    ///
980    /// # Arguments
981    ///
982    /// - `Vector3D` - The world-space point.
983    ///
984    /// # Returns
985    ///
986    /// - `Vector3D` - The screen-space point where x and y are in [0, 1] and z is the depth.
987    pub fn world_to_screen(&self, world: Vector3D) -> Vector3D {
988        let clip: Vector3D = self.view_proj_matrix().transform_point(world);
989        Vector3D::new(
990            (clip.get_x() + 1.0) * 0.5 * self.get_viewport_width(),
991            (1.0 - clip.get_y()) * 0.5 * self.get_viewport_height(),
992            clip.get_z(),
993        )
994    }
995
996    /// Projects a world-space point and returns whether it is within the camera frustum.
997    ///
998    /// # Arguments
999    ///
1000    /// - `Vector3D` - The world-space point.
1001    ///
1002    /// # Returns
1003    ///
1004    /// - `bool` - True if the point is within the frustum.
1005    pub fn in_frustum(&self, world: Vector3D) -> bool {
1006        let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1007        clip.get_x() >= -1.0
1008            && clip.get_x() <= 1.0
1009            && clip.get_y() >= -1.0
1010            && clip.get_y() <= 1.0
1011            && clip.get_z() >= -1.0
1012            && clip.get_z() <= 1.0
1013    }
1014
1015    /// Moves the camera position by the given offset, keeping the target offset by the same amount.
1016    ///
1017    /// # Arguments
1018    ///
1019    /// - `Vector3D` - The translation offset.
1020    pub fn translate(&mut self, offset: Vector3D) {
1021        self.set_position(self.get_position() + offset);
1022        self.set_target(self.get_target() + offset);
1023    }
1024
1025    /// Moves the camera position towards the target by the given distance.
1026    ///
1027    /// # Arguments
1028    ///
1029    /// - `f64` - The distance to zoom in (positive) or out (negative).
1030    pub fn zoom(&mut self, distance: f64) {
1031        let direction: Vector3D = self.forward();
1032        self.set_position(self.get_position() + direction.scaled(distance));
1033    }
1034
1035    /// Orbits the camera around the target by the given yaw and pitch angles.
1036    ///
1037    /// # Arguments
1038    ///
1039    /// - `f64` - The yaw delta in radians (horizontal rotation).
1040    /// - `f64` - The pitch delta in radians (vertical rotation).
1041    pub fn orbit(&mut self, yaw_delta: f64, pitch_delta: f64) {
1042        let offset: Vector3D = self.get_position() - self.get_target();
1043        let current_distance: f64 = offset.magnitude();
1044        let current_yaw: f64 = offset.get_x().atan2(offset.get_z());
1045        let horizontal_dist: f64 =
1046            (offset.get_x() * offset.get_x() + offset.get_z() * offset.get_z()).sqrt();
1047        let current_pitch: f64 = (offset.get_y() / horizontal_dist.max(EPSILON)).asin();
1048        let new_yaw: f64 = current_yaw + yaw_delta;
1049        let new_pitch: f64 = Numeric::clamp(
1050            current_pitch + pitch_delta,
1051            -HALF_PI + EPSILON,
1052            HALF_PI - EPSILON,
1053        );
1054        let cos_pitch: f64 = new_pitch.cos();
1055        self.set_position(
1056            self.get_target()
1057                + Vector3D::new(
1058                    new_yaw.sin() * cos_pitch * current_distance,
1059                    new_pitch.sin() * current_distance,
1060                    new_yaw.cos() * cos_pitch * current_distance,
1061                ),
1062        );
1063    }
1064}
1065
1066/// Implements `Default` for `Camera3D` as a camera at (0, 0, 5) looking at the origin.
1067impl Default for Camera3D {
1068    fn default() -> Camera3D {
1069        Camera3D::create(Vector3D::new(0.0, 0.0, 5.0), Vector3D::zero(), 800.0, 600.0)
1070    }
1071}
1072
1073/// Implements construction, presentation, and anti-aliasing methods for `SsaaCanvas`.
1074impl SsaaCanvas {
1075    /// Creates an `SsaaCanvas` from a CSS selector using the default scale factor.
1076    ///
1077    /// # Arguments
1078    ///
1079    /// - `S: AsRef<str>` - The CSS selector for the display canvas element.
1080    /// - `f64` - The logical display width in CSS pixels.
1081    /// - `f64` - The logical display height in CSS pixels.
1082    ///
1083    /// # Returns
1084    ///
1085    /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1086    pub fn from_selector<S>(canvas_selector: S, width: f64, height: f64) -> Option<SsaaCanvas>
1087    where
1088        S: AsRef<str>,
1089    {
1090        Self::from_selector_with_scale(
1091            canvas_selector,
1092            width,
1093            height,
1094            RENDERER_DEFAULT_SSAA_SCALE_FACTOR,
1095        )
1096    }
1097
1098    /// Creates an `SsaaCanvas` from a CSS selector with a custom SSAA scale factor.
1099    ///
1100    /// The offscreen canvas is created at `width * scale_factor` by `height * scale_factor`
1101    /// pixels, and its context is pre-scaled so that drawing code uses logical coordinates.
1102    ///
1103    /// # Arguments
1104    ///
1105    /// - `S: AsRef<str>` - The CSS selector for the display canvas element.
1106    /// - `f64` - The logical display width in CSS pixels.
1107    /// - `f64` - The logical display height in CSS pixels.
1108    /// - `f64` - The supersampling scale factor (e.g., 2.0 for 4x SSAA).
1109    ///
1110    /// # Returns
1111    ///
1112    /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1113    pub fn from_selector_with_scale<S>(
1114        canvas_selector: S,
1115        width: f64,
1116        height: f64,
1117        scale_factor: f64,
1118    ) -> Option<SsaaCanvas>
1119    where
1120        S: AsRef<str>,
1121    {
1122        let window_value: Window = window().expect("no global window exists");
1123        let document_value: Document = window_value.document().expect("should have a document");
1124        let element: Element = document_value
1125            .query_selector(canvas_selector.as_ref())
1126            .ok()
1127            .flatten()?;
1128        let display_canvas: HtmlCanvasElement = element.unchecked_into();
1129        let device_pixel_ratio: f64 = CanvasRenderer::detect_dpr();
1130        let physical_width: u32 = (width * device_pixel_ratio).round() as u32;
1131        let physical_height: u32 = (height * device_pixel_ratio).round() as u32;
1132        display_canvas.set_width(physical_width);
1133        display_canvas.set_height(physical_height);
1134        let display_context_object: Object = display_canvas
1135            .get_context(RENDERER_CONTEXT_TYPE_2D)
1136            .ok()
1137            .flatten()?;
1138        let display_context: CanvasRenderingContext2d = display_context_object.unchecked_into();
1139        let _: Result<(), JsValue> = display_context.scale(device_pixel_ratio, device_pixel_ratio);
1140        let offscreen_canvas: HtmlCanvasElement = document_value
1141            .create_element(RENDERER_ELEMENT_CANVAS)
1142            .ok()?
1143            .unchecked_into();
1144        let scaled_width: u32 = (width * scale_factor * device_pixel_ratio).round() as u32;
1145        let scaled_height: u32 = (height * scale_factor * device_pixel_ratio).round() as u32;
1146        offscreen_canvas.set_width(scaled_width);
1147        offscreen_canvas.set_height(scaled_height);
1148        let offscreen_context_object: Object = offscreen_canvas
1149            .get_context(RENDERER_CONTEXT_TYPE_2D)
1150            .ok()
1151            .flatten()?;
1152        let offscreen_context: CanvasRenderingContext2d = offscreen_context_object.unchecked_into();
1153        let _: Result<(), JsValue> = offscreen_context.scale(
1154            scale_factor * device_pixel_ratio,
1155            scale_factor * device_pixel_ratio,
1156        );
1157        let ssaa_canvas: SsaaCanvas = SsaaCanvas::new(
1158            display_canvas,
1159            display_context,
1160            offscreen_canvas,
1161            offscreen_context,
1162            scale_factor,
1163            width,
1164            height,
1165        );
1166        ssaa_canvas.enable_smoothing();
1167        Some(ssaa_canvas)
1168    }
1169
1170    /// Presents the offscreen buffer onto the display canvas with high-quality downscaling.
1171    ///
1172    /// Applies the active `quality` preset to the display context, clears the
1173    /// display canvas, then draws the offscreen canvas scaled down to the
1174    /// logical display size. This is the core SSAA step that produces smooth
1175    /// polygon edges.
1176    pub fn present(&self) {
1177        CanvasRenderer::apply_quality(self.get_display_context(), self.get_quality());
1178        self.get_display_context()
1179            .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1180        let _: Result<(), JsValue> = self
1181            .get_display_context()
1182            .draw_image_with_html_canvas_element_and_dw_and_dh(
1183                self.get_offscreen_canvas(),
1184                0.0,
1185                0.0,
1186                self.get_width(),
1187                self.get_height(),
1188            );
1189    }
1190
1191    /// Clears the offscreen buffer to transparent.
1192    pub fn clear(&self) {
1193        self.get_offscreen_context()
1194            .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1195    }
1196
1197    /// Clears the offscreen buffer and fills it with the given CSS color.
1198    ///
1199    /// # Arguments
1200    ///
1201    /// - `C: AsRef<str>` - The CSS color string.
1202    pub fn clear_color<C>(&self, color: C)
1203    where
1204        C: AsRef<str>,
1205    {
1206        self.get_offscreen_context()
1207            .set_fill_style_str(color.as_ref());
1208        self.get_offscreen_context()
1209            .fill_rect(0.0, 0.0, self.get_width(), self.get_height());
1210    }
1211
1212    /// Enables high-quality anti-aliasing on both the display and offscreen contexts.
1213    ///
1214    /// Applies the active `quality` preset to both contexts via the shared
1215    /// `apply_quality` helper.
1216    pub fn enable_smoothing(&self) {
1217        let quality: RenderQuality = self.get_quality();
1218        CanvasRenderer::apply_quality(self.get_display_context(), quality);
1219        CanvasRenderer::apply_quality(self.get_offscreen_context(), quality);
1220    }
1221}
1222
1223/// Implements CSS composite operation string conversion for `BlendMode`.
1224impl BlendMode {
1225    /// Returns the CSS `globalCompositeOperation` string for this blend mode.
1226    ///
1227    /// # Returns
1228    ///
1229    /// - `&str` - The CSS composite operation string.
1230    pub fn to_css(&self) -> &str {
1231        match self {
1232            BlendMode::Normal => BLEND_MODE_NORMAL,
1233            BlendMode::Multiply => BLEND_MODE_MULTIPLY,
1234            BlendMode::Screen => BLEND_MODE_SCREEN,
1235            BlendMode::Lighter => BLEND_MODE_LIGHTER,
1236            BlendMode::Overlay => BLEND_MODE_OVERLAY,
1237            BlendMode::Darken => BLEND_MODE_DARKEN,
1238            BlendMode::Lighten => BLEND_MODE_LIGHTEN,
1239            BlendMode::ColorDodge => BLEND_MODE_COLOR_DODGE,
1240            BlendMode::ColorBurn => BLEND_MODE_COLOR_BURN,
1241            BlendMode::HardLight => BLEND_MODE_HARD_LIGHT,
1242            BlendMode::SoftLight => BLEND_MODE_SOFT_LIGHT,
1243            BlendMode::Difference => BLEND_MODE_DIFFERENCE,
1244            BlendMode::Exclusion => BLEND_MODE_EXCLUSION,
1245            BlendMode::Hue => BLEND_MODE_HUE,
1246            BlendMode::Saturation => BLEND_MODE_SATURATION,
1247            BlendMode::Color => BLEND_MODE_COLOR,
1248            BlendMode::Luminosity => BLEND_MODE_LUMINOSITY,
1249        }
1250    }
1251}
1252
1253/// Implements construction and canvas gradient creation for `LinearGradient`.
1254impl LinearGradient {
1255    /// Creates a new linear gradient from two points and a list of color stops.
1256    ///
1257    /// # Arguments
1258    ///
1259    /// - `Vector2D` - The start point.
1260    /// - `Vector2D` - The end point.
1261    /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1262    ///
1263    /// # Returns
1264    ///
1265    /// - `LinearGradient` - The new gradient.
1266    pub fn create(start: Vector2D, end: Vector2D, stops: Vec<(f64, String)>) -> LinearGradient {
1267        LinearGradient::new(start, end, stops)
1268    }
1269
1270    /// Creates a `CanvasGradient` from this gradient definition on the given context.
1271    ///
1272    /// # Arguments
1273    ///
1274    /// - `&CanvasRenderingContext2d` - The canvas context.
1275    ///
1276    /// # Returns
1277    ///
1278    /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1279    pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1280        let canvas_gradient: CanvasGradient = context.create_linear_gradient(
1281            self.get_start().get_x(),
1282            self.get_start().get_y(),
1283            self.get_end().get_x(),
1284            self.get_end().get_y(),
1285        );
1286        for (position, color) in self.get_stops() {
1287            let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1288        }
1289        Some(canvas_gradient)
1290    }
1291}
1292
1293/// Implements construction and canvas gradient creation for `RadialGradient`.
1294impl RadialGradient {
1295    /// Creates a new radial gradient from inner and outer circles and color stops.
1296    ///
1297    /// # Arguments
1298    ///
1299    /// - `Vector2D` - The inner circle center.
1300    /// - `f64` - The inner circle radius.
1301    /// - `Vector2D` - The outer circle center.
1302    /// - `f64` - The outer circle radius.
1303    /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1304    ///
1305    /// # Returns
1306    ///
1307    /// - `RadialGradient` - The new gradient.
1308    pub fn create(
1309        inner_center: Vector2D,
1310        inner_radius: f64,
1311        outer_center: Vector2D,
1312        outer_radius: f64,
1313        stops: Vec<(f64, String)>,
1314    ) -> RadialGradient {
1315        RadialGradient::new(
1316            inner_center,
1317            inner_radius,
1318            outer_center,
1319            outer_radius,
1320            stops,
1321        )
1322    }
1323
1324    /// Creates a `CanvasGradient` from this gradient definition on the given context.
1325    ///
1326    /// # Arguments
1327    ///
1328    /// - `&CanvasRenderingContext2d` - The canvas context.
1329    ///
1330    /// # Returns
1331    ///
1332    /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1333    pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1334        let canvas_gradient: CanvasGradient = context
1335            .create_radial_gradient(
1336                self.get_inner_center().get_x(),
1337                self.get_inner_center().get_y(),
1338                self.get_inner_radius(),
1339                self.get_outer_center().get_x(),
1340                self.get_outer_center().get_y(),
1341                self.get_outer_radius(),
1342            )
1343            .ok()?;
1344        for (position, color) in self.get_stops() {
1345            let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1346        }
1347        Some(canvas_gradient)
1348    }
1349}
1350
1351/// Implements construction methods for `ShadowConfig`.
1352impl ShadowConfig {
1353    /// Creates a shadow configuration with default values.
1354    ///
1355    /// # Returns
1356    ///
1357    /// - `ShadowConfig` - The default shadow configuration.
1358    pub fn create() -> ShadowConfig {
1359        ShadowConfig::new(
1360            RENDERER_DEFAULT_SHADOW_COLOR.to_string(),
1361            RENDERER_DEFAULT_SHADOW_BLUR,
1362            0.0,
1363            0.0,
1364        )
1365    }
1366}
1367
1368/// Implements `Default` for `ShadowConfig` with default shadow values.
1369impl Default for ShadowConfig {
1370    fn default() -> ShadowConfig {
1371        ShadowConfig::create()
1372    }
1373}
1374
1375/// Implements construction methods for `RenderLayer`.
1376impl RenderLayer {
1377    /// Creates a render layer with the given z-index and visibility.
1378    ///
1379    /// # Arguments
1380    ///
1381    /// - `i32` - The z-index determining draw order.
1382    /// - `bool` - Whether the layer is visible.
1383    ///
1384    /// # Returns
1385    ///
1386    /// - `RenderLayer` - The new render layer.
1387    pub fn create(z_index: i32, visible: bool) -> RenderLayer {
1388        RenderLayer::new(z_index, visible)
1389    }
1390
1391    /// Creates a background render layer with z-index 0 and visibility enabled.
1392    ///
1393    /// # Returns
1394    ///
1395    /// - `RenderLayer` - The background layer.
1396    pub fn background() -> RenderLayer {
1397        RenderLayer::new(RENDERER_LAYER_BACKGROUND, true)
1398    }
1399
1400    /// Creates a foreground render layer with a high z-index and visibility enabled.
1401    ///
1402    /// # Returns
1403    ///
1404    /// - `RenderLayer` - The foreground layer.
1405    pub fn foreground() -> RenderLayer {
1406        RenderLayer::new(RENDERER_LAYER_FOREGROUND, true)
1407    }
1408
1409    /// Creates a UI overlay render layer with the highest z-index and visibility enabled.
1410    ///
1411    /// # Returns
1412    ///
1413    /// - `RenderLayer` - The UI overlay layer.
1414    pub fn ui() -> RenderLayer {
1415        RenderLayer::new(RENDERER_LAYER_UI, true)
1416    }
1417}
1418
1419/// Implements blend mode, shadow, and gradient rendering methods for `CanvasRenderer`.
1420impl CanvasRenderer {
1421    /// Sets the blend mode for compositing subsequent draw operations.
1422    ///
1423    /// # Arguments
1424    ///
1425    /// - `BlendMode` - The blend mode to apply.
1426    pub fn set_blend_mode(&self, mode: BlendMode) {
1427        let _: Result<(), JsValue> = self
1428            .get_context()
1429            .set_global_composite_operation(mode.to_css());
1430    }
1431
1432    /// Applies a shadow configuration for subsequent draw operations.
1433    ///
1434    /// # Arguments
1435    ///
1436    /// - `&ShadowConfig` - The shadow configuration to apply.
1437    pub fn set_shadow(&self, config: &ShadowConfig) {
1438        self.get_context()
1439            .set_shadow_color(config.get_color().as_str());
1440        self.get_context().set_shadow_blur(config.get_blur());
1441        self.get_context()
1442            .set_shadow_offset_x(config.get_offset_x());
1443        self.get_context()
1444            .set_shadow_offset_y(config.get_offset_y());
1445    }
1446
1447    /// Clears any previously applied shadow, disabling shadow rendering.
1448    pub fn clear_shadow(&self) {
1449        self.get_context().set_shadow_color("rgba(0, 0, 0, 0)");
1450        self.get_context().set_shadow_blur(0.0);
1451        self.get_context().set_shadow_offset_x(0.0);
1452        self.get_context().set_shadow_offset_y(0.0);
1453    }
1454
1455    /// Applies a linear gradient as the fill style for subsequent operations.
1456    ///
1457    /// # Arguments
1458    ///
1459    /// - `&LinearGradient` - The linear gradient to use as fill style.
1460    pub fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1461        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1462            self.get_context()
1463                .set_fill_style_canvas_gradient(&canvas_gradient);
1464        }
1465    }
1466
1467    /// Applies a radial gradient as the fill style for subsequent operations.
1468    ///
1469    /// # Arguments
1470    ///
1471    /// - `&RadialGradient` - The radial gradient to use as fill style.
1472    pub fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1473        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1474            self.get_context()
1475                .set_fill_style_canvas_gradient(&canvas_gradient);
1476        }
1477    }
1478
1479    /// Applies a linear gradient as the stroke style for subsequent operations.
1480    ///
1481    /// # Arguments
1482    ///
1483    /// - `&LinearGradient` - The linear gradient to use as stroke style.
1484    pub fn set_linear_gradient_stroke(&self, gradient: &LinearGradient) {
1485        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1486            self.get_context()
1487                .set_stroke_style_canvas_gradient(&canvas_gradient);
1488        }
1489    }
1490
1491    /// Applies a radial gradient as the stroke style for subsequent operations.
1492    ///
1493    /// # Arguments
1494    ///
1495    /// - `&RadialGradient` - The radial gradient to use as stroke style.
1496    pub fn set_radial_gradient_stroke(&self, gradient: &RadialGradient) {
1497        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1498            self.get_context()
1499                .set_stroke_style_canvas_gradient(&canvas_gradient);
1500        }
1501    }
1502}
1503
1504/// Implements the `RenderBackend` trait for `CanvasRenderer`, providing
1505/// a backend-agnostic rendering interface.
1506///
1507/// Each method forwards to the inherent `CanvasRenderer` method of the
1508/// same name, so the per-call documentation lives on the trait definition
1509/// in `engine::renderer::trait` — the inherent method is the source of
1510/// truth, this impl is the trait bridge.
1511impl RenderBackend for CanvasRenderer {
1512    /// Forwards to [`CanvasRenderer::clear`].
1513    fn clear(&self) {
1514        self.clear();
1515    }
1516
1517    /// Forwards to [`CanvasRenderer::clear_color`].
1518    fn clear_color<C>(&self, color: C)
1519    where
1520        C: AsRef<str>,
1521    {
1522        self.clear_color(color);
1523    }
1524
1525    /// Forwards to [`CanvasRenderer::save`].
1526    fn save(&self) {
1527        self.save();
1528    }
1529
1530    /// Forwards to [`CanvasRenderer::restore`].
1531    fn restore(&self) {
1532        self.restore();
1533    }
1534
1535    /// Forwards to [`CanvasRenderer::set_fill_color`].
1536    fn set_fill_color(&self, color: &str) {
1537        self.set_fill_color(color);
1538    }
1539
1540    /// Forwards to [`CanvasRenderer::set_stroke_color`].
1541    fn set_stroke_color(&self, color: &str) {
1542        self.set_stroke_color(color);
1543    }
1544
1545    /// Forwards to [`CanvasRenderer::set_line_width`].
1546    fn set_line_width(&self, width: f64) {
1547        self.set_line_width(width);
1548    }
1549
1550    /// Forwards to [`CanvasRenderer::set_global_alpha`].
1551    fn set_global_alpha(&self, alpha: f64) {
1552        self.set_global_alpha(alpha);
1553    }
1554
1555    /// Forwards to [`CanvasRenderer::set_blend_mode`].
1556    fn set_blend_mode(&self, mode: BlendMode) {
1557        self.set_blend_mode(mode);
1558    }
1559
1560    /// Forwards to [`CanvasRenderer::set_shadow`].
1561    fn set_shadow(&self, config: &ShadowConfig) {
1562        self.set_shadow(config);
1563    }
1564
1565    /// Forwards to [`CanvasRenderer::clear_shadow`].
1566    fn clear_shadow(&self) {
1567        self.clear_shadow();
1568    }
1569
1570    /// Forwards to [`CanvasRenderer::fill_rect`].
1571    fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1572        self.fill_rect(position, width, height);
1573    }
1574
1575    /// Forwards to [`CanvasRenderer::stroke_rect`].
1576    fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1577        self.stroke_rect(position, width, height);
1578    }
1579
1580    /// Forwards to [`CanvasRenderer::fill_circle`].
1581    fn fill_circle(&self, center: Vector2D, radius: f64) {
1582        self.fill_circle(center, radius);
1583    }
1584
1585    /// Forwards to [`CanvasRenderer::stroke_circle`].
1586    fn stroke_circle(&self, center: Vector2D, radius: f64) {
1587        self.stroke_circle(center, radius);
1588    }
1589
1590    /// Forwards to [`CanvasRenderer::draw_line`].
1591    fn draw_line(&self, start: Vector2D, end: Vector2D) {
1592        self.draw_line(start, end);
1593    }
1594
1595    /// Forwards to [`CanvasRenderer::fill_text`].
1596    fn fill_text(&self, text: &str, position: Vector2D) {
1597        self.fill_text(text, position);
1598    }
1599
1600    /// Forwards to [`CanvasRenderer::set_font`].
1601    fn set_font(&self, font: &str) {
1602        self.set_font(font);
1603    }
1604
1605    /// Forwards to [`CanvasRenderer::draw_image`].
1606    fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1607        self.draw_image(image, position, width, height);
1608    }
1609
1610    /// Forwards to [`CanvasRenderer::set_linear_gradient_fill`].
1611    fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1612        self.set_linear_gradient_fill(gradient);
1613    }
1614
1615    /// Forwards to [`CanvasRenderer::set_radial_gradient_fill`].
1616    fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1617        self.set_radial_gradient_fill(gradient);
1618    }
1619}
1620
1621/// Implements async initialization and GPU resource creation for `WebGpuRenderer`.
1622impl WebGpuRenderer {
1623    /// Returns `true` if `navigator.gpu` is exposed on the current origin.
1624    ///
1625    /// This is the synchronous half of the canonical WebGPU capability
1626    /// probe used by Three.js (`examples/jsm/capabilities/WebGPU.js`): it
1627    /// only checks that the browser surfaces the `GPU` interface at all.
1628    /// It does **not** request an adapter — a present `navigator.gpu`
1629    /// does not guarantee that a usable GPU adapter is reachable (Linux
1630    /// software-rendered sessions, headless browsers, GPU-blacklisted
1631    /// devices and sandboxed iframes all expose `navigator.gpu` while
1632    /// `requestAdapter()` resolves to `null` or hangs forever).
1633    ///
1634    /// Use this as the cheapest pre-flight check before showing a
1635    /// "needs HTTPS or localhost" prompt. For a definitive answer use
1636    /// [`Self::probe`] which also awaits `requestAdapter()`.
1637    ///
1638    /// # Returns
1639    ///
1640    /// - `bool` - `true` when `navigator.gpu` is a non-null, non-undefined
1641    ///   object; `false` otherwise (including the "no `window`" runtime
1642    ///   case, which `web_sys::window()` returns `None` for).
1643    pub fn is_available() -> bool {
1644        let window_value: Window = match window() {
1645            Some(value) => value,
1646            None => return false,
1647        };
1648        let navigator: Navigator = window_value.navigator();
1649        let gpu_result: Result<JsValue, JsValue> = Reflect::get(
1650            navigator.as_ref(),
1651            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1652        );
1653        match gpu_result {
1654            Ok(value) => !value.is_undefined() && !value.is_null(),
1655            Err(_) => false,
1656        }
1657    }
1658
1659    /// Probes whether a WebGPU adapter can actually be acquired.
1660    ///
1661    /// Mirrors Three.js' canonical capability probe exactly:
1662    /// ```text
1663    /// isAvailable = (navigator.gpu !== undefined)
1664    /// if (isAvailable) {
1665    ///     isAvailable = Boolean(await navigator.gpu.requestAdapter())
1666    /// }
1667    /// ```
1668    ///
1669    /// Wraps the adapter request in the same `Promise.race` timeout used
1670    /// by [`Self::init`] so that browsers which leave the adapter promise
1671    /// permanently pending (headless, sandboxed, device-lost) do not stall
1672    /// the UI forever. The timeout itself uses
1673    /// [`INIT_PROMISE_TIMEOUT_MILLIS`]; on timeout, `probe` returns
1674    /// `false` rather than an error so callers can treat it the same as
1675    /// "no adapter".
1676    ///
1677    /// # Returns
1678    ///
1679    /// - `bool` - `true` only when both `navigator.gpu` is present and
1680    ///   `requestAdapter()` resolves to a non-null adapter within the
1681    ///   timeout window. `false` covers every other case (no `window`,
1682    ///   missing `navigator.gpu`, reflect exception, adapter promise
1683    ///   rejected or timed out, adapter resolved to `null`/`undefined`).
1684    pub async fn probe() -> bool {
1685        if !Self::is_available() {
1686            return false;
1687        }
1688        let window_value: Window = match window() {
1689            Some(value) => value,
1690            None => return false,
1691        };
1692        let navigator: Navigator = window_value.navigator();
1693        let gpu: JsValue = match Reflect::get(
1694            navigator.as_ref(),
1695            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1696        ) {
1697            Ok(value) => value,
1698            Err(_) => return false,
1699        };
1700        let request_adapter_fn: Function =
1701            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
1702                Ok(value) => value.unchecked_into(),
1703                Err(_) => return false,
1704            };
1705        let adapter_promise: Promise = match request_adapter_fn.call0(&gpu) {
1706            Ok(value) => value.unchecked_into(),
1707            Err(_) => return false,
1708        };
1709        let adapter_value: JsValue =
1710            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
1711                Ok(value) => value,
1712                Err(_) => return false,
1713            };
1714        !adapter_value.is_undefined() && !adapter_value.is_null()
1715    }
1716
1717    /// Asynchronously initializes a WebGPU renderer from the given render configuration.
1718    ///
1719    /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
1720    /// and configures it with the preferred texture format. Returns `None` if
1721    /// WebGPU is not supported, the adapter/device request fails, or the canvas
1722    /// element is not found.
1723    ///
1724    /// # Arguments
1725    ///
1726    /// - `&RenderConfig` - The rendering configuration.
1727    ///
1728    /// # Returns
1729    ///
1730    /// - `Option<WebGpuRenderer>` - The initialized renderer, or `None` on failure.
1731    ///   Maximum time in milliseconds to wait for `requestAdapter` and
1732    ///   `requestDevice` before treating them as failed.
1733    ///
1734    /// Some browser GPU states (headless, no GPU, sandboxed, device-lost)
1735    /// leave the WebGPU adapter/device promises permanently pending instead
1736    /// of resolving to `null` or rejecting. Without a timeout the
1737    /// `JsFuture::from(...).await` inside `init` would hang forever and
1738    /// the UI would stay stuck on `Initializing...`. Wrapping each promise
1739    /// in `Promise.race` against a timer-rejected sibling forces the
1740    /// future to resolve so the caller's `let Some(...) = ... else { ... }`
1741    /// branch can run and report `WebGPU Not Supported`.
1742    /// Returns a Promise that rejects after `INIT_PROMISE_TIMEOUT_MILLIS`.
1743    fn timeout_promise() -> Promise {
1744        let window_value: Window = window().expect("no global window exists");
1745        Promise::new(&mut |_resolve: Function, reject: Function| {
1746            let reject_fn: Function = reject.clone();
1747            let timer: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
1748                let _: Result<JsValue, JsValue> = reject_fn.call1(
1749                    &JsValue::UNDEFINED,
1750                    &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
1751                );
1752            }));
1753            let _: Result<i32, JsValue> = window_value
1754                .set_timeout_with_callback_and_timeout_and_arguments_0(
1755                    timer.as_ref().unchecked_ref(),
1756                    INIT_PROMISE_TIMEOUT_MILLIS,
1757                );
1758            timer.forget();
1759        })
1760    }
1761
1762    /// Wraps `promise` in `Promise.race([promise, timeout_promise()])` so that
1763    /// awaiting it never blocks longer than `INIT_PROMISE_TIMEOUT_MILLIS`.
1764    ///
1765    /// Calls `Promise.race` via reflection because wasm-bindgen does not
1766    /// currently expose the static `race` method on `js_sys::Promise`.
1767    fn race_with_timeout(promise: Promise) -> Promise {
1768        let array: Array = Array::of2(&promise, &Self::timeout_promise());
1769        Promise::race(&array)
1770    }
1771
1772    /// Asynchronously initializes a WebGPU renderer from the given render configuration.
1773    ///
1774    /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
1775    /// and configures it with the preferred texture format. Returns `Err` if
1776    /// WebGPU is not supported, the adapter/device request fails, the canvas
1777    /// element is not found, or the adapter/device request hangs beyond
1778    /// `INIT_PROMISE_TIMEOUT_MILLIS` (a defensive timeout for browser GPU
1779    /// states that leave the WebGPU promises permanently pending).
1780    ///
1781    /// The engine no longer logs diagnostic output internally; instead each
1782    /// failure mode is returned as a distinct `WebGpuInitError` variant so
1783    /// the caller can decide how to surface it (typically via `Console::error`
1784    /// or by falling back to the Canvas 2D backend).
1785    ///
1786    /// # Arguments
1787    ///
1788    /// - `&RenderConfig` - The rendering configuration.
1789    ///
1790    /// # Returns
1791    ///
1792    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer, or
1793    ///   a typed error describing the specific failure.
1794    pub async fn init(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
1795        let window: Window = window().expect("no global window exists");
1796        let navigator: Navigator = window.navigator();
1797        let gpu_result: Result<JsValue, JsValue> = Reflect::get(
1798            navigator.as_ref(),
1799            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1800        );
1801        let gpu: JsValue = match gpu_result {
1802            Ok(value) => value,
1803            Err(err) => return Err(WebGpuInitError::NavigatorLookup(err)),
1804        };
1805        if gpu.is_undefined() || gpu.is_null() {
1806            return Err(WebGpuInitError::NavigatorGpuMissing);
1807        }
1808        let adapter_options: Object = Object::new();
1809        let _: Result<bool, JsValue> = Reflect::set(
1810            &adapter_options,
1811            &JsValue::from_str(WEBGPU_PROPERTY_POWER_PREFERENCE),
1812            &JsValue::from_str(config.power_preference.to_web_sys_string()),
1813        );
1814        let request_adapter_fn: Function =
1815            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
1816                Ok(value) => value.unchecked_into(),
1817                Err(err) => return Err(WebGpuInitError::RequestAdapterLookup(err)),
1818            };
1819        let adapter_promise: Promise = match request_adapter_fn.call1(&gpu, &adapter_options) {
1820            Ok(value) => value.unchecked_into(),
1821            Err(err) => return Err(WebGpuInitError::RequestAdapterCall(err)),
1822        };
1823        let adapter_value: JsValue =
1824            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
1825                Ok(value) => value,
1826                Err(err) => return Err(WebGpuInitError::AdapterPromise(err)),
1827            };
1828        if adapter_value.is_null() || adapter_value.is_undefined() {
1829            return Err(WebGpuInitError::AdapterUnavailable);
1830        }
1831        let device_descriptor: Object = Object::new();
1832        let request_device_fn: Function = match Reflect::get(
1833            &adapter_value,
1834            &JsValue::from_str(WEBGPU_METHOD_REQUEST_DEVICE),
1835        ) {
1836            Ok(value) => value.unchecked_into(),
1837            Err(err) => return Err(WebGpuInitError::RequestDeviceLookup(err)),
1838        };
1839        let device_promise: Promise =
1840            match request_device_fn.call1(&adapter_value, &device_descriptor) {
1841                Ok(value) => value.unchecked_into(),
1842                Err(err) => return Err(WebGpuInitError::RequestDeviceCall(err)),
1843            };
1844        let device_value: JsValue =
1845            match JsFuture::from(Self::race_with_timeout(device_promise)).await {
1846                Ok(value) => value,
1847                Err(err) => return Err(WebGpuInitError::DevicePromise(err)),
1848            };
1849        if device_value.is_null() || device_value.is_undefined() {
1850            return Err(WebGpuInitError::DeviceUnavailable);
1851        }
1852        let document: Document = window.document().expect("should have a document");
1853        let element: Element = match document.query_selector(&config.canvas_selector) {
1854            Ok(Some(el)) => el,
1855            Ok(None) => {
1856                return Err(WebGpuInitError::CanvasNotFound(
1857                    config.canvas_selector.clone(),
1858                ));
1859            }
1860            Err(err) => return Err(WebGpuInitError::CanvasQuery(err)),
1861        };
1862        let canvas: HtmlCanvasElement = element.unchecked_into();
1863        let context_object: Option<Object> = canvas.get_context(WEBGPU_CONTEXT_TYPE).ok().flatten();
1864        let context_object: Object = match context_object {
1865            Some(c) => c,
1866            None => return Err(WebGpuInitError::CanvasContextUnavailable),
1867        };
1868        let context: JsValue = context_object.into();
1869        let get_format_fn: Function =
1870            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_GET_PREFERRED_FORMAT)) {
1871                Ok(value) => value.unchecked_into(),
1872                Err(err) => return Err(WebGpuInitError::PreferredFormatLookup(err)),
1873            };
1874        let format_value: JsValue = match get_format_fn.call0(&gpu) {
1875            Ok(value) => value,
1876            Err(err) => return Err(WebGpuInitError::PreferredFormatCall(err)),
1877        };
1878        let format: String = match format_value.as_string() {
1879            Some(s) => s,
1880            None => return Err(WebGpuInitError::PreferredFormatType(format_value)),
1881        };
1882        // WebGPU's `configure` requires the canvas backing-store size to be
1883        // set BEFORE calling configure, otherwise the swap chain is created
1884        // at 0x0 and the first getCurrentTexture() returns an error.
1885        let dpr: f64 = CanvasRenderer::detect_dpr();
1886        let physical_width: u32 = (config.width * dpr).round() as u32;
1887        let physical_height: u32 = (config.height * dpr).round() as u32;
1888        canvas.set_width(physical_width);
1889        canvas.set_height(physical_height);
1890        let canvas_config: Object = Object::new();
1891        let _: Result<bool, JsValue> = Reflect::set(
1892            &canvas_config,
1893            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
1894            &device_value,
1895        );
1896        let _: Result<bool, JsValue> = Reflect::set(
1897            &canvas_config,
1898            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
1899            &format_value,
1900        );
1901        let configure_fn: Function =
1902            match Reflect::get(&context, &JsValue::from_str(WEBGPU_METHOD_CONFIGURE)) {
1903                Ok(value) => value.unchecked_into(),
1904                Err(err) => return Err(WebGpuInitError::ConfigureLookup(err)),
1905            };
1906        let _: Result<JsValue, JsValue> = configure_fn.call1(&context, &canvas_config);
1907        let queue: JsValue =
1908            match Reflect::get(&device_value, &JsValue::from_str(WEBGPU_PROPERTY_QUEUE)) {
1909                Ok(value) => value,
1910                Err(err) => return Err(WebGpuInitError::QueueLookup(err)),
1911            };
1912        Ok(WebGpuRenderer {
1913            device: device_value,
1914            queue,
1915            context,
1916            canvas,
1917            format,
1918            width: physical_width,
1919            height: physical_height,
1920            antialias: config.antialias,
1921            multisample_texture: None,
1922            multisample_view: None,
1923        })
1924    }
1925
1926    /// Allocates the multisampled intermediate texture used for MSAA.
1927    ///
1928    /// The returned tuple is `(GpuTexture, GpuTextureView)`:
1929    /// - `GpuTexture` has `sampleCount: 4` and `usage: RENDER_ATTACHMENT`
1930    ///   so it can be bound as a color attachment in `beginRenderPass`.
1931    /// - `GpuTextureView` is the default 2D view used as the color
1932    ///   attachment; the swap chain view is the `resolveTarget`.
1933    ///
1934    /// The texture size must match the swap chain physical size; mismatches
1935    /// are a WebGPU validation error. Returns `(JsValue::UNDEFINED,
1936    /// JsValue::UNDEFINED)` when allocation fails so callers can detect and
1937    /// fall back to MSAA=1.
1938    ///
1939    /// # Arguments
1940    ///
1941    /// - `u32` - Physical pixel width (DPR-multiplied).
1942    /// - `u32` - Physical pixel height.
1943    ///
1944    /// # Returns
1945    ///
1946    /// - `(JsValue, JsValue)` - The new texture and its default view, or
1947    ///   `JsValue::UNDEFINED` for both on allocation failure.
1948    fn create_multisample_texture(
1949        &self,
1950        physical_width: u32,
1951        physical_height: u32,
1952    ) -> (JsValue, JsValue) {
1953        let extent: Object = Object::new();
1954        let _: Result<bool, JsValue> = Reflect::set(
1955            &extent,
1956            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
1957            &JsValue::from_f64(f64::from(physical_width)),
1958        );
1959        let _: Result<bool, JsValue> = Reflect::set(
1960            &extent,
1961            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
1962            &JsValue::from_f64(f64::from(physical_height)),
1963        );
1964        let _: Result<bool, JsValue> = Reflect::set(
1965            &extent,
1966            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
1967            &JsValue::from_f64(1.0),
1968        );
1969        let descriptor: Object = Object::new();
1970        let _: Result<bool, JsValue> = Reflect::set(
1971            &descriptor,
1972            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
1973            &extent,
1974        );
1975        let _: Result<bool, JsValue> = Reflect::set(
1976            &descriptor,
1977            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
1978            &JsValue::from_str(&self.get_format()),
1979        );
1980        let _: Result<bool, JsValue> = Reflect::set(
1981            &descriptor,
1982            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
1983            &JsValue::from_f64(WEBGPU_TEXTURE_USAGE_RENDER_ATTACHMENT),
1984        );
1985        let _: Result<bool, JsValue> = Reflect::set(
1986            &descriptor,
1987            &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
1988            &JsValue::from_f64(4.0),
1989        );
1990        let create_texture_fn: Function = Reflect::get(
1991            self.get_device(),
1992            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
1993        )
1994        .unwrap_or(JsValue::UNDEFINED)
1995        .unchecked_into();
1996        let texture: JsValue = create_texture_fn
1997            .call1(self.get_device(), &descriptor)
1998            .unwrap_or(JsValue::UNDEFINED);
1999        if texture.is_undefined() {
2000            return (JsValue::UNDEFINED, JsValue::UNDEFINED);
2001        }
2002        let create_view_fn: Function =
2003            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2004                .unwrap_or(JsValue::UNDEFINED)
2005                .unchecked_into();
2006        let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
2007        if view.is_undefined() {
2008            return (texture, JsValue::UNDEFINED);
2009        }
2010        (texture, view)
2011    }
2012
2013    /// Resizes the canvas backing store and reconfigures the swap chain.
2014    ///
2015    /// WebGPU's `GpuCanvasContext.configure` is sticky: it sets the texture
2016    /// format and device once, but the swap chain tracks the canvas's
2017    /// `width`/`height` attributes. When the CSS layout size changes (a
2018    /// window resize, a panel toggle, a DPR change) the canvas keeps its
2019    /// old physical dimensions unless we explicitly update `width`/`height`
2020    /// and call `configure` again. Without this, subsequent
2021    /// `getCurrentTexture()` calls return a texture that no longer matches
2022    /// the visible region and the frame either stretches or freezes.
2023    ///
2024    /// Re-`configure`ing with the same `device` + `format` is the
2025    /// spec-defined way to swap in a fresh swap chain bound to the new
2026    /// backing-store size.
2027    ///
2028    /// # Arguments
2029    ///
2030    /// - `u32` - The new physical pixel width (already multiplied by DPR).
2031    /// - `u32` - The new physical pixel height.
2032    ///
2033    /// # Returns
2034    ///
2035    /// - `bool` - `true` on success, `false` if the swap chain or canvas
2036    ///   handles were missing or `configure` failed.
2037    pub fn resize(&mut self, physical_width: u32, physical_height: u32) -> bool {
2038        if self.get_canvas().is_null()
2039            || self.get_context().is_null()
2040            || self.get_device().is_undefined()
2041        {
2042            return false;
2043        }
2044        self.get_canvas().set_width(physical_width);
2045        self.get_canvas().set_height(physical_height);
2046        let format_value: JsValue = JsValue::from_str(&self.get_format());
2047        let canvas_config: Object = Object::new();
2048        let _: Result<bool, JsValue> = Reflect::set(
2049            &canvas_config,
2050            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
2051            self.get_device(),
2052        );
2053        let _: Result<bool, JsValue> = Reflect::set(
2054            &canvas_config,
2055            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2056            &format_value,
2057        );
2058        let configure_fn: Function = Reflect::get(
2059            self.get_context(),
2060            &JsValue::from_str(WEBGPU_METHOD_CONFIGURE),
2061        )
2062        .ok()
2063        .and_then(|value: JsValue| value.dyn_into::<Function>().ok())
2064        .unwrap_or_else(|| Function::new_no_args(""));
2065        if configure_fn
2066            .call1(self.get_context(), &canvas_config)
2067            .is_err()
2068        {
2069            return false;
2070        }
2071        self.set_width(physical_width);
2072        self.set_height(physical_height);
2073        // Rebuild the multisampled color texture to match the new backing
2074        // store size. `GpuTexture` width/height are immutable, so MSAA
2075        // requires recreating it on every resize. The previous texture (if
2076        // any) is left to the GPU's GC; we do not explicitly destroy it
2077        // because `destroy()` is a synchronous WebGPU call and the old
2078        // texture is no longer referenced by any in-flight command buffer
2079        // at this point in the frame loop.
2080        if self.get_antialias() {
2081            let (texture, view) = self.create_multisample_texture(physical_width, physical_height);
2082            if !view.is_undefined() {
2083                self.set_multisample_texture(Some(texture));
2084                self.set_multisample_view(Some(view));
2085            } else {
2086                self.set_multisample_texture(None);
2087                self.set_multisample_view(None);
2088            }
2089        }
2090        true
2091    }
2092
2093    /// Resizes the canvas backing store to match the canvas element's
2094    /// current CSS-rendered size in physical pixels (DPR applied).
2095    ///
2096    /// This is the right entry point when the render loop does not know
2097    /// the desired logical size ahead of time and wants to follow the
2098    /// element's actual layout box. It is also useful as a defensive
2099    /// recovery when the canvas was created while hidden (zero-sized
2100    /// parent) and is later shown at its real size.
2101    ///
2102    /// Reads `client_width` / `client_height` from the canvas element,
2103    /// multiplies by `detect_dpr()`, and forwards to [`Self::resize`].
2104    ///
2105    /// # Returns
2106    ///
2107    /// - `bool` - `true` if the resize succeeded, `false` if the canvas
2108    ///   was zero-sized (nothing to render to), detached (CSS layout
2109    ///   box collapses to 0), or the underlying resize rejected.
2110    pub fn sync_to_current_canvas(&mut self) -> bool {
2111        let canvas_width: u32 = self.get_canvas().width();
2112        let canvas_height: u32 = self.get_canvas().height();
2113        let client_width: u32 = self.get_canvas().client_width().try_into().unwrap_or(0);
2114        let client_height: u32 = self.get_canvas().client_height().try_into().unwrap_or(0);
2115        // Prefer the CSS layout box when it is non-zero. If the canvas
2116        // is hidden the client box collapses to 0; in that case fall
2117        // back to the current backing-store size so we do not
2118        // gratuitously resize to 0.
2119        let css_w: u32 = if client_width > 0 {
2120            client_width
2121        } else {
2122            canvas_width
2123        };
2124        let css_h: u32 = if client_height > 0 {
2125            client_height
2126        } else {
2127            canvas_height
2128        };
2129        if css_w == 0 || css_h == 0 {
2130            return false;
2131        }
2132        let dpr: f64 = CanvasRenderer::detect_dpr();
2133        let physical_width: u32 = (f64::from(css_w) * dpr).round() as u32;
2134        let physical_height: u32 = (f64::from(css_h) * dpr).round() as u32;
2135        self.resize(physical_width, physical_height)
2136    }
2137
2138    /// Creates a shader module from WGSL source code.
2139    ///
2140    /// # Arguments
2141    ///
2142    /// - `S: AsRef<str>` - The WGSL shader source code.
2143    ///
2144    /// # Returns
2145    ///
2146    /// - `JsValue` - The created shader module as a JavaScript value.
2147    pub(crate) fn create_shader_module<S>(&self, code: S) -> JsValue
2148    where
2149        S: AsRef<str>,
2150    {
2151        let descriptor: Object = Object::new();
2152        let _: Result<bool, JsValue> = Reflect::set(
2153            &descriptor,
2154            &JsValue::from_str(WEBGPU_PROPERTY_CODE),
2155            &JsValue::from_str(code.as_ref()),
2156        );
2157        let create_fn: Function = Reflect::get(
2158            self.get_device(),
2159            &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
2160        )
2161        .unwrap_or(JsValue::UNDEFINED)
2162        .unchecked_into();
2163        create_fn
2164            .call1(self.get_device(), &descriptor)
2165            .unwrap_or(JsValue::UNDEFINED)
2166    }
2167
2168    /// Creates a new command encoder for recording GPU commands.
2169    ///
2170    /// # Returns
2171    ///
2172    /// - `JsValue` - The created command encoder as a JavaScript value.
2173    pub(crate) fn create_command_encoder(&self) -> JsValue {
2174        let create_fn: Function = Reflect::get(
2175            self.get_device(),
2176            &JsValue::from_str(WEBGPU_METHOD_CREATE_COMMAND_ENCODER),
2177        )
2178        .unwrap_or(JsValue::UNDEFINED)
2179        .unchecked_into();
2180        create_fn
2181            .call0(self.get_device())
2182            .unwrap_or(JsValue::UNDEFINED)
2183    }
2184
2185    /// Returns the current texture view from the canvas swap chain.
2186    ///
2187    /// This texture view should be used as the color attachment target for
2188    /// render passes. The texture is automatically presented to the canvas
2189    /// when the command buffer is submitted.
2190    ///
2191    /// # Returns
2192    ///
2193    /// - `JsValue` - The current frame's texture view as a JavaScript value.
2194    pub(crate) fn get_current_texture_view(&self) -> JsValue {
2195        let get_texture_fn: Function = Reflect::get(
2196            self.get_context(),
2197            &JsValue::from_str(WEBGPU_METHOD_GET_CURRENT_TEXTURE),
2198        )
2199        .unwrap_or(JsValue::UNDEFINED)
2200        .unchecked_into();
2201        let texture: JsValue = get_texture_fn
2202            .call0(self.get_context())
2203            .unwrap_or(JsValue::UNDEFINED);
2204        let create_view_fn: Function =
2205            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2206                .unwrap_or(JsValue::UNDEFINED)
2207                .unchecked_into();
2208        create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED)
2209    }
2210
2211    /// Begins a render pass on the given command encoder with a clear color.
2212    ///
2213    /// The render pass targets the canvas's current texture and clears it
2214    /// to the specified color. The returned `JsValue` is a `GpuRenderPassEncoder`
2215    /// that can be used to issue draw commands. The pass must be ended (via `end()`)
2216    /// before the command encoder is finished.
2217    ///
2218    /// # Arguments
2219    ///
2220    /// - `&JsValue` - The command encoder to begin the pass on.
2221    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
2222    ///
2223    /// # Returns
2224    ///
2225    /// - `JsValue` - The active render pass encoder as a JavaScript value.
2226    pub(crate) fn begin_render_pass(
2227        &mut self,
2228        encoder: &JsValue,
2229        clear_color: (f64, f64, f64, f64),
2230    ) -> JsValue {
2231        let swap_chain_view: JsValue = self.get_current_texture_view();
2232        // Select the color attachment view and the resolve target based on
2233        // whether MSAA is enabled. With MSAA=4 we draw into a multisampled
2234        // intermediate texture and resolve into the swap chain at the end
2235        // of the pass; without MSAA we draw directly into the swap chain
2236        // and omit `resolveTarget`.
2237        let (color_view, resolve_view): (JsValue, Option<JsValue>) = if self.get_antialias() {
2238            let multisample_view: Option<JsValue> = self
2239                .get_multisample_view()
2240                .clone()
2241                .filter(|value: &JsValue| !value.is_undefined());
2242            let resolved: Option<JsValue> = match multisample_view {
2243                Some(view) => Some(view),
2244                None => {
2245                    // Lazy-init: the first render after `init()` or
2246                    // `resize()` finds an empty multisample view slot.
2247                    // Build it now (using the swap chain's current
2248                    // physical size) and cache it on the struct.
2249                    let width: u32 = self.get_width();
2250                    let height: u32 = self.get_height();
2251                    let (texture, view): (JsValue, JsValue) =
2252                        self.create_multisample_texture(width, height);
2253                    if !view.is_undefined() {
2254                        self.set_multisample_texture(Some(texture));
2255                        self.set_multisample_view(Some(view.clone()));
2256                        Some(view)
2257                    } else {
2258                        self.set_multisample_texture(None);
2259                        self.set_multisample_view(None);
2260                        None
2261                    }
2262                }
2263            };
2264            match resolved {
2265                Some(view) => (view, Some(swap_chain_view.clone())),
2266                None => (swap_chain_view.clone(), None),
2267            }
2268        } else {
2269            (swap_chain_view.clone(), None)
2270        };
2271        let color_dict: Object = Object::new();
2272        let _: Result<bool, JsValue> = Reflect::set(
2273            &color_dict,
2274            &JsValue::from_str(WEBGPU_PROPERTY_R),
2275            &JsValue::from_f64(clear_color.0),
2276        );
2277        let _: Result<bool, JsValue> = Reflect::set(
2278            &color_dict,
2279            &JsValue::from_str(WEBGPU_PROPERTY_G),
2280            &JsValue::from_f64(clear_color.1),
2281        );
2282        let _: Result<bool, JsValue> = Reflect::set(
2283            &color_dict,
2284            &JsValue::from_str(WEBGPU_PROPERTY_B),
2285            &JsValue::from_f64(clear_color.2),
2286        );
2287        let _: Result<bool, JsValue> = Reflect::set(
2288            &color_dict,
2289            &JsValue::from_str(WEBGPU_PROPERTY_A),
2290            &JsValue::from_f64(clear_color.3),
2291        );
2292        let attachment: Object = Object::new();
2293        let _: Result<bool, JsValue> = Reflect::set(
2294            &attachment,
2295            &JsValue::from_str(WEBGPU_PROPERTY_VIEW),
2296            &color_view,
2297        );
2298        let _: Result<bool, JsValue> = Reflect::set(
2299            &attachment,
2300            &JsValue::from_str(WEBGPU_PROPERTY_LOAD_OP),
2301            &JsValue::from_str(WEBGPU_LOAD_OP_CLEAR),
2302        );
2303        let _: Result<bool, JsValue> = Reflect::set(
2304            &attachment,
2305            &JsValue::from_str(WEBGPU_PROPERTY_STORE_OP),
2306            &JsValue::from_str(WEBGPU_STORE_OP_STORE),
2307        );
2308        let _: Result<bool, JsValue> = Reflect::set(
2309            &attachment,
2310            &JsValue::from_str(WEBGPU_PROPERTY_CLEAR_VALUE),
2311            &color_dict,
2312        );
2313        if let Some(target) = resolve_view.as_ref() {
2314            let _: Result<bool, JsValue> = Reflect::set(
2315                &attachment,
2316                &JsValue::from_str(WEBGPU_PROPERTY_RESOLVE_TARGET),
2317                target,
2318            );
2319        }
2320        let color_attachments: Array = Array::new();
2321        color_attachments.push(&attachment);
2322        let descriptor: Object = Object::new();
2323        let _: Result<bool, JsValue> = Reflect::set(
2324            &descriptor,
2325            &JsValue::from_str(WEBGPU_PROPERTY_COLOR_ATTACHMENTS),
2326            &color_attachments,
2327        );
2328        let begin_fn: Function =
2329            Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_BEGIN_RENDER_PASS))
2330                .unwrap_or(JsValue::UNDEFINED)
2331                .unchecked_into();
2332        begin_fn
2333            .call1(encoder, &descriptor)
2334            .unwrap_or(JsValue::UNDEFINED)
2335    }
2336
2337    /// Submits an array of command buffers to the GPU queue for execution.
2338    ///
2339    /// # Arguments
2340    ///
2341    /// - `&[JsValue]` - The command buffers to submit.
2342    pub(crate) fn submit(&self, command_buffers: &[JsValue]) {
2343        let array: Array = Array::new();
2344        for buffer in command_buffers {
2345            array.push(buffer);
2346        }
2347        let submit_fn: Function =
2348            Reflect::get(self.get_queue(), &JsValue::from_str(WEBGPU_METHOD_SUBMIT))
2349                .unwrap_or(JsValue::UNDEFINED)
2350                .unchecked_into();
2351        let _: Result<JsValue, JsValue> = submit_fn.call1(self.get_queue(), &array);
2352    }
2353
2354    /// Creates a simple render pipeline from a single WGSL shader source.
2355    ///
2356    /// The shader must contain `@vertex fn vs_main(...)` and
2357    /// `@fragment fn fs_main(...)` entry points. No vertex buffers are used;
2358    /// vertex positions should be derived from `@builtin(vertex_index)` in
2359    /// the shader. The pipeline uses auto-layout (`layout: null`), which works
2360    /// when the shader has no bind groups.
2361    ///
2362    /// # Arguments
2363    ///
2364    /// - `S: AsRef<str>` - The WGSL shader source code.
2365    ///
2366    /// # Returns
2367    ///
2368    /// - `JsValue` - The created render pipeline as a JavaScript value.
2369    pub fn create_render_pipeline<S>(&self, shader_code: S) -> JsValue
2370    where
2371        S: AsRef<str>,
2372    {
2373        let module: JsValue = self.create_shader_module(shader_code);
2374        let vertex_state: Object = Object::new();
2375        let _: Result<bool, JsValue> = Reflect::set(
2376            &vertex_state,
2377            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
2378            &module,
2379        );
2380        let _: Result<bool, JsValue> = Reflect::set(
2381            &vertex_state,
2382            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
2383            &JsValue::from_str(WEBGPU_VERTEX_ENTRY_POINT),
2384        );
2385        let _: Result<bool, JsValue> = Reflect::set(
2386            &vertex_state,
2387            &JsValue::from_str(WEBGPU_PROPERTY_BUFFERS),
2388            &Array::new(),
2389        );
2390        let target: Object = Object::new();
2391        let _: Result<bool, JsValue> = Reflect::set(
2392            &target,
2393            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2394            &JsValue::from_str(&self.get_format()),
2395        );
2396        let targets: Array = Array::new();
2397        targets.push(&target);
2398        let fragment_state: Object = Object::new();
2399        let _: Result<bool, JsValue> = Reflect::set(
2400            &fragment_state,
2401            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
2402            &module,
2403        );
2404        let _: Result<bool, JsValue> = Reflect::set(
2405            &fragment_state,
2406            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
2407            &JsValue::from_str(WEBGPU_FRAGMENT_ENTRY_POINT),
2408        );
2409        let _: Result<bool, JsValue> = Reflect::set(
2410            &fragment_state,
2411            &JsValue::from_str(WEBGPU_PROPERTY_TARGETS),
2412            &targets,
2413        );
2414        let primitive: Object = Object::new();
2415        let _: Result<bool, JsValue> = Reflect::set(
2416            &primitive,
2417            &JsValue::from_str(WEBGPU_PROPERTY_TOPOLOGY),
2418            &JsValue::from_str(WEBGPU_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST),
2419        );
2420        // Wire the renderer-level `antialias` flag through to MSAA sample count.
2421        // Previously the flag was stored on the struct but never read by the
2422        // pipeline builder, leaving every pipeline at MSAA=1 (no anti-aliasing)
2423        // — visible as sub-pixel aliasing on triangle edges, particularly at
2424        // small canvas sizes like the 600x400 game_2d example. Enabling MSAA=4
2425        // when `antialias` is true restores hardware multisampling so edges
2426        // resolve cleanly without per-edge shader work.
2427        let multisample: Object = Object::new();
2428        let _: Result<bool, JsValue> = Reflect::set(
2429            &multisample,
2430            &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
2431            &JsValue::from_f64(if self.get_antialias() { 4.0 } else { 1.0 }),
2432        );
2433        let descriptor: Object = Object::new();
2434        let _: Result<bool, JsValue> = Reflect::set(
2435            &descriptor,
2436            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
2437            &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
2438        );
2439        let _: Result<bool, JsValue> = Reflect::set(
2440            &descriptor,
2441            &JsValue::from_str(WEBGPU_PROPERTY_VERTEX),
2442            &vertex_state,
2443        );
2444        let _: Result<bool, JsValue> = Reflect::set(
2445            &descriptor,
2446            &JsValue::from_str(WEBGPU_PROPERTY_FRAGMENT),
2447            &fragment_state,
2448        );
2449        let _: Result<bool, JsValue> = Reflect::set(
2450            &descriptor,
2451            &JsValue::from_str(WEBGPU_PROPERTY_PRIMITIVE),
2452            &primitive,
2453        );
2454        let _: Result<bool, JsValue> = Reflect::set(
2455            &descriptor,
2456            &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLE),
2457            &multisample,
2458        );
2459        let create_fn: Function = Reflect::get(
2460            self.get_device(),
2461            &JsValue::from_str(WEBGPU_METHOD_CREATE_RENDER_PIPELINE),
2462        )
2463        .unwrap_or(JsValue::UNDEFINED)
2464        .unchecked_into();
2465        create_fn
2466            .call1(self.get_device(), &descriptor)
2467            .unwrap_or(JsValue::UNDEFINED)
2468    }
2469
2470    /// Sets the render pipeline on a render pass encoder.
2471    ///
2472    /// # Arguments
2473    ///
2474    /// - `&JsValue` - The render pass encoder.
2475    /// - `&JsValue` - The render pipeline to set.
2476    pub(crate) fn set_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
2477        let set_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_PIPELINE))
2478            .unwrap_or(JsValue::UNDEFINED)
2479            .unchecked_into();
2480        let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
2481    }
2482
2483    /// Draws primitives on a render pass encoder.
2484    ///
2485    /// # Arguments
2486    ///
2487    /// - `&JsValue` - The render pass encoder.
2488    /// - `u32` - The number of vertices to draw.
2489    /// - `u32` - The number of instances to draw.
2490    pub(crate) fn draw(&self, pass: &JsValue, vertex_count: u32, instance_count: u32) {
2491        let draw_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DRAW))
2492            .unwrap_or(JsValue::UNDEFINED)
2493            .unchecked_into();
2494        let _: Result<JsValue, JsValue> = draw_fn.call2(
2495            pass,
2496            &JsValue::from_f64(f64::from(vertex_count)),
2497            &JsValue::from_f64(f64::from(instance_count)),
2498        );
2499    }
2500
2501    /// Ends a render pass on the given pass encoder.
2502    ///
2503    /// # Arguments
2504    ///
2505    /// - `&JsValue` - The render pass encoder to end.
2506    pub(crate) fn end_render_pass(&self, pass: &JsValue) {
2507        let end_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_END))
2508            .unwrap_or(JsValue::UNDEFINED)
2509            .unchecked_into();
2510        let _: Result<JsValue, JsValue> = end_fn.call0(pass);
2511    }
2512
2513    /// Finishes a command encoder and returns the resulting command buffer.
2514    ///
2515    /// # Arguments
2516    ///
2517    /// - `&JsValue` - The command encoder to finish.
2518    ///
2519    /// # Returns
2520    ///
2521    /// - `JsValue` - The finished command buffer.
2522    pub(crate) fn finish_command_encoder(&self, encoder: &JsValue) -> JsValue {
2523        let finish_fn: Function = Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_FINISH))
2524            .unwrap_or(JsValue::UNDEFINED)
2525            .unchecked_into();
2526        finish_fn.call0(encoder).unwrap_or(JsValue::UNDEFINED)
2527    }
2528
2529    /// Renders a complete frame with a pipeline and animated clear color.
2530    ///
2531    /// This is a convenience method that creates a command encoder, begins a
2532    /// render pass with the given clear color, sets the pipeline, draws the
2533    /// specified number of vertices, ends the pass, finishes the encoder, and
2534    /// submits the command buffer.
2535    ///
2536    /// # Arguments
2537    ///
2538    /// - `&JsValue` - The render pipeline to use.
2539    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
2540    /// - `u32` - The number of vertices to draw.
2541    pub fn render_frame(
2542        &mut self,
2543        pipeline: &JsValue,
2544        clear_color: (f64, f64, f64, f64),
2545        vertex_count: u32,
2546    ) {
2547        let encoder: JsValue = self.create_command_encoder();
2548        let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
2549        self.set_pipeline(&pass, pipeline);
2550        self.draw(&pass, vertex_count, 1);
2551        self.end_render_pass(&pass);
2552        let command_buffer: JsValue = self.finish_command_encoder(&encoder);
2553        self.submit(&[command_buffer]);
2554    }
2555
2556    /// Releases all GPU resources held by this renderer.
2557    ///
2558    /// The teardown order matters per the WebGPU spec:
2559    ///   1. `GpuCanvasContext.unconfigure()` - releases the swap chain so
2560    ///      the DOM canvas can be GCed.
2561    ///   2. `GpuDevice.destroy()` - releases all child resources (buffers,
2562    ///      textures, pipelines) and the device itself.
2563    ///
2564    /// Callers should run this from a `use_cleanup` callback whenever the
2565    /// host component is being torn down (e.g. on a `match` arm switch).
2566    /// Without it the previous GPU device lingers until GC, and a fresh
2567    /// `init()` may either reuse the dead device (silent black canvas) or
2568    /// fail to acquire a new one until the old device is collected.
2569    ///
2570    /// `Reflect::get` failures and JS exceptions are swallowed - this is a
2571    /// best-effort cleanup path, and the engine must not panic during
2572    /// teardown.
2573    pub fn dispose(&self) {
2574        let context: &JsValue = self.get_context();
2575        if let Ok(unconfigure_fn) =
2576            Reflect::get(context, &JsValue::from_str(WEBGPU_METHOD_UNCONFIGURE))
2577            && let Ok(unconfigure_callable) = unconfigure_fn.dyn_into::<Function>()
2578        {
2579            let _: Result<JsValue, JsValue> = unconfigure_callable.call0(context);
2580        }
2581        let device: &JsValue = self.get_device();
2582        if let Ok(destroy_fn) = Reflect::get(device, &JsValue::from_str(WEBGPU_METHOD_DESTROY))
2583            && let Ok(destroy_callable) = destroy_fn.dyn_into::<Function>()
2584        {
2585            let _: Result<JsValue, JsValue> = destroy_callable.call0(device);
2586        }
2587    }
2588}
2589
2590/// Implements helper methods on `WebGpuInitError`.
2591///
2592/// These methods provide ergonomic access to the diagnostic code and the
2593/// underlying JS error value, which are useful when surfacing the failure
2594/// to the user (e.g. via `Console::error` from the example crate).
2595impl WebGpuInitError {
2596    /// Returns a short, machine-readable identifier for this error variant.
2597    ///
2598    /// Suitable for use as a stable error code in logs or telemetry.
2599    /// The codes are stable across releases.
2600    ///
2601    /// # Returns
2602    ///
2603    /// - `&'static str` - The error code (e.g. `"WEBGPU_NAVIGATOR_GPU_MISSING"`).
2604    pub fn code(&self) -> &'static str {
2605        match self {
2606            Self::NavigatorLookup(_) => "WEBGPU_NAVIGATOR_LOOKUP",
2607            Self::NavigatorGpuMissing => "WEBGPU_NAVIGATOR_GPU_MISSING",
2608            Self::RequestAdapterLookup(_) => "WEBGPU_REQUEST_ADAPTER_LOOKUP",
2609            Self::RequestAdapterCall(_) => "WEBGPU_REQUEST_ADAPTER_CALL",
2610            Self::AdapterPromise(_) => "WEBGPU_ADAPTER_PROMISE",
2611            Self::AdapterUnavailable => "WEBGPU_ADAPTER_UNAVAILABLE",
2612            Self::RequestDeviceLookup(_) => "WEBGPU_REQUEST_DEVICE_LOOKUP",
2613            Self::RequestDeviceCall(_) => "WEBGPU_REQUEST_DEVICE_CALL",
2614            Self::DevicePromise(_) => "WEBGPU_DEVICE_PROMISE",
2615            Self::DeviceUnavailable => "WEBGPU_DEVICE_UNAVAILABLE",
2616            Self::CanvasNotFound(_) => "WEBGPU_CANVAS_NOT_FOUND",
2617            Self::CanvasQuery(_) => "WEBGPU_CANVAS_QUERY",
2618            Self::CanvasContextUnavailable => "WEBGPU_CANVAS_CONTEXT_UNAVAILABLE",
2619            Self::PreferredFormatLookup(_) => "WEBGPU_PREFERRED_FORMAT_LOOKUP",
2620            Self::PreferredFormatCall(_) => "WEBGPU_PREFERRED_FORMAT_CALL",
2621            Self::PreferredFormatType(_) => "WEBGPU_PREFERRED_FORMAT_TYPE",
2622            Self::ConfigureLookup(_) => "WEBGPU_CONFIGURE_LOOKUP",
2623            Self::QueueLookup(_) => "WEBGPU_QUEUE_LOOKUP",
2624        }
2625    }
2626
2627    /// Returns the underlying JS error value if this variant carries one.
2628    ///
2629    /// Variants that do not capture a JS value (e.g. `NavigatorGpuMissing`,
2630    /// `AdapterUnavailable`, `CanvasNotFound`, `CanvasContextUnavailable`)
2631    /// return `None`.
2632    ///
2633    /// # Returns
2634    ///
2635    /// - `Option<&JsValue>` - The captured JS error, if any.
2636    pub fn js_error(&self) -> Option<&JsValue> {
2637        match self {
2638            Self::NavigatorLookup(err)
2639            | Self::RequestAdapterLookup(err)
2640            | Self::RequestAdapterCall(err)
2641            | Self::AdapterPromise(err)
2642            | Self::RequestDeviceLookup(err)
2643            | Self::RequestDeviceCall(err)
2644            | Self::DevicePromise(err)
2645            | Self::CanvasQuery(err)
2646            | Self::PreferredFormatLookup(err)
2647            | Self::PreferredFormatCall(err)
2648            | Self::PreferredFormatType(err)
2649            | Self::ConfigureLookup(err)
2650            | Self::QueueLookup(err) => Some(err),
2651            Self::NavigatorGpuMissing
2652            | Self::AdapterUnavailable
2653            | Self::DeviceUnavailable
2654            | Self::CanvasContextUnavailable
2655            | Self::CanvasNotFound(_) => None,
2656        }
2657    }
2658}
2659
2660/// Renders the JS-side error into a `String` when present, otherwise `"<none>"`.
2661fn js_error_to_string(value: &JsValue) -> String {
2662    if let Some(s) = value.as_string() {
2663        s
2664    } else if value.is_undefined() {
2665        "<undefined>".to_string()
2666    } else if value.is_null() {
2667        "<null>".to_string()
2668    } else {
2669        format!("{:?}", value)
2670    }
2671}
2672
2673/// Implements `std::fmt::Display` for `WebGpuInitError`.
2674///
2675/// The formatted message is intended for end-user diagnostic output
2676/// (typically forwarded to `Console::error` by the calling application)
2677/// and includes the variant code plus a human-readable description. When
2678/// the variant carries a JS error, its `Debug` form is appended.
2679impl std::fmt::Display for WebGpuInitError {
2680    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2681        match self {
2682            Self::NavigatorLookup(err) => write!(
2683                formatter,
2684                "[{}] Reflect::get(navigator, webgpu) failed: {}",
2685                self.code(),
2686                js_error_to_string(err),
2687            ),
2688            Self::NavigatorGpuMissing => write!(
2689                formatter,
2690                "[{}] navigator.gpu is missing - browser does not expose WebGPU on this origin",
2691                self.code(),
2692            ),
2693            Self::RequestAdapterLookup(err) => write!(
2694                formatter,
2695                "[{}] Reflect::get(gpu, requestAdapter) failed: {}",
2696                self.code(),
2697                js_error_to_string(err),
2698            ),
2699            Self::RequestAdapterCall(err) => write!(
2700                formatter,
2701                "[{}] gpu.requestAdapter() threw: {}",
2702                self.code(),
2703                js_error_to_string(err),
2704            ),
2705            Self::AdapterPromise(err) => write!(
2706                formatter,
2707                "[{}] adapter promise rejected or timed out: {}",
2708                self.code(),
2709                js_error_to_string(err),
2710            ),
2711            Self::AdapterUnavailable => write!(
2712                formatter,
2713                "[{}] requestAdapter returned null - no compatible GPU adapter for the requested powerPreference",
2714                self.code(),
2715            ),
2716            Self::RequestDeviceLookup(err) => write!(
2717                formatter,
2718                "[{}] Reflect::get(adapter, requestDevice) failed: {}",
2719                self.code(),
2720                js_error_to_string(err),
2721            ),
2722            Self::RequestDeviceCall(err) => write!(
2723                formatter,
2724                "[{}] adapter.requestDevice() threw: {}",
2725                self.code(),
2726                js_error_to_string(err),
2727            ),
2728            Self::DevicePromise(err) => write!(
2729                formatter,
2730                "[{}] device promise rejected or timed out: {}",
2731                self.code(),
2732                js_error_to_string(err),
2733            ),
2734            Self::DeviceUnavailable => write!(
2735                formatter,
2736                "[{}] requestDevice returned null - adapter could not allocate a device (possibly device-lost)",
2737                self.code(),
2738            ),
2739            Self::CanvasNotFound(selector) => write!(
2740                formatter,
2741                "[{}] canvas element {:?} not found in DOM",
2742                self.code(),
2743                selector,
2744            ),
2745            Self::CanvasQuery(err) => write!(
2746                formatter,
2747                "[{}] querySelector threw: {}",
2748                self.code(),
2749                js_error_to_string(err),
2750            ),
2751            Self::CanvasContextUnavailable => write!(
2752                formatter,
2753                "[{}] canvas.get_context('webgpu') returned null - the canvas may already be using another context type or WebGPU is disabled",
2754                self.code(),
2755            ),
2756            Self::PreferredFormatLookup(err) => write!(
2757                formatter,
2758                "[{}] Reflect::get(gpu, getPreferredCanvasFormat) failed: {}",
2759                self.code(),
2760                js_error_to_string(err),
2761            ),
2762            Self::PreferredFormatCall(err) => write!(
2763                formatter,
2764                "[{}] gpu.getPreferredCanvasFormat() threw: {}",
2765                self.code(),
2766                js_error_to_string(err),
2767            ),
2768            Self::PreferredFormatType(value) => write!(
2769                formatter,
2770                "[{}] getPreferredCanvasFormat returned non-string: {}",
2771                self.code(),
2772                js_error_to_string(value),
2773            ),
2774            Self::ConfigureLookup(err) => write!(
2775                formatter,
2776                "[{}] Reflect::get(context, configure) failed: {}",
2777                self.code(),
2778                js_error_to_string(err),
2779            ),
2780            Self::QueueLookup(err) => write!(
2781                formatter,
2782                "[{}] Reflect::get(device, queue) failed: {}",
2783                self.code(),
2784                js_error_to_string(err),
2785            ),
2786        }
2787    }
2788}
2789
2790/// Implements the standard `std::error::Error` trait for `WebGpuInitError`.
2791///
2792/// The `source()` method delegates to the underlying JS error's `toString()`
2793/// representation when present, otherwise returns `None`. The engine never
2794/// logs or prints anything; this impl exists solely so the error composes
2795/// with `Result`-based APIs and `?` operator chains.
2796impl std::error::Error for WebGpuInitError {}