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 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
- 1. Open `chrome://flags/#canvas-draw-element`
51
- 2. Set it to Enabled and relaunch Chrome
52
- 3. Reload the demo
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 the flag the demo runs the DOM fallback with 1,000 screens and says so.
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. The demo picks React components in the edit mode;
148
- `?pick=element` or `?pick=node` picks elements or whole screens, and `?edit=0` leaves clicks to
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 a CSS transform
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, {})` registers it. The `canvasTransform` option does not
368
- move the hit area.
369
- - **Hit-test position.** The hit area is the element's CSS box, so position it with
370
- `el.style.transform = canvas.getElementTransform(el, drawMatrix)`. The draw matrix maps the
371
- element's default-size snapshot (CSS px × DPR) to canvas pixels.
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 drawn2 = new DOMMatrix([
2118
- zoom,
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
- live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
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
- press = screenToWorld(camera, {
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
- pressedAt = performance.now();
2191
- if (effects.size) schedule();
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-DPCICAYZ.js.map
3893
+ //# sourceMappingURL=chunk-2EDYGRFR.js.map