@elizaos/capacitor-canvas 1.0.0 → 2.0.11-beta.7

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shaw Walters and elizaOS Contributors
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 ADDED
@@ -0,0 +1,140 @@
1
+ # @elizaos/capacitor-canvas
2
+
3
+ A [Capacitor](https://capacitorjs.com/) plugin for elizaOS that provides an interactive 2D canvas with layer management, drawing primitives, web view embedding, and an A2UI bridge for building rich visual UIs in Eliza agent surfaces.
4
+
5
+ Supported platforms: **browser**, **node** (Electrobun desktop), **iOS**, **Android**.
6
+
7
+ ## What it does
8
+
9
+ - **Canvas with layers** — create canvases of any size, manage named composited layers with independent opacity, z-index, and transform.
10
+ - **Drawing primitives** — rectangles (with corner radius), ellipses, lines, arbitrary paths (Bezier, arc, ellipse, closePath), text with font/align/baseline control, and images (URL or base64).
11
+ - **Batch drawing** — submit a typed array of draw commands in a single call for efficient rendering.
12
+ - **Gradients, blend modes, shadows, transforms** — applied per draw call or globally.
13
+ - **Export** — capture the canvas to a base64 PNG/JPEG/WEBP image or read raw pixel data.
14
+ - **Web view** — load any URL inline, fullscreen, or in a popup; evaluate JavaScript in it; capture a screenshot.
15
+ - **A2UI bridge** — push structured agent-to-UI messages (text cards, action buttons, forms, status indicators) into a loaded web view and receive back action events.
16
+ - **Touch/pointer events** — emit normalized `CanvasTouchEvent` on touch start/move/end/cancel and equivalent mouse drag.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ bun add @elizaos/capacitor-canvas
22
+ ```
23
+
24
+ Peer dependency (must be installed by the host):
25
+
26
+ ```bash
27
+ bun add @capacitor/core
28
+ ```
29
+
30
+ For iOS add the pod:
31
+
32
+ ```bash
33
+ npx cap sync ios
34
+ ```
35
+
36
+ The podspec is `ElizaosCapacitorCanvas.podspec`. iOS deployment target is 15.0, Swift 5.9, frameworks UIKit / CoreGraphics / WebKit.
37
+
38
+ ## Usage
39
+
40
+ ```ts
41
+ import { Canvas } from "@elizaos/capacitor-canvas";
42
+
43
+ // Create a canvas
44
+ const { canvasId } = await Canvas.create({ size: { width: 800, height: 600 } });
45
+
46
+ // Add a layer
47
+ const { layerId } = await Canvas.createLayer({
48
+ canvasId,
49
+ layer: { visible: true, opacity: 1, zIndex: 1 },
50
+ });
51
+
52
+ // Draw on it
53
+ await Canvas.drawRect({
54
+ canvasId,
55
+ rect: { x: 10, y: 10, width: 200, height: 100 },
56
+ fill: { color: { r: 255, g: 100, b: 0, a: 0.9 } },
57
+ cornerRadius: 8,
58
+ drawOptions: { layerId },
59
+ });
60
+
61
+ // Batch draw
62
+ await Canvas.drawBatch({
63
+ canvasId,
64
+ commands: [
65
+ { type: "ellipse", args: { center: { x: 400, y: 300 }, radiusX: 50, radiusY: 50, fill: { color: "#3399ff" } } },
66
+ { type: "text", args: { text: "Hello", position: { x: 400, y: 300 }, style: { font: "sans-serif", size: 24, color: "#fff", align: "center" } } },
67
+ ],
68
+ });
69
+
70
+ // Export to image
71
+ const image = await Canvas.toImage({ canvasId, format: "png" });
72
+ // image.base64, image.width, image.height
73
+
74
+ // Attach to DOM (browser/desktop)
75
+ await Canvas.attach({ canvasId, element: document.getElementById("canvas-host")! });
76
+
77
+ // Enable touch events
78
+ await Canvas.setTouchEnabled({ canvasId, enabled: true });
79
+ const handle = await Canvas.addListener("touch", (evt) => {
80
+ console.log(evt.type, evt.touches);
81
+ });
82
+
83
+ // Embed a web view
84
+ await Canvas.navigate({ url: "https://example.com", placement: "inline" });
85
+
86
+ // Evaluate JS in it
87
+ const { result } = await Canvas.eval({ script: "document.title" });
88
+
89
+ // Push A2UI messages
90
+ await Canvas.a2uiPush({
91
+ messages: [
92
+ { role: "assistant", type: "text", content: "Hello from the agent!" },
93
+ ],
94
+ });
95
+
96
+ // Listen for A2UI actions triggered in the web content
97
+ await Canvas.addListener("a2uiAction", (evt) => {
98
+ console.log(evt.action, evt.data);
99
+ });
100
+
101
+ // Cleanup
102
+ await handle.remove();
103
+ await Canvas.destroy({ canvasId });
104
+ ```
105
+
106
+ ## A2UI message types
107
+
108
+ | `type` | Use |
109
+ |-----------|-----|
110
+ | `text` | Plain text bubble |
111
+ | `card` | Structured card with title/body |
112
+ | `action` | Clickable action button |
113
+ | `form` | Input form |
114
+ | `list` | Ordered/unordered list |
115
+ | `image` | Image display |
116
+ | `status` | Status indicator |
117
+
118
+ ## Web view events
119
+
120
+ | Event | Payload | When |
121
+ |-------|---------|------|
122
+ | `webViewReady` | `{ url, title }` | Navigation completed |
123
+ | `navigationError` | `{ url, code, message }` | Load failed |
124
+ | `deepLink` | `{ url, path, params }` | `eliza://` URL intercepted |
125
+ | `a2uiAction` | `{ action, data, messageId? }` | Web content triggered an action |
126
+
127
+ ## Canvas events
128
+
129
+ | Event | Payload | When |
130
+ |-------|---------|------|
131
+ | `touch` | `CanvasTouchEvent` | Touch or mouse drag on canvas |
132
+ | `render` | `CanvasRenderEvent` | Each rendered frame (FPS telemetry) |
133
+
134
+ ## Notes
135
+
136
+ - `snapshot()` only works with `placement: "inline"` or `"fullscreen"`. Cross-origin iframes render an unavailable frame.
137
+ - `eval()` requires the loaded page to handle `eliza:eval` postMessages and reply with `eliza:evalResult`; times out after 5 seconds.
138
+ - `a2uiPush` and `a2uiReset` prefer the `window.elizaA2UI` bridge when present; otherwise fall back to `postMessage`.
139
+ - Call `attach()` before calling `setTouchEnabled()` — touch handlers are wired on attach.
140
+ - Layer canvases are absolute-positioned siblings of the base canvas element; the host container should be `position: relative`.
@@ -6,6 +6,16 @@ ext {
6
6
  }
7
7
 
8
8
  apply plugin: 'com.android.library'
9
+ // Explicitly apply the Kotlin Android plugin. The kotlin-gradle-plugin is on
10
+ // the root buildscript classpath, but without applying it here AGP 8.13 falls
11
+ // back to its "built-in Kotlin" compile path (build/intermediates/
12
+ // built_in_kotlinc), which compiles the .kt sources but does NOT bundle the
13
+ // resulting .class files into the *release* library jar. The app's
14
+ // :app:assembleRelease then links a library AAR with zero plugin classes, so
15
+ // the Capacitor plugin (and any manifest-declared component) is absent from
16
+ // the release dex. Applying the standard Kotlin plugin wires Kotlin
17
+ // compilation into both the debug and release jar-bundling tasks.
18
+ apply plugin: 'org.jetbrains.kotlin.android'
9
19
  android {
10
20
  namespace = "ai.eliza.plugins.canvas"
11
21
  compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
@@ -24,8 +34,12 @@ android {
24
34
  }
25
35
 
26
36
  compileOptions {
27
- sourceCompatibility JavaVersion.VERSION_17
28
- targetCompatibility JavaVersion.VERSION_17
37
+ sourceCompatibility JavaVersion.VERSION_21
38
+ targetCompatibility JavaVersion.VERSION_21
39
+ }
40
+
41
+ kotlinOptions {
42
+ jvmTarget = "21"
29
43
  }
30
44
 
31
45
  }
@@ -33,7 +47,7 @@ android {
33
47
  repositories {
34
48
  google()
35
49
  maven {
36
- url = uri(rootProject.ext.mavenCentralMirrorUrl)
50
+ url = uri(rootProject.ext.has('mavenCentralMirrorUrl') ? rootProject.ext.mavenCentralMirrorUrl : 'https://repo.maven.apache.org/maven2')
37
51
  }
38
52
  mavenCentral()
39
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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"}
package/dist/esm/web.js CHANGED
@@ -1,4 +1,60 @@
1
1
  import { WebPlugin } from "@capacitor/core";
2
+ const MAX_CANVAS_DIMENSION = 16384;
3
+ function assertFiniteNumber(value, label) {
4
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5
+ throw new Error(`${label} must be a finite number`);
6
+ }
7
+ return value;
8
+ }
9
+ function assertCanvasSize(size, label = "size") {
10
+ if (!size || typeof size !== "object") {
11
+ throw new Error(`${label} must include finite width and height`);
12
+ }
13
+ const width = assertFiniteNumber(size.width, `${label}.width`);
14
+ const height = assertFiniteNumber(size.height, `${label}.height`);
15
+ if (width <= 0 ||
16
+ height <= 0 ||
17
+ width > MAX_CANVAS_DIMENSION ||
18
+ height > MAX_CANVAS_DIMENSION) {
19
+ throw new Error(`${label}.width and ${label}.height must be between 1 and ${MAX_CANVAS_DIMENSION}`);
20
+ }
21
+ return { width, height };
22
+ }
23
+ function assertUnitInterval(value, label) {
24
+ const numberValue = assertFiniteNumber(value, label);
25
+ if (numberValue < 0 || numberValue > 1) {
26
+ throw new Error(`${label} must be between 0 and 1`);
27
+ }
28
+ return numberValue;
29
+ }
30
+ function assertQuality(value, label, fallback) {
31
+ if (value === undefined)
32
+ return fallback;
33
+ const numberValue = assertFiniteNumber(value, label);
34
+ if (numberValue < 0 || numberValue > 100) {
35
+ throw new Error(`${label} must be between 0 and 100`);
36
+ }
37
+ return numberValue / 100;
38
+ }
39
+ function assertLayerInput(layer) {
40
+ if (!layer || typeof layer !== "object") {
41
+ throw new Error("layer must be an object");
42
+ }
43
+ if (typeof layer.visible !== "boolean") {
44
+ throw new Error("layer.visible must be a boolean");
45
+ }
46
+ return {
47
+ ...layer,
48
+ opacity: assertUnitInterval(layer.opacity, "layer.opacity"),
49
+ zIndex: assertFiniteNumber(layer.zIndex, "layer.zIndex"),
50
+ };
51
+ }
52
+ function assertAttachElement(element) {
53
+ if (!element || typeof element.appendChild !== "function") {
54
+ throw new Error("element must be an HTMLElement-like append target");
55
+ }
56
+ return element;
57
+ }
2
58
  export class CanvasWeb extends WebPlugin {
3
59
  constructor() {
4
60
  super(...arguments);
@@ -11,10 +67,11 @@ export class CanvasWeb extends WebPlugin {
11
67
  this.messageListenerBound = false;
12
68
  }
13
69
  async create(options) {
70
+ const size = assertCanvasSize(options.size);
14
71
  const canvasId = `canvas_${this.nextCanvasId++}`;
15
72
  const canvas = document.createElement("canvas");
16
- canvas.width = options.size.width;
17
- canvas.height = options.size.height;
73
+ canvas.width = size.width;
74
+ canvas.height = size.height;
18
75
  canvas.style.width = "100%";
19
76
  canvas.style.height = "100%";
20
77
  const ctx = canvas.getContext("2d");
@@ -23,14 +80,14 @@ export class CanvasWeb extends WebPlugin {
23
80
  }
24
81
  if (options.backgroundColor) {
25
82
  ctx.fillStyle = this.colorToString(options.backgroundColor);
26
- ctx.fillRect(0, 0, options.size.width, options.size.height);
83
+ ctx.fillRect(0, 0, size.width, size.height);
27
84
  }
28
85
  const managedCanvas = {
29
86
  id: canvasId,
30
87
  canvas,
31
88
  ctx,
32
89
  layers: new Map(),
33
- size: options.size,
90
+ size,
34
91
  transform: {},
35
92
  touchEnabled: false,
36
93
  };
@@ -51,7 +108,7 @@ export class CanvasWeb extends WebPlugin {
51
108
  const managed = this.canvases.get(options.canvasId);
52
109
  if (!managed)
53
110
  throw new Error("Canvas not found");
54
- options.element.appendChild(managed.canvas);
111
+ assertAttachElement(options.element).appendChild(managed.canvas);
55
112
  this.setupTouchHandlers(managed);
56
113
  }
57
114
  async detach(options) {
@@ -64,15 +121,16 @@ export class CanvasWeb extends WebPlugin {
64
121
  const managed = this.canvases.get(options.canvasId);
65
122
  if (!managed)
66
123
  throw new Error("Canvas not found");
124
+ const size = assertCanvasSize(options.size);
67
125
  const imageData = managed.ctx.getImageData(0, 0, managed.canvas.width, managed.canvas.height);
68
- managed.canvas.width = options.size.width;
69
- managed.canvas.height = options.size.height;
70
- managed.size = options.size;
126
+ managed.canvas.width = size.width;
127
+ managed.canvas.height = size.height;
128
+ managed.size = size;
71
129
  managed.ctx.putImageData(imageData, 0, 0);
72
130
  for (const layer of managed.layers.values()) {
73
131
  const layerImageData = layer.ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height);
74
- layer.canvas.width = options.size.width;
75
- layer.canvas.height = options.size.height;
132
+ layer.canvas.width = size.width;
133
+ layer.canvas.height = size.height;
76
134
  layer.ctx.putImageData(layerImageData, 0, 0);
77
135
  }
78
136
  }
@@ -96,25 +154,26 @@ export class CanvasWeb extends WebPlugin {
96
154
  const managed = this.canvases.get(options.canvasId);
97
155
  if (!managed)
98
156
  throw new Error("Canvas not found");
157
+ const inputLayer = assertLayerInput(options.layer);
99
158
  const layerId = `layer_${this.nextLayerId++}`;
100
159
  const layerCanvas = document.createElement("canvas");
101
160
  layerCanvas.width = managed.size.width;
102
161
  layerCanvas.height = managed.size.height;
103
162
  layerCanvas.style.position = "absolute";
104
163
  layerCanvas.style.pointerEvents = "none";
105
- layerCanvas.style.display = options.layer.visible ? "block" : "none";
106
- layerCanvas.style.opacity = String(options.layer.opacity);
107
- layerCanvas.style.zIndex = String(options.layer.zIndex);
164
+ layerCanvas.style.display = inputLayer.visible ? "block" : "none";
165
+ layerCanvas.style.opacity = String(inputLayer.opacity);
166
+ layerCanvas.style.zIndex = String(inputLayer.zIndex);
108
167
  const layerCtx = layerCanvas.getContext("2d");
109
168
  if (!layerCtx)
110
169
  throw new Error("Failed to get layer context");
111
170
  const managedLayer = {
112
171
  id: layerId,
113
- name: options.layer.name,
114
- visible: options.layer.visible,
115
- opacity: options.layer.opacity,
116
- zIndex: options.layer.zIndex,
117
- transform: options.layer.transform,
172
+ name: inputLayer.name,
173
+ visible: inputLayer.visible,
174
+ opacity: inputLayer.opacity,
175
+ zIndex: inputLayer.zIndex,
176
+ transform: inputLayer.transform,
118
177
  canvas: layerCanvas,
119
178
  ctx: layerCtx,
120
179
  };
@@ -133,16 +192,21 @@ export class CanvasWeb extends WebPlugin {
133
192
  if (!layer)
134
193
  throw new Error("Layer not found");
135
194
  if (options.layer.visible !== undefined) {
195
+ if (typeof options.layer.visible !== "boolean") {
196
+ throw new Error("layer.visible must be a boolean");
197
+ }
136
198
  layer.visible = options.layer.visible;
137
199
  layer.canvas.style.display = options.layer.visible ? "block" : "none";
138
200
  }
139
201
  if (options.layer.opacity !== undefined) {
140
- layer.opacity = options.layer.opacity;
141
- layer.canvas.style.opacity = String(options.layer.opacity);
202
+ const opacity = assertUnitInterval(options.layer.opacity, "layer.opacity");
203
+ layer.opacity = opacity;
204
+ layer.canvas.style.opacity = String(opacity);
142
205
  }
143
206
  if (options.layer.zIndex !== undefined) {
144
- layer.zIndex = options.layer.zIndex;
145
- layer.canvas.style.zIndex = String(options.layer.zIndex);
207
+ const zIndex = assertFiniteNumber(options.layer.zIndex, "layer.zIndex");
208
+ layer.zIndex = zIndex;
209
+ layer.canvas.style.zIndex = String(zIndex);
146
210
  }
147
211
  if (options.layer.name !== undefined) {
148
212
  layer.name = options.layer.name;
@@ -364,7 +428,7 @@ export class CanvasWeb extends WebPlugin {
364
428
  if (!managed)
365
429
  throw new Error("Canvas not found");
366
430
  const format = options.format || "png";
367
- const quality = (options.quality || 100) / 100;
431
+ const quality = assertQuality(options.quality, "quality", 1);
368
432
  let sourceCanvas = managed.canvas;
369
433
  if (options.layerIds && options.layerIds.length > 0) {
370
434
  const tempCanvas = document.createElement("canvas");
@@ -519,10 +583,16 @@ export class CanvasWeb extends WebPlugin {
519
583
  throw new Error("No web view active or web view opened as popup (snapshot requires inline/fullscreen placement)");
520
584
  }
521
585
  const format = options?.format || "png";
522
- const quality = (options?.quality || 85) / 100;
586
+ const quality = assertQuality(options?.quality, "quality", 0.85);
523
587
  const iframeRect = this.webViewIframe.getBoundingClientRect();
524
588
  let width = Math.round(iframeRect.width) || 800;
525
589
  let height = Math.round(iframeRect.height) || 600;
590
+ if (options?.maxWidth !== undefined) {
591
+ assertFiniteNumber(options.maxWidth, "maxWidth");
592
+ if (options.maxWidth <= 0) {
593
+ throw new Error("maxWidth must be a positive finite number");
594
+ }
595
+ }
526
596
  if (options?.maxWidth && width > options.maxWidth) {
527
597
  const scale = options.maxWidth / width;
528
598
  height = Math.round(height * scale);
@@ -568,10 +638,10 @@ export class CanvasWeb extends WebPlugin {
568
638
  }
569
639
  }
570
640
  catch {
571
- // Cross-origin or serialization failed — fall through to placeholder
641
+ // Cross-origin or serialization failed — fall through to an unavailable frame.
572
642
  }
573
643
  if (!captured) {
574
- // Render a placeholder indicating cross-origin limitation
644
+ // Render an unavailable frame indicating the cross-origin limitation.
575
645
  ctx.fillStyle = "#f5f5f5";
576
646
  ctx.fillRect(0, 0, width, height);
577
647
  ctx.strokeStyle = "#ccc";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=web.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.test.d.ts","sourceRoot":"","sources":["../../src/web.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,123 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { CanvasWeb } from "./web";
4
+ function createContextStub() {
5
+ return {
6
+ beginPath: vi.fn(),
7
+ clearRect: vi.fn(),
8
+ drawImage: vi.fn(),
9
+ fill: vi.fn(),
10
+ fillRect: vi.fn(),
11
+ fillText: vi.fn(),
12
+ getImageData: vi.fn(() => ({
13
+ data: new Uint8ClampedArray(4),
14
+ width: 1,
15
+ height: 1,
16
+ })),
17
+ putImageData: vi.fn(),
18
+ restore: vi.fn(),
19
+ save: vi.fn(),
20
+ setTransform: vi.fn(),
21
+ stroke: vi.fn(),
22
+ toDataURL: vi.fn(() => "data:image/png;base64,ZmFrZQ=="),
23
+ };
24
+ }
25
+ describe("CanvasWeb validation", () => {
26
+ beforeEach(() => {
27
+ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(createContextStub());
28
+ vi.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockReturnValue("data:image/png;base64,ZmFrZQ==");
29
+ });
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ document.body.innerHTML = "";
33
+ });
34
+ it.each([
35
+ { width: 0, height: 100 },
36
+ { width: -1, height: 100 },
37
+ { width: Number.POSITIVE_INFINITY, height: 100 },
38
+ { width: Number.NaN, height: 100 },
39
+ { width: 20000, height: 100 },
40
+ ])("rejects malformed create size %#", async (size) => {
41
+ await expect(new CanvasWeb().create({ size })).rejects.toThrow(/size\.(width|height)|between 1 and 16384/);
42
+ });
43
+ it("rejects invalid attach targets before mutating the DOM", async () => {
44
+ const canvas = new CanvasWeb();
45
+ const { canvasId } = await canvas.create({
46
+ size: { width: 10, height: 10 },
47
+ });
48
+ await expect(canvas.attach({
49
+ canvasId,
50
+ element: {},
51
+ })).rejects.toThrow("element must be an HTMLElement-like append target");
52
+ expect(document.querySelector("canvas")).toBeNull();
53
+ });
54
+ it("validates resize before changing the existing canvas dimensions", async () => {
55
+ const canvas = new CanvasWeb();
56
+ const { canvasId } = await canvas.create({
57
+ size: { width: 10, height: 20 },
58
+ });
59
+ const host = document.createElement("div");
60
+ await canvas.attach({ canvasId, element: host });
61
+ await expect(canvas.resize({
62
+ canvasId,
63
+ size: { width: Number.POSITIVE_INFINITY, height: 40 },
64
+ })).rejects.toThrow("size.width must be a finite number");
65
+ const canvasElement = host.querySelector("canvas");
66
+ expect(canvasElement?.width).toBe(10);
67
+ expect(canvasElement?.height).toBe(20);
68
+ });
69
+ it.each([
70
+ { visible: true, opacity: -0.1, zIndex: 1 },
71
+ { visible: true, opacity: 1.1, zIndex: 1 },
72
+ { visible: "yes", opacity: 1, zIndex: 1 },
73
+ { visible: true, opacity: 1, zIndex: Number.NaN },
74
+ ])("rejects malformed layer metadata %#", async (layer) => {
75
+ const canvas = new CanvasWeb();
76
+ const { canvasId } = await canvas.create({
77
+ size: { width: 10, height: 10 },
78
+ });
79
+ await expect(canvas.createLayer({
80
+ canvasId,
81
+ layer: layer,
82
+ })).rejects.toThrow(/layer\.(visible|opacity|zIndex)/);
83
+ });
84
+ it("rejects invalid layer updates without changing the existing layer", async () => {
85
+ const canvas = new CanvasWeb();
86
+ const { canvasId } = await canvas.create({
87
+ size: { width: 10, height: 10 },
88
+ });
89
+ const { layerId } = await canvas.createLayer({
90
+ canvasId,
91
+ layer: { visible: true, opacity: 0.75, zIndex: 2 },
92
+ });
93
+ await expect(canvas.updateLayer({
94
+ canvasId,
95
+ layerId,
96
+ layer: { opacity: Number.NaN },
97
+ })).rejects.toThrow("layer.opacity must be a finite number");
98
+ await expect(canvas.getLayers({ canvasId })).resolves.toEqual({
99
+ layers: [
100
+ {
101
+ id: layerId,
102
+ name: undefined,
103
+ visible: true,
104
+ opacity: 0.75,
105
+ zIndex: 2,
106
+ transform: undefined,
107
+ },
108
+ ],
109
+ });
110
+ });
111
+ it.each([
112
+ -1,
113
+ 101,
114
+ Number.POSITIVE_INFINITY,
115
+ Number.NaN,
116
+ ])("rejects invalid image quality %s", async (quality) => {
117
+ const canvas = new CanvasWeb();
118
+ const { canvasId } = await canvas.create({
119
+ size: { width: 10, height: 10 },
120
+ });
121
+ await expect(canvas.toImage({ canvasId, quality })).rejects.toThrow(/quality must/);
122
+ });
123
+ });