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 +21 -0
- package/README.md +128 -2
- package/dist/chunk-VOFDBN5J.js +1715 -0
- package/dist/chunk-VOFDBN5J.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +26 -0
- package/dist/react.js +67 -0
- package/dist/react.js.map +1 -0
- package/dist/types-D8v10v6m.d.ts +101 -0
- package/package.json +74 -4
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
|
-
#
|
|
1
|
+
# gpucanvas
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Put thousands of live DOM nodes on an infinite canvas and let the GPU pan and zoom them.
|
|
4
|
+
|
|
5
|
+

|
|
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.
|