gpucanvas 0.3.1 → 0.4.0

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