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
//! Owned HTML5 canvas presentation through the browser PAL.
use super::{WebDocument, WebElement};
use std::io;
use wasm_bindgen::{Clamped, JsCast};
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
pub use crate::canvas_validation::{CanvasSize, RgbaFrame};
/// A browser canvas with a retained two-dimensional rendering context.
pub struct WebCanvas {
canvas: HtmlCanvasElement,
context: CanvasRenderingContext2d,
}
impl WebCanvas {
/// Wraps an existing DOM element as a canvas.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the element is not a canvas
/// or the browser cannot provide a two-dimensional context.
pub fn from_element(element: &WebElement) -> io::Result<Self> {
let canvas = element
.element
.clone()
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "DOM element is not a canvas")
})?;
let context = canvas
.get_context("2d")
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "canvas context lookup failed")
})?
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "canvas has no 2-D context")
})?
.dyn_into::<CanvasRenderingContext2d>()
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "canvas context is not 2-D")
})?;
Ok(Self { canvas, context })
}
/// Returns the canvas element's stable identifier.
#[must_use]
pub fn id(&self) -> String {
self.canvas.id()
}
/// Presents one validated RGBA8 frame at the canvas origin.
///
/// The frame remains borrowed for the duration of this call. The browser
/// owns any transfer needed by `ImageData` after the Web API boundary. A
/// matching canvas extent is retained so repeated animation frames do not
/// reset the bitmap before each upload.
///
/// # Errors
/// Returns [`io::ErrorKind::InvalidInput`] when the browser rejects the
/// canvas resize or image upload.
pub fn present(&self, frame: RgbaFrame<'_>) -> io::Result<()> {
let size = frame.size();
if !size.matches_dimensions(self.canvas.width(), self.canvas.height()) {
self.canvas.set_width(size.width());
self.canvas.set_height(size.height());
}
let image = ImageData::new_with_u8_clamped_array_and_sh(
Clamped(frame.pixels()),
size.width(),
size.height(),
)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "browser rejected RGBA image"))?;
self.context.put_image_data(&image, 0.0, 0.0).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"browser rejected canvas upload",
)
})
}
}
impl WebDocument {
/// Resolves a canvas by identifier in this document.
///
/// # Errors
/// Propagates the missing-element, element-kind and context errors from
/// [`WebCanvas::from_element`].
pub fn canvas_by_id(&self, id: &str) -> io::Result<WebCanvas> {
let element = self.get_element_by_id(id).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"canvas element identifier is absent",
)
})?;
WebCanvas::from_element(&element)
}
}