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.display_context, self.get_quality());
824        self.display_context
825            .clear_rect(0.0, 0.0, self.width, self.height);
826        let _ = self
827            .display_context
828            .draw_image_with_html_canvas_element_and_dw_and_dh(
829                &self.offscreen_canvas,
830                0.0,
831                0.0,
832                self.width,
833                self.height,
834            );
835    }
836
837    /// Clears the offscreen buffer to transparent.
838    pub fn clear(&self) {
839        self.offscreen_context
840            .clear_rect(0.0, 0.0, self.width, self.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.offscreen_context,
854            &JsValue::from_str(RENDERER_PROPERTY_FILL_STYLE),
855            &JsValue::from_str(color.as_ref()),
856        );
857        self.offscreen_context
858            .fill_rect(0.0, 0.0, self.width, self.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.display_context, quality);
868        CanvasRenderer::apply_quality(&self.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.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.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 `None` 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    /// Every early return logs a `console.error` describing the specific
1379    /// failure so the user can diagnose from DevTools why their environment
1380    /// can't render with WebGPU.
1381    ///
1382    /// # Arguments
1383    ///
1384    /// - `&RenderConfig` - The rendering configuration.
1385    ///
1386    /// # Returns
1387    ///
1388    /// - `Option<WebGpuRenderer>` - The initialized renderer, or `None` on failure.
1389    pub async fn init(config: &RenderConfig) -> Option<WebGpuRenderer> {
1390        let window: Window = window().expect("no global window exists");
1391        let navigator: Navigator = window.navigator();
1392        let gpu_result: Result<JsValue, JsValue> =
1393            Reflect::get(navigator.as_ref(), &JsValue::from_str(WEBGPU_CONTEXT_TYPE));
1394        let gpu: JsValue = match gpu_result {
1395            Ok(value) => value,
1396            Err(err) => {
1397                web_sys::console::error_1(&JsValue::from_str(&format!(
1398                    "[euv-engine][webgpu] init: Reflect::get(navigator, webgpu) failed: {:?}",
1399                    err,
1400                )));
1401                return None;
1402            }
1403        };
1404        if gpu.is_undefined() || gpu.is_null() {
1405            web_sys::console::error_1(&JsValue::from_str(
1406                "[euv-engine][webgpu] init: navigator.gpu is missing - \
1407                 browser does not expose WebGPU on this origin",
1408            ));
1409            return None;
1410        }
1411        let adapter_options: Object = Object::new();
1412        let _ = Reflect::set(
1413            &adapter_options,
1414            &JsValue::from_str(WEBGPU_PROPERTY_POWER_PREFERENCE),
1415            &JsValue::from_str(config.power_preference.to_web_sys_string()),
1416        );
1417        let request_adapter_fn: Function =
1418            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
1419                Ok(value) => value.unchecked_into(),
1420                Err(err) => {
1421                    web_sys::console::error_1(&JsValue::from_str(&format!(
1422                        "[euv-engine][webgpu] init: Reflect::get(gpu, requestAdapter) failed: {:?}",
1423                        err,
1424                    )));
1425                    return None;
1426                }
1427            };
1428        let adapter_promise: Promise = match request_adapter_fn.call1(&gpu, &adapter_options) {
1429            Ok(value) => value.unchecked_into(),
1430            Err(err) => {
1431                web_sys::console::error_1(&JsValue::from_str(&format!(
1432                    "[euv-engine][webgpu] init: gpu.requestAdapter() threw: {:?}",
1433                    err,
1434                )));
1435                return None;
1436            }
1437        };
1438        let adapter_value: JsValue =
1439            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
1440                Ok(value) => value,
1441                Err(err) => {
1442                    web_sys::console::error_1(&JsValue::from_str(&format!(
1443                        "[euv-engine][webgpu] init: adapter promise rejected or timed out: {:?}",
1444                        err,
1445                    )));
1446                    return None;
1447                }
1448            };
1449        if adapter_value.is_null() || adapter_value.is_undefined() {
1450            web_sys::console::error_1(&JsValue::from_str(
1451                "[euv-engine][webgpu] init: requestAdapter returned null - \
1452                 no compatible GPU adapter for the requested powerPreference",
1453            ));
1454            return None;
1455        }
1456        let device_descriptor: Object = Object::new();
1457        let request_device_fn: Function = match Reflect::get(
1458            &adapter_value,
1459            &JsValue::from_str(WEBGPU_METHOD_REQUEST_DEVICE),
1460        ) {
1461            Ok(value) => value.unchecked_into(),
1462            Err(err) => {
1463                web_sys::console::error_1(&JsValue::from_str(&format!(
1464                    "[euv-engine][webgpu] init: Reflect::get(adapter, requestDevice) failed: {:?}",
1465                    err,
1466                )));
1467                return None;
1468            }
1469        };
1470        let device_promise: Promise =
1471            match request_device_fn.call1(&adapter_value, &device_descriptor) {
1472                Ok(value) => value.unchecked_into(),
1473                Err(err) => {
1474                    web_sys::console::error_1(&JsValue::from_str(&format!(
1475                        "[euv-engine][webgpu] init: adapter.requestDevice() threw: {:?}",
1476                        err,
1477                    )));
1478                    return None;
1479                }
1480            };
1481        let device_value: JsValue =
1482            match JsFuture::from(Self::race_with_timeout(device_promise)).await {
1483                Ok(value) => value,
1484                Err(err) => {
1485                    web_sys::console::error_1(&JsValue::from_str(&format!(
1486                        "[euv-engine][webgpu] init: device promise rejected or timed out: {:?}",
1487                        err,
1488                    )));
1489                    return None;
1490                }
1491            };
1492        if device_value.is_null() || device_value.is_undefined() {
1493            web_sys::console::error_1(&JsValue::from_str(
1494                "[euv-engine][webgpu] init: requestDevice returned null - \
1495                 adapter could not allocate a device (possibly device-lost)",
1496            ));
1497            return None;
1498        }
1499        let document: Document = window.document().expect("should have a document");
1500        let element: Element = match document.query_selector(&config.canvas_selector) {
1501            Ok(Some(el)) => el,
1502            Ok(None) => {
1503                web_sys::console::error_1(&JsValue::from_str(&format!(
1504                    "[euv-engine][webgpu] init: canvas element {:?} not found in DOM",
1505                    &config.canvas_selector,
1506                )));
1507                return None;
1508            }
1509            Err(err) => {
1510                web_sys::console::error_1(&JsValue::from_str(&format!(
1511                    "[euv-engine][webgpu] init: querySelector threw: {:?}",
1512                    err,
1513                )));
1514                return None;
1515            }
1516        };
1517        let canvas: HtmlCanvasElement = element.unchecked_into();
1518        let context_object: Option<Object> = canvas.get_context(WEBGPU_CONTEXT_TYPE).ok().flatten();
1519        let context_object: Object = match context_object {
1520            Some(c) => c,
1521            None => {
1522                web_sys::console::error_1(&JsValue::from_str(
1523                    "[euv-engine][webgpu] init: canvas.get_context('webgpu') returned null - \
1524                     the canvas may already be using another context type or WebGPU is disabled",
1525                ));
1526                return None;
1527            }
1528        };
1529        let context: JsValue = context_object.into();
1530        let get_format_fn: Function = match Reflect::get(
1531            &gpu,
1532            &JsValue::from_str(WEBGPU_METHOD_GET_PREFERRED_FORMAT),
1533        ) {
1534            Ok(value) => value.unchecked_into(),
1535            Err(err) => {
1536                web_sys::console::error_1(&JsValue::from_str(&format!(
1537                    "[euv-engine][webgpu] init: Reflect::get(gpu, getPreferredCanvasFormat) failed: {:?}",
1538                    err,
1539                )));
1540                return None;
1541            }
1542        };
1543        let format_value: JsValue = match get_format_fn.call0(&gpu) {
1544            Ok(value) => value,
1545            Err(err) => {
1546                web_sys::console::error_1(&JsValue::from_str(&format!(
1547                    "[euv-engine][webgpu] init: gpu.getPreferredCanvasFormat() threw: {:?}",
1548                    err,
1549                )));
1550                return None;
1551            }
1552        };
1553        let format: String = match format_value.as_string() {
1554            Some(s) => s,
1555            None => {
1556                web_sys::console::error_1(&JsValue::from_str(&format!(
1557                    "[euv-engine][webgpu] init: getPreferredCanvasFormat returned non-string: {:?}",
1558                    format_value,
1559                )));
1560                return None;
1561            }
1562        };
1563        // WebGPU's `configure` requires the canvas backing-store size to be
1564        // set BEFORE calling configure, otherwise the swap chain is created
1565        // at 0x0 and the first getCurrentTexture() returns an error.
1566        let dpr: f64 = CanvasRenderer::detect_dpr();
1567        let physical_width: u32 = (config.width * dpr).round() as u32;
1568        let physical_height: u32 = (config.height * dpr).round() as u32;
1569        canvas.set_width(physical_width);
1570        canvas.set_height(physical_height);
1571        let canvas_config: Object = Object::new();
1572        let _ = Reflect::set(
1573            &canvas_config,
1574            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
1575            &device_value,
1576        );
1577        let _ = Reflect::set(
1578            &canvas_config,
1579            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
1580            &format_value,
1581        );
1582        let configure_fn: Function =
1583            match Reflect::get(&context, &JsValue::from_str(WEBGPU_METHOD_CONFIGURE)) {
1584                Ok(value) => value.unchecked_into(),
1585                Err(err) => {
1586                    web_sys::console::error_1(&JsValue::from_str(&format!(
1587                        "[euv-engine][webgpu] init: Reflect::get(context, configure) failed: {:?}",
1588                        err,
1589                    )));
1590                    return None;
1591                }
1592            };
1593        let _ = configure_fn.call1(&context, &canvas_config);
1594        let queue: JsValue =
1595            match Reflect::get(&device_value, &JsValue::from_str(WEBGPU_PROPERTY_QUEUE)) {
1596                Ok(value) => value,
1597                Err(err) => {
1598                    web_sys::console::error_1(&JsValue::from_str(&format!(
1599                        "[euv-engine][webgpu] init: Reflect::get(device, queue) failed: {:?}",
1600                        err,
1601                    )));
1602                    return None;
1603                }
1604            };
1605        Some(WebGpuRenderer {
1606            device: device_value,
1607            queue,
1608            context,
1609            canvas,
1610            format,
1611            width: physical_width,
1612            height: physical_height,
1613            antialias: config.antialias,
1614        })
1615    }
1616
1617    /// Resizes the canvas backing store and reconfigures the swap chain.
1618    ///
1619    /// WebGPU's `GpuCanvasContext.configure` is sticky: it sets the texture
1620    /// format and device once, but the swap chain tracks the canvas's
1621    /// `width`/`height` attributes. When the CSS layout size changes (a
1622    /// window resize, a panel toggle, a DPR change) the canvas keeps its
1623    /// old physical dimensions unless we explicitly update `width`/`height`
1624    /// and call `configure` again. Without this, subsequent
1625    /// `getCurrentTexture()` calls return a texture that no longer matches
1626    /// the visible region and the frame either stretches or freezes.
1627    ///
1628    /// Re-`configure`ing with the same `device` + `format` is the
1629    /// spec-defined way to swap in a fresh swap chain bound to the new
1630    /// backing-store size.
1631    ///
1632    /// # Arguments
1633    ///
1634    /// - `u32` - The new physical pixel width (already multiplied by DPR).
1635    /// - `u32` - The new physical pixel height.
1636    ///
1637    /// # Returns
1638    ///
1639    /// - `bool` - `true` on success, `false` if the swap chain or canvas
1640    ///   handles were missing or `configure` failed.
1641    pub fn resize(&mut self, physical_width: u32, physical_height: u32) -> bool {
1642        if self.canvas.is_null() || self.context.is_null() || self.device.is_undefined() {
1643            return false;
1644        }
1645        self.canvas.set_width(physical_width);
1646        self.canvas.set_height(physical_height);
1647        let format_value: JsValue = JsValue::from_str(&self.format);
1648        let canvas_config: Object = Object::new();
1649        let _ = Reflect::set(
1650            &canvas_config,
1651            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
1652            &self.device,
1653        );
1654        let _ = Reflect::set(
1655            &canvas_config,
1656            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
1657            &format_value,
1658        );
1659        let configure_fn: Function =
1660            Reflect::get(&self.context, &JsValue::from_str(WEBGPU_METHOD_CONFIGURE))
1661                .ok()
1662                .and_then(|value: JsValue| value.dyn_into::<Function>().ok())
1663                .unwrap_or_else(|| Function::new_no_args(""));
1664        if configure_fn.call1(&self.context, &canvas_config).is_err() {
1665            return false;
1666        }
1667        self.width = physical_width;
1668        self.height = physical_height;
1669        true
1670    }
1671
1672    /// Creates a shader module from WGSL source code.
1673    ///
1674    /// # Arguments
1675    ///
1676    /// - `S: AsRef<str>` - The WGSL shader source code.
1677    ///
1678    /// # Returns
1679    ///
1680    /// - `JsValue` - The created shader module as a JavaScript value.
1681    pub(crate) fn create_shader_module<S>(&self, code: S) -> JsValue
1682    where
1683        S: AsRef<str>,
1684    {
1685        let descriptor: Object = Object::new();
1686        let _ = Reflect::set(
1687            &descriptor,
1688            &JsValue::from_str(WEBGPU_PROPERTY_CODE),
1689            &JsValue::from_str(code.as_ref()),
1690        );
1691        let create_fn: Function = Reflect::get(
1692            self.get_device(),
1693            &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
1694        )
1695        .unwrap_or(JsValue::UNDEFINED)
1696        .unchecked_into();
1697        create_fn
1698            .call1(self.get_device(), &descriptor)
1699            .unwrap_or(JsValue::UNDEFINED)
1700    }
1701
1702    /// Creates a new command encoder for recording GPU commands.
1703    ///
1704    /// # Returns
1705    ///
1706    /// - `JsValue` - The created command encoder as a JavaScript value.
1707    pub(crate) fn create_command_encoder(&self) -> JsValue {
1708        let create_fn: Function = Reflect::get(
1709            self.get_device(),
1710            &JsValue::from_str(WEBGPU_METHOD_CREATE_COMMAND_ENCODER),
1711        )
1712        .unwrap_or(JsValue::UNDEFINED)
1713        .unchecked_into();
1714        create_fn
1715            .call0(self.get_device())
1716            .unwrap_or(JsValue::UNDEFINED)
1717    }
1718
1719    /// Returns the current texture view from the canvas swap chain.
1720    ///
1721    /// This texture view should be used as the color attachment target for
1722    /// render passes. The texture is automatically presented to the canvas
1723    /// when the command buffer is submitted.
1724    ///
1725    /// # Returns
1726    ///
1727    /// - `JsValue` - The current frame's texture view as a JavaScript value.
1728    pub(crate) fn get_current_texture_view(&self) -> JsValue {
1729        let get_texture_fn: Function = Reflect::get(
1730            self.get_context(),
1731            &JsValue::from_str(WEBGPU_METHOD_GET_CURRENT_TEXTURE),
1732        )
1733        .unwrap_or(JsValue::UNDEFINED)
1734        .unchecked_into();
1735        let texture: JsValue = get_texture_fn
1736            .call0(self.get_context())
1737            .unwrap_or(JsValue::UNDEFINED);
1738        let create_view_fn: Function =
1739            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
1740                .unwrap_or(JsValue::UNDEFINED)
1741                .unchecked_into();
1742        create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED)
1743    }
1744
1745    /// Begins a render pass on the given command encoder with a clear color.
1746    ///
1747    /// The render pass targets the canvas's current texture and clears it
1748    /// to the specified color. The returned `JsValue` is a `GpuRenderPassEncoder`
1749    /// that can be used to issue draw commands. The pass must be ended (via `end()`)
1750    /// before the command encoder is finished.
1751    ///
1752    /// # Arguments
1753    ///
1754    /// - `&JsValue` - The command encoder to begin the pass on.
1755    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
1756    ///
1757    /// # Returns
1758    ///
1759    /// - `JsValue` - The active render pass encoder as a JavaScript value.
1760    pub(crate) fn begin_render_pass(
1761        &self,
1762        encoder: &JsValue,
1763        clear_color: (f64, f64, f64, f64),
1764    ) -> JsValue {
1765        let view: JsValue = self.get_current_texture_view();
1766        let color_dict: Object = Object::new();
1767        let _ = Reflect::set(
1768            &color_dict,
1769            &JsValue::from_str(WEBGPU_PROPERTY_R),
1770            &JsValue::from_f64(clear_color.0),
1771        );
1772        let _ = Reflect::set(
1773            &color_dict,
1774            &JsValue::from_str(WEBGPU_PROPERTY_G),
1775            &JsValue::from_f64(clear_color.1),
1776        );
1777        let _ = Reflect::set(
1778            &color_dict,
1779            &JsValue::from_str(WEBGPU_PROPERTY_B),
1780            &JsValue::from_f64(clear_color.2),
1781        );
1782        let _ = Reflect::set(
1783            &color_dict,
1784            &JsValue::from_str(WEBGPU_PROPERTY_A),
1785            &JsValue::from_f64(clear_color.3),
1786        );
1787        let attachment: Object = Object::new();
1788        let _ = Reflect::set(&attachment, &JsValue::from_str(WEBGPU_PROPERTY_VIEW), &view);
1789        let _ = Reflect::set(
1790            &attachment,
1791            &JsValue::from_str(WEBGPU_PROPERTY_LOAD_OP),
1792            &JsValue::from_str(WEBGPU_LOAD_OP_CLEAR),
1793        );
1794        let _ = Reflect::set(
1795            &attachment,
1796            &JsValue::from_str(WEBGPU_PROPERTY_STORE_OP),
1797            &JsValue::from_str(WEBGPU_STORE_OP_STORE),
1798        );
1799        let _ = Reflect::set(
1800            &attachment,
1801            &JsValue::from_str(WEBGPU_PROPERTY_CLEAR_VALUE),
1802            &color_dict,
1803        );
1804        let color_attachments: Array = Array::new();
1805        color_attachments.push(&attachment);
1806        let descriptor: Object = Object::new();
1807        let _ = Reflect::set(
1808            &descriptor,
1809            &JsValue::from_str(WEBGPU_PROPERTY_COLOR_ATTACHMENTS),
1810            &color_attachments,
1811        );
1812        let begin_fn: Function =
1813            Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_BEGIN_RENDER_PASS))
1814                .unwrap_or(JsValue::UNDEFINED)
1815                .unchecked_into();
1816        begin_fn
1817            .call1(encoder, &descriptor)
1818            .unwrap_or(JsValue::UNDEFINED)
1819    }
1820
1821    /// Submits an array of command buffers to the GPU queue for execution.
1822    ///
1823    /// # Arguments
1824    ///
1825    /// - `&[JsValue]` - The command buffers to submit.
1826    pub(crate) fn submit(&self, command_buffers: &[JsValue]) {
1827        let array: Array = Array::new();
1828        for buffer in command_buffers {
1829            array.push(buffer);
1830        }
1831        let submit_fn: Function =
1832            Reflect::get(self.get_queue(), &JsValue::from_str(WEBGPU_METHOD_SUBMIT))
1833                .unwrap_or(JsValue::UNDEFINED)
1834                .unchecked_into();
1835        let _ = submit_fn.call1(self.get_queue(), &array);
1836    }
1837
1838    /// Creates a simple render pipeline from a single WGSL shader source.
1839    ///
1840    /// The shader must contain `@vertex fn vs_main(...)` and
1841    /// `@fragment fn fs_main(...)` entry points. No vertex buffers are used;
1842    /// vertex positions should be derived from `@builtin(vertex_index)` in
1843    /// the shader. The pipeline uses auto-layout (`layout: null`), which works
1844    /// when the shader has no bind groups.
1845    ///
1846    /// # Arguments
1847    ///
1848    /// - `S: AsRef<str>` - The WGSL shader source code.
1849    ///
1850    /// # Returns
1851    ///
1852    /// - `JsValue` - The created render pipeline as a JavaScript value.
1853    pub fn create_render_pipeline<S>(&self, shader_code: S) -> JsValue
1854    where
1855        S: AsRef<str>,
1856    {
1857        let module: JsValue = self.create_shader_module(shader_code);
1858        let vertex_state: Object = Object::new();
1859        let _ = Reflect::set(
1860            &vertex_state,
1861            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
1862            &module,
1863        );
1864        let _ = Reflect::set(
1865            &vertex_state,
1866            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
1867            &JsValue::from_str(WEBGPU_VERTEX_ENTRY_POINT),
1868        );
1869        let _ = Reflect::set(
1870            &vertex_state,
1871            &JsValue::from_str(WEBGPU_PROPERTY_BUFFERS),
1872            &Array::new(),
1873        );
1874        let target: Object = Object::new();
1875        let _ = Reflect::set(
1876            &target,
1877            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
1878            &JsValue::from_str(&self.format),
1879        );
1880        let targets: Array = Array::new();
1881        targets.push(&target);
1882        let fragment_state: Object = Object::new();
1883        let _ = Reflect::set(
1884            &fragment_state,
1885            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
1886            &module,
1887        );
1888        let _ = Reflect::set(
1889            &fragment_state,
1890            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
1891            &JsValue::from_str(WEBGPU_FRAGMENT_ENTRY_POINT),
1892        );
1893        let _ = Reflect::set(
1894            &fragment_state,
1895            &JsValue::from_str(WEBGPU_PROPERTY_TARGETS),
1896            &targets,
1897        );
1898        let primitive: Object = Object::new();
1899        let _ = Reflect::set(
1900            &primitive,
1901            &JsValue::from_str(WEBGPU_PROPERTY_TOPOLOGY),
1902            &JsValue::from_str(WEBGPU_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST),
1903        );
1904        let descriptor: Object = Object::new();
1905        let _ = Reflect::set(
1906            &descriptor,
1907            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
1908            &JsValue::null(),
1909        );
1910        let _ = Reflect::set(
1911            &descriptor,
1912            &JsValue::from_str(WEBGPU_PROPERTY_VERTEX),
1913            &vertex_state,
1914        );
1915        let _ = Reflect::set(
1916            &descriptor,
1917            &JsValue::from_str(WEBGPU_PROPERTY_FRAGMENT),
1918            &fragment_state,
1919        );
1920        let _ = Reflect::set(
1921            &descriptor,
1922            &JsValue::from_str(WEBGPU_PROPERTY_PRIMITIVE),
1923            &primitive,
1924        );
1925        let create_fn: Function = Reflect::get(
1926            self.get_device(),
1927            &JsValue::from_str(WEBGPU_METHOD_CREATE_RENDER_PIPELINE),
1928        )
1929        .unwrap_or(JsValue::UNDEFINED)
1930        .unchecked_into();
1931        create_fn
1932            .call1(self.get_device(), &descriptor)
1933            .unwrap_or(JsValue::UNDEFINED)
1934    }
1935
1936    /// Sets the render pipeline on a render pass encoder.
1937    ///
1938    /// # Arguments
1939    ///
1940    /// - `&JsValue` - The render pass encoder.
1941    /// - `&JsValue` - The render pipeline to set.
1942    pub(crate) fn set_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
1943        let set_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_PIPELINE))
1944            .unwrap_or(JsValue::UNDEFINED)
1945            .unchecked_into();
1946        let _ = set_fn.call1(pass, pipeline);
1947    }
1948
1949    /// Draws primitives on a render pass encoder.
1950    ///
1951    /// # Arguments
1952    ///
1953    /// - `&JsValue` - The render pass encoder.
1954    /// - `u32` - The number of vertices to draw.
1955    /// - `u32` - The number of instances to draw.
1956    pub(crate) fn draw(&self, pass: &JsValue, vertex_count: u32, instance_count: u32) {
1957        let draw_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DRAW))
1958            .unwrap_or(JsValue::UNDEFINED)
1959            .unchecked_into();
1960        let _ = draw_fn.call2(
1961            pass,
1962            &JsValue::from_f64(f64::from(vertex_count)),
1963            &JsValue::from_f64(f64::from(instance_count)),
1964        );
1965    }
1966
1967    /// Ends a render pass on the given pass encoder.
1968    ///
1969    /// # Arguments
1970    ///
1971    /// - `&JsValue` - The render pass encoder to end.
1972    pub(crate) fn end_render_pass(&self, pass: &JsValue) {
1973        let end_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_END))
1974            .unwrap_or(JsValue::UNDEFINED)
1975            .unchecked_into();
1976        let _ = end_fn.call0(pass);
1977    }
1978
1979    /// Finishes a command encoder and returns the resulting command buffer.
1980    ///
1981    /// # Arguments
1982    ///
1983    /// - `&JsValue` - The command encoder to finish.
1984    ///
1985    /// # Returns
1986    ///
1987    /// - `JsValue` - The finished command buffer.
1988    pub(crate) fn finish_command_encoder(&self, encoder: &JsValue) -> JsValue {
1989        let finish_fn: Function = Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_FINISH))
1990            .unwrap_or(JsValue::UNDEFINED)
1991            .unchecked_into();
1992        finish_fn.call0(encoder).unwrap_or(JsValue::UNDEFINED)
1993    }
1994
1995    /// Renders a complete frame with a pipeline and animated clear color.
1996    ///
1997    /// This is a convenience method that creates a command encoder, begins a
1998    /// render pass with the given clear color, sets the pipeline, draws the
1999    /// specified number of vertices, ends the pass, finishes the encoder, and
2000    /// submits the command buffer.
2001    ///
2002    /// # Arguments
2003    ///
2004    /// - `&JsValue` - The render pipeline to use.
2005    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
2006    /// - `u32` - The number of vertices to draw.
2007    pub fn render_frame(
2008        &self,
2009        pipeline: &JsValue,
2010        clear_color: (f64, f64, f64, f64),
2011        vertex_count: u32,
2012    ) {
2013        let encoder: JsValue = self.create_command_encoder();
2014        let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
2015        self.set_pipeline(&pass, pipeline);
2016        self.draw(&pass, vertex_count, 1);
2017        self.end_render_pass(&pass);
2018        let command_buffer: JsValue = self.finish_command_encoder(&encoder);
2019        self.submit(&[command_buffer]);
2020    }
2021}