gpucanvas 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -19
- package/dist/{chunk-DPCICAYZ.js → chunk-2EDYGRFR.js} +92 -14
- package/dist/chunk-2EDYGRFR.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/react.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-DPCICAYZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -29,13 +29,22 @@ The same session on both sides: 5,000 live React screens, the same camera path,
|
|
|
29
29
|
Marks from an agent, a wipe and a dissolve, the glass lens and shockwaves, all on live
|
|
30
30
|
components. Recorded in Chrome from the demo.
|
|
31
31
|
|
|
32
|
+
How it works, with the benchmarks and what Chrome's HTML-in-Canvas does differently from the
|
|
33
|
+
explainer: [gpucanvas: 5,000 Live React Screens on a WebGPU Infinite Canvas](https://berkant.me/blog/gpucanvas-webgpu-infinite-canvas).
|
|
34
|
+
|
|
32
35
|
## Try it
|
|
33
36
|
|
|
34
37
|
Open **[berkantay.github.io/gpucanvas](https://berkantay.github.io/gpucanvas/)** in Chrome 157 or
|
|
35
38
|
later. Thirty agent cursors build the first view, then the camera pulls out to all 5,000 screens.
|
|
36
39
|
|
|
37
40
|
- Click a screen to select it, double-click to go deeper, drag to move it. Double-click a text
|
|
38
|
-
field to type in it.
|
|
41
|
+
field to type in it, or switch the toolbar to Interact and use the screens like a normal page.
|
|
42
|
+
- In Interact, every button does something. Continue, Sign in and Back move the camera to the
|
|
43
|
+
next screen like a clickable prototype, Notifications and More open panels, Export saves a
|
|
44
|
+
CSV, Send adds the message and Share copies the link.
|
|
45
|
+
- The toolbar holds every setting: renderer, number of screens, Edit or Interact, what a click
|
|
46
|
+
selects, the shader effect, the agent cursors and the theme. They apply live, and the URL
|
|
47
|
+
remembers them.
|
|
39
48
|
- Pick a shader effect in the toolbar, or open one directly:
|
|
40
49
|
[glass](https://berkantay.github.io/gpucanvas/?effect=glass),
|
|
41
50
|
[shockwave](https://berkantay.github.io/gpucanvas/?effect=shockwave) (click anywhere),
|
|
@@ -43,15 +52,17 @@ later. Thirty agent cursors build the first view, then the camera pulls out to a
|
|
|
43
52
|
[glitch](https://berkantay.github.io/gpucanvas/?effect=glitch).
|
|
44
53
|
- Select a screen and press `D` to dissolve it, `W` to wipe it, or `S` to save it as a PNG.
|
|
45
54
|
- In the console, `agentView.describe()` lists what is in view, and `marks` draws on it.
|
|
46
|
-
- Press `b` to run the benchmark tour.
|
|
47
|
-
|
|
48
|
-
The GPU path needs HTML-in-Canvas, which is behind a flag:
|
|
55
|
+
- Press `b` to run the benchmark tour, `f` to fit every screen in view, `t` to switch the theme.
|
|
56
|
+
- On a phone: pinch to zoom, drag to pan, tap to use a screen.
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
The GPU path needs WebGPU and HTML-in-Canvas. Chrome 157 and later on a computer has both on by
|
|
59
|
+
default, so the demo opens on the GPU path. On older Chrome or other Chromium builds, turn it on
|
|
60
|
+
with `chrome://flags/#canvas-draw-element` and relaunch.
|
|
53
61
|
|
|
54
|
-
Without
|
|
62
|
+
Without HTML-in-Canvas the demo runs the DOM fallback with 1,000 screens and says so. Phones start
|
|
63
|
+
smaller so the tab is not killed for memory: 1,000 screens on the GPU path and 250 as DOM. Firefox,
|
|
64
|
+
Safari and every iPhone or iPad browser have no HTML-in-Canvas yet, so they run the DOM version and
|
|
65
|
+
say so.
|
|
55
66
|
|
|
56
67
|
Status: early (0.4). The API can still change.
|
|
57
68
|
|
|
@@ -144,9 +155,8 @@ canvas.setHover(el); // a lighter outline; null clears
|
|
|
144
155
|
canvas.screenToWorld(point); // surface pixels to world units, and worldToScreen back
|
|
145
156
|
```
|
|
146
157
|
|
|
147
|
-
Colors are the `selectionColor` and `hoverColor` options.
|
|
148
|
-
|
|
149
|
-
the components.
|
|
158
|
+
Colors are the `selectionColor` and `hoverColor` options. In the demo, the toolbar switches between
|
|
159
|
+
Edit and Interact (clicks reach the components) and picks components, elements or whole screens.
|
|
150
160
|
|
|
151
161
|
## Shader effects
|
|
152
162
|
|
|
@@ -326,9 +336,11 @@ to npm from GitHub Actions, with provenance, through npm trusted publishing. See
|
|
|
326
336
|
6. **Live updates.** A MutationObserver marks a node dirty when its content changes, and
|
|
327
337
|
loading images, web fonts and CSS transitions trigger a redraw. Animated content on nodes
|
|
328
338
|
under 200 screen pixels refreshes at most four times a second.
|
|
329
|
-
7. **Input.** Only the page under the pointer is registered for hit testing, and
|
|
330
|
-
puts the node under the pointer exactly where the GPU drew it. Clicks, typing, selection and
|
|
331
|
-
hover all go to the real element. A focused node keeps its page live.
|
|
339
|
+
7. **Input.** Only the page under the pointer is registered for hit testing, and its canvas
|
|
340
|
+
transform puts the node under the pointer exactly where the GPU drew it. Clicks, typing, selection and
|
|
341
|
+
hover all go to the real element. A focused node keeps its page live. Touch has no hover, so a
|
|
342
|
+
tap makes the node under the finger live and hands the click to the element it landed on. A
|
|
343
|
+
two-finger pinch zooms and pans the camera wherever the fingers land.
|
|
332
344
|
|
|
333
345
|
## Measured (M-series Mac, Chrome 157, 1600 x 900 window, DPR 1)
|
|
334
346
|
|
|
@@ -364,11 +376,11 @@ so a gap of a few percent, like frost above none, is noise rather than a speedup
|
|
|
364
376
|
## Chrome 157 findings (differ from the WICG explainer)
|
|
365
377
|
|
|
366
378
|
- **Hit-test registration.** A drawable is hit-tested only after
|
|
367
|
-
`canvas.updateElementGeometry(el,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
`
|
|
371
|
-
|
|
379
|
+
`canvas.updateElementGeometry(el, options)` registers it.
|
|
380
|
+
- **Hit-test position.** Chrome 157 hit tests at `options.canvasTransform`, a matrix in CSS
|
|
381
|
+
pixels from the element's border box to the canvas, and `getElementTransform(el)` returns it.
|
|
382
|
+
Chrome 156 ignored `canvasTransform` and hit tested at the CSS box, positioned with
|
|
383
|
+
`el.style.transform = canvas.getElementTransform(el, drawMatrix)`. gpucanvas handles both.
|
|
372
384
|
- **Transform changes.** Changing a drawable's CSS transform or pointer-events reports it in
|
|
373
385
|
`changedElements`.
|
|
374
386
|
- **Newly drawable elements.** An element that just became drawable is not reported in
|
|
@@ -56,7 +56,25 @@ function bindCameraInput(element, getCamera, onChange) {
|
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
let drag = null;
|
|
59
|
+
const touches = /* @__PURE__ */ new Map();
|
|
60
|
+
const pinch = () => {
|
|
61
|
+
const [a, b] = [...touches.values()];
|
|
62
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, distance: Math.hypot(a.x - b.x, a.y - b.y) };
|
|
63
|
+
};
|
|
59
64
|
const onPointerDown = (event) => {
|
|
65
|
+
if (event.pointerType === "touch") {
|
|
66
|
+
touches.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
67
|
+
if (touches.size === 2) {
|
|
68
|
+
drag = null;
|
|
69
|
+
for (const id of touches.keys()) {
|
|
70
|
+
try {
|
|
71
|
+
element.setPointerCapture(id);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
60
78
|
if (event.button !== 0 && event.button !== 1 || event.shiftKey) return;
|
|
61
79
|
if (event.target !== element && !event.target.hasAttribute?.("data-gpucanvas-surface"))
|
|
62
80
|
return;
|
|
@@ -64,6 +82,31 @@ function bindCameraInput(element, getCamera, onChange) {
|
|
|
64
82
|
element.setPointerCapture(event.pointerId);
|
|
65
83
|
};
|
|
66
84
|
const onPointerMove = (event) => {
|
|
85
|
+
const touch = touches.get(event.pointerId);
|
|
86
|
+
if (touch && touches.size === 2) {
|
|
87
|
+
const before = pinch();
|
|
88
|
+
touch.x = event.clientX;
|
|
89
|
+
touch.y = event.clientY;
|
|
90
|
+
const after = pinch();
|
|
91
|
+
if (!before.distance) return;
|
|
92
|
+
const bounds = element.getBoundingClientRect();
|
|
93
|
+
const zoomed = zoomAt(
|
|
94
|
+
getCamera(),
|
|
95
|
+
before.x - bounds.left,
|
|
96
|
+
before.y - bounds.top,
|
|
97
|
+
after.distance / before.distance
|
|
98
|
+
);
|
|
99
|
+
onChange({
|
|
100
|
+
...zoomed,
|
|
101
|
+
x: zoomed.x - (after.x - before.x) / zoomed.zoom,
|
|
102
|
+
y: zoomed.y - (after.y - before.y) / zoomed.zoom
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (touch) {
|
|
107
|
+
touch.x = event.clientX;
|
|
108
|
+
touch.y = event.clientY;
|
|
109
|
+
}
|
|
67
110
|
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
68
111
|
const camera = getCamera();
|
|
69
112
|
onChange({
|
|
@@ -75,6 +118,7 @@ function bindCameraInput(element, getCamera, onChange) {
|
|
|
75
118
|
drag.y = event.clientY;
|
|
76
119
|
};
|
|
77
120
|
const onPointerUp = (event) => {
|
|
121
|
+
touches.delete(event.pointerId);
|
|
78
122
|
if (drag?.pointerId === event.pointerId) drag = null;
|
|
79
123
|
};
|
|
80
124
|
element.addEventListener("wheel", onWheel, { passive: false });
|
|
@@ -1347,6 +1391,7 @@ var MISSING = 0;
|
|
|
1347
1391
|
var DIRTY = 1;
|
|
1348
1392
|
var BLURRY = 2;
|
|
1349
1393
|
var OVERSAMPLED = 3;
|
|
1394
|
+
var CANVAS_TRANSFORM_HIT_TESTS = typeof HTMLCanvasElement !== "undefined" && "getElementImageDefaultSize" in HTMLCanvasElement.prototype;
|
|
1350
1395
|
function supportsGpuCanvas() {
|
|
1351
1396
|
return typeof navigator !== "undefined" && "gpu" in navigator && typeof HTMLCanvasElement !== "undefined" && "requestPaint" in HTMLCanvasElement.prototype && "getElementTransform" in HTMLCanvasElement.prototype && typeof GPUQueue !== "undefined" && "drawElementImageToTexture" in GPUQueue.prototype;
|
|
1352
1397
|
}
|
|
@@ -2112,18 +2157,19 @@ async function createGpuCanvas(host, options) {
|
|
|
2112
2157
|
};
|
|
2113
2158
|
const positionLivePage = () => {
|
|
2114
2159
|
if (!live || livePositioned) return;
|
|
2115
|
-
const dpr = canvas.width / Math.max(1, canvas.clientWidth);
|
|
2116
2160
|
const { zoom } = camera;
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
0,
|
|
2120
|
-
0,
|
|
2121
|
-
zoom,
|
|
2122
|
-
((live.rect.x - camera.x) * zoom - live.pageX * zoom) * dpr,
|
|
2123
|
-
((live.rect.y - camera.y) * zoom - live.pageY * zoom) * dpr
|
|
2124
|
-
]);
|
|
2161
|
+
const x = (live.rect.x - camera.x - live.pageX) * zoom;
|
|
2162
|
+
const y = (live.rect.y - camera.y - live.pageY) * zoom;
|
|
2125
2163
|
try {
|
|
2126
|
-
|
|
2164
|
+
if (CANVAS_TRANSFORM_HIT_TESTS) {
|
|
2165
|
+
canvas.updateElementGeometry(live.page.element, {
|
|
2166
|
+
canvasTransform: new DOMMatrix([zoom, 0, 0, zoom, x, y])
|
|
2167
|
+
});
|
|
2168
|
+
} else {
|
|
2169
|
+
const dpr = canvas.width / Math.max(1, canvas.clientWidth);
|
|
2170
|
+
const drawn2 = new DOMMatrix([zoom, 0, 0, zoom, x * dpr, y * dpr]);
|
|
2171
|
+
live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
|
|
2172
|
+
}
|
|
2127
2173
|
live.element.style.pointerEvents = "auto";
|
|
2128
2174
|
livePositioned = true;
|
|
2129
2175
|
} catch {
|
|
@@ -2181,14 +2227,44 @@ async function createGpuCanvas(host, options) {
|
|
|
2181
2227
|
};
|
|
2182
2228
|
let press = FAR_AWAY;
|
|
2183
2229
|
let pressedAt = -Infinity;
|
|
2230
|
+
let tapped = false;
|
|
2184
2231
|
const onPointerDown = (event) => {
|
|
2185
2232
|
const bounds = canvas.getBoundingClientRect();
|
|
2186
|
-
|
|
2233
|
+
const at = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
2234
|
+
press = screenToWorld(camera, at);
|
|
2235
|
+
pressedAt = performance.now();
|
|
2236
|
+
if (event.pointerType !== "mouse") {
|
|
2237
|
+
pointer = at;
|
|
2238
|
+
pickLive();
|
|
2239
|
+
tapped = true;
|
|
2240
|
+
}
|
|
2241
|
+
if (effects.size) schedule();
|
|
2242
|
+
};
|
|
2243
|
+
const onClick = (event) => {
|
|
2244
|
+
if (!tapped) return;
|
|
2245
|
+
tapped = false;
|
|
2246
|
+
const bounds = canvas.getBoundingClientRect();
|
|
2247
|
+
const point = screenToWorld(camera, {
|
|
2187
2248
|
x: event.clientX - bounds.left,
|
|
2188
2249
|
y: event.clientY - bounds.top
|
|
2189
2250
|
});
|
|
2190
|
-
|
|
2191
|
-
|
|
2251
|
+
let top = null;
|
|
2252
|
+
for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
|
|
2253
|
+
if (!top || top.element.contains(event.target)) return;
|
|
2254
|
+
let target = elementInNode(top.element, top.rect, point);
|
|
2255
|
+
while (target && !(target instanceof HTMLElement)) target = target.parentElement;
|
|
2256
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2257
|
+
target.focus({ preventScroll: true });
|
|
2258
|
+
target.dispatchEvent(
|
|
2259
|
+
new MouseEvent("click", {
|
|
2260
|
+
bubbles: true,
|
|
2261
|
+
cancelable: true,
|
|
2262
|
+
composed: true,
|
|
2263
|
+
view: window,
|
|
2264
|
+
clientX: event.clientX,
|
|
2265
|
+
clientY: event.clientY
|
|
2266
|
+
})
|
|
2267
|
+
);
|
|
2192
2268
|
};
|
|
2193
2269
|
const onFocusIn = (event) => {
|
|
2194
2270
|
const node = nodeOf(event.target) ?? null;
|
|
@@ -2229,6 +2305,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2229
2305
|
surface.addEventListener("pointermove", onPointerMove);
|
|
2230
2306
|
surface.addEventListener("pointerleave", onPointerLeave);
|
|
2231
2307
|
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
2308
|
+
surface.addEventListener("click", onClick);
|
|
2232
2309
|
canvas.addEventListener("focusin", onFocusIn);
|
|
2233
2310
|
canvas.addEventListener("focusout", onFocusOut);
|
|
2234
2311
|
const usedAt = /* @__PURE__ */ new WeakMap();
|
|
@@ -2507,6 +2584,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2507
2584
|
surface.removeEventListener("pointermove", onPointerMove);
|
|
2508
2585
|
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
2509
2586
|
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
2587
|
+
surface.removeEventListener("click", onClick);
|
|
2510
2588
|
canvas.removeEventListener("focusin", onFocusIn);
|
|
2511
2589
|
canvas.removeEventListener("focusout", onFocusOut);
|
|
2512
2590
|
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
@@ -3812,4 +3890,4 @@ export {
|
|
|
3812
3890
|
swapNodes,
|
|
3813
3891
|
createGpuCanvas2 as createGpuCanvas
|
|
3814
3892
|
};
|
|
3815
|
-
//# sourceMappingURL=chunk-
|
|
3893
|
+
//# sourceMappingURL=chunk-2EDYGRFR.js.map
|