@elizaos/capacitor-canvas 2.0.0-beta.1 → 2.0.3-beta.3
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 +21 -0
- package/README.md +140 -0
- package/android/build.gradle +16 -2
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +96 -26
- package/dist/esm/web.test.d.ts +2 -0
- package/dist/esm/web.test.d.ts.map +1 -0
- package/dist/esm/web.test.js +123 -0
- package/dist/plugin.cjs.js +96 -26
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +96 -26
- package/dist/plugin.js.map +1 -1
- package/package.json +13 -9
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`.
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
28
|
-
targetCompatibility JavaVersion.
|
|
37
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
38
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
kotlinOptions {
|
|
42
|
+
jvmTarget = "21"
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
}
|
package/dist/esm/web.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EACV,eAAe,EAGf,eAAe,EACf,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,eAAe,EACf,WAAW,EACX,UAAU,EACV,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,oBAAoB,EAEpB,eAAe,EACf,cAAc,EACd,iBAAiB,EAClB,MAAM,eAAe,CAAC;AAEvB,KAAK,eAAe,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,oBAAoB,GACpB,aAAa,GACb,eAAe,CAAC;
|
|
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 =
|
|
17
|
-
canvas.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,
|
|
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
|
|
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 =
|
|
69
|
-
managed.canvas.height =
|
|
70
|
-
managed.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 =
|
|
75
|
-
layer.canvas.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 =
|
|
106
|
-
layerCanvas.style.opacity = String(
|
|
107
|
-
layerCanvas.style.zIndex = String(
|
|
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:
|
|
114
|
-
visible:
|
|
115
|
-
opacity:
|
|
116
|
-
zIndex:
|
|
117
|
-
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
|
-
|
|
141
|
-
layer.
|
|
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
|
-
|
|
145
|
-
layer.
|
|
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
|
|
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
|
|
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
|
|
641
|
+
// Cross-origin or serialization failed — fall through to an unavailable frame.
|
|
572
642
|
}
|
|
573
643
|
if (!captured) {
|
|
574
|
-
// Render
|
|
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 @@
|
|
|
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
|
+
});
|