Skip to content

Commit 3cd8c7d

Browse files
authored
Add OffscreenCanvas support (#84)
1 parent 5db2452 commit 3cd8c7d

1 file changed

Lines changed: 95 additions & 15 deletions

File tree

README.md

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,14 @@ The `drawElementImage()` method draws a child of the canvas into the canvas, and
4141
Similar methods are added for 3D contexts: `WebGLRenderingContext.texElementImage2D` and `copyElementImageToTexture`.
4242

4343
### 3. The `paint` event
44-
A `paint` event is added to `canvas` and fires if the rendering of any canvas children has changed. This event fires just after intersection observer steps have run during [update-the-rendering](https://html.spec.whatwg.org/#update-the-rendering). The event contains a list of the canvas children which have changed. Because CSS transforms on canvas children are ignored for rendering, changing the transform does not cause the `paint` event to fire in the next frame.
44+
A `paint` event is added to `canvas` elements and fires if the rendering of any canvas children has changed. This event fires just after intersection observer steps have run during [update-the-rendering](https://html.spec.whatwg.org/#update-the-rendering). The event contains a list of the canvas children which have changed. Because CSS transforms on canvas children are ignored for rendering, changing the transform does not cause the `paint` event to fire in the next frame.
4545

4646
To support application patterns which update every frame, a new `requestPaint()` function is added which will cause the `paint` event to fire once, even if no children have changed (analagous to `requestAnimationFrame()`).
4747

48+
The `paint` event also fires for `OffscreenCanvas` (main thread or worker) after the canvas element children have been prepared for rendering.
49+
4850
### Synchronization
51+
4952
Browser features like hit testing, intersection observer, and accessibility rely on an element's DOM location. To ensure these work, the element's `transform` property should be updated so that the DOM location matches the drawn location.
5053

5154
<details>
@@ -62,8 +65,9 @@ Where:
6265
* $$S_{\text{css} \to \text{grid}}$$: Scaling matrix converting CSS pixels to Canvas Grid pixels.
6366
</details>
6467

65-
To assist with synchronization, `drawElementImage` returns the CSS transform which can be applied to the element to keep it's location synchronized. For 3D contexts, the `getElementTransform(element, draw_transform)` helper method is provided which returns the CSS transform, provided a general transformation matrix.
68+
To assist with synchronization, `drawElementImage()` returns the CSS transform which can be applied to the element to keep its location synchronized. For 3D contexts, the `getElementTransform(element, drawTransform)` helper method is provided which returns the CSS transform, provided a general transformation matrix.
6669

70+
The transform used to draw the element on the worker thread needs to be synced back to the DOM, and can simply be `postMessage()`'d back to the main thread.
6771

6872
### Basic Example
6973

@@ -87,43 +91,119 @@ To assist with synchronization, `drawElementImage` returns the CSS transform whi
8791
</script>
8892
```
8993

94+
### OffscreenCanvas Example
95+
96+
In this example, `OffscreenCanvas` in a worker is used. The `canvas` child elements are represented as `ElementImage` objects in the `paint` event, and are distinguished by their IDs.
97+
98+
```html
99+
<!DOCTYPE html>
100+
<canvas id="canvas" style="width: 300px; height: 200px;" layoutsubtree>
101+
<div id="label">enter your fullname:</div>
102+
<input id="input">
103+
</canvas>
104+
105+
<script>
106+
// 1. Setup worker thread.
107+
const worker = new Worker("worker.js");
108+
109+
// 2. Transfer control to the worker.
110+
const offscreen = canvas.transferControlToOffscreen();
111+
worker.postMessage({ canvas: offscreen }, [offscreen]);
112+
113+
// 3. Synchronize the element's CSS transform to match its drawn location.
114+
worker.onmessage = (data) => {
115+
document.getElementById(data.id).style.transform = data.transform.toString();
116+
};
117+
</script>
118+
```
119+
120+
`worker.js`:
121+
122+
```javascript
123+
onmessage = ({data}) => {
124+
const ctx = data.canvas.getContext('2d');
125+
data.canvas.onpaint = (event) => {
126+
const changedLabel = event.changedElements.find(item => item.id === 'label');
127+
if (changedLabel) {
128+
let transform = ctx.drawElementImage(changedLabel, 0, 0);
129+
self.postMessage({id: 'label', transform: transform});
130+
}
131+
const changedInput = event.changedElements.find(item => item.id === 'input');
132+
if (changedInput) {
133+
let transform = ctx.drawElementImage(changedInput, 0, 100);
134+
self.postMessage({id: 'input', transform: transform}); }
135+
};
136+
};
137+
```
138+
90139
### IDL changes
91140

92141
```idl
93-
interface HTMLCanvasElement {
94-
attribute boolean layoutSubtree;
142+
partial interface HTMLCanvasElement {
143+
[CEReactions, Reflect] attribute boolean layoutSubtree;
95144
96145
attribute EventHandler onpaint;
97146
98147
void requestPaint();
99148
100-
DOMMatrix getElementTransform(Element element, DOMMatrix draw_transform);
101-
}
149+
DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform);
150+
};
151+
152+
partial interface OffscreenCanvas {
153+
attribute EventHandler onpaint;
154+
155+
void requestPaint();
156+
157+
DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform);
158+
};
159+
160+
partial interface CanvasRenderingContext2D {
161+
DOMMatrix drawElementImage((Element or ElementImage) element,
162+
unrestricted double x, unrestricted double y);
163+
164+
DOMMatrix drawElementImage((Element or ElementImage) element,
165+
unrestricted double x, unrestricted double y,
166+
unrestricted double dwidth, unrestricted double dheight);
167+
};
102168
103-
interface CanvasRenderingContext2D {
104-
DOMMatrix drawElementImage(Element element, unrestricted double x, unrestricted double y);
169+
partial interface OffscreenCanvasRenderingContext2D {
170+
DOMMatrix drawElementImage((Element or ElementImage) element,
171+
unrestricted double x, unrestricted double y);
105172
106-
DOMMatrix drawElementImage(Element element, unrestricted double x, unrestricted double y,
173+
DOMMatrix drawElementImage((Element or ElementImage) element,
174+
unrestricted double x, unrestricted double y,
107175
unrestricted double dwidth, unrestricted double dheight);
108176
};
109177
110-
interface WebGLRenderingContext {
178+
partial interface WebGLRenderingContext {
111179
void texElementImage2D(GLenum target, GLint level, GLint internalformat,
112-
GLenum format, GLenum type, Element element);
180+
GLenum format, GLenum type, (Element or ElementImage) element);
113181
};
114182
115-
interface GPUQueue {
116-
void copyElementImageToTexture(Element source, GPUImageCopyTextureTagged destination);
183+
partial interface GPUQueue {
184+
void copyElementImageToTexture((Element or ElementImage) source,
185+
GPUImageCopyTextureTagged destination);
117186
}
118187
188+
[Exposed=(Window,Worker)]
119189
interface PaintEvent : Event {
120190
constructor(DOMString type, optional PaintEventInit eventInitDict);
121191
122-
readonly attribute FrozenArray<Element> changed;
192+
readonly attribute FrozenArray<Element or ElementImage> changedElements;
123193
};
124194
125195
dictionary PaintEventInit : EventInit {
126-
sequence<Element> changed = [];
196+
sequence<Element or ElementImage> changedElements = [];
197+
};
198+
199+
[Exposed=(Window,Worker)]
200+
interface ElementImage {
201+
// dimensions in device pixels
202+
readonly attribute unsigned long width;
203+
readonly attribute unsigned long height;
204+
205+
// value of `id` attribute on element, or the empty string
206+
readonly attribute DOMString id;
127207
};
128208
```
129209

0 commit comments

Comments
 (0)