@cyanheads/pixoo-mcp-server 1.2.1 → 1.2.2

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.
Files changed (48) hide show
  1. package/AGENTS.md +5 -2
  2. package/CLAUDE.md +5 -2
  3. package/README.md +13 -9
  4. package/changelog/1.2.x/1.2.2.md +40 -0
  5. package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.d.ts.map +1 -1
  6. package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.js +4 -1
  7. package/dist/mcp-server/resources/definitions/pixoo-design-guide.resource.js.map +1 -1
  8. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts +96 -2
  9. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js +85 -10
  11. package/dist/mcp-server/tools/definitions/pixoo-compose-scene.tool.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.js +14 -6
  14. package/dist/mcp-server/tools/definitions/pixoo-design-brief.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts +2 -0
  16. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js +27 -12
  18. package/dist/mcp-server/tools/definitions/pixoo-display-text.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.d.ts +30 -1
  20. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js +105 -26
  22. package/dist/mcp-server/tools/definitions/pixoo-push-image.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/finish-schema.d.ts +28 -0
  24. package/dist/mcp-server/tools/finish-schema.d.ts.map +1 -0
  25. package/dist/mcp-server/tools/finish-schema.js +42 -0
  26. package/dist/mcp-server/tools/finish-schema.js.map +1 -0
  27. package/dist/renderer/finish.d.ts +34 -0
  28. package/dist/renderer/finish.d.ts.map +1 -0
  29. package/dist/renderer/finish.js +62 -0
  30. package/dist/renderer/finish.js.map +1 -0
  31. package/dist/renderer/keyframes.d.ts +5 -1
  32. package/dist/renderer/keyframes.d.ts.map +1 -1
  33. package/dist/renderer/keyframes.js +16 -2
  34. package/dist/renderer/keyframes.js.map +1 -1
  35. package/dist/renderer/remote-image.d.ts +9 -7
  36. package/dist/renderer/remote-image.d.ts.map +1 -1
  37. package/dist/renderer/remote-image.js +13 -21
  38. package/dist/renderer/remote-image.js.map +1 -1
  39. package/dist/renderer/scene-renderer.d.ts +45 -6
  40. package/dist/renderer/scene-renderer.d.ts.map +1 -1
  41. package/dist/renderer/scene-renderer.js +203 -65
  42. package/dist/renderer/scene-renderer.js.map +1 -1
  43. package/dist/renderer/text-engine.d.ts +24 -5
  44. package/dist/renderer/text-engine.d.ts.map +1 -1
  45. package/dist/renderer/text-engine.js +51 -28
  46. package/dist/renderer/text-engine.js.map +1 -1
  47. package/package.json +2 -2
  48. package/server.json +3 -3
@@ -1,11 +1,9 @@
1
1
  /**
2
- * @fileoverview Remote image fetch — downloads an https image to a temp PNG for the
3
- * toolkit's `loadImage`, which reads from disk only. Shared by the scene renderer's
4
- * image elements and the pixoo_push_image tool.
2
+ * @fileoverview Remote image fetch — downloads an https image's encoded bytes into
3
+ * memory for the toolkit's image loaders. Shared by the scene renderer's image elements
4
+ * and the pixoo_push_image tool.
5
5
  * @module renderer/remote-image
6
6
  */
7
- import * as os from 'node:os';
8
- import * as path from 'node:path';
9
7
  import { notFound } from '@cyanheads/mcp-ts-core/errors';
10
8
  import { fetchWithTimeout } from '@cyanheads/mcp-ts-core/utils';
11
9
  /** Wall-clock budget for a remote image fetch, headers through body. */
@@ -17,8 +15,9 @@ export function isRemoteSource(source) {
17
15
  return source.startsWith('https://') || source.startsWith('http://');
18
16
  }
19
17
  /**
20
- * Fetch an https image and write it to a temp PNG, returning the path. The caller
21
- * owns the file and must unlink it once `loadImage` has read it.
18
+ * Fetch an https image and return its bytes as served, undecoded. Nothing is written to
19
+ * disk: the caller passes the bytes to the toolkit's loader, where a body that is not an image
20
+ * fails to decode.
22
21
  *
23
22
  * Private and loopback addresses are deliberately reachable: this server drives a
24
23
  * LAN device, so a NAS or local web server is a legitimate image host.
@@ -27,19 +26,20 @@ export function isRemoteSource(source) {
27
26
  * down instead of reading on to the deadline or the byte cap.
28
27
  *
29
28
  * @throws {McpError} NotFound with `reason: 'asset_not_found'` for a non-https URL,
30
- * an unreachable or non-2xx endpoint, or a response over {@link MAX_IMAGE_BYTES}.
29
+ * an unreachable or non-2xx endpoint, or a response over {@link MAX_IMAGE_BYTES}. The
30
+ * message names the failure; the recovery is the calling tool's declared one.
31
31
  * @throws {McpError} RequestCancelled when `ctx.signal` aborts while the body streams.
32
32
  * @throws {McpError} Timeout when the body outlasts {@link FETCH_TIMEOUT_MS}, or when the
33
33
  * `ctx.signal` abort is a caller-side deadline (a `TimeoutError` reason). A tool handler
34
34
  * still answers the deadline as RequestCancelled: the handler factory settles any throw
35
35
  * after the request's signal fired as a cancellation.
36
36
  */
37
- export async function fetchRemoteImageToTempPng(source, ctx) {
37
+ export async function fetchRemoteImageBytes(source, ctx) {
38
38
  if (!source.startsWith('https://')) {
39
39
  throw notFound(`Only https URLs are supported. Received: "${source}".`, {
40
40
  reason: 'asset_not_found',
41
41
  url: source,
42
- recovery: { hint: 'Use an https URL, or pass an absolute local file path instead.' },
42
+ ...ctx.recoveryFor('asset_not_found'),
43
43
  });
44
44
  }
45
45
  // A throw here after the signal fired still reaches the caller as a cancellation:
@@ -47,18 +47,14 @@ export async function fetchRemoteImageToTempPng(source, ctx) {
47
47
  const resp = await fetchWithTimeout(source, FETCH_TIMEOUT_MS, ctx, {
48
48
  signal: ctx.signal,
49
49
  }).catch((err) => {
50
- throw notFound(`Failed to fetch image from "${source}": ${err instanceof Error ? err.message : String(err)}`, {
51
- reason: 'asset_not_found',
52
- url: source,
53
- recovery: { hint: 'Check the URL is reachable and returns an image, then retry.' },
54
- });
50
+ throw notFound(`Failed to fetch image from "${source}": ${err instanceof Error ? err.message : String(err)}`, { reason: 'asset_not_found', url: source, ...ctx.recoveryFor('asset_not_found') });
55
51
  });
56
52
  /** Both size gates fail the same way; only the field naming the measurement differs. */
57
53
  const tooLarge = (bytes, field) => notFound(`Image response too large (${bytes} bytes; limit: ${MAX_IMAGE_BYTES}).`, {
58
54
  reason: 'asset_not_found',
59
55
  url: source,
60
56
  [field]: bytes,
61
- recovery: { hint: 'Downscale the image before hosting it, or point at a smaller file.' },
57
+ ...ctx.recoveryFor('asset_not_found'),
62
58
  });
63
59
  // content-length is advisory: it bails before the body is read, but the byte
64
60
  // count taken while streaming is the gate that actually holds.
@@ -83,10 +79,6 @@ export async function fetchRemoteImageToTempPng(source, ctx) {
83
79
  chunks.push(value);
84
80
  }
85
81
  }
86
- const buf = Buffer.concat(chunks, received);
87
- const { default: sharp } = await import('sharp');
88
- const tmpPath = path.join(os.tmpdir(), `pixoo-img-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
89
- await sharp(buf).png().toFile(tmpPath);
90
- return tmpPath;
82
+ return Buffer.concat(chunks, received);
91
83
  }
92
84
  //# sourceMappingURL=remote-image.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote-image.js","sourceRoot":"","sources":["../../src/renderer/remote-image.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,wEAAwE;AACxE,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,0DAA0D;AAC1D,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzC,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,MAAc,EAAE,GAAY;IAC1E,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,CAAC,6CAA6C,MAAM,IAAI,EAAE;YACtE,MAAM,EAAE,iBAAiB;YACzB,GAAG,EAAE,MAAM;YACX,QAAQ,EAAE,EAAE,IAAI,EAAE,gEAAgE,EAAE;SACrF,CAAC,CAAC;IACL,CAAC;IAED,kFAAkF;IAClF,oFAAoF;IACpF,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE;QACjE,MAAM,EAAE,GAAG,CAAC,MAAM;KACnB,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACxB,MAAM,QAAQ,CACZ,+BAA+B,MAAM,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAC7F;YACE,MAAM,EAAE,iBAAiB;YACzB,GAAG,EAAE,MAAM;YACX,QAAQ,EAAE,EAAE,IAAI,EAAE,8DAA8D,EAAE;SACnF,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,wFAAwF;IACxF,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAE,KAAqC,EAAE,EAAE,CACxE,QAAQ,CAAC,6BAA6B,KAAK,kBAAkB,eAAe,IAAI,EAAE;QAChF,MAAM,EAAE,iBAAiB;QACzB,GAAG,EAAE,MAAM;QACX,CAAC,KAAK,CAAC,EAAE,KAAK;QACd,QAAQ,EAAE,EAAE,IAAI,EAAE,oEAAoE,EAAE;KACzF,CAAC,CAAC;IAEL,6EAA6E;IAC7E,+DAA+D;IAC/D,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IACtE,IAAI,aAAa,GAAG,eAAe,EAAE,CAAC;QACpC,MAAM,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAC1B,MAAM,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;IACtC,IAAI,MAAM,EAAE,CAAC;QACX,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC;YAC7B,IAAI,QAAQ,GAAG,eAAe,EAAE,CAAC;gBAC/B,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;gBACtB,MAAM,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;YACzC,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAE5C,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CACvB,EAAE,CAAC,MAAM,EAAE,EACX,aAAa,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CACrE,CAAC;IACF,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"remote-image.js","sourceRoot":"","sources":["../../src/renderer/remote-image.ts"],"names":[],"mappings":"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"}
@@ -3,9 +3,10 @@
3
3
  * @module renderer/scene-renderer
4
4
  */
5
5
  import type { Context } from '@cyanheads/mcp-ts-core';
6
- import { Canvas, downsampleSprite, type PixooSize } from '@cyanheads/pixoo-toolkit';
6
+ import { type BlendMode, Canvas, downsampleSprite, type PixooSize } from '@cyanheads/pixoo-toolkit';
7
+ import { type Finish } from './finish.js';
7
8
  import { type EffectName, type KeyframeMap } from './keyframes.js';
8
- import { type LayoutEntry, type SemanticX, type SemanticY, type TextStyle } from './text-engine.js';
9
+ import { type FontVariant, type LayoutEntry, type SemanticX, type SemanticY, type TextStyle } from './text-engine.js';
9
10
  import { type PaletteName, type ThemeName } from './themes.js';
10
11
  /** Background specification. */
11
12
  export type BackgroundSpec = string | {
@@ -27,6 +28,8 @@ export interface EffectSpec {
27
28
  /** Base element properties. */
28
29
  interface BaseElement {
29
30
  animate?: KeyframeMap;
31
+ /** How the element's pixels combine with what lies beneath (default `normal`, source-over). */
32
+ blend?: BlendMode;
30
33
  dx?: number;
31
34
  dy?: number;
32
35
  effect?: EffectSpec;
@@ -36,7 +39,7 @@ interface BaseElement {
36
39
  /** Text element. */
37
40
  export interface TextElement extends BaseElement {
38
41
  color?: string;
39
- font?: 'standard' | 'compact';
42
+ font?: FontVariant;
40
43
  style?: TextStyle;
41
44
  text: string;
42
45
  type: 'text';
@@ -66,6 +69,8 @@ export interface RectElement extends BaseElement {
66
69
  to: string;
67
70
  };
68
71
  h: number;
72
+ /** `borderColor` border thickness in whole pixels, growing inward (default 1). */
73
+ strokeWidth?: number;
69
74
  type: 'rect';
70
75
  w: number;
71
76
  x: number;
@@ -73,16 +78,24 @@ export interface RectElement extends BaseElement {
73
78
  }
74
79
  /** Circle element. */
75
80
  export interface CircleElement extends BaseElement {
81
+ /** Shade the outline's pixels by coverage (default false). Outline only. */
82
+ antialias?: boolean;
76
83
  color?: string;
77
84
  cx: number;
78
85
  cy: number;
79
86
  fill?: boolean;
80
87
  radius: number;
88
+ /** Outline thickness in whole pixels, centered on the circle (default 1). Outline only. */
89
+ strokeWidth?: number;
81
90
  type: 'circle';
82
91
  }
83
92
  /** Line element. */
84
93
  export interface LineElement extends BaseElement {
94
+ /** Shade the line's pixels by coverage (default false). */
95
+ antialias?: boolean;
85
96
  color?: string;
97
+ /** Thickness in whole pixels, centered on the line (default 1). */
98
+ strokeWidth?: number;
86
99
  type: 'line';
87
100
  x0: number;
88
101
  x1: number;
@@ -132,6 +145,8 @@ export interface PixelsElement extends BaseElement {
132
145
  }
133
146
  /** Image element. */
134
147
  export interface ImageElement extends BaseElement {
148
+ /** Palette finish applied to the loaded image before it is drawn. */
149
+ finish?: Finish;
135
150
  fit?: 'contain' | 'cover' | 'fill';
136
151
  h?: number;
137
152
  kernel?: 'nearest' | 'lanczos3' | 'mitchell';
@@ -154,21 +169,45 @@ export interface SpriteElement extends BaseElement {
154
169
  y?: number;
155
170
  }
156
171
  export type SceneElement = TextElement | IconElement | RectElement | CircleElement | LineElement | ProgressElement | SparklineElement | BitmapElement | PixelsElement | ImageElement | SpriteElement;
172
+ /** A sprite sheet downsampled to its cell grid. */
173
+ type SpriteSheet = Awaited<ReturnType<typeof downsampleSprite>>;
157
174
  /**
158
175
  * Pre-loaded asset cache for images and sprites. Images are keyed by element: the
159
- * loader bakes each element's fit, size, and position into its canvas, so two
160
- * elements sharing a source still need a canvas each.
176
+ * loader bakes an image's fit, kernel, size, and position into its canvas, and a
177
+ * `finish` is applied to a copy of it, so elements share a canvas only when they share
178
+ * a source, all of those, and a finish. Sprites are keyed by {@link spriteKey}.
161
179
  */
162
180
  export interface AssetCache {
163
181
  images: Map<ImageElement, Canvas>;
164
- sprites: Map<string, Awaited<ReturnType<typeof downsampleSprite>>>;
182
+ sprites: Map<string, SpriteSheet>;
165
183
  }
184
+ /**
185
+ * The message for an image source that was read but did not decode: the source as the
186
+ * caller passed it, then the decoder's reason. Only the reason's first line is kept —
187
+ * libvips repeats itself across several lines on a corrupt header.
188
+ */
189
+ export declare function decodeFailureMessage(label: string, source: string, err: unknown): string;
166
190
  /**
167
191
  * Preload all async assets referenced in elements. Images load onto a `size` canvas,
168
192
  * the scene's own, so an image with no `w`/`h` fits the display.
169
193
  *
194
+ * Each distinct load runs once per call, however many elements share it: a URL is
195
+ * fetched once, a sprite sheet decodes once per grid, and an image decodes once per
196
+ * placement — source, fit, kernel, position, and size, all of which `loadImage` bakes
197
+ * into its canvas. `loadImage` decodes and places in one call, so a source placed two
198
+ * ways decodes twice. An image's `finish` runs once per placement and finish, on the
199
+ * placement's canvas, which it never mutates: elements differing only in `finish` share
200
+ * the decode and nothing after it.
201
+ *
170
202
  * `ctx` is threaded through so a remote image fetch correlates to the originating
171
203
  * request in logs and traces, and stops when that request is cancelled.
204
+ *
205
+ * @throws {McpError} NotFound with `reason: 'asset_not_found'` for a missing or
206
+ * unreadable path, a sprite path given as a URL, or a URL that cannot be fetched.
207
+ * @throws {McpError} InvalidParams with `reason: 'invalid_image'` for an image source
208
+ * or sprite sheet that was read but did not decode.
209
+ * @throws {Error} The toolkit's `Unknown color` error for an unresolvable `finish`
210
+ * palette entry.
172
211
  */
173
212
  export declare function preloadAssets(elements: SceneElement[], ctx: Context, size: PixooSize): Promise<AssetCache>;
174
213
  /** Apply a background spec to a canvas. */
@@ -1 +1 @@
1
- {"version":3,"file":"scene-renderer.d.ts","sourceRoot":"","sources":["../../src/renderer/scene-renderer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scene-renderer.d.ts","sourceRoot":"","sources":["../../src/renderer/scene-renderer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEtD,OAAO,EACL,KAAK,SAAS,EACd,MAAM,EACN,gBAAgB,EAMhB,KAAK,SAAS,EAKf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,KAAK,MAAM,EAAe,MAAM,aAAa,CAAC;AAEvD,OAAO,EAEL,KAAK,UAAU,EAGf,KAAK,WAAW,EACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,WAAW,EAGhB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAA+B,KAAK,WAAW,EAAU,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAEpG,gCAAgC;AAChC,MAAM,MAAM,cAAc,GACtB,MAAM,GACN;IAAE,QAAQ,EAAE;QAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACjE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AAEzB,yCAAyC;AACzC,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,+BAA+B;AAC/B,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,oBAAoB;AACpB,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,SAAS,CAAC;IACd,CAAC,CAAC,EAAE,SAAS,CAAC;CACf;AAED,oBAAoB;AACpB,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,SAAS,CAAC;IACd,CAAC,CAAC,EAAE,SAAS,CAAC;CACf;AAED,yBAAyB;AACzB,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,GAAG,GAAG,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,CAAC,EAAE,MAAM,CAAC;IACV,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,sBAAsB;AACtB,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,4EAA4E;IAC5E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,oBAAoB;AACpB,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,2BAA2B;AAC3B,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,wBAAwB;AACxB,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,6BAA6B;AAC7B,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,IAAI,EAAE,KAAK,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,qBAAqB;AACrB,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IACnC,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,sBAAsB;AACtB,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,CAAC,CAAC,EAAE,SAAS,CAAC;IACd,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,WAAW,GACX,aAAa,GACb,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,YAAY,GACZ,aAAa,CAAC;AAElB,mDAAmD;AACnD,KAAK,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACnC;AA0BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAGxF;AA6DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,YAAY,EAAE,EACxB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,SAAS,GACd,OAAO,CAAC,UAAU,CAAC,CA2CrB;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAyCxE;AA8JD,iEAAiE;AACjE,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,WAAW,EAAE,GAC3B,IAAI,CAsVN;AAED,+BAA+B;AAC/B,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,cAAc,EAC1B,QAAQ,EAAE,YAAY,EAAE,EACxB,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,EAAE,GAAG,EAAE,GAAG,EAAO,GACtB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,WAAW,EAAE,CAAA;CAAE,CAYlD;AAED,oCAAoC;AACpC,wBAAsB,WAAW,CAC/B,UAAU,EAAE,cAAc,EAC1B,QAAQ,EAAE,YAAY,EAAE,EACxB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,EACZ,IAAI,GAAE,SAAc,GACnB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,WAAW,EAAE,CAAA;CAAE,CAAC,CAsB7D"}
@@ -4,51 +4,115 @@
4
4
  */
5
5
  import * as fs from 'node:fs/promises';
6
6
  import { invalidParams, notFound } from '@cyanheads/mcp-ts-core/errors';
7
- import { Canvas, downsampleSprite, drawText, FONT_3x5, FONT_5x7, lerpColor, loadImage, measureText, renderSprite, renderSvgPath, resolveColor, } from '@cyanheads/pixoo-toolkit';
7
+ import { Canvas, downsampleSprite, drawText, FONT_3x5, lerpColor, loadImage, measureText, renderSprite, renderSvgPath, resolveColor, } from '@cyanheads/pixoo-toolkit';
8
+ import { finishFrame } from './finish.js';
8
9
  import { ICONS } from './icons.js';
9
10
  import { compileEffect, getKeyframeValue, interpolateColorKeyframe, } from './keyframes.js';
10
- import { fetchRemoteImageToTempPng, isRemoteSource } from './remote-image.js';
11
- import { drawStyledText, resolveX, resolveY, } from './text-engine.js';
11
+ import { fetchRemoteImageBytes, isRemoteSource } from './remote-image.js';
12
+ import { boxFits, drawStyledText, FONT_FACES, resolveX, resolveY, } from './text-engine.js';
12
13
  import { PALETTES, THEMES } from './themes.js';
14
+ /** The cache key of a sprite sheet: its path and grid, which fix the downsample. */
15
+ function spriteKey(el) {
16
+ return `${el.path}:${el.cols}:${el.rows}`;
17
+ }
13
18
  /**
14
19
  * Confirm a local image or sprite-sheet path is readable before the toolkit's
15
20
  * loader reaches it — the loader fails a missing file with an unclassified error.
16
21
  *
17
- * @throws {McpError} NotFound with `reason: 'asset_not_found'` when the path is
18
- * missing or unreadable.
22
+ * @throws {McpError} NotFound with `reason: 'asset_not_found'` and the calling tool's
23
+ * declared recovery when the path is missing or unreadable.
19
24
  */
20
- async function assertReadableAsset(assetPath, label) {
25
+ async function assertReadableAsset(assetPath, label, ctx) {
21
26
  try {
22
27
  await fs.access(assetPath, fs.constants.R_OK);
23
28
  }
24
29
  catch (err) {
25
- throw notFound(`${label} not found or unreadable: "${assetPath}".`, {
26
- reason: 'asset_not_found',
27
- path: assetPath,
28
- recovery: { hint: 'Pass an absolute path to an existing, readable image file.' },
29
- }, { cause: err });
30
+ throw notFound(`${label} not found or unreadable: "${assetPath}".`, { reason: 'asset_not_found', path: assetPath, ...ctx.recoveryFor('asset_not_found') }, { cause: err });
30
31
  }
31
32
  }
33
+ /**
34
+ * The message for an image source that was read but did not decode: the source as the
35
+ * caller passed it, then the decoder's reason. Only the reason's first line is kept —
36
+ * libvips repeats itself across several lines on a corrupt header.
37
+ */
38
+ export function decodeFailureMessage(label, source, err) {
39
+ const reason = (err instanceof Error ? err.message : String(err)).split('\n')[0];
40
+ return `${label} "${source}" could not be decoded: ${reason}.`;
41
+ }
42
+ /**
43
+ * The error for an image source or sprite sheet that was read but did not decode — a
44
+ * text file, an HTML page, a truncated download. Carries the calling tool's declared
45
+ * `invalid_image` recovery.
46
+ */
47
+ function undecodableAsset(el, err, ctx) {
48
+ const [label, field, value] = el.type === 'image' ? ['Image source', 'source', el.source] : ['Sprite sheet', 'path', el.path];
49
+ return invalidParams(decodeFailureMessage(label, value, err), { reason: 'invalid_image', [field]: value, ...ctx.recoveryFor('invalid_image') }, { cause: err });
50
+ }
51
+ /**
52
+ * An image source as `loadImage` takes it: a URL's fetched bytes, or a local path
53
+ * confirmed readable.
54
+ */
55
+ async function readImageSource(source, ctx) {
56
+ if (isRemoteSource(source))
57
+ return fetchRemoteImageBytes(source, ctx);
58
+ await assertReadableAsset(source, 'Image file', ctx);
59
+ return source;
60
+ }
61
+ /**
62
+ * Downsample a sprite element's sheet to its grid. The sheet must be a local file;
63
+ * a URL is refused without being fetched.
64
+ */
65
+ async function loadSpriteSheet(el, ctx) {
66
+ if (isRemoteSource(el.path)) {
67
+ throw notFound(`Sprite sheet path "${el.path}" is a URL; sprite sheets take an absolute local path.`, { reason: 'asset_not_found', path: el.path, ...ctx.recoveryFor('asset_not_found') });
68
+ }
69
+ await assertReadableAsset(el.path, 'Sprite sheet', ctx);
70
+ return downsampleSprite(el.path, el.cols, el.rows).catch((err) => {
71
+ throw undecodableAsset(el, err, ctx);
72
+ });
73
+ }
74
+ /**
75
+ * The in-flight load for `key`, started by `load` on first request. Every caller
76
+ * awaits the same promise, so a failed load rejects each of them with one error.
77
+ */
78
+ function loadOnce(loads, key, load) {
79
+ let pending = loads.get(key);
80
+ if (!pending) {
81
+ pending = load();
82
+ loads.set(key, pending);
83
+ }
84
+ return pending;
85
+ }
32
86
  /**
33
87
  * Preload all async assets referenced in elements. Images load onto a `size` canvas,
34
88
  * the scene's own, so an image with no `w`/`h` fits the display.
35
89
  *
90
+ * Each distinct load runs once per call, however many elements share it: a URL is
91
+ * fetched once, a sprite sheet decodes once per grid, and an image decodes once per
92
+ * placement — source, fit, kernel, position, and size, all of which `loadImage` bakes
93
+ * into its canvas. `loadImage` decodes and places in one call, so a source placed two
94
+ * ways decodes twice. An image's `finish` runs once per placement and finish, on the
95
+ * placement's canvas, which it never mutates: elements differing only in `finish` share
96
+ * the decode and nothing after it.
97
+ *
36
98
  * `ctx` is threaded through so a remote image fetch correlates to the originating
37
99
  * request in logs and traces, and stops when that request is cancelled.
100
+ *
101
+ * @throws {McpError} NotFound with `reason: 'asset_not_found'` for a missing or
102
+ * unreadable path, a sprite path given as a URL, or a URL that cannot be fetched.
103
+ * @throws {McpError} InvalidParams with `reason: 'invalid_image'` for an image source
104
+ * or sprite sheet that was read but did not decode.
105
+ * @throws {Error} The toolkit's `Unknown color` error for an unresolvable `finish`
106
+ * palette entry.
38
107
  */
39
108
  export async function preloadAssets(elements, ctx, size) {
40
109
  const cache = { images: new Map(), sprites: new Map() };
110
+ const sourceLoads = new Map();
111
+ const imageLoads = new Map();
112
+ const sheetLoads = new Map();
41
113
  await Promise.all(elements.map(async (el) => {
42
114
  if (el.type === 'image') {
43
- const source = el.source;
44
- // The toolkit's loadImage reads from disk, so a remote source is
45
- // staged to a temp PNG first and unlinked once it has been read.
46
- const remote = isRemoteSource(source);
47
- if (!remote)
48
- await assertReadableAsset(source, 'Image file');
49
- const tmpPathToCleanup = remote ? await fetchRemoteImageToTempPng(source, ctx) : undefined;
50
- const localPath = tmpPathToCleanup ?? source;
51
- const loadOpts = {
115
+ const placement = {
52
116
  size,
53
117
  fit: el.fit ?? 'contain',
54
118
  kernel: el.kernel ?? 'nearest',
@@ -56,22 +120,25 @@ export async function preloadAssets(elements, ctx, size) {
56
120
  y: typeof el.y === 'number' ? el.y : 0,
57
121
  };
58
122
  if (el.w !== undefined)
59
- loadOpts.width = el.w;
123
+ placement.width = el.w;
60
124
  if (el.h !== undefined)
61
- loadOpts.height = el.h;
62
- const canvas = await loadImage(localPath, loadOpts);
63
- if (tmpPathToCleanup) {
64
- fs.unlink(tmpPathToCleanup).catch(() => undefined);
65
- }
125
+ placement.height = el.h;
126
+ const placed = JSON.stringify([el.source, placement]);
127
+ const load = () => loadOnce(imageLoads, placed, async () => {
128
+ const image = await loadOnce(sourceLoads, el.source, () => readImageSource(el.source, ctx));
129
+ return loadImage(image, placement).catch((err) => {
130
+ throw undecodableAsset(el, err, ctx);
131
+ });
132
+ });
133
+ const { finish } = el;
134
+ const canvas = await (finish
135
+ ? loadOnce(imageLoads, JSON.stringify([el.source, placement, finish]), async () => finishFrame(await load(), finish))
136
+ : load());
66
137
  cache.images.set(el, canvas);
67
138
  }
68
139
  else if (el.type === 'sprite') {
69
- const key = `${el.path}:${el.cols}:${el.rows}`;
70
- if (!cache.sprites.has(key)) {
71
- await assertReadableAsset(el.path, 'Sprite sheet');
72
- const sprite = await downsampleSprite(el.path, el.cols, el.rows);
73
- cache.sprites.set(key, sprite);
74
- }
140
+ const key = spriteKey(el);
141
+ cache.sprites.set(key, await loadOnce(sheetLoads, key, () => loadSpriteSheet(el, ctx)));
75
142
  }
76
143
  }));
77
144
  return cache;
@@ -164,8 +231,81 @@ function renderIconRamp(canvas, paths, svgViewBox, targetRect, stop) {
164
231
  * The element fits when the box lies wholly on the canvas.
165
232
  */
166
233
  function placedEntry(element, type, box, canvas) {
167
- const fits = box.x >= 0 && box.y >= 0 && box.x + box.w <= canvas.width && box.y + box.h <= canvas.height;
168
- return { element, type, box, fits, action: 'none' };
234
+ return { element, type, box, fits: boxFits(box, canvas.width, canvas.height), action: 'none' };
235
+ }
236
+ /** The element's stroke, each option at its default when omitted. */
237
+ function strokeOf(el) {
238
+ return { width: el.strokeWidth ?? 1, antialias: el.antialias ?? false };
239
+ }
240
+ /**
241
+ * Whether the toolkit draws `stroke` as a band rather than a 1px Bresenham line or
242
+ * midpoint circle, whose pixels stay within the endpoints or the diameter.
243
+ */
244
+ function isBand({ width, antialias }) {
245
+ return width > 1 || antialias;
246
+ }
247
+ /**
248
+ * The box of every pixel a band line lights, on the canvas or off it — the toolkit's stroke
249
+ * geometry: the major axis steps from one endpoint's pixel to the other's, and each step
250
+ * covers `width/2 · √(1 + slope²)` either side of the centerline. Aliased, the endpoints
251
+ * are floored and a pixel lights when its center lies in that half-open span; anti-aliased,
252
+ * the endpoints are exact and a pixel lights when it overlaps the span at all.
253
+ */
254
+ function bandLineBox(ex0, ey0, ex1, ey1, { width, antialias }) {
255
+ const snap = antialias ? (v) => v : Math.floor;
256
+ const [x0, y0, x1, y1] = [snap(ex0), snap(ey0), snap(ex1), snap(ey1)];
257
+ const xMajor = Math.abs(x1 - x0) >= Math.abs(y1 - y0);
258
+ const [a0, b0, a1, b1] = xMajor ? [x0, y0, x1, y1] : [y0, x0, y1, x1];
259
+ const slope = a1 === a0 ? 0 : (b1 - b0) / (a1 - a0);
260
+ const half = (width / 2) * Math.sqrt(1 + slope * slope);
261
+ const first = Math.floor(Math.min(a0, a1));
262
+ const last = Math.floor(Math.max(a0, a1));
263
+ // The centerline is straight, so its first and last steps bound it
264
+ const [m0, m1] = [b0 + (first - a0) * slope, b0 + (last - a0) * slope];
265
+ const lo = Math.min(m0, m1) - half;
266
+ const hi = Math.max(m0, m1) + half;
267
+ const minorFirst = antialias ? Math.floor(lo - 0.5) + 1 : Math.ceil(lo);
268
+ const minorSpan = (antialias ? Math.ceil(hi + 0.5) : Math.ceil(hi)) - minorFirst;
269
+ const majorSpan = last - first + 1;
270
+ return xMajor
271
+ ? { x: first, y: minorFirst, w: majorSpan, h: minorSpan }
272
+ : { x: minorFirst, y: first, w: minorSpan, h: majorSpan };
273
+ }
274
+ /**
275
+ * The box of every pixel a band ring lights: each pixel center nearer the circle's center
276
+ * than `radius + width/2`, or `radius + width/2 + ½` anti-aliased, where the band's
277
+ * coverage runs out.
278
+ */
279
+ function bandRingBox(cx, cy, radius, stroke) {
280
+ const reach = radius + stroke.width / 2 + (stroke.antialias ? 0.5 : 0);
281
+ const x = Math.floor(cx - reach) + 1;
282
+ const y = Math.floor(cy - reach) + 1;
283
+ return { x, y, w: Math.ceil(cx + reach) - x, h: Math.ceil(cy + reach) - y };
284
+ }
285
+ /**
286
+ * Composite an element's own scratch layer onto the canvas. `normal` lands each pixel
287
+ * source-over at its alpha × `opacity`, so a soft edge keeps its falloff as the element
288
+ * fades. Any other mode scales the layer's alpha by `opacity`, then blits the layer in
289
+ * that mode.
290
+ */
291
+ function compositeLayer(canvas, layer, opacity, blend) {
292
+ const fade = opacity / 100;
293
+ if (blend === 'normal') {
294
+ for (let y = 0; y < layer.height; y++) {
295
+ for (let x = 0; x < layer.width; x++) {
296
+ const [r, g, b, a] = layer.getPixelRgba(x, y);
297
+ if (a > 0)
298
+ canvas.blendPixel(x, y, [r, g, b], (a / 255) * fade);
299
+ }
300
+ }
301
+ return;
302
+ }
303
+ if (fade < 1) {
304
+ const { buffer } = layer;
305
+ for (let i = 3; i < buffer.length; i += 4)
306
+ buffer[i] = Math.round((buffer[i] ?? 0) * fade);
307
+ }
308
+ canvas.blit(layer, 0, 0, { mode: blend });
169
309
  }
170
310
  /** Render a single element onto a canvas at a specific frame. */
171
311
  export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets, layoutEntries) {
@@ -173,7 +313,7 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
173
313
  const kf = el.animate
174
314
  ? el.animate
175
315
  : el.effect
176
- ? compileEffect(el.effect.name, el.effect, totalFrames)
316
+ ? compileEffect(el.effect.name, el.effect, totalFrames, elIdx)
177
317
  : undefined;
178
318
  const visible = kf
179
319
  ? Boolean(getKeyframeValue(kf, 'visible', frameIdx, el.visible ?? true))
@@ -188,8 +328,9 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
188
328
  // A keyframed `color` stands in for the element's own `color` on this frame.
189
329
  const colorFrames = kf?.['color'];
190
330
  const keyframedColor = colorFrames ? interpolateColorKeyframe(colorFrames, frameIdx) : undefined;
191
- // If opacity < 100, render to scratch canvas and blit with alpha
192
- const target = opacity < 100 ? new Canvas(canvas.width) : canvas;
331
+ // Under opacity or a blend mode, the element draws on its own layer, composited at the end
332
+ const blend = el.blend ?? 'normal';
333
+ const target = opacity < 100 || blend !== 'normal' ? new Canvas(canvas.width) : canvas;
193
334
  switch (el.type) {
194
335
  case 'text': {
195
336
  // A copy: the per-frame color must not stick to the caller's style object.
@@ -197,10 +338,10 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
197
338
  const color = keyframedColor ?? el.color;
198
339
  if (!style.color && color)
199
340
  style.color = color;
200
- const fontVariant = el.font === 'compact' ? 'compact' : 'standard';
341
+ const fontVariant = el.font ?? 'standard';
201
342
  const px = el.x ?? 0;
202
343
  const py = el.y ?? 0;
203
- const font = fontVariant === 'compact' ? FONT_3x5 : FONT_5x7;
344
+ const font = FONT_FACES[fontVariant];
204
345
  const scale = style.scale ?? 1;
205
346
  const textW = measureText(el.text, { font, scale });
206
347
  const textH = font.height * scale;
@@ -282,7 +423,9 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
282
423
  target.fillRect(x, y, el.w, el.h, resolveColor(fill));
283
424
  }
284
425
  if (el.borderColor) {
285
- target.drawRect(x, y, el.w, el.h, resolveColor(el.borderColor));
426
+ target.drawRect(x, y, el.w, el.h, resolveColor(el.borderColor), {
427
+ width: el.strokeWidth ?? 1,
428
+ });
286
429
  }
287
430
  layoutEntries.push(placedEntry(elIdx, 'rect', { x, y, w: el.w, h: el.h }, canvas));
288
431
  break;
@@ -292,15 +435,18 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
292
435
  const cy = el.cy + dy;
293
436
  const colorSpec = keyframedColor ?? el.color;
294
437
  const color = colorSpec ? resolveColor(colorSpec) : [255, 255, 255];
438
+ const stroke = strokeOf(el);
295
439
  if (el.fill !== false) {
296
440
  target.fillCircle(cx, cy, el.radius, color);
297
441
  }
298
442
  else {
299
- target.drawCircle(cx, cy, el.radius, color);
443
+ target.drawCircle(cx, cy, el.radius, color, stroke);
300
444
  }
301
- // The circle covers its center pixel plus `radius` on each side.
445
+ // The circle covers its center pixel plus `radius` on each side; a band ring reaches past it.
302
446
  const diameter = el.radius * 2 + 1;
303
- const box = { x: cx - el.radius, y: cy - el.radius, w: diameter, h: diameter };
447
+ const box = el.fill === false && isBand(stroke)
448
+ ? bandRingBox(cx, cy, el.radius, stroke)
449
+ : { x: cx - el.radius, y: cy - el.radius, w: diameter, h: diameter };
304
450
  layoutEntries.push(placedEntry(elIdx, 'circle', box, canvas));
305
451
  break;
306
452
  }
@@ -311,14 +457,17 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
311
457
  const y0 = el.y0 + dy;
312
458
  const x1 = el.x1 + dx;
313
459
  const y1 = el.y1 + dy;
314
- target.drawLine(x0, y0, x1, y1, color);
315
- // Both endpoints are drawn, so the line spans one pixel more than their distance.
316
- const box = {
317
- x: Math.min(x0, x1),
318
- y: Math.min(y0, y1),
319
- w: Math.abs(x1 - x0) + 1,
320
- h: Math.abs(y1 - y0) + 1,
321
- };
460
+ const stroke = strokeOf(el);
461
+ target.drawLine(x0, y0, x1, y1, color, stroke);
462
+ // A 1px line draws both endpoints, so it spans one pixel more than their distance.
463
+ const box = isBand(stroke)
464
+ ? bandLineBox(x0, y0, x1, y1, stroke)
465
+ : {
466
+ x: Math.min(x0, x1),
467
+ y: Math.min(y0, y1),
468
+ w: Math.abs(x1 - x0) + 1,
469
+ h: Math.abs(y1 - y0) + 1,
470
+ };
322
471
  layoutEntries.push(placedEntry(elIdx, 'line', box, canvas));
323
472
  break;
324
473
  }
@@ -441,8 +590,7 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
441
590
  break;
442
591
  }
443
592
  case 'sprite': {
444
- const key = `${el.path}:${el.cols}:${el.rows}`;
445
- const sprite = assets.sprites.get(key);
593
+ const sprite = assets.sprites.get(spriteKey(el));
446
594
  if (sprite) {
447
595
  const scale = el.scale ?? 1;
448
596
  const spriteW = sprite.cols * scale;
@@ -468,18 +616,8 @@ export function renderElement(canvas, el, elIdx, frameIdx, totalFrames, assets,
468
616
  break;
469
617
  }
470
618
  }
471
- // Blit scratch canvas onto main if we used opacity
472
- if (opacity < 100 && target !== canvas) {
473
- const alpha = opacity / 100;
474
- for (let py_ = 0; py_ < canvas.height; py_++) {
475
- for (let px_ = 0; px_ < canvas.width; px_++) {
476
- const [r, g, b, a] = target.getPixelRgba(px_, py_);
477
- if (a > 0) {
478
- canvas.blendPixel(px_, py_, [r, g, b], alpha);
479
- }
480
- }
481
- }
482
- }
619
+ if (target !== canvas)
620
+ compositeLayer(canvas, target, opacity, blend);
483
621
  }
484
622
  /** Render a complete frame. */
485
623
  export function renderFrame(frameIdx, totalFrames, background, elements, assets, size = 64) {