gpucanvas 0.3.0 → 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 +157 -9
- package/dist/{chunk-RBXGH44N.js → chunk-BQQVSR4B.js} +661 -9
- package/dist/chunk-BQQVSR4B.js.map +1 -0
- package/dist/{editor-BSRqjhN8.d.ts → editor-D87kaZBM.d.ts} +10 -1
- package/dist/index.d.ts +165 -3
- package/dist/index.js +17 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/package.json +5 -3
- package/dist/chunk-RBXGH44N.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
# gpucanvas
|
|
2
2
|
|
|
3
|
-
Put thousands of live DOM nodes on an infinite canvas
|
|
3
|
+
Put thousands of live DOM nodes on an infinite canvas: the GPU draws them, an editor selects,
|
|
4
|
+
moves and resizes them, shaders and transitions run on them, and an AI agent can read them and
|
|
5
|
+
point at them.
|
|
4
6
|
|
|
5
7
|

|
|
6
8
|
|
|
7
9
|
The same session on both sides: 5,000 live React screens, the same camera path, a message typed and sent. Left, every screen is plain DOM under a CSS transform. Right, gpucanvas. Recorded in Chrome on a MacBook Pro; the frame rates are measured live in each run. Run it yourself in the [live demo](https://berkantay.github.io/gpucanvas/).
|
|
8
10
|
|
|
11
|
+
- **[Canvas](#how-it-works).** Live React or plain DOM nodes, drawn by WebGPU in one instanced
|
|
12
|
+
pass. Pan and zoom never touch the DOM, and clicks, typing and state still reach the real
|
|
13
|
+
elements.
|
|
14
|
+
- **[Editor](#editing).** Click to select, double-click to go a layer deeper, down to any element
|
|
15
|
+
or React component. Move and resize with snapping guides.
|
|
16
|
+
- **[Shader effects](#shader-effects).** Run WGSL on live components: a glass lens, frost,
|
|
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
|
+

|
|
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.
|
|
31
|
+
|
|
9
32
|
## Try it
|
|
10
33
|
|
|
11
|
-
Open **[berkantay.github.io/gpucanvas](https://berkantay.github.io/gpucanvas/)** in Chrome 157 or
|
|
34
|
+
Open **[berkantay.github.io/gpucanvas](https://berkantay.github.io/gpucanvas/)** in Chrome 157 or
|
|
35
|
+
later. Thirty agent cursors build the first view, then the camera pulls out to all 5,000 screens.
|
|
36
|
+
|
|
37
|
+
- Click a screen to select it, double-click to go deeper, drag to move it.
|
|
38
|
+
- Pick a shader effect in the toolbar, or open one directly:
|
|
39
|
+
[glass](https://berkantay.github.io/gpucanvas/?effect=glass),
|
|
40
|
+
[shockwave](https://berkantay.github.io/gpucanvas/?effect=shockwave) (click anywhere),
|
|
41
|
+
[frost](https://berkantay.github.io/gpucanvas/?effect=frost),
|
|
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.
|
|
45
|
+
- Press `b` to run the benchmark tour.
|
|
12
46
|
|
|
13
47
|
The GPU path needs HTML-in-Canvas, which is behind a flag:
|
|
14
48
|
|
|
@@ -18,7 +52,7 @@ The GPU path needs HTML-in-Canvas, which is behind a flag:
|
|
|
18
52
|
|
|
19
53
|
Without the flag the demo runs the DOM fallback with 1,000 screens and says so.
|
|
20
54
|
|
|
21
|
-
Status: early (0.
|
|
55
|
+
Status: early (0.4). The API can still change.
|
|
22
56
|
|
|
23
57
|
```sh
|
|
24
58
|
npm install gpucanvas
|
|
@@ -131,8 +165,9 @@ Built in:
|
|
|
131
165
|
`dither` (a lens of big dithered pixels).
|
|
132
166
|
- On each press: `shockwave`, a ring that bends the node as it spreads.
|
|
133
167
|
- Running on their own: `glitch`, `vhs`, `glow`, `hue`, `ripple`.
|
|
134
|
-
- Still: `blur`, `pixelate`, `grayscale
|
|
135
|
-
|
|
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).
|
|
136
171
|
|
|
137
172
|
Animated effects draw every frame while their node is in view; still ones cost nothing once
|
|
138
173
|
drawn.
|
|
@@ -158,12 +193,104 @@ const scanlines = defineEffect({
|
|
|
158
193
|
`EffectInput` has `uv` (0 to 1 across the node), `size` in CSS pixels, `time` in seconds,
|
|
159
194
|
`zoom`, the four `params`, the `pointer` and the last `press` in the node's uv, and
|
|
160
195
|
`sincePress` in seconds. `source(uv)` reads the node's pixels, premultiplied, and is
|
|
161
|
-
transparent outside it; `hash` and `noise` give random values. An effect that draws past its
|
|
162
|
-
`pad`. Effects read the node's whole snapshot, so very
|
|
196
|
+
transparent outside it; `hash` and `noise` give random values. An effect that draws past its
|
|
197
|
+
node, like a glow, declares how far with `pad`. Effects read the node's whole snapshot, so very
|
|
198
|
+
close in they are softer than plain nodes. In React, `<CanvasNode effect={effects.glass}>` does
|
|
199
|
+
the same.
|
|
163
200
|
|
|
164
201
|
The DOM backend shows effects that have a `css` look as a CSS `filter` (animated ones as filter
|
|
165
202
|
animations) and the rest as plain nodes. The demo's toolbar puts one effect on every component.
|
|
166
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
|
+
|
|
167
294
|
## Run the demo
|
|
168
295
|
|
|
169
296
|
```sh
|
|
@@ -171,6 +298,12 @@ bun install
|
|
|
171
298
|
bun run dev # http://localhost:5199/?mode=gpu&n=10000 (mode: auto | gpu | dom)
|
|
172
299
|
```
|
|
173
300
|
|
|
301
|
+
## Releases
|
|
302
|
+
|
|
303
|
+
Publishing a GitHub release for a `vX.Y.Z` tag that matches `package.json` publishes the package
|
|
304
|
+
to npm from GitHub Actions, with provenance, through npm trusted publishing. See
|
|
305
|
+
[release.yml](.github/workflows/release.yml).
|
|
306
|
+
|
|
174
307
|
## How it works
|
|
175
308
|
|
|
176
309
|
1. **Pages.** Nodes are live DOM inside `<canvas content="drawable">`, packed side by side into
|
|
@@ -186,7 +319,8 @@ bun run dev # http://localhost:5199/?mode=gpu&n=10000 (mode: auto | gpu
|
|
|
186
319
|
4. **Tiles up close.** Past 100% zoom on a 2x display, only the visible part of a node is
|
|
187
320
|
rasterized, as sharp tiles, center first. While its content animates, one half-resolution preview stands in.
|
|
188
321
|
5. **One draw call.** Each frame culls nodes against the view and draws every visible one with
|
|
189
|
-
one instanced quad draw
|
|
322
|
+
one instanced quad draw, or one per run of nodes sharing a shader effect. Panning and zooming
|
|
323
|
+
never touch the DOM. Raster work has a
|
|
190
324
|
per-frame budget, so the camera stays smooth while pixels fill in.
|
|
191
325
|
6. **Live updates.** A MutationObserver marks a node dirty when its content changes, and
|
|
192
326
|
loading images, web fonts and CSS transitions trigger a redraw. Animated content on nodes
|
|
@@ -200,7 +334,7 @@ bun run dev # http://localhost:5199/?mode=gpu&n=10000 (mode: auto | gpu
|
|
|
200
334
|
The demo's benchmark (press `b`) runs a 30-second tour: a message typed and sent, a chart
|
|
201
335
|
animating, a push to 500% zoom, shift-click picks and a selection sweep, while 30 agent cursors
|
|
202
336
|
build screens around the view. Every frame is timed. At 5,000 screens it measures 113 to 115 fps
|
|
203
|
-
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.
|
|
204
338
|
|
|
205
339
|
The table below was measured with the 0.1 benchmark, a 94-second session from the overview into
|
|
206
340
|
clusters of app screens, close-ups up to 500% zoom, 20 clicks and typed messages, then back out.
|
|
@@ -212,6 +346,20 @@ clusters of app screens, close-ups up to 500% zoom, 20 clicks and typed messages
|
|
|
212
346
|
|
|
213
347
|
DOM is the same screens in one CSS-transformed container, which is also the fallback backend.
|
|
214
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
|
+
|
|
215
363
|
## Chrome 157 findings (differ from the WICG explainer)
|
|
216
364
|
|
|
217
365
|
- **Hit-test registration.** A drawable is hit-tested only after
|