gpucanvas 0.2.0 → 0.3.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,14 +1,32 @@
1
1
  # gpucanvas
2
2
 
3
- Put thousands of live DOM nodes on an infinite canvas and let the GPU pan and zoom them.
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
5
 
5
6
  ![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)
6
7
 
7
8
  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
9
 
10
+ - **[Canvas](#how-it-works).** Live React or plain DOM nodes, drawn by WebGPU in one instanced
11
+ pass. Pan and zoom never touch the DOM, and clicks, typing and state still reach the real
12
+ elements.
13
+ - **[Editor](#editing).** Click to select, double-click to go a layer deeper, down to any element
14
+ or React component. Move and resize with snapping guides.
15
+ - **[Shader effects](#shader-effects).** Run WGSL on live components: a glass lens, frost,
16
+ shockwaves, glitch, VHS, or your own.
17
+
9
18
  ## Try it
10
19
 
11
- Open **[berkantay.github.io/gpucanvas](https://berkantay.github.io/gpucanvas/)** in Chrome 157 or later. Thirty agent cursors build the first view, then the camera pulls out to all 5,000 screens. Press `b` to run the benchmark tour.
20
+ Open **[berkantay.github.io/gpucanvas](https://berkantay.github.io/gpucanvas/)** in Chrome 157 or
21
+ later. Thirty agent cursors build the first view, then the camera pulls out to all 5,000 screens.
22
+
23
+ - Click a screen to select it, double-click to go deeper, drag to move it.
24
+ - Pick a shader effect in the toolbar, or open one directly:
25
+ [glass](https://berkantay.github.io/gpucanvas/?effect=glass),
26
+ [shockwave](https://berkantay.github.io/gpucanvas/?effect=shockwave) (click anywhere),
27
+ [frost](https://berkantay.github.io/gpucanvas/?effect=frost),
28
+ [glitch](https://berkantay.github.io/gpucanvas/?effect=glitch).
29
+ - Press `b` to run the benchmark tour.
12
30
 
13
31
  The GPU path needs HTML-in-Canvas, which is behind a flag:
14
32
 
@@ -18,7 +36,7 @@ The GPU path needs HTML-in-Canvas, which is behind a flag:
18
36
 
19
37
  Without the flag the demo runs the DOM fallback with 1,000 screens and says so.
20
38
 
21
- Status: early (0.2). The API can still change.
39
+ Status: early (0.3). The API can still change.
22
40
 
23
41
  ```sh
24
42
  npm install gpucanvas
@@ -41,17 +59,130 @@ HTML-in-Canvas are available (Chrome 157+), nodes are snapshotted into a GPU atl
41
59
  one instanced draw call. Everywhere else it falls back to a CSS-transformed container with the
42
60
  same API.
43
61
 
44
- ## Selection
62
+ ## Editing
63
+
64
+ `bindEditor` turns the canvas into an editor: select, drill in, move and resize. The GPU draws
65
+ the outlines, 2 CSS pixels wide at any zoom, and they follow their element's layout. Names,
66
+ sizes, handles and snapping guides sit in a thin overlay above it.
67
+
68
+ ```ts
69
+ import { bindEditor } from 'gpucanvas';
70
+
71
+ const editor = bindEditor(canvas, {
72
+ pick: 'element', // 'node' (default), 'element', or a function
73
+ trigger: 'click', // an edit mode; the default 'shift-click' leaves clicks to your components
74
+ onChange: (layers) => console.log(layers),
75
+ onTransform: ({ phase, layers, to }) => {
76
+ // The editor never moves anything itself. Apply `to` with updateNode, state or styles.
77
+ },
78
+ });
79
+ ```
80
+
81
+ A click selects the outermost layer under the pointer, a whole node. Each double-click goes one
82
+ layer deeper, and later clicks stay at that depth. Wrappers the size of their parent are skipped,
83
+ so every double-click visibly changes something.
84
+
85
+ | Input | Does |
86
+ | ------------------ | ---------------------------------------------------------------------------- |
87
+ | Double-click | One layer deeper |
88
+ | Enter, Shift+Enter | First child, parent |
89
+ | Tab, Shift+Tab | Next and previous sibling |
90
+ | Escape | Cancels a drag, then goes up a layer, then clears |
91
+ | Drag | Moves, snapping to the edges and centers of neighbors (Shift locks the axis) |
92
+ | Handles | Resize (Shift on a corner keeps the ratio) |
93
+ | Drag on open space | A box that selects the layers it touches, inside the layer you are in |
45
94
 
46
- Selected nodes get an outline drawn by the GPU, 2 CSS pixels wide at any zoom.
95
+ With the default `trigger: 'shift-click'`, the same works with Shift held, and clicks still reach
96
+ your components. A layer is an array of elements, so one layer can be a component that renders
97
+ several. `pick` decides the layers:
98
+
99
+ - `'node'`: whole nodes only.
100
+ - `'element'`: every element from the node down to the one under the pointer.
101
+ - a function of that element, returning the layers that hold it, outermost first.
102
+
103
+ In React, `useEditor` gives the selection as state, and `pickComponents` makes the layers React
104
+ components:
105
+
106
+ ```tsx
107
+ import { labelComponent, pickComponents, useEditor } from 'gpucanvas/react';
108
+
109
+ // Inside <GpuCanvas>:
110
+ const { selected, clear } = useEditor({ pick: pickComponents, label: labelComponent });
111
+ // Click a screen, double-click: SignIn, SignInCard, Button.
112
+ ```
113
+
114
+ `pickComponents` and `componentOf` read React's internal fibers, as click-to-component tools do.
115
+ Production builds usually minify component names, so mark components with your own attributes
116
+ when you need names there.
117
+
118
+ The building blocks, for your own interactions:
47
119
 
48
120
  ```ts
49
- const hits = canvas.getNodesIn({ x, y, width, height }); // world units, such as a selection box
50
- canvas.setSelection(hits); // replaces the selection; [] clears it
121
+ canvas.getElementAt(point); // deepest element at a world point
122
+ canvas.getElementRect(el); // any element's rect in world units
123
+ canvas.getNodeOf(el); // the node that holds it
124
+ canvas.getNodesIn(rect); // nodes overlapping a world rect
125
+ canvas.setSelection(elements); // outline these; [] clears
126
+ canvas.setHover(el); // a lighter outline; null clears
127
+ canvas.screenToWorld(point); // surface pixels to world units, and worldToScreen back
51
128
  ```
52
129
 
53
- The color is the `selectionColor` option. Shift-drags on the background don't pan, so an app
54
- can use them for a selection box, as the demo does.
130
+ Colors are the `selectionColor` and `hoverColor` options. The demo picks React components in the edit mode;
131
+ `?pick=element` or `?pick=node` picks elements or whole screens, and `?edit=0` leaves clicks to
132
+ the components.
133
+
134
+ ## Shader effects
135
+
136
+ Components stay live DOM, and the GPU draws them through a shader. One call per node:
137
+
138
+ ```ts
139
+ import { effects } from 'gpucanvas';
140
+
141
+ canvas.setEffect(node, effects.ripple); // the effect's default parameters
142
+ canvas.setEffect(node, effects.glow, [1, 0.4, 0.2, 32]); // orange, 32 px reach
143
+ canvas.setEffect(node, null); // back to plain
144
+ ```
145
+
146
+ Built in:
147
+
148
+ - Following the pointer: `glass` (a magnifying lens), `frost` (ice, clear around the pointer),
149
+ `dither` (a lens of big dithered pixels).
150
+ - On each press: `shockwave`, a ring that bends the node as it spreads.
151
+ - 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).
154
+
155
+ Animated effects draw every frame while their node is in view; still ones cost nothing once
156
+ drawn.
157
+
158
+ Your own is a WGSL function over the node's pixels:
159
+
160
+ ```ts
161
+ import { defineEffect } from 'gpucanvas';
162
+
163
+ const scanlines = defineEffect({
164
+ name: 'scanlines',
165
+ animated: true,
166
+ params: [3], // line spacing in CSS pixels
167
+ wgsl: /* wgsl */ `
168
+ fn effect(input: EffectInput) -> vec4f {
169
+ let color = source(input.uv);
170
+ let y = input.uv.y * input.size.y + input.time * 20.0;
171
+ return color * (0.85 + 0.15 * step(0.5, fract(y / input.params.x)));
172
+ }`,
173
+ });
174
+ ```
175
+
176
+ `EffectInput` has `uv` (0 to 1 across the node), `size` in CSS pixels, `time` in seconds,
177
+ `zoom`, the four `params`, the `pointer` and the last `press` in the node's uv, and
178
+ `sincePress` in seconds. `source(uv)` reads the node's pixels, premultiplied, and is
179
+ transparent outside it; `hash` and `noise` give random values. An effect that draws past its
180
+ node, like a glow, declares how far with `pad`. Effects read the node's whole snapshot, so very
181
+ close in they are softer than plain nodes. In React, `<CanvasNode effect={effects.glass}>` does
182
+ the same.
183
+
184
+ The DOM backend shows effects that have a `css` look as a CSS `filter` (animated ones as filter
185
+ animations) and the rest as plain nodes. The demo's toolbar puts one effect on every component.
55
186
 
56
187
  ## Run the demo
57
188
 
@@ -60,6 +191,12 @@ bun install
60
191
  bun run dev # http://localhost:5199/?mode=gpu&n=10000 (mode: auto | gpu | dom)
61
192
  ```
62
193
 
194
+ ## Releases
195
+
196
+ Publishing a GitHub release for a `vX.Y.Z` tag that matches `package.json` publishes the package
197
+ to npm from GitHub Actions, with provenance, through npm trusted publishing. See
198
+ [release.yml](.github/workflows/release.yml).
199
+
63
200
  ## How it works
64
201
 
65
202
  1. **Pages.** Nodes are live DOM inside `<canvas content="drawable">`, packed side by side into
@@ -75,7 +212,8 @@ bun run dev # http://localhost:5199/?mode=gpu&n=10000 (mode: auto | gpu
75
212
  4. **Tiles up close.** Past 100% zoom on a 2x display, only the visible part of a node is
76
213
  rasterized, as sharp tiles, center first. While its content animates, one half-resolution preview stands in.
77
214
  5. **One draw call.** Each frame culls nodes against the view and draws every visible one with
78
- one instanced quad draw. Panning and zooming never touch the DOM. Raster work has a
215
+ one instanced quad draw, or one per run of nodes sharing a shader effect. Panning and zooming
216
+ never touch the DOM. Raster work has a
79
217
  per-frame budget, so the camera stays smooth while pixels fill in.
80
218
  6. **Live updates.** A MutationObserver marks a node dirty when its content changes, and
81
219
  loading images, web fonts and CSS transitions trigger a redraw. Animated content on nodes
@@ -124,6 +262,8 @@ DOM is the same screens in one CSS-transformed container, which is also the fall
124
262
 
125
263
  ## Next
126
264
 
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.
127
267
  - **Keyboard and accessibility.** Parked pages use `content-visibility: hidden`, so Tab skips
128
268
  their nodes. A focused node already stays live; moving focus across pages needs a focus model.
129
269
  - **Workers.** Composite in a worker with `captureElementImage`, and drive culling from the GPU.