euv_engine/renderer/canvas/impl.rs
1use super::*;
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 ///
11 /// # Returns
12 ///
13 /// - `Camera2D` - The new camera.
14 pub fn create(viewport_width: f64, viewport_height: f64) -> Camera2D {
15 Camera2D::new(
16 Vector2D::zero(),
17 RENDERER_DEFAULT_CAMERA_ZOOM,
18 RENDERER_DEFAULT_CAMERA_ROTATION,
19 viewport_width,
20 viewport_height,
21 )
22 }
23
24 /// Converts a world-space point to screen-space coordinates.
25 ///
26 /// # Arguments
27 ///
28 /// - `Vector2D` - The world-space point.
29 ///
30 /// # Returns
31 ///
32 /// - `Vector2D` - The screen-space point.
33 pub fn world_to_screen(&self, world: Vector2D) -> Vector2D {
34 let relative: Vector2D = world - self.get_position();
35 let rotated: Vector2D = relative.rotated(-self.get_rotation());
36 Vector2D::new(
37 rotated.get_x() * self.get_zoom() + self.get_viewport_width() * 0.5,
38 rotated.get_y() * self.get_zoom() + self.get_viewport_height() * 0.5,
39 )
40 }
41
42 /// Converts a screen-space point to world-space coordinates.
43 ///
44 /// # Arguments
45 ///
46 /// - `Vector2D` - The screen-space point.
47 ///
48 /// # Returns
49 ///
50 /// - `Vector2D` - The world-space point.
51 pub fn screen_to_world(&self, screen: Vector2D) -> Vector2D {
52 let relative: Vector2D = Vector2D::new(
53 (screen.get_x() - self.get_viewport_width() * 0.5) / self.get_zoom(),
54 (screen.get_y() - self.get_viewport_height() * 0.5) / self.get_zoom(),
55 );
56 let rotated: Vector2D = relative.rotated(self.get_rotation());
57 rotated + self.get_position()
58 }
59
60 /// Moves the camera position by the given offset.
61 ///
62 /// # Arguments
63 ///
64 /// - `Vector2D` - The translation offset in world space.
65 pub fn translate(&mut self, offset: Vector2D) {
66 self.set_position(self.get_position() + offset);
67 }
68
69 /// Adjusts the zoom by the given factor, clamped to a minimum of `EPSILON`.
70 ///
71 /// # Arguments
72 ///
73 /// - `f64` - The zoom multiplier.
74 pub fn zoom_by(&mut self, factor: f64) {
75 self.set_zoom((self.get_zoom() * factor).max(EPSILON));
76 }
77}
78
79/// Implements `Default` for `Camera2D` as a camera at the origin with 800x600 viewport.
80impl Default for Camera2D {
81 /// Constructs a default [`Camera2D`] value.
82 ///
83 /// # Returns
84 ///
85 /// - `Camera2D` - A default-constructed instance with the documented initial state.
86 fn default() -> Camera2D {
87 Camera2D::create(800.0, 600.0)
88 }
89}
90
91/// Implements static font and color utility methods for `CanvasRenderer`.
92impl CanvasRenderer {
93 /// Builds a CSS font string from font size and family.
94 ///
95 /// # Arguments
96 ///
97 /// - `f64` - The font size in pixels.
98 /// - `F` - The font family name.
99 ///
100 /// # Returns
101 ///
102 /// - `String` - The CSS font string (e.g., `"16px sans-serif"`).
103 pub fn font<F>(size: f64, family: F) -> String
104 where
105 F: AsRef<str>,
106 {
107 let family: &str = family.as_ref();
108 format!("{size}px {family}")
109 }
110
111 /// Creates a default font string using the default font size and family.
112 ///
113 /// # Returns
114 ///
115 /// - `String` - The default CSS font string.
116 pub fn default_font() -> String {
117 Self::font(RENDERER_DEFAULT_FONT_SIZE, RENDERER_DEFAULT_FONT_FAMILY)
118 }
119
120 /// Enables high-quality anti-aliasing on an arbitrary canvas 2D context.
121 ///
122 /// Applies the `High` rendering quality preset via `apply_quality`,
123 /// which sets `imageSmoothingEnabled`, `imageSmoothingQuality = "high"`,
124 /// and `textRendering = "geometricPrecision"` on the given context.
125 ///
126 /// Use this static helper when you manage your own `CanvasRenderingContext2d`
127 /// and don't hold a `CanvasRenderer` instance. For instances, call
128 /// `renderer.enable_smoothing()` instead.
129 ///
130 /// # Arguments
131 ///
132 /// - `&CanvasRenderingContext2d` - The canvas context to configure.
133 pub fn enable_smoothing_on(context: &CanvasRenderingContext2d) {
134 Self::apply_quality(context, RenderQuality::High);
135 }
136
137 /// Detects the host device pixel ratio (HiDPI scale factor) via reflection.
138 ///
139 /// Reads `window.devicePixelRatio` using `Reflect::get` because the
140 /// `web-sys` `Window` features currently in use do not expose a native
141 /// getter for this property. Falls back to
142 /// `RENDERER_DEFAULT_DEVICE_PIXEL_RATIO` (1.0) when the global window or
143 /// the value is missing, not a finite number, or below 1.0.
144 ///
145 /// # Returns
146 ///
147 /// - `f64` - The detected device pixel ratio (clamped to `>= 1.0`).
148 pub fn detect_dpr() -> f64 {
149 let Some(window_value) = window() else {
150 return RENDERER_DEFAULT_DEVICE_PIXEL_RATIO;
151 };
152 let raw: Option<f64> = Reflect::get(
153 window_value.as_ref(),
154 &JsValue::from_str(RENDERER_PROPERTY_DEVICE_PIXEL_RATIO),
155 )
156 .ok()
157 .and_then(|value: JsValue| value.as_f64());
158 raw.filter(|value: &f64| value.is_finite() && *value >= 1.0)
159 .unwrap_or(RENDERER_DEFAULT_DEVICE_PIXEL_RATIO)
160 }
161
162 /// Applies the given `RenderQuality` preset to an arbitrary canvas context.
163 ///
164 /// Sets `imageSmoothingEnabled`, `imageSmoothingQuality`, and
165 /// `textRendering` according to the supplied quality. `Low` disables
166 /// smoothing (intended for use with CSS `image-rendering: pixelated`),
167 /// `Medium` and `High` enable it with the matching quality level.
168 ///
169 /// # Arguments
170 ///
171 /// - `&CanvasRenderingContext2d` - The target context.
172 /// - `RenderQuality` - The quality preset to apply.
173 pub(crate) fn apply_quality(context: &CanvasRenderingContext2d, quality: RenderQuality) {
174 let smoothing_enabled: bool = !matches!(quality, RenderQuality::Low);
175 context.set_image_smoothing_enabled(smoothing_enabled);
176 let quality_value: &str = match quality {
177 RenderQuality::Low => RENDERER_IMAGE_SMOOTHING_QUALITY_LOW,
178 RenderQuality::Medium => RENDERER_IMAGE_SMOOTHING_QUALITY_MEDIUM,
179 RenderQuality::High => RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH,
180 };
181 let _: Result<bool, JsValue> = Reflect::set(
182 context,
183 &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
184 &JsValue::from_str(quality_value),
185 );
186 let _: Result<bool, JsValue> = Reflect::set(
187 context,
188 &JsValue::from_str(RENDERER_PROPERTY_TEXT_RENDERING),
189 &JsValue::from_str(RENDERER_TEXT_RENDERING_GEOMETRIC_PRECISION),
190 );
191 }
192}
193
194/// Implements static CSS conversion for `Color`.
195impl Color {
196 /// Converts a `Color` to a CSS `rgba()` string suitable for canvas fill or stroke styles.
197 ///
198 /// # Arguments
199 ///
200 /// - `&Color` - The color to convert.
201 ///
202 /// # Returns
203 ///
204 /// - `String` - The CSS `rgba()` color string.
205 pub fn to_css(color: &Color) -> String {
206 color.to_css_rgba()
207 }
208}
209
210/// Implements drawing and camera management methods for `CanvasRenderer`.
211/// Implements recording and replay for `DrawList`.
212impl DrawList {
213 /// Creates an empty draw list.
214 ///
215 /// # Returns
216 ///
217 /// - `DrawList` - The new empty draw list.
218 pub fn create() -> DrawList {
219 DrawList::new(Vec::new())
220 }
221
222 /// Returns whether the list contains no commands.
223 ///
224 /// # Returns
225 ///
226 /// - `bool` - `true` if there are no recorded commands.
227 pub fn is_empty(&self) -> bool {
228 self.get_commands().is_empty()
229 }
230
231 /// Returns the number of recorded commands.
232 ///
233 /// # Returns
234 ///
235 /// - `usize` - The command count.
236 pub fn len(&self) -> usize {
237 self.get_commands().len()
238 }
239
240 /// Returns the recorded commands as a slice for replay iteration.
241 ///
242 /// # Returns
243 ///
244 /// - `&[DrawCommand]` - The commands in the order they were recorded.
245 pub fn commands(&self) -> &[DrawCommand] {
246 self.get_commands().as_slice()
247 }
248
249 /// Removes all recorded commands, keeping the allocated capacity for reuse
250 /// on the next frame.
251 pub fn clear(&mut self) {
252 self.get_mut_commands().clear();
253 }
254
255 /// Records a fill-rectangle command.
256 ///
257 /// # Arguments
258 ///
259 /// - `Vector2D` - 2D vector (`Vector2D`).
260 /// - `f64` - A 64-bit float (`f64`).
261 /// - `Color` - A `Color` parameter.
262 pub fn fill_rect(&mut self, position: Vector2D, width: f64, height: f64, color: Color) {
263 self.get_mut_commands().push(DrawCommand::FillRect {
264 position,
265 width,
266 height,
267 color,
268 });
269 }
270
271 /// Records a stroke-rectangle command.
272 ///
273 /// # Arguments
274 ///
275 /// - `Vector2D` - 2D vector (`Vector2D`).
276 /// - `f64` - A 64-bit float (`f64`).
277 /// - `Color` - A `Color` parameter.
278 pub fn stroke_rect(
279 &mut self,
280 position: Vector2D,
281 width: f64,
282 height: f64,
283 color: Color,
284 line_width: f64,
285 ) {
286 self.get_mut_commands().push(DrawCommand::StrokeRect {
287 position,
288 width,
289 height,
290 color,
291 line_width,
292 });
293 }
294
295 /// Records a fill-circle command.
296 ///
297 /// # Arguments
298 ///
299 /// - `Vector2D` - 2D vector (`Vector2D`).
300 /// - `f64` - A 64-bit float (`f64`).
301 /// - `Color` - A `Color` parameter.
302 pub fn fill_circle(&mut self, center: Vector2D, radius: f64, color: Color) {
303 self.get_mut_commands().push(DrawCommand::FillCircle {
304 center,
305 radius,
306 color,
307 });
308 }
309
310 /// Records a stroke-circle command.
311 ///
312 /// # Arguments
313 ///
314 /// - `Vector2D` - 2D vector (`Vector2D`).
315 /// - `f64` - A 64-bit float (`f64`).
316 /// - `Color` - A `Color` parameter.
317 pub fn stroke_circle(&mut self, center: Vector2D, radius: f64, color: Color, line_width: f64) {
318 self.get_mut_commands().push(DrawCommand::StrokeCircle {
319 center,
320 radius,
321 color,
322 line_width,
323 });
324 }
325
326 /// Records a line-segment command.
327 ///
328 /// # Arguments
329 ///
330 /// - `Vector2D` - 2D vector (`Vector2D`).
331 /// - `Color` - A `Color` parameter.
332 /// - `f64` - A 64-bit float (`f64`).
333 pub fn draw_line(&mut self, start: Vector2D, end: Vector2D, color: Color, line_width: f64) {
334 self.get_mut_commands().push(DrawCommand::Line {
335 start,
336 end,
337 color,
338 line_width,
339 });
340 }
341
342 /// Records a fill-text command.
343 ///
344 /// # Arguments
345 ///
346 /// - `T` - A generic type parameter.
347 /// - `Vector2D` - 2D vector (`Vector2D`).
348 /// - `Color` - A `Color` parameter.
349 /// - `F` - A generic type parameter.
350 pub fn fill_text<T, F>(&mut self, text: T, position: Vector2D, color: Color, font: F)
351 where
352 T: AsRef<str>,
353 F: AsRef<str>,
354 {
355 self.get_mut_commands().push(DrawCommand::FillText {
356 text: text.as_ref().to_string(),
357 position,
358 color,
359 font: font.as_ref().to_string(),
360 });
361 }
362
363 /// Records a transformed sprite draw command.
364 ///
365 /// # Arguments
366 ///
367 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
368 /// - `Rect` - A `Rect` parameter.
369 /// - `Transform2D` - A `Transform2D` parameter.
370 pub fn draw_sprite(&mut self, image: &HtmlImageElement, source: Rect, transform: Transform2D) {
371 self.get_mut_commands().push(DrawCommand::DrawSprite {
372 image: image.clone(),
373 source,
374 transform,
375 });
376 }
377
378 /// Records an image sub-region draw command (no rotation).
379 ///
380 /// # Arguments
381 ///
382 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
383 /// - `Rect` - A `Rect` parameter.
384 /// - `Vector2D` - 2D vector (`Vector2D`).
385 /// - `f64` - A 64-bit float (`f64`).
386 pub fn draw_image_rect(
387 &mut self,
388 image: &HtmlImageElement,
389 source: Rect,
390 dest_position: Vector2D,
391 dest_width: f64,
392 dest_height: f64,
393 ) {
394 self.get_mut_commands().push(DrawCommand::DrawImageRect {
395 image: image.clone(),
396 source,
397 dest_position,
398 dest_width,
399 dest_height,
400 });
401 }
402
403 /// Records a global-alpha state change.
404 ///
405 /// # Arguments
406 ///
407 /// - `f64` - A 64-bit float (`f64`).
408 pub fn set_global_alpha(&mut self, alpha: f64) {
409 self.get_mut_commands()
410 .push(DrawCommand::SetGlobalAlpha { alpha });
411 }
412
413 /// Records a blend-mode state change.
414 ///
415 /// # Arguments
416 ///
417 /// - `BlendMode` - A `BlendMode` parameter.
418 pub fn set_blend_mode(&mut self, mode: BlendMode) {
419 self.get_mut_commands()
420 .push(DrawCommand::SetBlendMode { mode });
421 }
422}
423
424/// Inherent implementation of [`CanvasRenderer`].
425impl CanvasRenderer {
426 /// Creates a new renderer from a canvas element selector and viewport dimensions.
427 ///
428 /// # Arguments
429 ///
430 /// - `S` - The CSS selector for the canvas element.
431 /// - `f64` - The viewport width.
432 ///
433 /// # Returns
434 ///
435 /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas was not found.
436 pub fn from_selector<S>(
437 canvas_selector: S,
438 viewport_width: f64,
439 viewport_height: f64,
440 ) -> Option<CanvasRenderer>
441 where
442 S: AsRef<str>,
443 {
444 let window_value: Window = window()?;
445 let document_value: Document = window_value.document()?;
446 let element: Element = document_value
447 .query_selector(canvas_selector.as_ref())
448 .ok()
449 .flatten()?;
450 let canvas_element: HtmlCanvasElement = element.unchecked_into();
451 let context_object: Object = canvas_element
452 .get_context(RENDERER_CONTEXT_TYPE_2D)
453 .ok()
454 .flatten()?;
455 let context: CanvasRenderingContext2d = context_object.unchecked_into();
456 let renderer: CanvasRenderer = CanvasRenderer::new(
457 context,
458 Camera2D::create(viewport_width, viewport_height),
459 RenderQuality::default(),
460 );
461 renderer.enable_smoothing();
462 Some(renderer)
463 }
464
465 /// Enables high-quality anti-aliasing on the canvas context by setting
466 /// `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`.
467 ///
468 /// Applies the active `quality` preset via the shared `apply_quality`
469 /// helper so that all smoothing-related settings are kept in sync.
470 pub(crate) fn enable_smoothing(&self) {
471 Self::apply_quality(self.get_context(), self.get_quality());
472 }
473
474 /// Clears the entire canvas viewport.
475 pub fn clear(&self) {
476 self.get_context().clear_rect(
477 0.0,
478 0.0,
479 self.get_camera().get_viewport_width(),
480 self.get_camera().get_viewport_height(),
481 );
482 }
483
484 /// Clears the canvas and fills it with the given CSS color string.
485 ///
486 /// # Arguments
487 ///
488 /// - `C` - The CSS color string (e.g., `"#000000"`).
489 pub fn clear_color<C>(&self, color: C)
490 where
491 C: AsRef<str>,
492 {
493 self.get_context().set_fill_style_str(color.as_ref());
494 self.get_context().fill_rect(
495 0.0,
496 0.0,
497 self.get_camera().get_viewport_width(),
498 self.get_camera().get_viewport_height(),
499 );
500 }
501
502 /// Saves the current canvas state (transform, styles) onto the state stack.
503 pub fn save(&self) {
504 self.get_context().save();
505 }
506
507 /// Restores the most recently saved canvas state.
508 pub fn restore(&self) {
509 self.get_context().restore();
510 }
511
512 /// Replays a recorded `DrawList` onto this renderer's canvas.
513 ///
514 /// Convenience wrapper around `replay_context` using this renderer's context.
515 ///
516 /// # Arguments
517 ///
518 /// - `&DrawList` - The recorded commands to replay.
519 pub fn replay(&self, list: &DrawList) {
520 Self::replay_context(self.get_context(), list);
521 }
522
523 /// Replays a recorded `DrawList` onto an arbitrary canvas 2D context in a
524 /// single batched pass.
525 ///
526 /// Consecutive same-style shapes are merged into one path (one `begin_path`
527 /// plus one `fill`/`stroke` per style run), fill/stroke colors and line
528 /// widths are only re-applied when they change, and sprites are drawn with a
529 /// single `set_transform` rather than a save/restore pair. This collapses
530 /// the per-shape canvas state churn of immediate-mode drawing.
531 ///
532 /// The canvas transform and global alpha are reset to identity / 1.0 when
533 /// replay finishes, so callers can sandwich the call between
534 /// `save()`/`apply_camera()` and `restore()` without leaking state.
535 ///
536 /// # Arguments
537 ///
538 /// - `&CanvasRenderingContext2d` - The target canvas 2D context.
539 /// - `&DrawList` - The recorded commands to replay.
540 pub(crate) fn replay_context(context: &CanvasRenderingContext2d, list: &DrawList) {
541 let mut current_fill: Option<Color> = None;
542 let mut current_stroke: Option<Color> = None;
543 let mut current_line_width: f64 = f64::NAN;
544 // Reused scratch for `write_css_rgba` — avoids one `Color::to_css`
545 // String allocation per style change during replay.
546 let mut css_buf: String = String::new();
547 // Whether a same-style path run is currently open.
548 let mut run_open: bool = false;
549 let mut run_is_fill: bool = true;
550 let mut run_key: Option<(u8, Color, f64)> = None;
551 /// Computes the batching key for a [`DrawCommand`].
552 ///
553 /// # Arguments
554 ///
555 /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
556 ///
557 /// # Returns
558 ///
559 /// - `Option<(u8, Color, f64)>` - `Some(...)` on success, `None` otherwise.
560 fn batch_key(command: &DrawCommand) -> Option<(u8, Color, f64)> {
561 match command {
562 DrawCommand::FillRect { color, .. } | DrawCommand::FillCircle { color, .. } => {
563 Some((0, *color, 0.0))
564 }
565 DrawCommand::StrokeRect {
566 color, line_width, ..
567 }
568 | DrawCommand::StrokeCircle {
569 color, line_width, ..
570 }
571 | DrawCommand::Line {
572 color, line_width, ..
573 } => Some((1, *color, *line_width)),
574 _ => None,
575 }
576 }
577 /// Emits the geometry for the supplied [`DrawCommand`] into the canvas context.
578 ///
579 /// # Arguments
580 ///
581 /// - `&CanvasRenderingContext2d` - Shared reference to a `CanvasRenderingContext2d`.
582 /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
583 fn emit_geometry(context: &CanvasRenderingContext2d, command: &DrawCommand) {
584 match command {
585 DrawCommand::FillRect {
586 position,
587 width,
588 height,
589 ..
590 }
591 | DrawCommand::StrokeRect {
592 position,
593 width,
594 height,
595 ..
596 } => {
597 context.rect(position.get_x(), position.get_y(), *width, *height);
598 }
599 DrawCommand::FillCircle { center, radius, .. }
600 | DrawCommand::StrokeCircle { center, radius, .. } => {
601 context.move_to(center.get_x() + radius, center.get_y());
602 let _: Result<(), JsValue> =
603 context.arc(center.get_x(), center.get_y(), *radius, 0.0, TWO_PI);
604 }
605 DrawCommand::Line { start, end, .. } => {
606 context.move_to(start.get_x(), start.get_y());
607 context.line_to(end.get_x(), end.get_y());
608 }
609 _ => {}
610 }
611 }
612 for command in list.commands() {
613 let key: Option<(u8, Color, f64)> = batch_key(command);
614 // Close the open run if this command breaks it or starts a new style.
615 if run_open && key != run_key {
616 if run_is_fill {
617 context.fill();
618 } else {
619 context.stroke();
620 }
621 run_open = false;
622 }
623 if let Some(current_key) = key {
624 // Begin (or continue) a same-style path run.
625 if !run_open {
626 let (kind, color, line_width) = current_key;
627 if kind == 0 {
628 if current_fill != Some(color) {
629 css_buf.clear();
630 color.write_css_rgba(&mut css_buf);
631 context.set_fill_style_str(&css_buf);
632 current_fill = Some(color);
633 }
634 run_is_fill = true;
635 } else {
636 if current_stroke != Some(color) {
637 css_buf.clear();
638 color.write_css_rgba(&mut css_buf);
639 context.set_stroke_style_str(&css_buf);
640 current_stroke = Some(color);
641 }
642 if current_line_width != line_width {
643 context.set_line_width(line_width);
644 current_line_width = line_width;
645 }
646 run_is_fill = false;
647 }
648 context.begin_path();
649 run_open = true;
650 run_key = Some(current_key);
651 }
652 emit_geometry(context, command);
653 continue;
654 }
655 // Non-batchable command: draw it immediately.
656 match command {
657 DrawCommand::FillText {
658 text,
659 position,
660 color,
661 font,
662 } => {
663 if current_fill != Some(*color) {
664 css_buf.clear();
665 color.write_css_rgba(&mut css_buf);
666 context.set_fill_style_str(&css_buf);
667 current_fill = Some(*color);
668 }
669 context.set_font(font);
670 let _: Result<(), JsValue> =
671 context.fill_text(text, position.get_x(), position.get_y());
672 }
673 DrawCommand::DrawSprite {
674 image,
675 source,
676 transform,
677 } => {
678 draw_sprite_immediate(context, image, source, transform);
679 }
680 DrawCommand::DrawImageRect {
681 image,
682 source,
683 dest_position,
684 dest_width,
685 dest_height,
686 } => {
687 let _: Result<(), JsValue> = context
688 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
689 image,
690 source.get_x(),
691 source.get_y(),
692 source.get_width(),
693 source.get_height(),
694 dest_position.get_x(),
695 dest_position.get_y(),
696 *dest_width,
697 *dest_height,
698 );
699 }
700 DrawCommand::SetGlobalAlpha { alpha } => {
701 context.set_global_alpha(Numeric::clamp(*alpha, 0.0, 1.0));
702 }
703 DrawCommand::SetBlendMode { mode } => {
704 let _: Result<(), JsValue> =
705 context.set_global_composite_operation(mode.to_css());
706 }
707 _ => {}
708 }
709 }
710 // Flush any trailing open run.
711 if run_open {
712 if run_is_fill {
713 context.fill();
714 } else {
715 context.stroke();
716 }
717 }
718 let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
719 context.set_global_alpha(1.0);
720 }
721
722 /// Applies the camera transform to the canvas context.
723 ///
724 /// Translates to the screen center, applies zoom and rotation,
725 /// then offsets by the negative camera position.
726 pub fn apply_camera(&self) {
727 let camera: Camera2D = self.get_camera();
728 let _: Result<(), JsValue> = self.get_context().translate(
729 camera.get_viewport_width() * 0.5,
730 camera.get_viewport_height() * 0.5,
731 );
732 let _: Result<(), JsValue> = self
733 .get_context()
734 .scale(camera.get_zoom(), camera.get_zoom());
735 let _: Result<(), JsValue> = self.get_context().rotate(camera.get_rotation());
736 let _: Result<(), JsValue> = self.get_context().translate(
737 -camera.get_position().get_x(),
738 -camera.get_position().get_y(),
739 );
740 }
741
742 /// Sets the fill color for subsequent fill operations.
743 ///
744 /// # Arguments
745 ///
746 /// - `C` - The CSS color string.
747 pub fn set_fill_color<C>(&self, color: C)
748 where
749 C: AsRef<str>,
750 {
751 self.get_context().set_fill_style_str(color.as_ref());
752 }
753
754 /// Sets the stroke color for subsequent stroke operations.
755 ///
756 /// # Arguments
757 ///
758 /// - `C` - The CSS color string.
759 pub fn set_stroke_color<C>(&self, color: C)
760 where
761 C: AsRef<str>,
762 {
763 self.get_context().set_stroke_style_str(color.as_ref());
764 }
765
766 /// Sets the line width for subsequent stroke operations.
767 ///
768 /// # Arguments
769 ///
770 /// - `f64` - The line width in pixels.
771 pub fn set_line_width(&self, width: f64) {
772 self.get_context().set_line_width(width);
773 }
774
775 /// Sets the global alpha (opacity) for all subsequent drawing operations.
776 ///
777 /// # Arguments
778 ///
779 /// - `f64` - The alpha value in the range 0.0 to 1.0.
780 pub fn set_global_alpha(&self, alpha: f64) {
781 self.get_context()
782 .set_global_alpha(Numeric::clamp(alpha, 0.0, 1.0));
783 }
784
785 /// Fills a rectangle at the given world-space position and dimensions.
786 ///
787 /// # Arguments
788 ///
789 /// - `Vector2D` - The top-left position in world space.
790 /// - `f64` - The width.
791 pub fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
792 self.get_context()
793 .fill_rect(position.get_x(), position.get_y(), width, height);
794 }
795
796 /// Strokes the outline of a rectangle at the given world-space position and dimensions.
797 ///
798 /// # Arguments
799 ///
800 /// - `Vector2D` - The top-left position in world space.
801 /// - `f64` - The width.
802 pub fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
803 self.get_context()
804 .stroke_rect(position.get_x(), position.get_y(), width, height);
805 }
806
807 /// Fills a circle at the given world-space center with the specified radius.
808 ///
809 /// # Arguments
810 ///
811 /// - `Vector2D` - The center in world space.
812 /// - `f64` - The radius.
813 pub fn fill_circle(&self, center: Vector2D, radius: f64) {
814 self.get_context().begin_path();
815 self.get_context()
816 .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
817 .unwrap_or(());
818 self.get_context().fill();
819 }
820
821 /// Strokes the outline of a circle at the given world-space center.
822 ///
823 /// # Arguments
824 ///
825 /// - `Vector2D` - The center in world space.
826 /// - `f64` - The radius.
827 pub fn stroke_circle(&self, center: Vector2D, radius: f64) {
828 self.get_context().begin_path();
829 self.get_context()
830 .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
831 .unwrap_or(());
832 self.get_context().stroke();
833 }
834
835 /// Draws a line segment between two world-space points.
836 ///
837 /// # Arguments
838 ///
839 /// - `Vector2D` - The start point.
840 pub fn draw_line(&self, start: Vector2D, end: Vector2D) {
841 self.get_context().begin_path();
842 self.get_context().move_to(start.get_x(), start.get_y());
843 self.get_context().line_to(end.get_x(), end.get_y());
844 self.get_context().stroke();
845 }
846
847 /// Fills text at the given world-space position.
848 ///
849 /// # Arguments
850 ///
851 /// - `T` - The text to draw.
852 /// - `Vector2D` - The position in world space.
853 pub fn fill_text<T>(&self, text: T, position: Vector2D)
854 where
855 T: AsRef<str>,
856 {
857 self.get_context()
858 .fill_text(text.as_ref(), position.get_x(), position.get_y())
859 .unwrap_or(());
860 }
861
862 /// Sets the font for subsequent text rendering.
863 ///
864 /// # Arguments
865 ///
866 /// - `F` - The CSS font string (e.g., `"16px sans-serif"`).
867 pub fn set_font<F>(&self, font: F)
868 where
869 F: AsRef<str>,
870 {
871 self.get_context().set_font(font.as_ref());
872 }
873
874 /// Draws an image element at the given world-space position and dimensions.
875 ///
876 /// # Arguments
877 ///
878 /// - `&HtmlImageElement` - The image element to draw.
879 /// - `Vector2D` - The top-left position in world space.
880 /// - `f64` - The destination width.
881 pub fn draw_image(
882 &self,
883 image: &HtmlImageElement,
884 position: Vector2D,
885 width: f64,
886 height: f64,
887 ) {
888 let _: Result<(), JsValue> = self
889 .get_context()
890 .draw_image_with_html_image_element_and_dw_and_dh(
891 image,
892 position.get_x(),
893 position.get_y(),
894 width,
895 height,
896 );
897 }
898
899 /// Draws a sub-region of an image element at the given world-space position.
900 ///
901 /// # Arguments
902 ///
903 /// - `&HtmlImageElement` - The image element to draw.
904 /// - `Rect` - The source rectangle within the image.
905 /// - `Vector2D` - The destination top-left position in world space.
906 /// - `f64` - The destination width.
907 pub fn draw_image_rect(
908 &self,
909 image: &HtmlImageElement,
910 source: Rect,
911 dest_position: Vector2D,
912 dest_width: f64,
913 dest_height: f64,
914 ) {
915 let _: Result<(), JsValue> = self
916 .get_context()
917 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
918 image,
919 source.get_x(),
920 source.get_y(),
921 source.get_width(),
922 source.get_height(),
923 dest_position.get_x(),
924 dest_position.get_y(),
925 dest_width,
926 dest_height,
927 );
928 }
929}
930
931/// Implements 3D camera transformation and projection methods for `Camera3D`.
932impl Camera3D {
933 /// Creates a new 3D camera at the given position looking at the target.
934 ///
935 /// # Arguments
936 ///
937 /// - `Vector3D` - The eye position.
938 /// - `f64` - The viewport width.
939 ///
940 /// # Returns
941 ///
942 /// - `Camera3D` - The new camera.
943 pub fn create(
944 position: Vector3D,
945 target: Vector3D,
946 viewport_width: f64,
947 viewport_height: f64,
948 ) -> Camera3D {
949 let mut camera: Camera3D = Camera3D::new(position, target, viewport_width, viewport_height);
950 camera.set_up(Vector3D::up());
951 camera.set_fov(DEFAULT_CAMERA_FOV);
952 camera.set_near(DEFAULT_CAMERA_NEAR);
953 camera.set_far(DEFAULT_CAMERA_FAR);
954 camera
955 }
956
957 /// Returns the aspect ratio (width / height).
958 ///
959 /// # Returns
960 ///
961 /// - `f64` - The aspect ratio.
962 pub fn aspect(&self) -> f64 {
963 if self.get_viewport_height() < EPSILON {
964 return 1.0;
965 }
966 self.get_viewport_width() / self.get_viewport_height()
967 }
968
969 /// Returns the forward direction (from position to target, normalized).
970 ///
971 /// # Returns
972 ///
973 /// - `Vector3D` - The forward direction.
974 pub fn forward(&self) -> Vector3D {
975 (self.get_target() - self.get_position()).normalized()
976 }
977
978 /// Returns the right direction (cross product of forward and up).
979 ///
980 /// # Returns
981 ///
982 /// - `Vector3D` - The right direction.
983 pub fn right(&self) -> Vector3D {
984 self.forward().cross(self.get_up()).normalized()
985 }
986
987 /// Returns the view matrix for this camera.
988 ///
989 /// # Returns
990 ///
991 /// - `Matrix4x4` - The view matrix.
992 pub fn view_matrix(&self) -> Matrix4x4 {
993 Matrix4x4::look_at(self.get_position(), self.get_target(), self.get_up())
994 }
995
996 /// Returns the perspective projection matrix for this camera.
997 ///
998 /// # Returns
999 ///
1000 /// - `Matrix4x4` - The projection matrix.
1001 pub(crate) fn projection_matrix(&self) -> Matrix4x4 {
1002 Matrix4x4::perspective(
1003 self.get_fov(),
1004 self.aspect(),
1005 self.get_near(),
1006 self.get_far(),
1007 )
1008 }
1009
1010 /// Returns the combined view-projection matrix.
1011 ///
1012 /// # Returns
1013 ///
1014 /// - `Matrix4x4` - The view-projection matrix.
1015 pub fn view_proj_matrix(&self) -> Matrix4x4 {
1016 self.projection_matrix().multiply(self.view_matrix())
1017 }
1018
1019 /// Converts a 3D world-space point to screen-space (NDC) coordinates.
1020 ///
1021 /// # Arguments
1022 ///
1023 /// - `Vector3D` - The world-space point.
1024 ///
1025 /// # Returns
1026 ///
1027 /// - `Vector3D` - The screen-space point where x and y are in [0, 1] and z is the depth.
1028 pub fn world_to_screen(&self, world: Vector3D) -> Vector3D {
1029 let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1030 Vector3D::new(
1031 (clip.get_x() + 1.0) * 0.5 * self.get_viewport_width(),
1032 (1.0 - clip.get_y()) * 0.5 * self.get_viewport_height(),
1033 clip.get_z(),
1034 )
1035 }
1036
1037 /// Projects a world-space point and returns whether it is within the camera frustum.
1038 ///
1039 /// # Arguments
1040 ///
1041 /// - `Vector3D` - The world-space point.
1042 ///
1043 /// # Returns
1044 ///
1045 /// - `bool` - True if the point is within the frustum.
1046 pub fn in_frustum(&self, world: Vector3D) -> bool {
1047 let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1048 clip.get_x() >= -1.0
1049 && clip.get_x() <= 1.0
1050 && clip.get_y() >= -1.0
1051 && clip.get_y() <= 1.0
1052 && clip.get_z() >= -1.0
1053 && clip.get_z() <= 1.0
1054 }
1055
1056 /// Moves the camera position by the given offset, keeping the target offset by the same amount.
1057 ///
1058 /// # Arguments
1059 ///
1060 /// - `Vector3D` - The translation offset.
1061 pub fn translate(&mut self, offset: Vector3D) {
1062 self.set_position(self.get_position() + offset);
1063 self.set_target(self.get_target() + offset);
1064 }
1065
1066 /// Moves the camera position towards the target by the given distance.
1067 ///
1068 /// # Arguments
1069 ///
1070 /// - `f64` - The distance to zoom in (positive) or out (negative).
1071 pub fn zoom(&mut self, distance: f64) {
1072 let direction: Vector3D = self.forward();
1073 self.set_position(self.get_position() + direction.scaled(distance));
1074 }
1075
1076 /// Orbits the camera around the target by the given yaw and pitch angles.
1077 ///
1078 /// # Arguments
1079 ///
1080 /// - `f64` - The yaw delta in radians (horizontal rotation).
1081 pub fn orbit(&mut self, yaw_delta: f64, pitch_delta: f64) {
1082 let offset: Vector3D = self.get_position() - self.get_target();
1083 let current_distance: f64 = offset.magnitude();
1084 let current_yaw: f64 = offset.get_x().atan2(offset.get_z());
1085 let horizontal_dist: f64 =
1086 (offset.get_x() * offset.get_x() + offset.get_z() * offset.get_z()).sqrt();
1087 let current_pitch: f64 = (offset.get_y() / horizontal_dist.max(EPSILON)).asin();
1088 let new_yaw: f64 = current_yaw + yaw_delta;
1089 let new_pitch: f64 = Numeric::clamp(
1090 current_pitch + pitch_delta,
1091 -HALF_PI + EPSILON,
1092 HALF_PI - EPSILON,
1093 );
1094 let cos_pitch: f64 = new_pitch.cos();
1095 self.set_position(
1096 self.get_target()
1097 + Vector3D::new(
1098 new_yaw.sin() * cos_pitch * current_distance,
1099 new_pitch.sin() * current_distance,
1100 new_yaw.cos() * cos_pitch * current_distance,
1101 ),
1102 );
1103 }
1104}
1105
1106/// Implements `Default` for `Camera3D` as a camera at (0, 0, 5) looking at the origin.
1107impl Default for Camera3D {
1108 /// Constructs a default [`Camera3D`] value.
1109 ///
1110 /// # Returns
1111 ///
1112 /// - `Camera3D` - A default-constructed instance with the documented initial state.
1113 fn default() -> Camera3D {
1114 Camera3D::create(Vector3D::new(0.0, 0.0, 5.0), Vector3D::zero(), 800.0, 600.0)
1115 }
1116}
1117
1118/// Implements construction, presentation, and anti-aliasing methods for `SsaaCanvas`.
1119impl SsaaCanvas {
1120 /// Creates an `SsaaCanvas` from a CSS selector using the default scale factor.
1121 ///
1122 /// # Arguments
1123 ///
1124 /// - `S` - The CSS selector for the display canvas element.
1125 /// - `f64` - The logical display width in CSS pixels.
1126 ///
1127 /// # Returns
1128 ///
1129 /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1130 pub fn from_selector<S>(canvas_selector: S, width: f64, height: f64) -> Option<SsaaCanvas>
1131 where
1132 S: AsRef<str>,
1133 {
1134 Self::from_selector_with_scale(
1135 canvas_selector,
1136 width,
1137 height,
1138 RENDERER_DEFAULT_SSAA_SCALE_FACTOR,
1139 )
1140 }
1141
1142 /// Creates an `SsaaCanvas` from a CSS selector with a custom SSAA scale factor.
1143 ///
1144 /// The offscreen canvas is created at `width * scale_factor` by `height * scale_factor`
1145 /// pixels, and its context is pre-scaled so that drawing code uses logical coordinates.
1146 ///
1147 /// # Arguments
1148 ///
1149 /// - `S` - The CSS selector for the display canvas element.
1150 /// - `f64` - The logical display width in CSS pixels.
1151 ///
1152 /// # Returns
1153 ///
1154 /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1155 pub fn from_selector_with_scale<S>(
1156 canvas_selector: S,
1157 width: f64,
1158 height: f64,
1159 scale_factor: f64,
1160 ) -> Option<SsaaCanvas>
1161 where
1162 S: AsRef<str>,
1163 {
1164 let window_value: Window = window()?;
1165 let document_value: Document = window_value.document()?;
1166 let element: Element = document_value
1167 .query_selector(canvas_selector.as_ref())
1168 .ok()
1169 .flatten()?;
1170 let display_canvas: HtmlCanvasElement = element.unchecked_into();
1171 let device_pixel_ratio: f64 = CanvasRenderer::detect_dpr();
1172 let physical_width: u32 = (width * device_pixel_ratio).round() as u32;
1173 let physical_height: u32 = (height * device_pixel_ratio).round() as u32;
1174 display_canvas.set_width(physical_width);
1175 display_canvas.set_height(physical_height);
1176 let display_context_object: Object = display_canvas
1177 .get_context(RENDERER_CONTEXT_TYPE_2D)
1178 .ok()
1179 .flatten()?;
1180 let display_context: CanvasRenderingContext2d = display_context_object.unchecked_into();
1181 let _: Result<(), JsValue> = display_context.scale(device_pixel_ratio, device_pixel_ratio);
1182 let offscreen_canvas: HtmlCanvasElement = document_value
1183 .create_element(RENDERER_ELEMENT_CANVAS)
1184 .ok()?
1185 .unchecked_into();
1186 let scaled_width: u32 = (width * scale_factor * device_pixel_ratio).round() as u32;
1187 let scaled_height: u32 = (height * scale_factor * device_pixel_ratio).round() as u32;
1188 offscreen_canvas.set_width(scaled_width);
1189 offscreen_canvas.set_height(scaled_height);
1190 let offscreen_context_object: Object = offscreen_canvas
1191 .get_context(RENDERER_CONTEXT_TYPE_2D)
1192 .ok()
1193 .flatten()?;
1194 let offscreen_context: CanvasRenderingContext2d = offscreen_context_object.unchecked_into();
1195 let _: Result<(), JsValue> = offscreen_context.scale(
1196 scale_factor * device_pixel_ratio,
1197 scale_factor * device_pixel_ratio,
1198 );
1199 let ssaa_canvas: SsaaCanvas = SsaaCanvas::new(
1200 display_canvas,
1201 display_context,
1202 offscreen_canvas,
1203 offscreen_context,
1204 width,
1205 height,
1206 );
1207 ssaa_canvas.enable_smoothing();
1208 Some(ssaa_canvas)
1209 }
1210
1211 /// Presents the offscreen buffer onto the display canvas with high-quality downscaling.
1212 ///
1213 /// Clears the display canvas, then draws the offscreen canvas scaled
1214 /// down to the logical display size. The active `quality` preset is
1215 /// applied once at construction via `enable_smoothing` — there is
1216 /// no need to re-apply it every frame, since the preset never
1217 /// changes mid-render.
1218 ///
1219 /// #32: the previous version called `apply_quality` on every
1220 /// `present()`, costing `set_image_smoothing_enabled` + 2×Reflect
1221 /// + 4×from_str ≈ 7 JS crossings per frame for a value that was
1222 /// invariant across the entire session.
1223 pub fn present(&self) {
1224 self.get_display_context()
1225 .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1226 let _: Result<(), JsValue> = self
1227 .get_display_context()
1228 .draw_image_with_html_canvas_element_and_dw_and_dh(
1229 self.get_offscreen_canvas(),
1230 0.0,
1231 0.0,
1232 self.get_width(),
1233 self.get_height(),
1234 );
1235 }
1236
1237 /// Clears the offscreen buffer to transparent.
1238 pub fn clear(&self) {
1239 self.get_offscreen_context()
1240 .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1241 }
1242
1243 /// Clears the offscreen buffer and fills it with the given CSS color.
1244 ///
1245 /// # Arguments
1246 ///
1247 /// - `C` - The CSS color string.
1248 pub fn clear_color<C>(&self, color: C)
1249 where
1250 C: AsRef<str>,
1251 {
1252 self.get_offscreen_context()
1253 .set_fill_style_str(color.as_ref());
1254 self.get_offscreen_context()
1255 .fill_rect(0.0, 0.0, self.get_width(), self.get_height());
1256 }
1257
1258 /// Enables high-quality anti-aliasing on both the display and offscreen contexts.
1259 ///
1260 /// Applies the active `quality` preset to both contexts via the shared
1261 /// `apply_quality` helper.
1262 pub(crate) fn enable_smoothing(&self) {
1263 let quality: RenderQuality = self.get_quality();
1264 CanvasRenderer::apply_quality(self.get_display_context(), quality);
1265 CanvasRenderer::apply_quality(self.get_offscreen_context(), quality);
1266 }
1267}
1268
1269/// Implements construction and canvas gradient creation for `LinearGradient`.
1270impl LinearGradient {
1271 /// Creates a new linear gradient from two points and a list of color stops.
1272 ///
1273 /// # Arguments
1274 ///
1275 /// - `Vector2D` - The start point.
1276 /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1277 ///
1278 /// # Returns
1279 ///
1280 /// - `LinearGradient` - The new gradient.
1281 pub fn create(start: Vector2D, end: Vector2D, stops: Vec<(f64, String)>) -> LinearGradient {
1282 LinearGradient::new(start, end, stops)
1283 }
1284
1285 /// Creates a `CanvasGradient` from this gradient definition on the given context.
1286 ///
1287 /// # Arguments
1288 ///
1289 /// - `&CanvasRenderingContext2d` - The canvas context.
1290 ///
1291 /// # Returns
1292 ///
1293 /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1294 pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1295 let canvas_gradient: CanvasGradient = context.create_linear_gradient(
1296 self.get_start().get_x(),
1297 self.get_start().get_y(),
1298 self.get_end().get_x(),
1299 self.get_end().get_y(),
1300 );
1301 for (position, color) in self.get_stops() {
1302 let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1303 }
1304 Some(canvas_gradient)
1305 }
1306}
1307
1308/// Implements construction and canvas gradient creation for `RadialGradient`.
1309impl RadialGradient {
1310 /// Creates a new radial gradient from inner and outer circles and color stops.
1311 ///
1312 /// # Arguments
1313 ///
1314 /// - `Vector2D` - The inner circle center.
1315 /// - `f64` - The inner circle radius.
1316 /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1317 ///
1318 /// # Returns
1319 ///
1320 /// - `RadialGradient` - The new gradient.
1321 pub fn create(
1322 inner_center: Vector2D,
1323 inner_radius: f64,
1324 outer_center: Vector2D,
1325 outer_radius: f64,
1326 stops: Vec<(f64, String)>,
1327 ) -> RadialGradient {
1328 RadialGradient::new(
1329 inner_center,
1330 inner_radius,
1331 outer_center,
1332 outer_radius,
1333 stops,
1334 )
1335 }
1336
1337 /// Creates a `CanvasGradient` from this gradient definition on the given context.
1338 ///
1339 /// # Arguments
1340 ///
1341 /// - `&CanvasRenderingContext2d` - The canvas context.
1342 ///
1343 /// # Returns
1344 ///
1345 /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1346 pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1347 let canvas_gradient: CanvasGradient = context
1348 .create_radial_gradient(
1349 self.get_inner_center().get_x(),
1350 self.get_inner_center().get_y(),
1351 self.get_inner_radius(),
1352 self.get_outer_center().get_x(),
1353 self.get_outer_center().get_y(),
1354 self.get_outer_radius(),
1355 )
1356 .ok()?;
1357 for (position, color) in self.get_stops() {
1358 let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1359 }
1360 Some(canvas_gradient)
1361 }
1362}
1363
1364/// Implements construction methods for `ShadowConfig`.
1365impl ShadowConfig {
1366 /// Creates a shadow configuration with default values.
1367 ///
1368 /// # Returns
1369 ///
1370 /// - `ShadowConfig` - The default shadow configuration.
1371 pub fn create() -> ShadowConfig {
1372 ShadowConfig::new(
1373 RENDERER_DEFAULT_SHADOW_COLOR.to_string(),
1374 RENDERER_DEFAULT_SHADOW_BLUR,
1375 0.0,
1376 0.0,
1377 )
1378 }
1379}
1380
1381/// Implements `Default` for `ShadowConfig` with default shadow values.
1382impl Default for ShadowConfig {
1383 /// Constructs a default [`ShadowConfig`] value.
1384 ///
1385 /// # Returns
1386 ///
1387 /// - `ShadowConfig` - A default-constructed instance with the documented initial state.
1388 fn default() -> ShadowConfig {
1389 ShadowConfig::create()
1390 }
1391}
1392
1393/// Implements construction methods for `RenderLayer`.
1394impl RenderLayer {
1395 /// Creates a render layer with the given z-index and visibility.
1396 ///
1397 /// # Arguments
1398 ///
1399 /// - `i32` - The z-index determining draw order.
1400 /// - `bool` - Whether the layer is visible.
1401 ///
1402 /// # Returns
1403 ///
1404 /// - `RenderLayer` - The new render layer.
1405 pub fn create(z_index: i32, visible: bool) -> RenderLayer {
1406 RenderLayer::new(z_index, visible)
1407 }
1408
1409 /// Creates a background render layer with z-index 0 and visibility enabled.
1410 ///
1411 /// # Returns
1412 ///
1413 /// - `RenderLayer` - The background layer.
1414 pub fn background() -> RenderLayer {
1415 RenderLayer::new(RENDERER_LAYER_BACKGROUND, true)
1416 }
1417
1418 /// Creates a foreground render layer with a high z-index and visibility enabled.
1419 ///
1420 /// # Returns
1421 ///
1422 /// - `RenderLayer` - The foreground layer.
1423 pub fn foreground() -> RenderLayer {
1424 RenderLayer::new(RENDERER_LAYER_FOREGROUND, true)
1425 }
1426
1427 /// Creates a UI overlay render layer with the highest z-index and visibility enabled.
1428 ///
1429 /// # Returns
1430 ///
1431 /// - `RenderLayer` - The UI overlay layer.
1432 pub fn ui() -> RenderLayer {
1433 RenderLayer::new(RENDERER_LAYER_UI, true)
1434 }
1435}
1436
1437/// Implements blend mode, shadow, and gradient rendering methods for `CanvasRenderer`.
1438impl CanvasRenderer {
1439 /// Sets the blend mode for compositing subsequent draw operations.
1440 ///
1441 /// # Arguments
1442 ///
1443 /// - `BlendMode` - The blend mode to apply.
1444 pub fn set_blend_mode(&self, mode: BlendMode) {
1445 let _: Result<(), JsValue> = self
1446 .get_context()
1447 .set_global_composite_operation(mode.to_css());
1448 }
1449
1450 /// Applies a shadow configuration for subsequent draw operations.
1451 ///
1452 /// # Arguments
1453 ///
1454 /// - `&ShadowConfig` - The shadow configuration to apply.
1455 pub fn set_shadow(&self, config: &ShadowConfig) {
1456 self.get_context()
1457 .set_shadow_color(config.get_color().as_str());
1458 self.get_context().set_shadow_blur(config.get_blur());
1459 self.get_context()
1460 .set_shadow_offset_x(config.get_offset_x());
1461 self.get_context()
1462 .set_shadow_offset_y(config.get_offset_y());
1463 }
1464
1465 /// Clears any previously applied shadow, disabling shadow rendering.
1466 pub fn clear_shadow(&self) {
1467 self.get_context()
1468 .set_shadow_color(RENDERER_TRANSPARENT_SHADOW_COLOR);
1469 self.get_context().set_shadow_blur(0.0);
1470 self.get_context().set_shadow_offset_x(0.0);
1471 self.get_context().set_shadow_offset_y(0.0);
1472 }
1473
1474 /// Applies a linear gradient as the fill style for subsequent operations.
1475 ///
1476 /// # Arguments
1477 ///
1478 /// - `&LinearGradient` - The linear gradient to use as fill style.
1479 pub fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1480 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1481 self.get_context()
1482 .set_fill_style_canvas_gradient(&canvas_gradient);
1483 }
1484 }
1485
1486 /// Applies a radial gradient as the fill style for subsequent operations.
1487 ///
1488 /// # Arguments
1489 ///
1490 /// - `&RadialGradient` - The radial gradient to use as fill style.
1491 pub fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1492 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1493 self.get_context()
1494 .set_fill_style_canvas_gradient(&canvas_gradient);
1495 }
1496 }
1497
1498 /// Applies a linear gradient as the stroke style for subsequent operations.
1499 ///
1500 /// # Arguments
1501 ///
1502 /// - `&LinearGradient` - The linear gradient to use as stroke style.
1503 pub fn set_linear_gradient_stroke(&self, gradient: &LinearGradient) {
1504 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1505 self.get_context()
1506 .set_stroke_style_canvas_gradient(&canvas_gradient);
1507 }
1508 }
1509
1510 /// Applies a radial gradient as the stroke style for subsequent operations.
1511 ///
1512 /// # Arguments
1513 ///
1514 /// - `&RadialGradient` - The radial gradient to use as stroke style.
1515 pub fn set_radial_gradient_stroke(&self, gradient: &RadialGradient) {
1516 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1517 self.get_context()
1518 .set_stroke_style_canvas_gradient(&canvas_gradient);
1519 }
1520 }
1521}
1522
1523/// Implements the `RenderBackend` trait for `CanvasRenderer`, providing
1524/// a backend-agnostic rendering interface.
1525///
1526/// Each method forwards to the inherent `CanvasRenderer` method of the
1527/// same name, so the per-call documentation lives on the trait definition
1528/// in `engine::renderer::trait` — the inherent method is the source of
1529/// truth, this impl is the trait bridge.
1530impl RenderBackend for CanvasRenderer {
1531 /// Forwards to [`CanvasRenderer::clear`].
1532 fn clear(&self) {
1533 self.clear();
1534 }
1535
1536 /// Forwards to [`CanvasRenderer::clear_color`].
1537 ///
1538 /// # Arguments
1539 ///
1540 /// - `C` - A generic type parameter.
1541 fn clear_color<C>(&self, color: C)
1542 where
1543 C: AsRef<str>,
1544 {
1545 self.clear_color(color);
1546 }
1547
1548 /// Forwards to [`CanvasRenderer::save`].
1549 fn save(&self) {
1550 self.save();
1551 }
1552
1553 /// Forwards to [`CanvasRenderer::restore`].
1554 fn restore(&self) {
1555 self.restore();
1556 }
1557
1558 /// Forwards to [`CanvasRenderer::set_fill_color`].
1559 ///
1560 /// # Arguments
1561 ///
1562 /// - `&str` - Shared reference to a `str`.
1563 fn set_fill_color(&self, color: &str) {
1564 self.set_fill_color(color);
1565 }
1566
1567 /// Forwards to [`CanvasRenderer::set_stroke_color`].
1568 ///
1569 /// # Arguments
1570 ///
1571 /// - `&str` - Shared reference to a `str`.
1572 fn set_stroke_color(&self, color: &str) {
1573 self.set_stroke_color(color);
1574 }
1575
1576 /// Forwards to [`CanvasRenderer::set_line_width`].
1577 ///
1578 /// # Arguments
1579 ///
1580 /// - `f64` - A 64-bit float (`f64`).
1581 fn set_line_width(&self, width: f64) {
1582 self.set_line_width(width);
1583 }
1584
1585 /// Forwards to [`CanvasRenderer::set_global_alpha`].
1586 ///
1587 /// # Arguments
1588 ///
1589 /// - `f64` - A 64-bit float (`f64`).
1590 fn set_global_alpha(&self, alpha: f64) {
1591 self.set_global_alpha(alpha);
1592 }
1593
1594 /// Forwards to [`CanvasRenderer::set_blend_mode`].
1595 ///
1596 /// # Arguments
1597 ///
1598 /// - `BlendMode` - A `BlendMode` parameter.
1599 fn set_blend_mode(&self, mode: BlendMode) {
1600 self.set_blend_mode(mode);
1601 }
1602
1603 /// Forwards to [`CanvasRenderer::set_shadow`].
1604 ///
1605 /// # Arguments
1606 ///
1607 /// - `&ShadowConfig` - Shared reference to a `ShadowConfig`.
1608 fn set_shadow(&self, config: &ShadowConfig) {
1609 self.set_shadow(config);
1610 }
1611
1612 /// Forwards to [`CanvasRenderer::clear_shadow`].
1613 fn clear_shadow(&self) {
1614 self.clear_shadow();
1615 }
1616
1617 /// Forwards to [`CanvasRenderer::fill_rect`].
1618 ///
1619 /// # Arguments
1620 ///
1621 /// - `Vector2D` - 2D vector (`Vector2D`).
1622 /// - `f64` - A 64-bit float (`f64`).
1623 fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1624 self.fill_rect(position, width, height);
1625 }
1626
1627 /// Forwards to [`CanvasRenderer::stroke_rect`].
1628 ///
1629 /// # Arguments
1630 ///
1631 /// - `Vector2D` - 2D vector (`Vector2D`).
1632 /// - `f64` - A 64-bit float (`f64`).
1633 fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1634 self.stroke_rect(position, width, height);
1635 }
1636
1637 /// Forwards to [`CanvasRenderer::fill_circle`].
1638 ///
1639 /// # Arguments
1640 ///
1641 /// - `Vector2D` - 2D vector (`Vector2D`).
1642 /// - `f64` - A 64-bit float (`f64`).
1643 fn fill_circle(&self, center: Vector2D, radius: f64) {
1644 self.fill_circle(center, radius);
1645 }
1646
1647 /// Forwards to [`CanvasRenderer::stroke_circle`].
1648 ///
1649 /// # Arguments
1650 ///
1651 /// - `Vector2D` - 2D vector (`Vector2D`).
1652 /// - `f64` - A 64-bit float (`f64`).
1653 fn stroke_circle(&self, center: Vector2D, radius: f64) {
1654 self.stroke_circle(center, radius);
1655 }
1656
1657 /// Forwards to [`CanvasRenderer::draw_line`].
1658 ///
1659 /// # Arguments
1660 ///
1661 /// - `Vector2D` - 2D vector (`Vector2D`).
1662 fn draw_line(&self, start: Vector2D, end: Vector2D) {
1663 self.draw_line(start, end);
1664 }
1665
1666 /// Forwards to [`CanvasRenderer::fill_text`].
1667 ///
1668 /// # Arguments
1669 ///
1670 /// - `&str` - Shared reference to a `str`.
1671 /// - `Vector2D` - 2D vector (`Vector2D`).
1672 fn fill_text(&self, text: &str, position: Vector2D) {
1673 self.fill_text(text, position);
1674 }
1675
1676 /// Forwards to [`CanvasRenderer::set_font`].
1677 ///
1678 /// # Arguments
1679 ///
1680 /// - `&str` - Shared reference to a `str`.
1681 fn set_font(&self, font: &str) {
1682 self.set_font(font);
1683 }
1684
1685 /// Forwards to [`CanvasRenderer::draw_image`].
1686 ///
1687 /// # Arguments
1688 ///
1689 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
1690 /// - `Vector2D` - 2D vector (`Vector2D`).
1691 /// - `f64` - A 64-bit float (`f64`).
1692 fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1693 self.draw_image(image, position, width, height);
1694 }
1695
1696 /// Forwards to [`CanvasRenderer::set_linear_gradient_fill`].
1697 ///
1698 /// # Arguments
1699 ///
1700 /// - `&LinearGradient` - Shared reference to a `LinearGradient`.
1701 fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1702 self.set_linear_gradient_fill(gradient);
1703 }
1704
1705 /// Forwards to [`CanvasRenderer::set_radial_gradient_fill`].
1706 ///
1707 /// # Arguments
1708 ///
1709 /// - `&RadialGradient` - Shared reference to a `RadialGradient`.
1710 fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1711 self.set_radial_gradient_fill(gradient);
1712 }
1713}