gpucanvas 0.3.1 → 0.4.1

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
@@ -1,7 +1,8 @@
1
1
  # gpucanvas
2
2
 
3
3
  Put thousands of live DOM nodes on an infinite canvas: the GPU draws them, an editor selects,
4
- moves and resizes them, and shaders run on them.
4
+ moves and resizes them, shaders and transitions run on them, and an AI agent can read them and
5
+ point at them.
5
6
 
6
7
  ![The same 5,000 live React screens as plain DOM (left) and in gpucanvas (right), with each side's frame rate measured live](docs/benchmark.gif)
7
8
 
@@ -14,18 +15,34 @@ The same session on both sides: 5,000 live React screens, the same camera path,
14
15
  or React component. Move and resize with snapping guides.
15
16
  - **[Shader effects](#shader-effects).** Run WGSL on live components: a glass lens, frost,
16
17
  shockwaves, glitch, VHS, or your own.
18
+ - **[Snapshots](#snapshots).** Any node as an image, pixel diffs between versions, and a
19
+ filmstrip of its history.
20
+ - **[Agent view](#agent-view).** What is on the canvas as a short JSON tree with ids, and an
21
+ image of any region, for an AI agent to read and point at.
22
+ - **[Transitions](#transitions).** Dissolve, wipe or fade a node out and another in, on the
23
+ GPU, while both stay live.
24
+ - **[Marks](#marks).** Arrows between elements, comment pins, notes and live cursors that
25
+ follow the camera.
26
+
27
+ ![An agent cursor flags a heading, links two buttons and drops a pin; the screen wipes out and dissolves back; a glass lens follows the pointer; shockwaves ripple from clicks](docs/features.gif)
28
+
29
+ Marks from an agent, a wipe and a dissolve, the glass lens and shockwaves, all on live
30
+ components. Recorded in Chrome from the demo.
17
31
 
18
32
  ## Try it
19
33
 
20
34
  Open **[berkantay.github.io/gpucanvas](https://berkantay.github.io/gpucanvas/)** in Chrome 157 or
21
35
  later. Thirty agent cursors build the first view, then the camera pulls out to all 5,000 screens.
22
36
 
23
- - Click a screen to select it, double-click to go deeper, drag to move it.
37
+ - 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.
24
39
  - Pick a shader effect in the toolbar, or open one directly:
25
40
  [glass](https://berkantay.github.io/gpucanvas/?effect=glass),
26
41
  [shockwave](https://berkantay.github.io/gpucanvas/?effect=shockwave) (click anywhere),
27
42
  [frost](https://berkantay.github.io/gpucanvas/?effect=frost),
28
43
  [glitch](https://berkantay.github.io/gpucanvas/?effect=glitch).
44
+ - Select a screen and press `D` to dissolve it, `W` to wipe it, or `S` to save it as a PNG.
45
+ - In the console, `agentView.describe()` lists what is in view, and `marks` draws on it.
29
46
  - Press `b` to run the benchmark tour.
30
47
 
31
48
  The GPU path needs HTML-in-Canvas, which is behind a flag:
@@ -36,7 +53,7 @@ The GPU path needs HTML-in-Canvas, which is behind a flag:
36
53
 
37
54
  Without the flag the demo runs the DOM fallback with 1,000 screens and says so.
38
55
 
39
- Status: early (0.3). The API can still change.
56
+ Status: early (0.4). The API can still change.
40
57
 
41
58
  ```sh
42
59
  npm install gpucanvas
@@ -84,10 +101,10 @@ so every double-click visibly changes something.
84
101
 
85
102
  | Input | Does |
86
103
  | ------------------ | ---------------------------------------------------------------------------- |
87
- | Double-click | One layer deeper |
104
+ | Double-click | One layer deeper; on a text field, starts typing in it |
88
105
  | Enter, Shift+Enter | First child, parent |
89
106
  | Tab, Shift+Tab | Next and previous sibling |
90
- | Escape | Cancels a drag, then goes up a layer, then clears |
107
+ | Escape | Ends typing, cancels a drag, then goes up a layer, then clears |
91
108
  | Drag | Moves, snapping to the edges and centers of neighbors (Shift locks the axis) |
92
109
  | Handles | Resize (Shift on a corner keeps the ratio) |
93
110
  | Drag on open space | A box that selects the layers it touches, inside the layer you are in |
@@ -149,8 +166,9 @@ Built in:
149
166
  `dither` (a lens of big dithered pixels).
150
167
  - On each press: `shockwave`, a ring that bends the node as it spreads.
151
168
  - Running on their own: `glitch`, `vhs`, `glow`, `hue`, `ripple`.
152
- - Still: `blur`, `pixelate`, `grayscale`, and `dissolve` for a node leaving or arriving (its
153
- parameter is the progress, 0 to 1).
169
+ - Still: `blur`, `pixelate`, `grayscale`.
170
+ - [Transitions](#transitions): `dissolve`, `wipe` and `fade`, for a node leaving or arriving
171
+ (the first parameter is the progress, 0 to 1).
154
172
 
155
173
  Animated effects draw every frame while their node is in view; still ones cost nothing once
156
174
  drawn.
@@ -184,6 +202,96 @@ the same.
184
202
  The DOM backend shows effects that have a `css` look as a CSS `filter` (animated ones as filter
185
203
  animations) and the rest as plain nodes. The demo's toolbar puts one effect on every component.
186
204
 
205
+ ## Snapshots
206
+
207
+ Every node is already drawn by the GPU, so its pixels are one call away, with no headless
208
+ browser:
209
+
210
+ ```ts
211
+ import { diffImages, recordFilmstrip, toBlob } from 'gpucanvas';
212
+
213
+ const before = await canvas.snapshot(node, { scale: 2 }); // ImageData, drawn fresh
214
+ const png = await toBlob(before); // a PNG Blob
215
+
216
+ // ...the screen changes...
217
+ const after = await canvas.snapshot(node, { scale: 2 });
218
+ const { changed, bounds, image } = diffImages(before, after); // what moved, and where
219
+
220
+ // A snapshot after every change, once it settles.
221
+ const strip = recordFilmstrip(canvas, node, { scale: 0.25, max: 30 });
222
+ strip.frames; // [{ at, image }, ...]
223
+ strip.stop();
224
+ ```
225
+
226
+ A snapshot is the node as it looks now, at any scale, whether or not it is in view. It takes a
227
+ paint or two, about 40 ms for a 1,500 by 1,000 pixel screen. In the demo, select a screen and
228
+ press `S` to save it. Snapshots need the GPU backend; the DOM backend rejects.
229
+
230
+ ## Agent view
231
+
232
+ An agent reads the canvas the way a person sees it: what is there, what it says, and where:
233
+
234
+ ```ts
235
+ import { createAgentView } from 'gpucanvas';
236
+
237
+ const view = createAgentView(canvas);
238
+ view.describe(); // what is in view, as JSON
239
+ // [{ id: 'e35', tag: 'div', rect: { x: -3552, y: -2561, width: 1496, height: 958 }, children: [
240
+ // { id: 'e6', tag: 'h1', role: 'heading', text: 'Welcome back', rect: { ... } },
241
+ // { id: 'e18', tag: 'input', role: 'textbox', name: 'Email', rect: { ... } },
242
+ // { id: 'e23', tag: 'input', role: 'checkbox', name: 'Keep me signed in', checked: true, ... },
243
+ // { id: 'e25', tag: 'button', role: 'button', text: 'Sign in', rect: { ... } }, ...] }]
244
+
245
+ view.element('e25'); // the element behind an id
246
+ await view.capture('e6', { scale: 2 }); // ImageData of that element
247
+ await view.capture({ x: 0, y: 0, width: 4000, height: 3000 }, { scale: 0.25 }); // any region
248
+ ```
249
+
250
+ Wrappers that add nothing are left out, so a sign-in screen is about 35 entries and 3.5 KB of
251
+ JSON, read in 3 ms. Pass `rect` to read another region and `name` to label elements with your
252
+ own names, such as their React component's. `capture` needs the GPU backend.
253
+
254
+ ## Transitions
255
+
256
+ Any effect whose first parameter is progress (0 whole, 1 gone) animates as a transition:
257
+ `dissolve`, `wipe` and `fade` are built in.
258
+
259
+ ```ts
260
+ import { animateEffect, effects, swapNodes } from 'gpucanvas';
261
+
262
+ await animateEffect(canvas, node, effects.wipe, { duration: 600, params: [90] }); // wipe downward
263
+ await animateEffect(canvas, node, effects.wipe, { from: 1, to: 0 }); // and back
264
+
265
+ // One screen burns away while the next appears in its place.
266
+ await swapNodes(canvas, oldScreen, newScreen, { effect: effects.dissolve, duration: 800 });
267
+ canvas.removeNode(oldScreen);
268
+ ```
269
+
270
+ A node that ends whole loses the effect; one that ends gone stays hidden until it is removed.
271
+ Starting another animation on a node replaces the running one, which resolves `false`. In the
272
+ demo, select a screen and press `D` to dissolve it or `W` to wipe it. The DOM backend fades.
273
+
274
+ ## Marks
275
+
276
+ Draw on top of the nodes. Marks follow the camera and the elements they are attached to, and
277
+ stay the same size on screen at any zoom:
278
+
279
+ ```ts
280
+ import { createMarks } from 'gpucanvas';
281
+
282
+ const marks = createMarks(canvas);
283
+ marks.connect(button, nextScreen, { label: 'goes to' }); // a curved arrow
284
+ marks.pin(card, { label: '3', text: 'Rename to Continue?', onClick: openThread });
285
+ marks.note(view.element('e6'), 'This heading is 2px off the grid'); // what an agent means
286
+ const cursor = marks.cursor('Ada', { color: '#e11d48' });
287
+ cursor.move({ x: 120, y: 80 }); // glides there, in world units
288
+ marks.clear();
289
+ ```
290
+
291
+ Arrows and pins also take a world point in place of an element. Marks are SVG and HTML in a
292
+ layer above the canvas, so they work with both backends. In the demo, both are on `window`:
293
+ `agentView` and `marks`.
294
+
187
295
  ## Run the demo
188
296
 
189
297
  ```sh
@@ -227,7 +335,7 @@ to npm from GitHub Actions, with provenance, through npm trusted publishing. See
227
335
  The demo's benchmark (press `b`) runs a 30-second tour: a message typed and sent, a chart
228
336
  animating, a push to 500% zoom, shift-click picks and a selection sweep, while 30 agent cursors
229
337
  build screens around the view. Every frame is timed. At 5,000 screens it measures 113 to 115 fps
230
- for gpucanvas and 18 to 20 for DOM.
338
+ for gpucanvas and 18 to 20 for DOM, and 138 for gpucanvas in the effects run below.
231
339
 
232
340
  The table below was measured with the 0.1 benchmark, a 94-second session from the overview into
233
341
  clusters of app screens, close-ups up to 500% zoom, 20 clicks and typed messages, then back out.
@@ -239,6 +347,20 @@ clusters of app screens, close-ups up to 500% zoom, 20 clicks and typed messages
239
347
 
240
348
  DOM is the same screens in one CSS-transformed container, which is also the fallback backend.
241
349
 
350
+ Shader effects on every one of the 5,000 screens, the same 30-second tour, vsync off:
351
+
352
+ | Effect | gpucanvas avg fps | p95 |
353
+ | -------------- | ----------------- | ------- |
354
+ | none | 138 | 15.2 ms |
355
+ | glow | 135 | 15.6 ms |
356
+ | blur (32 taps) | 138 | 16.7 ms |
357
+ | glass | 122 | 18.2 ms |
358
+ | frost | 146 | 13.8 ms |
359
+
360
+ Effects draw from each node's snapshot in the same pass, so most cost nothing measurable; glass,
361
+ which samples around the pointer with a rim and shadow, costs about 12%. Each figure is one run,
362
+ so a gap of a few percent, like frost above none, is noise rather than a speedup.
363
+
242
364
  ## Chrome 157 findings (differ from the WICG explainer)
243
365
 
244
366
  - **Hit-test registration.** A drawable is hit-tested only after
@@ -262,8 +384,6 @@ DOM is the same screens in one CSS-transformed container, which is also the fall
262
384
 
263
385
  ## Next
264
386
 
265
- - **Snapshots as data.** Every node is already a texture: export it as an image without a
266
- headless browser, diff two versions of a screen on the GPU, keep a filmstrip of its history.
267
387
  - **Keyboard and accessibility.** Parked pages use `content-visibility: hidden`, so Tab skips
268
388
  their nodes. A focused node already stays live; moving focus across pages needs a focus model.
269
389
  - **Workers.** Composite in a worker with `captureElementImage`, and drive culling from the GPU.