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 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. 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.
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
- press = screenToWorld(camera, {
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
- pressedAt = performance.now();
2191
- if (effects.size) schedule();
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-DPCICAYZ.js.map
3891
+ //# sourceMappingURL=chunk-TC6XBFOY.js.map