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.
1198impl RenderBackend for CanvasRenderer {
1199    fn clear(&self) {
1200        self.clear();
1201    }
1202
1203    fn clear_color<C>(&self, color: C)
1204    where
1205        C: AsRef<str>,
1206    {
1207        self.clear_color(color);
1208    }
1209
1210    fn save(&self) {
1211        self.save();
1212    }
1213
1214    fn restore(&self) {
1215        self.restore();
1216    }
1217
1218    fn set_fill_color(&self, color: &str) {
1219        self.set_fill_color(color);
1220    }
1221
1222    fn set_stroke_color(&self, color: &str) {
1223        self.set_stroke_color(color);
1224    }
1225
1226    fn set_line_width(&self, width: f64) {
1227        self.set_line_width(width);
1228    }
1229
1230    fn set_global_alpha(&self, alpha: f64) {
1231        self.set_global_alpha(alpha);
1232    }
1233
1234    fn set_blend_mode(&self, mode: BlendMode) {
1235        self.set_blend_mode(mode);
1236    }
1237
1238    fn set_shadow(&self, config: &ShadowConfig) {
1239        self.set_shadow(config);
1240    }
1241
1242    fn clear_shadow(&self) {
1243        self.clear_shadow();
1244    }
1245
1246    fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1247        self.fill_rect(position, width, height);
1248    }
1249
1250    fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1251        self.stroke_rect(position, width, height);
1252    }
1253
1254    fn fill_circle(&self, center: Vector2D, radius: f64) {
1255        self.fill_circle(center, radius);
1256    }
1257
1258    fn stroke_circle(&self, center: Vector2D, radius: f64) {
1259        self.stroke_circle(center, radius);
1260    }
1261
1262    fn draw_line(&self, start: Vector2D, end: Vector2D) {
1263        self.draw_line(start, end);
1264    }
1265
1266    fn fill_text(&self, text: &str, position: Vector2D) {
1267        self.fill_text(text, position);
1268    }
1269
1270    fn set_font(&self, font: &str) {
1271        self.set_font(font);
1272    }
1273
1274    fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1275        self.draw_image(image, position, width, height);
1276    }
1277
1278    fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1279        self.set_linear_gradient_fill(gradient);
1280    }
1281
1282    fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1283        self.set_radial_gradient_fill(gradient);
1284    }
1285}