canvas-ultrafast 1.0.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 ADDED
@@ -0,0 +1,146 @@
1
+ # canvas-ultrafast
2
+
3
+ WebGL2-accelerated Canvas 2D rendering engine with triple-buffered framebuffers, command recording, and zero runtime dependencies.
4
+
5
+ ## Why
6
+
7
+ The browser's built-in Canvas 2D context is convenient but synchronous — every `fillRect()` or `fillText()` call immediately rasterizes, blocking the main thread and coupling draw timing to display refresh. canvas-ultrafast decouples _what_ you draw from _when_ it reaches the screen:
8
+
9
+ 1. **Record** — A `CanvasAPI` object captures draw calls as inert command objects instead of executing them.
10
+ 2. **Execute** — A `Canvas2DShim` translates those commands into batched WebGL2 draw calls, writing into an off-screen framebuffer.
11
+ 3. **Display** — A triple-buffered FBO pipeline rotates write → ready → display, so the screen always shows the most recent fully-rendered frame with no tearing and no synchronization overhead.
12
+
13
+ This architecture lets downstream consumers (like [maalata](https://github.com/emansom/maalata)) insert arbitrary latency stages, throttle rendering to retro frame rates, or apply post-processing shaders — all without modifying canvas-ultrafast itself.
14
+
15
+ ## How it works
16
+
17
+ ### Triple-buffered FBOs
18
+
19
+ Three framebuffer objects rotate through three roles:
20
+
21
+ ```
22
+ ┌───────────┐ submitBatch() ┌───────────┐ RAF loop ┌───────────┐
23
+ │ Write │ ──── swap ──────────► │ Ready │ ──── blit ────────► │ Display │
24
+ │ (drawing) │ │ (latest) │ │ (screen) │
25
+ └───────────┘ └───────────┘ └───────────┘
26
+ ```
27
+
28
+ - **Write** — currently receiving draw commands via WebGL.
29
+ - **Ready** — holds the most recent complete frame; swapped in atomically by `submitBatch()`.
30
+ - **Display** — blitted to the canvas each animation frame.
31
+
32
+ Because JavaScript is single-threaded, the swap is lock-free — no mutexes, no fences, just index rotation.
33
+
34
+ ### Command recording
35
+
36
+ `CanvasAPI` implements the familiar Canvas 2D interface (`fillRect`, `fillText`, `translate`, `save`/`restore`, …) but stores every call as a `CanvasCommand` instead of executing it:
37
+
38
+ ```ts
39
+ type CanvasCommand =
40
+ | { type: 'property'; name: string; value: unknown }
41
+ | { type: 'method'; name: string; args: unknown[] };
42
+ ```
43
+
44
+ Commands accumulate until drained via `takeCommands()`, which returns the buffer and resets it — a classic double-buffer drain pattern.
45
+
46
+ ### WebGL execution
47
+
48
+ `Canvas2DShim` walks a command batch and maps each operation to WebGL2 primitives:
49
+
50
+ | Canvas 2D operation | WebGL2 implementation |
51
+ |---|---|
52
+ | `clearRect` | `gl.scissor()` + `gl.clear()` |
53
+ | `fillRect` | Unit quad VBO + flat-color shader + matrix transform |
54
+ | `strokeRect` | Four thin quads (one per edge) |
55
+ | `beginPath`/`lineTo`/`stroke` | Line segments expanded to quads on CPU, rendered as triangles |
56
+ | `fillText`/`strokeText` | Rasterize to OffscreenCanvas 2D, upload as texture, draw textured quad |
57
+ | `save`/`restore` | Matrix stack + state stack push/pop |
58
+ | Transforms | 3×3 affine matrix stack (column-major for `uniformMatrix3fv`) |
59
+
60
+ ### Text rendering
61
+
62
+ Text is rasterized on a dedicated `OffscreenCanvas` (512×128) using the browser's native 2D text shaping, then uploaded to a WebGL texture via `texImage2D()`. This reuses the browser's full font stack — kerning, ligatures, `font` shorthand — without reimplementing any of it.
63
+
64
+ ## API overview
65
+
66
+ ```ts
67
+ import { UltrafastRenderer, CanvasAPI } from 'canvas-ultrafast';
68
+
69
+ // Create renderer — attaches a <canvas> with a WebGL2 context
70
+ const renderer = new UltrafastRenderer(width, height, container);
71
+
72
+ // Get the recording API (Canvas 2D-compatible)
73
+ const api: CanvasAPI = renderer.getCanvasAPI();
74
+
75
+ // Draw as usual
76
+ api.fillStyle = '#ff0000';
77
+ api.fillRect(10, 10, 100, 50);
78
+ api.font = '24px monospace';
79
+ api.fillText('hello', 20, 80);
80
+
81
+ // In the default auto-flush mode, the RAF loop drains commands
82
+ // and displays each frame automatically. Nothing else needed.
83
+ ```
84
+
85
+ ### Key exports
86
+
87
+ | Export | Role |
88
+ |---|---|
89
+ | `UltrafastRenderer` | WebGL2 context, FBO management, display loop |
90
+ | `CanvasAPI` | Command recording interface (Canvas 2D-compatible) |
91
+ | `CanvasCommand` | Type definition for recorded commands |
92
+ | `MatrixStack` | 3×3 affine transform stack |
93
+ | `parseColor()` | CSS color string → `Float32Array [r,g,b,a]` |
94
+
95
+ ### Renderer methods
96
+
97
+ | Method | Description |
98
+ |---|---|
99
+ | `getCanvasAPI()` | Return the `CanvasAPI` instance |
100
+ | `submitBatch(commands)` | Execute commands into the write FBO, swap write ↔ ready |
101
+ | `startDisplay()` / `stopDisplay()` | Control the RAF display loop |
102
+ | `getReadyTexture()` | Return the ready FBO's texture for external rendering |
103
+ | `getGL()` | Access the underlying `WebGL2RenderingContext` |
104
+ | `getCanvas()` / `getCanvasSize()` | Canvas element and dimensions |
105
+ | `screenshot()` | Capture the current display frame as `ImageBitmap` |
106
+ | `destroy()` | Release all WebGL resources |
107
+
108
+ ## Extension points
109
+
110
+ canvas-ultrafast is designed to be taken over by a downstream consumer. The pattern:
111
+
112
+ ```ts
113
+ const renderer = new UltrafastRenderer(w, h, el);
114
+ renderer.stopDisplay(); // halt the built-in RAF loop
115
+
116
+ // Now you control timing:
117
+ const commands = renderer.getCanvasAPI().takeCommands();
118
+ renderer.submitBatch(commands); // write FBO ← commands, swap write ↔ ready
119
+
120
+ // Read the result for custom post-processing:
121
+ const texture = renderer.getReadyTexture();
122
+ const gl = renderer.getGL();
123
+ // → bind texture, apply your own shader, draw to screen
124
+ ```
125
+
126
+ [maalata](https://github.com/emansom/maalata) uses this to insert a 4-stage latency pipeline (USB → OS → App → LCD) and apply CRT post-processing (barrel distortion, scanlines, chromatic aberration, phosphor glow, black frame insertion) — all without any changes to canvas-ultrafast.
127
+
128
+ ## Inspiration & prior art
129
+
130
+ canvas-ultrafast builds on well-established techniques from graphics programming and the web platform. Credit where it's due:
131
+
132
+ **Triple buffering** — A standard technique in graphics and game engines for decoupling rendering from display, eliminating tearing without the latency penalty of double buffering. Widely used in GPU drivers and compositors.
133
+
134
+ **Command recording / deferred rendering** — Inspired by the command buffer model in modern graphics APIs (Vulkan, Metal, Direct3D 12), where draw calls are recorded first and submitted for execution later. This separation enables batching, reordering, and custom timing.
135
+
136
+ **WebGL-accelerated Canvas 2D** — Libraries like [PixiJS](https://pixijs.com/), [Two.js](https://two.js.org/), and Google's [Skia/CanvasKit](https://skia.org/docs/user/modules/canvaskit/) have long demonstrated the performance benefits of executing 2D drawing operations through WebGL rather than the browser's built-in Canvas 2D implementation.
137
+
138
+ **`desynchronized` canvas hint** — The [`desynchronized`](https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-desynchronized) context attribute from the HTML spec (originating from the [Low Latency Canvas](https://discourse.wicg.io/t/proposal-delegated-ink-trail/4255/) proposal) bypasses the compositor for lower-latency rendering. canvas-ultrafast enables this by default.
139
+
140
+ **OffscreenCanvas for text rasterization** — Using [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) with a 2D context as a glyph rendering surface, then uploading to a WebGL texture. This avoids reimplementing font shaping while keeping the rendering pipeline in WebGL.
141
+
142
+ **VBO orphaning** — A well-known pattern for streaming dynamic geometry to the GPU without stalling the pipeline. By calling `bufferData()` with a new size before `bufferSubData()`, the driver can allocate a fresh buffer and let the GPU finish reading from the old one. Described in the [OpenGL wiki](https://www.khronos.org/opengl/wiki/Buffer_Object_Streaming#Buffer_re-specification).
143
+
144
+ ## License
145
+
146
+ [AGPL-3.0-only](LICENSE)