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