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