gpucanvas 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Berkant Ay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,129 @@
1
- # Temporary Holding Version
1
+ # gpucanvas
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Put thousands of live DOM nodes on an infinite canvas and let the GPU pan and zoom them.
4
+
5
+ ![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
+ 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
+ ## Try it
10
+
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.
12
+
13
+ The GPU path needs HTML-in-Canvas, which is behind a flag:
14
+
15
+ 1. Open `chrome://flags/#canvas-draw-element`
16
+ 2. Set it to Enabled and relaunch Chrome
17
+ 3. Reload the demo
18
+
19
+ Without the flag the demo runs the DOM fallback with 1,000 screens and says so.
20
+
21
+ Status: early (0.2). The API can still change.
22
+
23
+ ```sh
24
+ npm install gpucanvas
25
+ ```
26
+
27
+ ```tsx
28
+ import { GpuCanvas, CanvasNode } from 'gpucanvas/react';
29
+
30
+ <GpuCanvas style={{ width: '100vw', height: '100vh' }}>
31
+ {cards.map((card) => (
32
+ <CanvasNode key={card.id} x={card.x} y={card.y} width={240} height={150}>
33
+ <Card {...card} />
34
+ </CanvasNode>
35
+ ))}
36
+ </GpuCanvas>;
37
+ ```
38
+
39
+ Nodes stay real DOM: React state, events, CSS and accessibility work as usual. Where WebGPU and
40
+ HTML-in-Canvas are available (Chrome 157+), nodes are snapshotted into a GPU atlas and drawn in
41
+ one instanced draw call. Everywhere else it falls back to a CSS-transformed container with the
42
+ same API.
43
+
44
+ ## Selection
45
+
46
+ Selected nodes get an outline drawn by the GPU, 2 CSS pixels wide at any zoom.
47
+
48
+ ```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
51
+ ```
52
+
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.
55
+
56
+ ## Run the demo
57
+
58
+ ```sh
59
+ bun install
60
+ bun run dev # http://localhost:5199/?mode=gpu&n=10000 (mode: auto | gpu | dom)
61
+ ```
62
+
63
+ ## How it works
64
+
65
+ 1. **Pages.** Nodes are live DOM inside `<canvas content="drawable">`, packed side by side into
66
+ pages of 2048 CSS pixels. Chrome records a page as one snapshot, so one
67
+ `drawElementImageToTexture` call rasterizes dozens of nodes at once.
68
+ 2. **Rasterize, then park.** A page with nodes waiting for pixels becomes `drawable`. In the next
69
+ paint event it is drawn into a texture-array atlas, whole or one node at a time when only a
70
+ few changed. Then it is parked with `content-visibility: hidden`, which keeps layout but skips
71
+ paint, so the browser spends nothing on it per frame.
72
+ 3. **Thumbnails first.** Every node gets a 1/16 scale thumbnail in an atlas layer that is never
73
+ evicted, so a zoomed-out view of thousands of screens is always filled in. Sharper snapshots
74
+ go on top at power-of-two scales, so zooming reuses them.
75
+ 4. **Tiles up close.** Past 100% zoom on a 2x display, only the visible part of a node is
76
+ rasterized, as sharp tiles, center first. While its content animates, one half-resolution preview stands in.
77
+ 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
79
+ per-frame budget, so the camera stays smooth while pixels fill in.
80
+ 6. **Live updates.** A MutationObserver marks a node dirty when its content changes, and
81
+ loading images, web fonts and CSS transitions trigger a redraw. Animated content on nodes
82
+ under 200 screen pixels refreshes at most four times a second.
83
+ 7. **Input.** Only the page under the pointer is registered for hit testing, and a CSS transform
84
+ puts the node under the pointer exactly where the GPU drew it. Clicks, typing, selection and
85
+ hover all go to the real element. A focused node keeps its page live.
86
+
87
+ ## Measured (M-series Mac, Chrome 157, 1600 x 900 window, DPR 1)
88
+
89
+ The demo's benchmark (press `b`) runs a 30-second tour: a message typed and sent, a chart
90
+ animating, a push to 500% zoom, shift-click picks and a selection sweep, while 30 agent cursors
91
+ build screens around the view. Every frame is timed. At 5,000 screens it measures 113 to 115 fps
92
+ for gpucanvas and 18 to 20 for DOM.
93
+
94
+ The table below was measured with the 0.1 benchmark, a 94-second session from the overview into
95
+ clusters of app screens, close-ups up to 500% zoom, 20 clicks and typed messages, then back out.
96
+
97
+ | Screens | gpucanvas avg fps | gpucanvas p95 | DOM avg fps | DOM p95 | Mount gpucanvas / DOM |
98
+ | ------- | ----------------- | ------------- | ----------- | -------- | --------------------- |
99
+ | 5,000 | 117 | 9.2 ms | 23 | 50.2 ms | 1.2 s / 1.3 s |
100
+ | 10,000 | 108 | 16.7 ms | 9 | 133.8 ms | 1.8 s / 3.4 s |
101
+
102
+ DOM is the same screens in one CSS-transformed container, which is also the fallback backend.
103
+
104
+ ## Chrome 157 findings (differ from the WICG explainer)
105
+
106
+ - **Hit-test registration.** A drawable is hit-tested only after
107
+ `canvas.updateElementGeometry(el, {})` registers it. The `canvasTransform` option does not
108
+ move the hit area.
109
+ - **Hit-test position.** The hit area is the element's CSS box, so position it with
110
+ `el.style.transform = canvas.getElementTransform(el, drawMatrix)`. The draw matrix maps the
111
+ element's default-size snapshot (CSS px × DPR) to canvas pixels.
112
+ - **Transform changes.** Changing a drawable's CSS transform or pointer-events reports it in
113
+ `changedElements`.
114
+ - **Newly drawable elements.** An element that just became drawable is not reported in
115
+ `changedElements`. Its snapshot is available from the next paint event.
116
+ - **Per-call cost.** Each `drawElementImageToTexture` call allocates and fully rasterizes, about
117
+ 250 to 550 calls per second in total. This, not compositing, limits the fill rate, which is
118
+ why nodes are batched into pages.
119
+ - **Cropping.** A source rectangle still rasterizes the whole source element at the requested
120
+ scale before cropping. Zoomed in, a node therefore moves to a page of its own.
121
+ - **Drawable count.** Recording cost scales with the number of drawables (and non-`display:none`
122
+ canvas children) on every `requestPaint`. 10,000 always-drawable nodes run at 22 fps even when
123
+ idle.
124
+
125
+ ## Next
126
+
127
+ - **Keyboard and accessibility.** Parked pages use `content-visibility: hidden`, so Tab skips
128
+ their nodes. A focused node already stays live; moving focus across pages needs a focus model.
129
+ - **Workers.** Composite in a worker with `captureElementImage`, and drive culling from the GPU.