gpucanvas 0.4.1 → 0.4.2
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 +21 -7
- package/dist/{chunk-DPCICAYZ.js → chunk-TC6XBFOY.js} +80 -4
- package/dist/chunk-TC6XBFOY.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,7 +52,8 @@ 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.
|
|
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.
|
|
47
57
|
|
|
48
58
|
The GPU path needs HTML-in-Canvas, which is behind a flag:
|
|
49
59
|
|
|
@@ -51,7 +61,10 @@ The GPU path needs HTML-in-Canvas, which is behind a flag:
|
|
|
51
61
|
2. Set it to Enabled and relaunch Chrome
|
|
52
62
|
3. Reload the demo
|
|
53
63
|
|
|
54
|
-
Without the flag the demo runs the DOM fallback with 1,000 screens and says so.
|
|
64
|
+
Without the flag the demo runs the DOM fallback with 1,000 screens and says so. Phones start
|
|
65
|
+
smaller so the tab is not killed for memory: 1,000 screens on the GPU path (Chrome on Android with
|
|
66
|
+
the flag) and 250 as DOM. iPhone and iPad browsers have no HTML-in-Canvas yet, so they run the DOM
|
|
67
|
+
version and say so.
|
|
55
68
|
|
|
56
69
|
Status: early (0.4). The API can still change.
|
|
57
70
|
|
|
@@ -144,9 +157,8 @@ canvas.setHover(el); // a lighter outline; null clears
|
|
|
144
157
|
canvas.screenToWorld(point); // surface pixels to world units, and worldToScreen back
|
|
145
158
|
```
|
|
146
159
|
|
|
147
|
-
Colors are the `selectionColor` and `hoverColor` options.
|
|
148
|
-
|
|
149
|
-
the components.
|
|
160
|
+
Colors are the `selectionColor` and `hoverColor` options. In the demo, the toolbar switches between
|
|
161
|
+
Edit and Interact (clicks reach the components) and picks components, elements or whole screens.
|
|
150
162
|
|
|
151
163
|
## Shader effects
|
|
152
164
|
|
|
@@ -328,7 +340,9 @@ to npm from GitHub Actions, with provenance, through npm trusted publishing. See
|
|
|
328
340
|
under 200 screen pixels refreshes at most four times a second.
|
|
329
341
|
7. **Input.** Only the page under the pointer is registered for hit testing, and a CSS transform
|
|
330
342
|
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.
|
|
343
|
+
hover all go to the real element. A focused node keeps its page live. Touch has no hover, so a
|
|
344
|
+
tap makes the node under the finger live and hands the click to the element it landed on. A
|
|
345
|
+
two-finger pinch zooms and pans the camera wherever the fingers land.
|
|
332
346
|
|
|
333
347
|
## Measured (M-series Mac, Chrome 157, 1600 x 900 window, DPR 1)
|
|
334
348
|
|
|
@@ -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 });
|
|
@@ -2181,14 +2225,44 @@ async function createGpuCanvas(host, options) {
|
|
|
2181
2225
|
};
|
|
2182
2226
|
let press = FAR_AWAY;
|
|
2183
2227
|
let pressedAt = -Infinity;
|
|
2228
|
+
let tapped = false;
|
|
2184
2229
|
const onPointerDown = (event) => {
|
|
2185
2230
|
const bounds = canvas.getBoundingClientRect();
|
|
2186
|
-
|
|
2231
|
+
const at = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
2232
|
+
press = screenToWorld(camera, at);
|
|
2233
|
+
pressedAt = performance.now();
|
|
2234
|
+
if (event.pointerType !== "mouse") {
|
|
2235
|
+
pointer = at;
|
|
2236
|
+
pickLive();
|
|
2237
|
+
tapped = true;
|
|
2238
|
+
}
|
|
2239
|
+
if (effects.size) schedule();
|
|
2240
|
+
};
|
|
2241
|
+
const onClick = (event) => {
|
|
2242
|
+
if (!tapped) return;
|
|
2243
|
+
tapped = false;
|
|
2244
|
+
const bounds = canvas.getBoundingClientRect();
|
|
2245
|
+
const point = screenToWorld(camera, {
|
|
2187
2246
|
x: event.clientX - bounds.left,
|
|
2188
2247
|
y: event.clientY - bounds.top
|
|
2189
2248
|
});
|
|
2190
|
-
|
|
2191
|
-
|
|
2249
|
+
let top = null;
|
|
2250
|
+
for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
|
|
2251
|
+
if (!top || top.element.contains(event.target)) return;
|
|
2252
|
+
let target = elementInNode(top.element, top.rect, point);
|
|
2253
|
+
while (target && !(target instanceof HTMLElement)) target = target.parentElement;
|
|
2254
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2255
|
+
target.focus({ preventScroll: true });
|
|
2256
|
+
target.dispatchEvent(
|
|
2257
|
+
new MouseEvent("click", {
|
|
2258
|
+
bubbles: true,
|
|
2259
|
+
cancelable: true,
|
|
2260
|
+
composed: true,
|
|
2261
|
+
view: window,
|
|
2262
|
+
clientX: event.clientX,
|
|
2263
|
+
clientY: event.clientY
|
|
2264
|
+
})
|
|
2265
|
+
);
|
|
2192
2266
|
};
|
|
2193
2267
|
const onFocusIn = (event) => {
|
|
2194
2268
|
const node = nodeOf(event.target) ?? null;
|
|
@@ -2229,6 +2303,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2229
2303
|
surface.addEventListener("pointermove", onPointerMove);
|
|
2230
2304
|
surface.addEventListener("pointerleave", onPointerLeave);
|
|
2231
2305
|
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
2306
|
+
surface.addEventListener("click", onClick);
|
|
2232
2307
|
canvas.addEventListener("focusin", onFocusIn);
|
|
2233
2308
|
canvas.addEventListener("focusout", onFocusOut);
|
|
2234
2309
|
const usedAt = /* @__PURE__ */ new WeakMap();
|
|
@@ -2507,6 +2582,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2507
2582
|
surface.removeEventListener("pointermove", onPointerMove);
|
|
2508
2583
|
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
2509
2584
|
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
2585
|
+
surface.removeEventListener("click", onClick);
|
|
2510
2586
|
canvas.removeEventListener("focusin", onFocusIn);
|
|
2511
2587
|
canvas.removeEventListener("focusout", onFocusOut);
|
|
2512
2588
|
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
@@ -3812,4 +3888,4 @@ export {
|
|
|
3812
3888
|
swapNodes,
|
|
3813
3889
|
createGpuCanvas2 as createGpuCanvas
|
|
3814
3890
|
};
|
|
3815
|
-
//# sourceMappingURL=chunk-
|
|
3891
|
+
//# sourceMappingURL=chunk-TC6XBFOY.js.map
|