@glissade/backend-canvas2d 0.2.0 → 0.4.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,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,33 @@
1
- import { DisplayList, DrawCommand, FontSpec, VideoFrameSource } from "@glissade/scene";
1
+ import { DisplayList, DrawCommand, FontSpec, ShaderCaps, ShaderRef, TextMetricsLite, TextMetricsLite as TextMetricsLite$1, VideoFrameSource } from "@glissade/scene";
2
2
 
3
3
  //#region src/index.d.ts
4
4
 
5
+ /**
6
+ * §3.7 shader runner seam: @glissade/effects-webgpu registers here at load
7
+ * time (the loadYogaLayoutEngine pattern). This package never imports GPU
8
+ * code — headless paths stay clean by construction.
9
+ */
10
+ interface ShaderRunner {
11
+ apply(layer: AnyCanvas, shader: ShaderRef, w: number, h: number): Drawable | null;
12
+ }
13
+ declare function setShaderRunner(runner: ShaderRunner | null): void;
5
14
  type Drawable = Exclude<CanvasImageSource, SVGImageElement>;
6
15
  type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
7
- interface TextMetricsLite {
8
- width: number;
9
- ascent: number;
10
- descent: number;
11
- }
12
16
  declare class Canvas2DBackend {
13
17
  private readonly target;
14
- private readonly pool;
15
- private pathCache;
16
- private readonly images;
17
- private readonly videos;
18
- constructor(target: AnyCanvas);
18
+ private readonly raster;
19
+ constructor(target: AnyCanvas, opts?: {
20
+ shaderCaps?: ShaderCaps;
21
+ });
19
22
  /** Register a decoded still (kind 'image' assets). */
20
23
  setImageAsset(assetId: string, image: Drawable): void;
21
24
  /** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
22
25
  setVideoAsset(assetId: string, source: VideoFrameSource): void;
23
- private resolveDrawable;
24
- measureText(text: string, font: FontSpec): TextMetricsLite;
26
+ measureText(text: string, font: FontSpec): TextMetricsLite$1;
25
27
  render(list: DisplayList): void;
26
28
  readPixels(): Promise<Uint8ClampedArray>;
27
29
  dispose(): void;
28
30
  private context;
29
- private path;
30
- private acquire;
31
- private release;
32
31
  }
33
32
  //#endregion
34
- export { Canvas2DBackend, type DisplayList, type DrawCommand, TextMetricsLite };
33
+ export { Canvas2DBackend, type DisplayList, type DrawCommand, ShaderRunner, type TextMetricsLite, setShaderRunner };
package/dist/index.js CHANGED
@@ -1,71 +1,34 @@
1
- import { ColdAssetError } from "@glissade/scene";
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). Groups realize as pooled temporary canvases so
6
- * group opacity/blend composite correctly (children don't individually fade).
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;
9
+ let shaderRunner = null;
10
+ function setShaderRunner(runner) {
11
+ shaderRunner = runner;
34
12
  }
35
13
  var Canvas2DBackend = class {
36
14
  target;
37
- pool = [];
38
- pathCache = /* @__PURE__ */ new WeakMap();
39
- images = /* @__PURE__ */ new Map();
40
- videos = /* @__PURE__ */ new Map();
41
- constructor(target) {
15
+ raster;
16
+ constructor(target, opts = {}) {
42
17
  this.target = target;
18
+ this.raster = new Raster2D({
19
+ context: (c) => this.context(c),
20
+ createCanvas: (w, h) => new OffscreenCanvas(w, h),
21
+ newPath: () => new Path2D(),
22
+ applyShader: (layer, shader, w, h) => shaderRunner?.apply(layer, shader, w, h) ?? null
23
+ }, opts.shaderCaps ?? "warn");
43
24
  }
44
25
  /** Register a decoded still (kind 'image' assets). */
45
26
  setImageAsset(assetId, image) {
46
- this.images.set(assetId, image);
27
+ this.raster.setImageAsset(assetId, image);
47
28
  }
48
29
  /** Register a warmed-on-demand video source (kind 'video' assets, §3.8). */
49
30
  setVideoAsset(assetId, source) {
50
- this.videos.set(assetId, source);
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`);
31
+ this.raster.setVideoAsset(assetId, source);
69
32
  }
70
33
  measureText(text, font) {
71
34
  const ctx = this.context(this.target);
@@ -80,133 +43,19 @@ var Canvas2DBackend = class {
80
43
  };
81
44
  }
82
45
  render(list) {
83
- const base = this.context(this.target);
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");
46
+ this.raster.render(this.target, list);
176
47
  }
177
48
  async readPixels() {
178
49
  return this.context(this.target).getImageData(0, 0, this.target.width, this.target.height).data;
179
50
  }
180
51
  dispose() {
181
- this.pool.length = 0;
52
+ this.raster.dispose();
182
53
  }
183
54
  context(canvas) {
184
55
  const ctx = canvas.getContext("2d");
185
56
  if (!ctx) throw new Error("canvas 2d context unavailable");
186
57
  return ctx;
187
58
  }
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
59
  };
211
60
  //#endregion
212
- export { Canvas2DBackend };
61
+ export { Canvas2DBackend, setShaderRunner };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/backend-canvas2d",
3
- "version": "0.2.0",
3
+ "version": "0.4.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.2.0",
19
- "@glissade/scene": "0.2.0"
18
+ "@glissade/core": "0.4.0",
19
+ "@glissade/scene": "0.4.0"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",