@glissade/backend-canvas2d 0.1.0 → 0.3.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 +26 -0
- package/dist/index.d.ts +4 -16
- package/dist/index.js +14 -170
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @glissade/backend-canvas2d
|
|
2
|
+
|
|
3
|
+
The browser rasterizer: consumes a DisplayList and draws it to a `<canvas>` / `OffscreenCanvas` 2D context — transforms, paths, text, group compositing. Per-path deterministic twin of `@glissade/backend-skia`; the two are held together by an SSIM parity suite in CI.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i @glissade/backend-canvas2d
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Canvas2DBackend } from '@glissade/backend-canvas2d';
|
|
11
|
+
import { evaluate } from '@glissade/scene';
|
|
12
|
+
|
|
13
|
+
const backend = new Canvas2DBackend(canvas);
|
|
14
|
+
backend.render(evaluate(scene, doc, t));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Most apps don't use this directly — `mount()` from `@glissade/player` wires it up.
|
|
18
|
+
|
|
19
|
+
## Part of glissade
|
|
20
|
+
|
|
21
|
+
*(glide & slide)* — programmatic motion graphics for TypeScript: realtime-first in any web page, deterministic headless video export from the same code, a visual studio over the same document. No generator functions.
|
|
22
|
+
|
|
23
|
+
- [Repository & full README](https://github.com/tyevco/glissade)
|
|
24
|
+
- [Getting started](https://github.com/tyevco/glissade/blob/main/docs/getting-started.md) · [Concepts](https://github.com/tyevco/glissade/blob/main/docs/concepts.md) · [Interactivity](https://github.com/tyevco/glissade/blob/main/docs/interactivity.md)
|
|
25
|
+
|
|
26
|
+
Apache-2.0.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
import { DisplayList, DrawCommand, FontSpec, VideoFrameSource } from "@glissade/scene";
|
|
1
|
+
import { DisplayList, DrawCommand, FontSpec, TextMetricsLite, TextMetricsLite as TextMetricsLite$1, VideoFrameSource } from "@glissade/scene";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
|
|
5
5
|
type Drawable = Exclude<CanvasImageSource, SVGImageElement>;
|
|
6
6
|
type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
7
|
-
interface TextMetricsLite {
|
|
8
|
-
width: number;
|
|
9
|
-
ascent: number;
|
|
10
|
-
descent: number;
|
|
11
|
-
}
|
|
12
7
|
declare class Canvas2DBackend {
|
|
13
8
|
private readonly target;
|
|
14
|
-
private readonly
|
|
15
|
-
private pathCache;
|
|
16
|
-
private readonly images;
|
|
17
|
-
private readonly videos;
|
|
9
|
+
private readonly raster;
|
|
18
10
|
constructor(target: AnyCanvas);
|
|
19
11
|
/** Register a decoded still (kind 'image' assets). */
|
|
20
12
|
setImageAsset(assetId: string, image: Drawable): void;
|
|
21
13
|
/** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
|
|
22
14
|
setVideoAsset(assetId: string, source: VideoFrameSource): void;
|
|
23
|
-
|
|
24
|
-
measureText(text: string, font: FontSpec): TextMetricsLite;
|
|
15
|
+
measureText(text: string, font: FontSpec): TextMetricsLite$1;
|
|
25
16
|
render(list: DisplayList): void;
|
|
26
17
|
readPixels(): Promise<Uint8ClampedArray>;
|
|
27
18
|
dispose(): void;
|
|
28
19
|
private context;
|
|
29
|
-
private path;
|
|
30
|
-
private acquire;
|
|
31
|
-
private release;
|
|
32
20
|
}
|
|
33
21
|
//#endregion
|
|
34
|
-
export { Canvas2DBackend, type DisplayList, type DrawCommand, TextMetricsLite };
|
|
22
|
+
export { Canvas2DBackend, type DisplayList, type DrawCommand, type TextMetricsLite };
|
package/dist/index.js
CHANGED
|
@@ -1,71 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Raster2D, fontString } from "@glissade/scene";
|
|
2
2
|
//#region src/index.ts
|
|
3
3
|
/**
|
|
4
4
|
* @glissade/backend-canvas2d — rasterize DisplayLists onto Canvas 2D
|
|
5
|
-
* (DESIGN.md §3.4–§3.5).
|
|
6
|
-
*
|
|
5
|
+
* (DESIGN.md §3.4–§3.5). A thin adapter over the shared Raster2D interpreter
|
|
6
|
+
* in @glissade/scene: this file owns only the DOM canvas flavor (context
|
|
7
|
+
* acquisition, OffscreenCanvas layers, Path2D) and text measurement.
|
|
7
8
|
*/
|
|
8
|
-
function fontString(font) {
|
|
9
|
-
return `${font.style === "italic" ? "italic " : ""}${font.weight !== void 0 && font.weight !== 400 ? `${font.weight} ` : ""}${font.size}px ${font.family}`;
|
|
10
|
-
}
|
|
11
|
-
function buildPath(segs) {
|
|
12
|
-
const p = new Path2D();
|
|
13
|
-
for (const seg of segs) switch (seg[0]) {
|
|
14
|
-
case "M":
|
|
15
|
-
p.moveTo(seg[1], seg[2]);
|
|
16
|
-
break;
|
|
17
|
-
case "L":
|
|
18
|
-
p.lineTo(seg[1], seg[2]);
|
|
19
|
-
break;
|
|
20
|
-
case "C":
|
|
21
|
-
p.bezierCurveTo(seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]);
|
|
22
|
-
break;
|
|
23
|
-
case "Q":
|
|
24
|
-
p.quadraticCurveTo(seg[1], seg[2], seg[3], seg[4]);
|
|
25
|
-
break;
|
|
26
|
-
case "E":
|
|
27
|
-
p.ellipse(seg[1], seg[2], seg[3], seg[4], seg[5], seg[6], seg[7]);
|
|
28
|
-
break;
|
|
29
|
-
case "Z":
|
|
30
|
-
p.closePath();
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
return p;
|
|
34
|
-
}
|
|
35
9
|
var Canvas2DBackend = class {
|
|
36
10
|
target;
|
|
37
|
-
|
|
38
|
-
pathCache = /* @__PURE__ */ new WeakMap();
|
|
39
|
-
images = /* @__PURE__ */ new Map();
|
|
40
|
-
videos = /* @__PURE__ */ new Map();
|
|
11
|
+
raster;
|
|
41
12
|
constructor(target) {
|
|
42
13
|
this.target = target;
|
|
14
|
+
this.raster = new Raster2D({
|
|
15
|
+
context: (c) => this.context(c),
|
|
16
|
+
createCanvas: (w, h) => new OffscreenCanvas(w, h),
|
|
17
|
+
newPath: () => new Path2D()
|
|
18
|
+
});
|
|
43
19
|
}
|
|
44
20
|
/** Register a decoded still (kind 'image' assets). */
|
|
45
21
|
setImageAsset(assetId, image) {
|
|
46
|
-
this.
|
|
22
|
+
this.raster.setImageAsset(assetId, image);
|
|
47
23
|
}
|
|
48
24
|
/** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
|
|
49
25
|
setVideoAsset(assetId, source) {
|
|
50
|
-
this.
|
|
51
|
-
}
|
|
52
|
-
resolveDrawable(res, id) {
|
|
53
|
-
if (res.kind === "image") {
|
|
54
|
-
const img = this.images.get(res.assetId);
|
|
55
|
-
if (!img) throw new ColdAssetError(res.assetId, "no decoded image registered");
|
|
56
|
-
return img;
|
|
57
|
-
}
|
|
58
|
-
if (res.kind === "videoFrame") {
|
|
59
|
-
const source = this.videos.get(res.assetId);
|
|
60
|
-
if (!source) throw new ColdAssetError(res.assetId, "no VideoFrameSource registered", res.mediaT);
|
|
61
|
-
try {
|
|
62
|
-
return source.getFrameSync(res.mediaT);
|
|
63
|
-
} catch (e) {
|
|
64
|
-
if (e instanceof ColdAssetError) throw new ColdAssetError(res.assetId, e.detail, res.mediaT);
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
throw new Error(`resource ${id} is not drawable`);
|
|
26
|
+
this.raster.setVideoAsset(assetId, source);
|
|
69
27
|
}
|
|
70
28
|
measureText(text, font) {
|
|
71
29
|
const ctx = this.context(this.target);
|
|
@@ -80,133 +38,19 @@ var Canvas2DBackend = class {
|
|
|
80
38
|
};
|
|
81
39
|
}
|
|
82
40
|
render(list) {
|
|
83
|
-
|
|
84
|
-
const { w, h } = list.size;
|
|
85
|
-
if (this.target.width !== w) this.target.width = w;
|
|
86
|
-
if (this.target.height !== h) this.target.height = h;
|
|
87
|
-
base.resetTransform();
|
|
88
|
-
base.clearRect(0, 0, w, h);
|
|
89
|
-
const layers = [{
|
|
90
|
-
ctx: base,
|
|
91
|
-
canvas: null,
|
|
92
|
-
opacity: 1,
|
|
93
|
-
blend: "source-over"
|
|
94
|
-
}];
|
|
95
|
-
const ctxOf = () => layers[layers.length - 1].ctx;
|
|
96
|
-
for (const cmd of list.commands) switch (cmd.op) {
|
|
97
|
-
case "save":
|
|
98
|
-
ctxOf().save();
|
|
99
|
-
break;
|
|
100
|
-
case "restore":
|
|
101
|
-
ctxOf().restore();
|
|
102
|
-
break;
|
|
103
|
-
case "transform":
|
|
104
|
-
ctxOf().transform(cmd.m[0], cmd.m[1], cmd.m[2], cmd.m[3], cmd.m[4], cmd.m[5]);
|
|
105
|
-
break;
|
|
106
|
-
case "clip":
|
|
107
|
-
ctxOf().clip(this.path(list.resources, cmd.path), cmd.rule ?? "nonzero");
|
|
108
|
-
break;
|
|
109
|
-
case "fillPath": {
|
|
110
|
-
const ctx = ctxOf();
|
|
111
|
-
ctx.fillStyle = cmd.paint.color;
|
|
112
|
-
ctx.fill(this.path(list.resources, cmd.path));
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case "strokePath": {
|
|
116
|
-
const ctx = ctxOf();
|
|
117
|
-
ctx.strokeStyle = cmd.paint.color;
|
|
118
|
-
ctx.lineWidth = cmd.stroke.width;
|
|
119
|
-
ctx.lineCap = cmd.stroke.cap ?? "butt";
|
|
120
|
-
ctx.lineJoin = cmd.stroke.join ?? "miter";
|
|
121
|
-
if (cmd.stroke.dash) ctx.setLineDash(cmd.stroke.dash);
|
|
122
|
-
ctx.stroke(this.path(list.resources, cmd.path));
|
|
123
|
-
if (cmd.stroke.dash) ctx.setLineDash([]);
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
case "fillText": {
|
|
127
|
-
const ctx = ctxOf();
|
|
128
|
-
ctx.font = fontString(cmd.font);
|
|
129
|
-
ctx.fillStyle = cmd.paint.color;
|
|
130
|
-
ctx.textBaseline = "alphabetic";
|
|
131
|
-
ctx.textAlign = cmd.align ?? "left";
|
|
132
|
-
ctx.fillText(cmd.text, cmd.x, cmd.y);
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
case "drawImage": {
|
|
136
|
-
const res = list.resources[cmd.image];
|
|
137
|
-
if (!res) throw new Error(`drawImage references missing resource ${cmd.image}`);
|
|
138
|
-
const drawable = this.resolveDrawable(res, cmd.image);
|
|
139
|
-
const ctx = ctxOf();
|
|
140
|
-
if (cmd.smoothing !== void 0) ctx.imageSmoothingEnabled = cmd.smoothing;
|
|
141
|
-
const { x, y, w: dw, h: dh } = cmd.dst;
|
|
142
|
-
if (cmd.src) ctx.drawImage(drawable, cmd.src.x, cmd.src.y, cmd.src.w, cmd.src.h, x, y, dw, dh);
|
|
143
|
-
else ctx.drawImage(drawable, x, y, dw, dh);
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
case "pushGroup": {
|
|
147
|
-
const parent = ctxOf();
|
|
148
|
-
const layerCanvas = this.acquire(w, h);
|
|
149
|
-
const layerCtx = layerCanvas.getContext("2d");
|
|
150
|
-
layerCtx.resetTransform();
|
|
151
|
-
layerCtx.clearRect(0, 0, w, h);
|
|
152
|
-
layerCtx.setTransform(parent.getTransform());
|
|
153
|
-
layers.push({
|
|
154
|
-
ctx: layerCtx,
|
|
155
|
-
canvas: layerCanvas,
|
|
156
|
-
opacity: cmd.opacity,
|
|
157
|
-
blend: cmd.blend
|
|
158
|
-
});
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case "popGroup": {
|
|
162
|
-
const layer = layers.pop();
|
|
163
|
-
if (!layer || layer.canvas === null) throw new Error("popGroup without matching pushGroup");
|
|
164
|
-
const parent = ctxOf();
|
|
165
|
-
parent.save();
|
|
166
|
-
parent.resetTransform();
|
|
167
|
-
parent.globalAlpha = layer.opacity;
|
|
168
|
-
parent.globalCompositeOperation = layer.blend;
|
|
169
|
-
parent.drawImage(layer.canvas, 0, 0);
|
|
170
|
-
parent.restore();
|
|
171
|
-
this.release(layer.canvas);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (layers.length !== 1) throw new Error("unbalanced pushGroup/popGroup in DisplayList");
|
|
41
|
+
this.raster.render(this.target, list);
|
|
176
42
|
}
|
|
177
43
|
async readPixels() {
|
|
178
44
|
return this.context(this.target).getImageData(0, 0, this.target.width, this.target.height).data;
|
|
179
45
|
}
|
|
180
46
|
dispose() {
|
|
181
|
-
this.
|
|
47
|
+
this.raster.dispose();
|
|
182
48
|
}
|
|
183
49
|
context(canvas) {
|
|
184
50
|
const ctx = canvas.getContext("2d");
|
|
185
51
|
if (!ctx) throw new Error("canvas 2d context unavailable");
|
|
186
52
|
return ctx;
|
|
187
53
|
}
|
|
188
|
-
path(resources, id) {
|
|
189
|
-
const res = resources[id];
|
|
190
|
-
if (!res || res.kind !== "path") throw new Error(`resource ${id} is not a path`);
|
|
191
|
-
let p = this.pathCache.get(res);
|
|
192
|
-
if (!p) {
|
|
193
|
-
p = buildPath(res.segs);
|
|
194
|
-
this.pathCache.set(res, p);
|
|
195
|
-
}
|
|
196
|
-
return p;
|
|
197
|
-
}
|
|
198
|
-
acquire(w, h) {
|
|
199
|
-
const pooled = this.pool.pop();
|
|
200
|
-
if (pooled) {
|
|
201
|
-
if (pooled.width !== w) pooled.width = w;
|
|
202
|
-
if (pooled.height !== h) pooled.height = h;
|
|
203
|
-
return pooled;
|
|
204
|
-
}
|
|
205
|
-
return new OffscreenCanvas(w, h);
|
|
206
|
-
}
|
|
207
|
-
release(canvas) {
|
|
208
|
-
if (this.pool.length < 8) this.pool.push(canvas);
|
|
209
|
-
}
|
|
210
54
|
};
|
|
211
55
|
//#endregion
|
|
212
56
|
export { Canvas2DBackend };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/backend-canvas2d",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "glissade Canvas 2D render backend: DisplayList -> CanvasRenderingContext2D/OffscreenCanvas.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@glissade/core": "0.
|
|
19
|
-
"@glissade/scene": "0.
|
|
18
|
+
"@glissade/core": "0.3.0",
|
|
19
|
+
"@glissade/scene": "0.3.0"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|