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}