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    /// - `&str` - The font family name.
95    ///
96    /// # Returns
97    ///
98    /// - `String` - The CSS font string (e.g., `"16px sans-serif"`).
99    pub fn build_font_string(size: f64, family: &str) -> String {
100        format!("{}px {}", size, family)
101    }
102
103    /// Creates a default font string using the default font size and family.
104    ///
105    /// # Returns
106    ///
107    /// - `String` - The default CSS font string.
108    pub fn default_font_string() -> String {
109        Self::build_font_string(RENDERER_DEFAULT_FONT_SIZE, RENDERER_DEFAULT_FONT_FAMILY)
110    }
111
112    /// Enables high-quality anti-aliasing on an arbitrary canvas 2D context.
113    ///
114    /// Sets `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`
115    /// on the given context. This is a static utility for code that manages its own
116    /// `CanvasRenderingContext2d` without using a `CanvasRenderer` instance.
117    ///
118    /// # Arguments
119    ///
120    /// - `&CanvasRenderingContext2d` - The canvas context to configure.
121    pub fn enable_context_anti_aliasing(context: &CanvasRenderingContext2d) {
122        let _ = Reflect::set(
123            context,
124            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_ENABLED),
125            &JsValue::from_bool(true),
126        );
127        let _ = Reflect::set(
128            context,
129            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
130            &JsValue::from_str(RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH),
131        );
132        let _ = Reflect::set(
133            context,
134            &JsValue::from_str(RENDERER_PROPERTY_TEXT_RENDERING),
135            &JsValue::from_str(RENDERER_TEXT_RENDERING_GEOMETRIC_PRECISION),
136        );
137    }
138}
139
140/// Implements static CSS conversion for `Color`.
141impl Color {
142    /// Converts a `Color` to a CSS `rgba()` string suitable for canvas fill or stroke styles.
143    ///
144    /// # Arguments
145    ///
146    /// - `&Color` - The color to convert.
147    ///
148    /// # Returns
149    ///
150    /// - `String` - The CSS `rgba()` color string.
151    pub fn to_css(color: &Color) -> String {
152        color.to_css_rgba()
153    }
154}
155
156/// Implements drawing and camera management methods for `CanvasRenderer`.
157impl CanvasRenderer {
158    /// Creates a new renderer from a canvas element selector and viewport dimensions.
159    ///
160    /// # Arguments
161    ///
162    /// - `&str` - The CSS selector for the canvas element.
163    /// - `f64` - The viewport width.
164    /// - `f64` - The viewport height.
165    ///
166    /// # Returns
167    ///
168    /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas was not found.
169    pub fn from_selector(
170        canvas_selector: &str,
171        viewport_width: f64,
172        viewport_height: f64,
173    ) -> Option<CanvasRenderer> {
174        let window_value: Window = window().expect("no global window exists");
175        let document_value: Document = window_value.document().expect("should have a document");
176        let element: Element = document_value
177            .query_selector(canvas_selector)
178            .ok()
179            .flatten()?;
180        let canvas_element: HtmlCanvasElement = element.unchecked_into();
181        let context_object: Object = canvas_element
182            .get_context(RENDERER_CONTEXT_TYPE_2D)
183            .ok()
184            .flatten()?;
185        let context: CanvasRenderingContext2d = context_object.unchecked_into();
186        Some(CanvasRenderer::new(
187            context,
188            Camera2D::create(viewport_width, viewport_height),
189        ))
190    }
191
192    /// Enables high-quality anti-aliasing on the canvas context by setting
193    /// `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`.
194    ///
195    /// This improves the quality of image scaling operations (e.g., `draw_image`)
196    /// but does not affect vector primitives like lines and fills, which are
197    /// already anti-aliased by the browser's 2D canvas implementation.
198    pub fn enable_anti_aliasing(&self) {
199        let _ = Reflect::set(
200            self.get_context(),
201            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_ENABLED),
202            &JsValue::from_bool(true),
203        );
204        let _ = Reflect::set(
205            self.get_context(),
206            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
207            &JsValue::from_str(RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH),
208        );
209    }
210
211    /// Clears the entire canvas viewport.
212    pub fn clear(&self) {
213        self.get_context().clear_rect(
214            0.0,
215            0.0,
216            self.get_camera().get_viewport_width(),
217            self.get_camera().get_viewport_height(),
218        );
219    }
220
221    /// Clears the canvas and fills it with the given CSS color string.
222    ///
223    /// # Arguments
224    ///
225    /// - `&str` - The CSS color string (e.g., `"#000000"`).
226    pub fn clear_with_color(&self, color: &str) {
227        let _ = Reflect::set(
228            self.get_context(),
229            &JsValue::from_str(RENDERER_PROPERTY_FILL_STYLE),
230            &JsValue::from_str(color),
231        );
232        self.get_context().fill_rect(
233            0.0,
234            0.0,
235            self.get_camera().get_viewport_width(),
236            self.get_camera().get_viewport_height(),
237        );
238    }
239
240    /// Saves the current canvas state (transform, styles) onto the state stack.
241    pub fn save(&self) {
242        self.get_context().save();
243    }
244
245    /// Restores the most recently saved canvas state.
246    pub fn restore(&self) {
247        self.get_context().restore();
248    }
249
250    /// Applies the camera transform to the canvas context.
251    ///
252    /// Translates to the screen center, applies zoom and rotation,
253    /// then offsets by the negative camera position.
254    pub fn apply_camera(&self) {
255        let camera: Camera2D = self.get_camera();
256        let _ = self.get_context().translate(
257            camera.get_viewport_width() * 0.5,
258            camera.get_viewport_height() * 0.5,
259        );
260        let _ = self
261            .get_context()
262            .scale(camera.get_zoom(), camera.get_zoom());
263        let _ = self.get_context().rotate(camera.get_rotation());
264        let _ = self.get_context().translate(
265            -camera.get_position().get_x(),
266            -camera.get_position().get_y(),
267        );
268    }
269
270    /// Sets the fill color for subsequent fill operations.
271    ///
272    /// # Arguments
273    ///
274    /// - `&str` - The CSS color string.
275    pub fn set_fill_color(&self, color: &str) {
276        let _ = Reflect::set(
277            self.get_context(),
278            &JsValue::from_str(RENDERER_PROPERTY_FILL_STYLE),
279            &JsValue::from_str(color),
280        );
281    }
282
283    /// Sets the stroke color for subsequent stroke operations.
284    ///
285    /// # Arguments
286    ///
287    /// - `&str` - The CSS color string.
288    pub fn set_stroke_color(&self, color: &str) {
289        let _ = Reflect::set(
290            self.get_context(),
291            &JsValue::from_str(RENDERER_PROPERTY_STROKE_STYLE),
292            &JsValue::from_str(color),
293        );
294    }
295
296    /// Sets the line width for subsequent stroke operations.
297    ///
298    /// # Arguments
299    ///
300    /// - `f64` - The line width in pixels.
301    pub fn set_line_width(&self, width: f64) {
302        let _ = Reflect::set(
303            self.get_context(),
304            &JsValue::from_str(RENDERER_PROPERTY_LINE_WIDTH),
305            &JsValue::from_f64(width),
306        );
307    }
308
309    /// Sets the global alpha (opacity) for all subsequent drawing operations.
310    ///
311    /// # Arguments
312    ///
313    /// - `f64` - The alpha value in the range 0.0 to 1.0.
314    pub fn set_global_alpha(&self, alpha: f64) {
315        let _ = Reflect::set(
316            self.get_context(),
317            &JsValue::from_str(RENDERER_PROPERTY_GLOBAL_ALPHA),
318            &JsValue::from_f64(Numeric::clamp(alpha, 0.0, 1.0)),
319        );
320    }
321
322    /// Fills a rectangle at the given world-space position and dimensions.
323    ///
324    /// # Arguments
325    ///
326    /// - `Vector2D` - The top-left position in world space.
327    /// - `f64` - The width.
328    /// - `f64` - The height.
329    pub fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
330        self.get_context()
331            .fill_rect(position.get_x(), position.get_y(), width, height);
332    }
333
334    /// Strokes the outline of a rectangle at the given world-space position and dimensions.
335    ///
336    /// # Arguments
337    ///
338    /// - `Vector2D` - The top-left position in world space.
339    /// - `f64` - The width.
340    /// - `f64` - The height.
341    pub fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
342        self.get_context()
343            .stroke_rect(position.get_x(), position.get_y(), width, height);
344    }
345
346    /// Fills a circle at the given world-space center with the specified radius.
347    ///
348    /// # Arguments
349    ///
350    /// - `Vector2D` - The center in world space.
351    /// - `f64` - The radius.
352    pub fn fill_circle(&self, center: Vector2D, radius: f64) {
353        self.get_context().begin_path();
354        self.get_context()
355            .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
356            .unwrap_or(());
357        self.get_context().fill();
358    }
359
360    /// Strokes the outline of a circle at the given world-space center.
361    ///
362    /// # Arguments
363    ///
364    /// - `Vector2D` - The center in world space.
365    /// - `f64` - The radius.
366    pub fn stroke_circle(&self, center: Vector2D, radius: f64) {
367        self.get_context().begin_path();
368        self.get_context()
369            .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
370            .unwrap_or(());
371        self.get_context().stroke();
372    }
373
374    /// Draws a line segment between two world-space points.
375    ///
376    /// # Arguments
377    ///
378    /// - `Vector2D` - The start point.
379    /// - `Vector2D` - The end point.
380    pub fn draw_line(&self, start: Vector2D, end: Vector2D) {
381        self.get_context().begin_path();
382        self.get_context().move_to(start.get_x(), start.get_y());
383        self.get_context().line_to(end.get_x(), end.get_y());
384        self.get_context().stroke();
385    }
386
387    /// Fills text at the given world-space position.
388    ///
389    /// # Arguments
390    ///
391    /// - `&str` - The text to draw.
392    /// - `Vector2D` - The position in world space.
393    pub fn fill_text(&self, text: &str, position: Vector2D) {
394        self.get_context()
395            .fill_text(text, position.get_x(), position.get_y())
396            .unwrap_or(());
397    }
398
399    /// Sets the font for subsequent text rendering.
400    ///
401    /// # Arguments
402    ///
403    /// - `&str` - The CSS font string (e.g., `"16px sans-serif"`).
404    pub fn set_font(&self, font: &str) {
405        let _ = Reflect::set(
406            self.get_context(),
407            &JsValue::from_str(RENDERER_PROPERTY_FONT),
408            &JsValue::from_str(font),
409        );
410    }
411
412    /// Draws an image element at the given world-space position and dimensions.
413    ///
414    /// # Arguments
415    ///
416    /// - `&HtmlImageElement` - The image element to draw.
417    /// - `Vector2D` - The top-left position in world space.
418    /// - `f64` - The destination width.
419    /// - `f64` - The destination height.
420    pub fn draw_image(
421        &self,
422        image: &HtmlImageElement,
423        position: Vector2D,
424        width: f64,
425        height: f64,
426    ) {
427        let _ = self
428            .get_context()
429            .draw_image_with_html_image_element_and_dw_and_dh(
430                image,
431                position.get_x(),
432                position.get_y(),
433                width,
434                height,
435            );
436    }
437
438    /// Draws a sub-region of an image element at the given world-space position.
439    ///
440    /// # Arguments
441    ///
442    /// - `&HtmlImageElement` - The image element to draw.
443    /// - `Rect` - The source rectangle within the image.
444    /// - `Vector2D` - The destination top-left position in world space.
445    /// - `f64` - The destination width.
446    /// - `f64` - The destination height.
447    pub fn draw_image_subregion(
448        &self,
449        image: &HtmlImageElement,
450        source: Rect,
451        dest_position: Vector2D,
452        dest_width: f64,
453        dest_height: f64,
454    ) {
455        let _ = self
456            .get_context()
457            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
458                image,
459                source.get_x(),
460                source.get_y(),
461                source.get_width(),
462                source.get_height(),
463                dest_position.get_x(),
464                dest_position.get_y(),
465                dest_width,
466                dest_height,
467            );
468    }
469}
470
471/// Implements 3D camera transformation and projection methods for `Camera3D`.
472impl Camera3D {
473    /// Creates a new 3D camera at the given position looking at the target.
474    ///
475    /// # Arguments
476    ///
477    /// - `Vector3D` - The eye position.
478    /// - `Vector3D` - The target position to look at.
479    /// - `f64` - The viewport width.
480    /// - `f64` - The viewport height.
481    ///
482    /// # Returns
483    ///
484    /// - `Camera3D` - The new camera.
485    pub fn create(
486        position: Vector3D,
487        target: Vector3D,
488        viewport_width: f64,
489        viewport_height: f64,
490    ) -> Camera3D {
491        let mut camera: Camera3D = Camera3D::new(position, target, viewport_width, viewport_height);
492        camera.set_up(Vector3D::up());
493        camera.set_fov(DEFAULT_CAMERA_FOV);
494        camera.set_near(DEFAULT_CAMERA_NEAR);
495        camera.set_far(DEFAULT_CAMERA_FAR);
496        camera
497    }
498
499    /// Returns the aspect ratio (width / height).
500    ///
501    /// # Returns
502    ///
503    /// - `f64` - The aspect ratio.
504    pub fn aspect(&self) -> f64 {
505        if self.get_viewport_height() < EPSILON {
506            return 1.0;
507        }
508        self.get_viewport_width() / self.get_viewport_height()
509    }
510
511    /// Returns the forward direction (from position to target, normalized).
512    ///
513    /// # Returns
514    ///
515    /// - `Vector3D` - The forward direction.
516    pub fn forward(&self) -> Vector3D {
517        (self.get_target() - self.get_position()).normalized()
518    }
519
520    /// Returns the right direction (cross product of forward and up).
521    ///
522    /// # Returns
523    ///
524    /// - `Vector3D` - The right direction.
525    pub fn right(&self) -> Vector3D {
526        self.forward().cross(self.get_up()).normalized()
527    }
528
529    /// Returns the view matrix for this camera.
530    ///
531    /// # Returns
532    ///
533    /// - `Matrix4x4` - The view matrix.
534    pub fn view_matrix(&self) -> Matrix4x4 {
535        Matrix4x4::look_at(self.get_position(), self.get_target(), self.get_up())
536    }
537
538    /// Returns the perspective projection matrix for this camera.
539    ///
540    /// # Returns
541    ///
542    /// - `Matrix4x4` - The projection matrix.
543    pub fn projection_matrix(&self) -> Matrix4x4 {
544        Matrix4x4::perspective(
545            self.get_fov(),
546            self.aspect(),
547            self.get_near(),
548            self.get_far(),
549        )
550    }
551
552    /// Returns the combined view-projection matrix.
553    ///
554    /// # Returns
555    ///
556    /// - `Matrix4x4` - The view-projection matrix.
557    pub fn view_projection_matrix(&self) -> Matrix4x4 {
558        self.projection_matrix().multiply(self.view_matrix())
559    }
560
561    /// Converts a 3D world-space point to screen-space (NDC) coordinates.
562    ///
563    /// # Arguments
564    ///
565    /// - `Vector3D` - The world-space point.
566    ///
567    /// # Returns
568    ///
569    /// - `Vector3D` - The screen-space point where x and y are in [0, 1] and z is the depth.
570    pub fn world_to_screen(&self, world: Vector3D) -> Vector3D {
571        let clip: Vector3D = self.view_projection_matrix().transform_point(world);
572        Vector3D::new(
573            (clip.get_x() + 1.0) * 0.5 * self.get_viewport_width(),
574            (1.0 - clip.get_y()) * 0.5 * self.get_viewport_height(),
575            clip.get_z(),
576        )
577    }
578
579    /// Projects a world-space point and returns whether it is within the camera frustum.
580    ///
581    /// # Arguments
582    ///
583    /// - `Vector3D` - The world-space point.
584    ///
585    /// # Returns
586    ///
587    /// - `bool` - True if the point is within the frustum.
588    pub fn is_in_frustum(&self, world: Vector3D) -> bool {
589        let clip: Vector3D = self.view_projection_matrix().transform_point(world);
590        clip.get_x() >= -1.0
591            && clip.get_x() <= 1.0
592            && clip.get_y() >= -1.0
593            && clip.get_y() <= 1.0
594            && clip.get_z() >= -1.0
595            && clip.get_z() <= 1.0
596    }
597
598    /// Moves the camera position by the given offset, keeping the target offset by the same amount.
599    ///
600    /// # Arguments
601    ///
602    /// - `Vector3D` - The translation offset.
603    pub fn translate(&mut self, offset: Vector3D) {
604        self.set_position(self.get_position() + offset);
605        self.set_target(self.get_target() + offset);
606    }
607
608    /// Moves the camera position towards the target by the given distance.
609    ///
610    /// # Arguments
611    ///
612    /// - `f64` - The distance to zoom in (positive) or out (negative).
613    pub fn zoom(&mut self, distance: f64) {
614        let direction: Vector3D = self.forward();
615        self.set_position(self.get_position() + direction.scaled(distance));
616    }
617
618    /// Orbits the camera around the target by the given yaw and pitch angles.
619    ///
620    /// # Arguments
621    ///
622    /// - `f64` - The yaw delta in radians (horizontal rotation).
623    /// - `f64` - The pitch delta in radians (vertical rotation).
624    pub fn orbit(&mut self, yaw_delta: f64, pitch_delta: f64) {
625        let offset: Vector3D = self.get_position() - self.get_target();
626        let current_distance: f64 = offset.magnitude();
627        let current_yaw: f64 = offset.get_x().atan2(offset.get_z());
628        let horizontal_dist: f64 =
629            (offset.get_x() * offset.get_x() + offset.get_z() * offset.get_z()).sqrt();
630        let current_pitch: f64 = (offset.get_y() / horizontal_dist.max(EPSILON)).asin();
631        let new_yaw: f64 = current_yaw + yaw_delta;
632        let new_pitch: f64 = Numeric::clamp(
633            current_pitch + pitch_delta,
634            -HALF_PI + EPSILON,
635            HALF_PI - EPSILON,
636        );
637        let cos_pitch: f64 = new_pitch.cos();
638        self.set_position(
639            self.get_target()
640                + Vector3D::new(
641                    new_yaw.sin() * cos_pitch * current_distance,
642                    new_pitch.sin() * current_distance,
643                    new_yaw.cos() * cos_pitch * current_distance,
644                ),
645        );
646    }
647}
648
649/// Implements `Default` for `Camera3D` as a camera at (0, 0, 5) looking at the origin.
650impl Default for Camera3D {
651    fn default() -> Camera3D {
652        Camera3D::create(Vector3D::new(0.0, 0.0, 5.0), Vector3D::zero(), 800.0, 600.0)
653    }
654}
655
656/// Implements construction, presentation, and anti-aliasing methods for `SsaaCanvas`.
657impl SsaaCanvas {
658    /// Creates an `SsaaCanvas` from a CSS selector using the default scale factor.
659    ///
660    /// # Arguments
661    ///
662    /// - `&str` - The CSS selector for the display canvas element.
663    /// - `f64` - The logical display width in CSS pixels.
664    /// - `f64` - The logical display height in CSS pixels.
665    ///
666    /// # Returns
667    ///
668    /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
669    pub fn from_selector(canvas_selector: &str, width: f64, height: f64) -> Option<SsaaCanvas> {
670        Self::from_selector_with_scale(
671            canvas_selector,
672            width,
673            height,
674            RENDERER_DEFAULT_SSAA_SCALE_FACTOR,
675        )
676    }
677
678    /// Creates an `SsaaCanvas` from a CSS selector with a custom SSAA scale factor.
679    ///
680    /// The offscreen canvas is created at `width * scale_factor` by `height * scale_factor`
681    /// pixels, and its context is pre-scaled so that drawing code uses logical coordinates.
682    ///
683    /// # Arguments
684    ///
685    /// - `&str` - The CSS selector for the display canvas element.
686    /// - `f64` - The logical display width in CSS pixels.
687    /// - `f64` - The logical display height in CSS pixels.
688    /// - `f64` - The supersampling scale factor (e.g., 2.0 for 4x SSAA).
689    ///
690    /// # Returns
691    ///
692    /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
693    pub fn from_selector_with_scale(
694        canvas_selector: &str,
695        width: f64,
696        height: f64,
697        scale_factor: f64,
698    ) -> Option<SsaaCanvas> {
699        let window_value: Window = window().expect("no global window exists");
700        let document_value: Document = window_value.document().expect("should have a document");
701        let element: Element = document_value
702            .query_selector(canvas_selector)
703            .ok()
704            .flatten()?;
705        let display_canvas: HtmlCanvasElement = element.unchecked_into();
706        display_canvas.set_width(width as u32);
707        display_canvas.set_height(height as u32);
708        let display_context_object: Object = display_canvas
709            .get_context(RENDERER_CONTEXT_TYPE_2D)
710            .ok()
711            .flatten()?;
712        let display_context: CanvasRenderingContext2d = display_context_object.unchecked_into();
713        let offscreen_canvas: HtmlCanvasElement = document_value
714            .create_element(RENDERER_ELEMENT_CANVAS)
715            .ok()?
716            .unchecked_into();
717        let scaled_width: u32 = (width * scale_factor) as u32;
718        let scaled_height: u32 = (height * scale_factor) as u32;
719        offscreen_canvas.set_width(scaled_width);
720        offscreen_canvas.set_height(scaled_height);
721        let offscreen_context_object: Object = offscreen_canvas
722            .get_context(RENDERER_CONTEXT_TYPE_2D)
723            .ok()
724            .flatten()?;
725        let offscreen_context: CanvasRenderingContext2d = offscreen_context_object.unchecked_into();
726        let _ = offscreen_context.scale(scale_factor, scale_factor);
727        let ssaa_canvas: SsaaCanvas = SsaaCanvas::new(
728            display_canvas,
729            display_context,
730            offscreen_canvas,
731            offscreen_context,
732            scale_factor,
733            width,
734            height,
735        );
736        ssaa_canvas.enable_anti_aliasing();
737        Some(ssaa_canvas)
738    }
739
740    /// Presents the offscreen buffer onto the display canvas with high-quality downscaling.
741    ///
742    /// Enables `imageSmoothingEnabled` and `imageSmoothingQuality = "high"` on the
743    /// display context, clears the display canvas, then draws the offscreen canvas
744    /// scaled down to the logical display size. This is the core SSAA step that
745    /// produces smooth polygon edges.
746    pub fn present(&self) {
747        let _ = Reflect::set(
748            &self.display_context,
749            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_ENABLED),
750            &JsValue::from_bool(true),
751        );
752        let _ = Reflect::set(
753            &self.display_context,
754            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
755            &JsValue::from_str(RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH),
756        );
757        self.display_context
758            .clear_rect(0.0, 0.0, self.width, self.height);
759        let _ = self
760            .display_context
761            .draw_image_with_html_canvas_element_and_dw_and_dh(
762                &self.offscreen_canvas,
763                0.0,
764                0.0,
765                self.width,
766                self.height,
767            );
768    }
769
770    /// Clears the offscreen buffer to transparent.
771    pub fn clear(&self) {
772        self.offscreen_context
773            .clear_rect(0.0, 0.0, self.width, self.height);
774    }
775
776    /// Clears the offscreen buffer and fills it with the given CSS color.
777    ///
778    /// # Arguments
779    ///
780    /// - `&str` - The CSS color string.
781    pub fn clear_with_color(&self, color: &str) {
782        let _ = Reflect::set(
783            &self.offscreen_context,
784            &JsValue::from_str(RENDERER_PROPERTY_FILL_STYLE),
785            &JsValue::from_str(color),
786        );
787        self.offscreen_context
788            .fill_rect(0.0, 0.0, self.width, self.height);
789    }
790
791    /// Enables high-quality anti-aliasing on both the display and offscreen contexts.
792    ///
793    /// Sets `imageSmoothingEnabled = true` and `imageSmoothingQuality = "high"`
794    /// on both contexts to ensure optimal image scaling quality.
795    pub fn enable_anti_aliasing(&self) {
796        let _ = Reflect::set(
797            &self.display_context,
798            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_ENABLED),
799            &JsValue::from_bool(true),
800        );
801        let _ = Reflect::set(
802            &self.display_context,
803            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
804            &JsValue::from_str(RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH),
805        );
806        let _ = Reflect::set(
807            &self.offscreen_context,
808            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_ENABLED),
809            &JsValue::from_bool(true),
810        );
811        let _ = Reflect::set(
812            &self.offscreen_context,
813            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
814            &JsValue::from_str(RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH),
815        );
816    }
817}
818
819/// Implements CSS composite operation string conversion for `BlendMode`.
820impl BlendMode {
821    /// Returns the CSS `globalCompositeOperation` string for this blend mode.
822    ///
823    /// # Returns
824    ///
825    /// - `&str` - The CSS composite operation string.
826    pub fn to_css_string(&self) -> &str {
827        match self {
828            BlendMode::Normal => BLEND_MODE_NORMAL,
829            BlendMode::Multiply => BLEND_MODE_MULTIPLY,
830            BlendMode::Screen => BLEND_MODE_SCREEN,
831            BlendMode::Lighter => BLEND_MODE_LIGHTER,
832            BlendMode::Overlay => BLEND_MODE_OVERLAY,
833            BlendMode::Darken => BLEND_MODE_DARKEN,
834            BlendMode::Lighten => BLEND_MODE_LIGHTEN,
835            BlendMode::ColorDodge => BLEND_MODE_COLOR_DODGE,
836            BlendMode::ColorBurn => BLEND_MODE_COLOR_BURN,
837            BlendMode::HardLight => BLEND_MODE_HARD_LIGHT,
838            BlendMode::SoftLight => BLEND_MODE_SOFT_LIGHT,
839            BlendMode::Difference => BLEND_MODE_DIFFERENCE,
840            BlendMode::Exclusion => BLEND_MODE_EXCLUSION,
841            BlendMode::Hue => BLEND_MODE_HUE,
842            BlendMode::Saturation => BLEND_MODE_SATURATION,
843            BlendMode::Color => BLEND_MODE_COLOR,
844            BlendMode::Luminosity => BLEND_MODE_LUMINOSITY,
845        }
846    }
847}
848
849/// Implements construction and canvas gradient creation for `LinearGradient`.
850impl LinearGradient {
851    /// Creates a new linear gradient from two points and a list of color stops.
852    ///
853    /// # Arguments
854    ///
855    /// - `Vector2D` - The start point.
856    /// - `Vector2D` - The end point.
857    /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
858    ///
859    /// # Returns
860    ///
861    /// - `LinearGradient` - The new gradient.
862    pub fn create(start: Vector2D, end: Vector2D, stops: Vec<(f64, String)>) -> LinearGradient {
863        LinearGradient::new(start, end, stops)
864    }
865
866    /// Creates a `CanvasGradient` from this gradient definition on the given context.
867    ///
868    /// # Arguments
869    ///
870    /// - `&CanvasRenderingContext2d` - The canvas context.
871    ///
872    /// # Returns
873    ///
874    /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
875    pub fn to_canvas_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
876        let canvas_gradient: CanvasGradient = context.create_linear_gradient(
877            self.get_start().get_x(),
878            self.get_start().get_y(),
879            self.get_end().get_x(),
880            self.get_end().get_y(),
881        );
882        for (position, color) in &self.stops {
883            let _ = canvas_gradient.add_color_stop(*position as f32, color);
884        }
885        Some(canvas_gradient)
886    }
887}
888
889/// Implements construction and canvas gradient creation for `RadialGradient`.
890impl RadialGradient {
891    /// Creates a new radial gradient from inner and outer circles and color stops.
892    ///
893    /// # Arguments
894    ///
895    /// - `Vector2D` - The inner circle center.
896    /// - `f64` - The inner circle radius.
897    /// - `Vector2D` - The outer circle center.
898    /// - `f64` - The outer circle radius.
899    /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
900    ///
901    /// # Returns
902    ///
903    /// - `RadialGradient` - The new gradient.
904    pub fn create(
905        inner_center: Vector2D,
906        inner_radius: f64,
907        outer_center: Vector2D,
908        outer_radius: f64,
909        stops: Vec<(f64, String)>,
910    ) -> RadialGradient {
911        RadialGradient::new(
912            inner_center,
913            inner_radius,
914            outer_center,
915            outer_radius,
916            stops,
917        )
918    }
919
920    /// Creates a `CanvasGradient` from this gradient definition on the given context.
921    ///
922    /// # Arguments
923    ///
924    /// - `&CanvasRenderingContext2d` - The canvas context.
925    ///
926    /// # Returns
927    ///
928    /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
929    pub fn to_canvas_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
930        let canvas_gradient: CanvasGradient = context
931            .create_radial_gradient(
932                self.get_inner_center().get_x(),
933                self.get_inner_center().get_y(),
934                self.get_inner_radius(),
935                self.get_outer_center().get_x(),
936                self.get_outer_center().get_y(),
937                self.get_outer_radius(),
938            )
939            .ok()?;
940        for (position, color) in &self.stops {
941            let _ = canvas_gradient.add_color_stop(*position as f32, color);
942        }
943        Some(canvas_gradient)
944    }
945}
946
947/// Implements construction methods for `ShadowConfig`.
948impl ShadowConfig {
949    /// Creates a shadow configuration with default values.
950    ///
951    /// # Returns
952    ///
953    /// - `ShadowConfig` - The default shadow configuration.
954    pub fn create() -> ShadowConfig {
955        ShadowConfig::new(
956            RENDERER_DEFAULT_SHADOW_COLOR.to_string(),
957            RENDERER_DEFAULT_SHADOW_BLUR,
958            0.0,
959            0.0,
960        )
961    }
962}
963
964/// Implements `Default` for `ShadowConfig` with default shadow values.
965impl Default for ShadowConfig {
966    fn default() -> ShadowConfig {
967        ShadowConfig::create()
968    }
969}
970
971/// Implements construction methods for `RenderLayer`.
972impl RenderLayer {
973    /// Creates a render layer with the given z-index and visibility.
974    ///
975    /// # Arguments
976    ///
977    /// - `i32` - The z-index determining draw order.
978    /// - `bool` - Whether the layer is visible.
979    ///
980    /// # Returns
981    ///
982    /// - `RenderLayer` - The new render layer.
983    pub fn create(z_index: i32, visible: bool) -> RenderLayer {
984        RenderLayer::new(z_index, visible)
985    }
986
987    /// Creates a background render layer with z-index 0 and visibility enabled.
988    ///
989    /// # Returns
990    ///
991    /// - `RenderLayer` - The background layer.
992    pub fn background() -> RenderLayer {
993        RenderLayer::new(RENDERER_LAYER_BACKGROUND, true)
994    }
995
996    /// Creates a foreground render layer with a high z-index and visibility enabled.
997    ///
998    /// # Returns
999    ///
1000    /// - `RenderLayer` - The foreground layer.
1001    pub fn foreground() -> RenderLayer {
1002        RenderLayer::new(RENDERER_LAYER_FOREGROUND, true)
1003    }
1004
1005    /// Creates a UI overlay render layer with the highest z-index and visibility enabled.
1006    ///
1007    /// # Returns
1008    ///
1009    /// - `RenderLayer` - The UI overlay layer.
1010    pub fn ui() -> RenderLayer {
1011        RenderLayer::new(RENDERER_LAYER_UI, true)
1012    }
1013}
1014
1015/// Implements blend mode, shadow, and gradient rendering methods for `CanvasRenderer`.
1016impl CanvasRenderer {
1017    /// Sets the blend mode for compositing subsequent draw operations.
1018    ///
1019    /// # Arguments
1020    ///
1021    /// - `BlendMode` - The blend mode to apply.
1022    pub fn set_blend_mode(&self, mode: BlendMode) {
1023        let _ = Reflect::set(
1024            self.get_context(),
1025            &JsValue::from_str(RENDERER_PROPERTY_GLOBAL_COMPOSITE_OPERATION),
1026            &JsValue::from_str(mode.to_css_string()),
1027        );
1028    }
1029
1030    /// Applies a shadow configuration for subsequent draw operations.
1031    ///
1032    /// # Arguments
1033    ///
1034    /// - `&ShadowConfig` - The shadow configuration to apply.
1035    pub fn set_shadow(&self, config: &ShadowConfig) {
1036        let _ = Reflect::set(
1037            self.get_context(),
1038            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_COLOR),
1039            &JsValue::from_str(config.get_color().as_str()),
1040        );
1041        let _ = Reflect::set(
1042            self.get_context(),
1043            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_BLUR),
1044            &JsValue::from_f64(config.get_blur()),
1045        );
1046        let _ = Reflect::set(
1047            self.get_context(),
1048            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_OFFSET_X),
1049            &JsValue::from_f64(config.get_offset_x()),
1050        );
1051        let _ = Reflect::set(
1052            self.get_context(),
1053            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_OFFSET_Y),
1054            &JsValue::from_f64(config.get_offset_y()),
1055        );
1056    }
1057
1058    /// Clears any previously applied shadow, disabling shadow rendering.
1059    pub fn clear_shadow(&self) {
1060        let _ = Reflect::set(
1061            self.get_context(),
1062            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_COLOR),
1063            &JsValue::from_str("rgba(0, 0, 0, 0)"),
1064        );
1065        let _ = Reflect::set(
1066            self.get_context(),
1067            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_BLUR),
1068            &JsValue::from_f64(0.0),
1069        );
1070        let _ = Reflect::set(
1071            self.get_context(),
1072            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_OFFSET_X),
1073            &JsValue::from_f64(0.0),
1074        );
1075        let _ = Reflect::set(
1076            self.get_context(),
1077            &JsValue::from_str(RENDERER_PROPERTY_SHADOW_OFFSET_Y),
1078            &JsValue::from_f64(0.0),
1079        );
1080    }
1081
1082    /// Applies a linear gradient as the fill style for subsequent operations.
1083    ///
1084    /// # Arguments
1085    ///
1086    /// - `&LinearGradient` - The linear gradient to use as fill style.
1087    pub fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1088        if let Some(canvas_gradient) = gradient.to_canvas_gradient(self.get_context()) {
1089            let _ = Reflect::set(
1090                self.get_context(),
1091                &JsValue::from_str(RENDERER_PROPERTY_FILL_STYLE),
1092                &canvas_gradient,
1093            );
1094        }
1095    }
1096
1097    /// Applies a radial gradient as the fill style for subsequent operations.
1098    ///
1099    /// # Arguments
1100    ///
1101    /// - `&RadialGradient` - The radial gradient to use as fill style.
1102    pub fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1103        if let Some(canvas_gradient) = gradient.to_canvas_gradient(self.get_context()) {
1104            let _ = Reflect::set(
1105                self.get_context(),
1106                &JsValue::from_str(RENDERER_PROPERTY_FILL_STYLE),
1107                &canvas_gradient,
1108            );
1109        }
1110    }
1111
1112    /// Applies a linear gradient as the stroke style for subsequent operations.
1113    ///
1114    /// # Arguments
1115    ///
1116    /// - `&LinearGradient` - The linear gradient to use as stroke style.
1117    pub fn set_linear_gradient_stroke(&self, gradient: &LinearGradient) {
1118        if let Some(canvas_gradient) = gradient.to_canvas_gradient(self.get_context()) {
1119            let _ = Reflect::set(
1120                self.get_context(),
1121                &JsValue::from_str(RENDERER_PROPERTY_STROKE_STYLE),
1122                &canvas_gradient,
1123            );
1124        }
1125    }
1126
1127    /// Applies a radial gradient as the stroke style for subsequent operations.
1128    ///
1129    /// # Arguments
1130    ///
1131    /// - `&RadialGradient` - The radial gradient to use as stroke style.
1132    pub fn set_radial_gradient_stroke(&self, gradient: &RadialGradient) {
1133        if let Some(canvas_gradient) = gradient.to_canvas_gradient(self.get_context()) {
1134            let _ = Reflect::set(
1135                self.get_context(),
1136                &JsValue::from_str(RENDERER_PROPERTY_STROKE_STYLE),
1137                &canvas_gradient,
1138            );
1139        }
1140    }
1141}
1142
1143/// Implements the `RenderBackend` trait for `CanvasRenderer`, providing
1144/// a backend-agnostic rendering interface.
1145impl RenderBackend for CanvasRenderer {
1146    fn clear(&self) {
1147        self.clear();
1148    }
1149
1150    fn clear_with_color(&self, color: &str) {
1151        self.clear_with_color(color);
1152    }
1153
1154    fn save(&self) {
1155        self.save();
1156    }
1157
1158    fn restore(&self) {
1159        self.restore();
1160    }
1161
1162    fn set_fill_color(&self, color: &str) {
1163        self.set_fill_color(color);
1164    }
1165
1166    fn set_stroke_color(&self, color: &str) {
1167        self.set_stroke_color(color);
1168    }
1169
1170    fn set_line_width(&self, width: f64) {
1171        self.set_line_width(width);
1172    }
1173
1174    fn set_global_alpha(&self, alpha: f64) {
1175        self.set_global_alpha(alpha);
1176    }
1177
1178    fn set_blend_mode(&self, mode: BlendMode) {
1179        self.set_blend_mode(mode);
1180    }
1181
1182    fn set_shadow(&self, config: &ShadowConfig) {
1183        self.set_shadow(config);
1184    }
1185
1186    fn clear_shadow(&self) {
1187        self.clear_shadow();
1188    }
1189
1190    fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1191        self.fill_rect(position, width, height);
1192    }
1193
1194    fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1195        self.stroke_rect(position, width, height);
1196    }
1197
1198    fn fill_circle(&self, center: Vector2D, radius: f64) {
1199        self.fill_circle(center, radius);
1200    }
1201
1202    fn stroke_circle(&self, center: Vector2D, radius: f64) {
1203        self.stroke_circle(center, radius);
1204    }
1205
1206    fn draw_line(&self, start: Vector2D, end: Vector2D) {
1207        self.draw_line(start, end);
1208    }
1209
1210    fn fill_text(&self, text: &str, position: Vector2D) {
1211        self.fill_text(text, position);
1212    }
1213
1214    fn set_font(&self, font: &str) {
1215        self.set_font(font);
1216    }
1217
1218    fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1219        self.draw_image(image, position, width, height);
1220    }
1221
1222    fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1223        self.set_linear_gradient_fill(gradient);
1224    }
1225
1226    fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1227        self.set_radial_gradient_fill(gradient);
1228    }
1229}