Skip to main content

euv_engine/renderer/
impl.rs

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