1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use super::*;
impl CanvasRayTraceBackend {
/// Acquires the 2D context and allocates the surface's image and
/// backing array.
///
/// # Arguments
///
/// - `&HtmlCanvasElement` - The canvas to draw into.
/// - `u32` - The surface width in pixels.
/// - `u32` - The surface height in pixels.
///
/// # Returns
///
/// - `Result<Self, JsValue>` - The ready backend, or the reason the
/// browser refused a 2D context or an image.
pub fn attach(canvas: &HtmlCanvasElement, width: u32, height: u32) -> Result<Self, JsValue> {
let context: CanvasRenderingContext2d = attach_canvas_context(canvas)?;
let (image, backing): (ImageData, Uint8ClampedArray) =
allocate_canvas_surface(width, height)?;
Ok(Self {
context,
image,
backing,
image_width: width,
image_height: height,
})
}
/// The largest per-axis surface length this backend accepts.
///
/// Canvas 2D exposes no driver limit of its own: the browser
/// rejects an oversized surface through the `ImageData`
/// constructor, which is a per-call error, not a queryable
/// budget. Reporting `None` leaves that rejection to the one place
/// that can actually observe it.
///
/// # Returns
///
/// - `Option<u32>` - `None`, because no Canvas 2D limit exists to
/// enforce up front.
pub fn dimension_limit(&self) -> Option<u32> {
None
}
/// Reallocates the surface only when the size actually changed.
///
/// The new image and backing array are allocated first and swapped
/// in only once the allocation has been accepted, so a rejected
/// size leaves the previous, still renderable, surface untouched.
/// The canvas element's own width and height are the caller's to
/// commit: this backend never depends on them having changed
/// already.
///
/// # Arguments
///
/// - `u32` - The surface width in pixels.
/// - `u32` - The surface height in pixels.
///
/// # Returns
///
/// - `Result<(), JsValue>` - `Ok` once the surface is sized.
pub fn resize(&mut self, width: u32, height: u32) -> Result<(), JsValue> {
if self.get_image_width() == width && self.get_image_height() == height {
return Ok(());
}
let (image, backing): (ImageData, Uint8ClampedArray) =
allocate_canvas_surface(width, height)?;
self.set_image(image);
self.set_backing(backing);
self.set_image_width(width);
self.set_image_height(height);
Ok(())
}
/// Copies the traced RGBA bytes into the reused image and blits it.
///
/// The bytes go through the backing array's own `copy_from`, which
/// writes the JavaScript `Uint8ClampedArray` the `ImageData` was
/// constructed over. `ImageData::data` cannot be used for this:
/// its `Clamped<Vec<u8>>` is a Rust copy, so writing into it leaves
/// the image's own store untouched and the blit presents the
/// original transparent pixels.
///
/// The length is exact. A buffer that no longer matches the surface
/// is an error rather than a truncated or padded frame, because a
/// clipped upload still produces a picture that only a pixel
/// comparison can catch.
///
/// # Arguments
///
/// - `&[u8]` - The traced pixels, one RGBA byte per channel.
///
/// # Returns
///
/// - `Result<(), JsValue>` - `Ok` once the blit was accepted.
pub fn present(&mut self, pixels: &[u8]) -> Result<(), JsValue> {
let width: u32 = self.get_image_width();
let height: u32 = self.get_image_height();
if width == 0 || height == 0 {
return Err(backend_error(
ERROR_CANVAS_RELEASED,
ERROR_CANVAS_RELEASED.to_string(),
));
}
let expected: usize = match surface_rgba_length(width, height) {
Some(bytes) => bytes,
None => {
return Err(backend_error(
ERROR_DIMENSION_OVERFLOW,
format!("{width}x{height} overflows the addressable byte count"),
));
}
};
if pixels.len() != expected {
return Err(backend_error(
ERROR_CANVAS_PIXEL_LENGTH,
format!("{} bytes for {expected} byte surface", pixels.len()),
));
}
self.get_backing().copy_from(pixels);
match self.get_context().put_image_data(
self.get_image(),
CANVAS_BLIT_ORIGIN,
CANVAS_BLIT_ORIGIN,
) {
Ok(()) => Ok(()),
Err(reason) => Err(backend_error(ERROR_CANVAS_PUT, describe(&reason))),
}
}
/// Marks the surface as released.
///
/// The `ImageData`, its backing array, and the 2D context are
/// JavaScript objects owned by this struct, so they are reclaimed
/// when the adapter holding the backend is dropped; there is no
/// explicit teardown call for any of them. What release does own
/// is the surface size: zeroing it means a later `present` is
/// rejected on the length check instead of blitting a surface the
/// caller has already torn down. The 2D context belongs to the
/// canvas, so it is never destroyed here.
///
/// # Returns
///
/// - `()` - Cleanup never fails.
pub fn release(&mut self) {
self.set_image_width(0);
self.set_image_height(0);
}
}