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}