@elizaos/capacitor-canvas 1.0.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/ElizaosCapacitorCanvas.podspec +18 -0
- package/android/build.gradle +48 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ai/eliza/plugins/canvas/CanvasPlugin.kt +2175 -0
- package/dist/esm/definitions.d.ts +473 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +154 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +857 -0
- package/dist/plugin.cjs.js +873 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +876 -0
- package/dist/plugin.js.map +1 -0
- package/electrobun/src/index.ts +872 -0
- package/electrobun/tsconfig.json +18 -0
- package/ios/Sources/CanvasPlugin/CanvasPlugin.swift +1933 -0
- package/package.json +81 -0
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
export class CanvasWeb extends WebPlugin {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.canvases = new Map();
|
|
6
|
+
this.nextCanvasId = 1;
|
|
7
|
+
this.nextLayerId = 1;
|
|
8
|
+
this.pluginListeners = [];
|
|
9
|
+
this.webViewIframe = null;
|
|
10
|
+
this.webViewPopup = null;
|
|
11
|
+
this.messageListenerBound = false;
|
|
12
|
+
}
|
|
13
|
+
async create(options) {
|
|
14
|
+
const canvasId = `canvas_${this.nextCanvasId++}`;
|
|
15
|
+
const canvas = document.createElement("canvas");
|
|
16
|
+
canvas.width = options.size.width;
|
|
17
|
+
canvas.height = options.size.height;
|
|
18
|
+
canvas.style.width = "100%";
|
|
19
|
+
canvas.style.height = "100%";
|
|
20
|
+
const ctx = canvas.getContext("2d");
|
|
21
|
+
if (!ctx) {
|
|
22
|
+
throw new Error("Failed to get 2D context");
|
|
23
|
+
}
|
|
24
|
+
if (options.backgroundColor) {
|
|
25
|
+
ctx.fillStyle = this.colorToString(options.backgroundColor);
|
|
26
|
+
ctx.fillRect(0, 0, options.size.width, options.size.height);
|
|
27
|
+
}
|
|
28
|
+
const managedCanvas = {
|
|
29
|
+
id: canvasId,
|
|
30
|
+
canvas,
|
|
31
|
+
ctx,
|
|
32
|
+
layers: new Map(),
|
|
33
|
+
size: options.size,
|
|
34
|
+
transform: {},
|
|
35
|
+
touchEnabled: false,
|
|
36
|
+
};
|
|
37
|
+
this.canvases.set(canvasId, managedCanvas);
|
|
38
|
+
return { canvasId };
|
|
39
|
+
}
|
|
40
|
+
async destroy(options) {
|
|
41
|
+
const managed = this.canvases.get(options.canvasId);
|
|
42
|
+
if (!managed)
|
|
43
|
+
return;
|
|
44
|
+
managed.layers.forEach((layer) => {
|
|
45
|
+
layer.canvas.remove();
|
|
46
|
+
});
|
|
47
|
+
managed.canvas.remove();
|
|
48
|
+
this.canvases.delete(options.canvasId);
|
|
49
|
+
}
|
|
50
|
+
async attach(options) {
|
|
51
|
+
const managed = this.canvases.get(options.canvasId);
|
|
52
|
+
if (!managed)
|
|
53
|
+
throw new Error("Canvas not found");
|
|
54
|
+
options.element.appendChild(managed.canvas);
|
|
55
|
+
this.setupTouchHandlers(managed);
|
|
56
|
+
}
|
|
57
|
+
async detach(options) {
|
|
58
|
+
const managed = this.canvases.get(options.canvasId);
|
|
59
|
+
if (!managed)
|
|
60
|
+
throw new Error("Canvas not found");
|
|
61
|
+
managed.canvas.remove();
|
|
62
|
+
}
|
|
63
|
+
async resize(options) {
|
|
64
|
+
const managed = this.canvases.get(options.canvasId);
|
|
65
|
+
if (!managed)
|
|
66
|
+
throw new Error("Canvas not found");
|
|
67
|
+
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;
|
|
71
|
+
managed.ctx.putImageData(imageData, 0, 0);
|
|
72
|
+
for (const layer of managed.layers.values()) {
|
|
73
|
+
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;
|
|
76
|
+
layer.ctx.putImageData(layerImageData, 0, 0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async clear(options) {
|
|
80
|
+
const managed = this.canvases.get(options.canvasId);
|
|
81
|
+
if (!managed)
|
|
82
|
+
throw new Error("Canvas not found");
|
|
83
|
+
const ctx = options.layerId
|
|
84
|
+
? managed.layers.get(options.layerId)?.ctx
|
|
85
|
+
: managed.ctx;
|
|
86
|
+
if (!ctx)
|
|
87
|
+
throw new Error("Context not found");
|
|
88
|
+
if (options.rect) {
|
|
89
|
+
ctx.clearRect(options.rect.x, options.rect.y, options.rect.width, options.rect.height);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
ctx.clearRect(0, 0, managed.size.width, managed.size.height);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async createLayer(options) {
|
|
96
|
+
const managed = this.canvases.get(options.canvasId);
|
|
97
|
+
if (!managed)
|
|
98
|
+
throw new Error("Canvas not found");
|
|
99
|
+
const layerId = `layer_${this.nextLayerId++}`;
|
|
100
|
+
const layerCanvas = document.createElement("canvas");
|
|
101
|
+
layerCanvas.width = managed.size.width;
|
|
102
|
+
layerCanvas.height = managed.size.height;
|
|
103
|
+
layerCanvas.style.position = "absolute";
|
|
104
|
+
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);
|
|
108
|
+
const layerCtx = layerCanvas.getContext("2d");
|
|
109
|
+
if (!layerCtx)
|
|
110
|
+
throw new Error("Failed to get layer context");
|
|
111
|
+
const managedLayer = {
|
|
112
|
+
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,
|
|
118
|
+
canvas: layerCanvas,
|
|
119
|
+
ctx: layerCtx,
|
|
120
|
+
};
|
|
121
|
+
managed.layers.set(layerId, managedLayer);
|
|
122
|
+
const parent = managed.canvas.parentElement;
|
|
123
|
+
if (parent) {
|
|
124
|
+
parent.appendChild(layerCanvas);
|
|
125
|
+
}
|
|
126
|
+
return { layerId };
|
|
127
|
+
}
|
|
128
|
+
async updateLayer(options) {
|
|
129
|
+
const managed = this.canvases.get(options.canvasId);
|
|
130
|
+
if (!managed)
|
|
131
|
+
throw new Error("Canvas not found");
|
|
132
|
+
const layer = managed.layers.get(options.layerId);
|
|
133
|
+
if (!layer)
|
|
134
|
+
throw new Error("Layer not found");
|
|
135
|
+
if (options.layer.visible !== undefined) {
|
|
136
|
+
layer.visible = options.layer.visible;
|
|
137
|
+
layer.canvas.style.display = options.layer.visible ? "block" : "none";
|
|
138
|
+
}
|
|
139
|
+
if (options.layer.opacity !== undefined) {
|
|
140
|
+
layer.opacity = options.layer.opacity;
|
|
141
|
+
layer.canvas.style.opacity = String(options.layer.opacity);
|
|
142
|
+
}
|
|
143
|
+
if (options.layer.zIndex !== undefined) {
|
|
144
|
+
layer.zIndex = options.layer.zIndex;
|
|
145
|
+
layer.canvas.style.zIndex = String(options.layer.zIndex);
|
|
146
|
+
}
|
|
147
|
+
if (options.layer.name !== undefined) {
|
|
148
|
+
layer.name = options.layer.name;
|
|
149
|
+
}
|
|
150
|
+
if (options.layer.transform !== undefined) {
|
|
151
|
+
layer.transform = options.layer.transform;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async deleteLayer(options) {
|
|
155
|
+
const managed = this.canvases.get(options.canvasId);
|
|
156
|
+
if (!managed)
|
|
157
|
+
throw new Error("Canvas not found");
|
|
158
|
+
const layer = managed.layers.get(options.layerId);
|
|
159
|
+
if (!layer)
|
|
160
|
+
throw new Error("Layer not found");
|
|
161
|
+
layer.canvas.remove();
|
|
162
|
+
managed.layers.delete(options.layerId);
|
|
163
|
+
}
|
|
164
|
+
async getLayers(options) {
|
|
165
|
+
const managed = this.canvases.get(options.canvasId);
|
|
166
|
+
if (!managed)
|
|
167
|
+
throw new Error("Canvas not found");
|
|
168
|
+
const layers = Array.from(managed.layers.values()).map((layer) => ({
|
|
169
|
+
id: layer.id,
|
|
170
|
+
name: layer.name,
|
|
171
|
+
visible: layer.visible,
|
|
172
|
+
opacity: layer.opacity,
|
|
173
|
+
zIndex: layer.zIndex,
|
|
174
|
+
transform: layer.transform,
|
|
175
|
+
}));
|
|
176
|
+
return { layers };
|
|
177
|
+
}
|
|
178
|
+
async drawRect(options) {
|
|
179
|
+
const ctx = this.getContext(options.canvasId, options.drawOptions?.layerId);
|
|
180
|
+
this.applyDrawOptions(ctx, options.canvasId, options.drawOptions);
|
|
181
|
+
ctx.beginPath();
|
|
182
|
+
if (options.cornerRadius && options.cornerRadius > 0) {
|
|
183
|
+
const r = options.cornerRadius;
|
|
184
|
+
const { x, y, width, height } = options.rect;
|
|
185
|
+
ctx.moveTo(x + r, y);
|
|
186
|
+
ctx.lineTo(x + width - r, y);
|
|
187
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
188
|
+
ctx.lineTo(x + width, y + height - r);
|
|
189
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
190
|
+
ctx.lineTo(x + r, y + height);
|
|
191
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
192
|
+
ctx.lineTo(x, y + r);
|
|
193
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
194
|
+
ctx.closePath();
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
ctx.rect(options.rect.x, options.rect.y, options.rect.width, options.rect.height);
|
|
198
|
+
}
|
|
199
|
+
if (options.fill) {
|
|
200
|
+
ctx.fillStyle = this.createFillStyle(ctx, options.fill);
|
|
201
|
+
ctx.fill();
|
|
202
|
+
}
|
|
203
|
+
if (options.stroke) {
|
|
204
|
+
this.applyStrokeStyle(ctx, options.stroke);
|
|
205
|
+
ctx.stroke();
|
|
206
|
+
}
|
|
207
|
+
ctx.restore();
|
|
208
|
+
}
|
|
209
|
+
async drawEllipse(options) {
|
|
210
|
+
const ctx = this.getContext(options.canvasId, options.drawOptions?.layerId);
|
|
211
|
+
this.applyDrawOptions(ctx, options.canvasId, options.drawOptions);
|
|
212
|
+
ctx.beginPath();
|
|
213
|
+
ctx.ellipse(options.center.x, options.center.y, options.radiusX, options.radiusY, 0, 0, Math.PI * 2);
|
|
214
|
+
if (options.fill) {
|
|
215
|
+
ctx.fillStyle = this.createFillStyle(ctx, options.fill);
|
|
216
|
+
ctx.fill();
|
|
217
|
+
}
|
|
218
|
+
if (options.stroke) {
|
|
219
|
+
this.applyStrokeStyle(ctx, options.stroke);
|
|
220
|
+
ctx.stroke();
|
|
221
|
+
}
|
|
222
|
+
ctx.restore();
|
|
223
|
+
}
|
|
224
|
+
async drawLine(options) {
|
|
225
|
+
const ctx = this.getContext(options.canvasId, options.drawOptions?.layerId);
|
|
226
|
+
this.applyDrawOptions(ctx, options.canvasId, options.drawOptions);
|
|
227
|
+
this.applyStrokeStyle(ctx, options.stroke);
|
|
228
|
+
ctx.beginPath();
|
|
229
|
+
ctx.moveTo(options.from.x, options.from.y);
|
|
230
|
+
ctx.lineTo(options.to.x, options.to.y);
|
|
231
|
+
ctx.stroke();
|
|
232
|
+
ctx.restore();
|
|
233
|
+
}
|
|
234
|
+
async drawPath(options) {
|
|
235
|
+
const ctx = this.getContext(options.canvasId, options.drawOptions?.layerId);
|
|
236
|
+
this.applyDrawOptions(ctx, options.canvasId, options.drawOptions);
|
|
237
|
+
ctx.beginPath();
|
|
238
|
+
for (const cmd of options.path.commands) {
|
|
239
|
+
switch (cmd.type) {
|
|
240
|
+
case "moveTo":
|
|
241
|
+
ctx.moveTo(cmd.args[0], cmd.args[1]);
|
|
242
|
+
break;
|
|
243
|
+
case "lineTo":
|
|
244
|
+
ctx.lineTo(cmd.args[0], cmd.args[1]);
|
|
245
|
+
break;
|
|
246
|
+
case "quadraticCurveTo":
|
|
247
|
+
ctx.quadraticCurveTo(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3]);
|
|
248
|
+
break;
|
|
249
|
+
case "bezierCurveTo":
|
|
250
|
+
ctx.bezierCurveTo(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3], cmd.args[4], cmd.args[5]);
|
|
251
|
+
break;
|
|
252
|
+
case "arcTo":
|
|
253
|
+
ctx.arcTo(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3], cmd.args[4]);
|
|
254
|
+
break;
|
|
255
|
+
case "arc":
|
|
256
|
+
ctx.arc(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3], cmd.args[4], cmd.args[5] === 1);
|
|
257
|
+
break;
|
|
258
|
+
case "ellipse":
|
|
259
|
+
ctx.ellipse(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3], cmd.args[4], cmd.args[5], cmd.args[6], cmd.args[7] === 1);
|
|
260
|
+
break;
|
|
261
|
+
case "rect":
|
|
262
|
+
ctx.rect(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3]);
|
|
263
|
+
break;
|
|
264
|
+
case "closePath":
|
|
265
|
+
ctx.closePath();
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (options.fill) {
|
|
270
|
+
ctx.fillStyle = this.createFillStyle(ctx, options.fill);
|
|
271
|
+
ctx.fill();
|
|
272
|
+
}
|
|
273
|
+
if (options.stroke) {
|
|
274
|
+
this.applyStrokeStyle(ctx, options.stroke);
|
|
275
|
+
ctx.stroke();
|
|
276
|
+
}
|
|
277
|
+
ctx.restore();
|
|
278
|
+
}
|
|
279
|
+
async drawText(options) {
|
|
280
|
+
const ctx = this.getContext(options.canvasId, options.drawOptions?.layerId);
|
|
281
|
+
this.applyDrawOptions(ctx, options.canvasId, options.drawOptions);
|
|
282
|
+
ctx.font = `${options.style.size}px ${options.style.font}`;
|
|
283
|
+
ctx.fillStyle = this.colorToString(options.style.color);
|
|
284
|
+
ctx.textAlign = options.style.align || "left";
|
|
285
|
+
ctx.textBaseline = (options.style.baseline ||
|
|
286
|
+
"alphabetic");
|
|
287
|
+
if (options.style.maxWidth) {
|
|
288
|
+
ctx.fillText(options.text, options.position.x, options.position.y, options.style.maxWidth);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
ctx.fillText(options.text, options.position.x, options.position.y);
|
|
292
|
+
}
|
|
293
|
+
ctx.restore();
|
|
294
|
+
}
|
|
295
|
+
async drawImage(options) {
|
|
296
|
+
const ctx = this.getContext(options.canvasId, options.drawOptions?.layerId);
|
|
297
|
+
this.applyDrawOptions(ctx, options.canvasId, options.drawOptions);
|
|
298
|
+
const img = new Image();
|
|
299
|
+
if (typeof options.image === "string") {
|
|
300
|
+
img.src = options.image;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
img.src = `data:image/${options.image.format};base64,${options.image.base64}`;
|
|
304
|
+
}
|
|
305
|
+
await new Promise((resolve, reject) => {
|
|
306
|
+
img.onload = () => resolve();
|
|
307
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
308
|
+
});
|
|
309
|
+
if (options.srcRect) {
|
|
310
|
+
ctx.drawImage(img, options.srcRect.x, options.srcRect.y, options.srcRect.width, options.srcRect.height, options.destRect.x, options.destRect.y, options.destRect.width, options.destRect.height);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
ctx.drawImage(img, options.destRect.x, options.destRect.y, options.destRect.width, options.destRect.height);
|
|
314
|
+
}
|
|
315
|
+
ctx.restore();
|
|
316
|
+
}
|
|
317
|
+
async drawBatch(options) {
|
|
318
|
+
const base = { canvasId: options.canvasId };
|
|
319
|
+
for (const cmd of options.commands) {
|
|
320
|
+
switch (cmd.type) {
|
|
321
|
+
case "rect":
|
|
322
|
+
await this.drawRect({ ...base, ...cmd.args });
|
|
323
|
+
break;
|
|
324
|
+
case "ellipse":
|
|
325
|
+
await this.drawEllipse({ ...base, ...cmd.args });
|
|
326
|
+
break;
|
|
327
|
+
case "line":
|
|
328
|
+
await this.drawLine({ ...base, ...cmd.args });
|
|
329
|
+
break;
|
|
330
|
+
case "path":
|
|
331
|
+
await this.drawPath({ ...base, ...cmd.args });
|
|
332
|
+
break;
|
|
333
|
+
case "text":
|
|
334
|
+
await this.drawText({ ...base, ...cmd.args });
|
|
335
|
+
break;
|
|
336
|
+
case "image":
|
|
337
|
+
await this.drawImage({ ...base, ...cmd.args });
|
|
338
|
+
break;
|
|
339
|
+
case "clear":
|
|
340
|
+
await this.clear({ ...base, ...cmd.args });
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async getPixelData(options) {
|
|
346
|
+
const managed = this.canvases.get(options.canvasId);
|
|
347
|
+
if (!managed)
|
|
348
|
+
throw new Error("Canvas not found");
|
|
349
|
+
const rect = options.rect || {
|
|
350
|
+
x: 0,
|
|
351
|
+
y: 0,
|
|
352
|
+
width: managed.size.width,
|
|
353
|
+
height: managed.size.height,
|
|
354
|
+
};
|
|
355
|
+
const imageData = managed.ctx.getImageData(rect.x, rect.y, rect.width, rect.height);
|
|
356
|
+
return {
|
|
357
|
+
data: imageData.data,
|
|
358
|
+
width: imageData.width,
|
|
359
|
+
height: imageData.height,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
async toImage(options) {
|
|
363
|
+
const managed = this.canvases.get(options.canvasId);
|
|
364
|
+
if (!managed)
|
|
365
|
+
throw new Error("Canvas not found");
|
|
366
|
+
const format = options.format || "png";
|
|
367
|
+
const quality = (options.quality || 100) / 100;
|
|
368
|
+
let sourceCanvas = managed.canvas;
|
|
369
|
+
if (options.layerIds && options.layerIds.length > 0) {
|
|
370
|
+
const tempCanvas = document.createElement("canvas");
|
|
371
|
+
tempCanvas.width = managed.size.width;
|
|
372
|
+
tempCanvas.height = managed.size.height;
|
|
373
|
+
const tempCtx = tempCanvas.getContext("2d");
|
|
374
|
+
if (!tempCtx)
|
|
375
|
+
throw new Error("Failed to create temp canvas");
|
|
376
|
+
for (const layerId of options.layerIds) {
|
|
377
|
+
const layer = managed.layers.get(layerId);
|
|
378
|
+
if (layer?.visible) {
|
|
379
|
+
tempCtx.globalAlpha = layer.opacity;
|
|
380
|
+
tempCtx.drawImage(layer.canvas, 0, 0);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
sourceCanvas = tempCanvas;
|
|
384
|
+
}
|
|
385
|
+
const mimeType = format === "png"
|
|
386
|
+
? "image/png"
|
|
387
|
+
: format === "webp"
|
|
388
|
+
? "image/webp"
|
|
389
|
+
: "image/jpeg";
|
|
390
|
+
const dataUrl = sourceCanvas.toDataURL(mimeType, quality);
|
|
391
|
+
const base64 = dataUrl.split(",")[1];
|
|
392
|
+
return {
|
|
393
|
+
base64,
|
|
394
|
+
format,
|
|
395
|
+
width: managed.size.width,
|
|
396
|
+
height: managed.size.height,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async setTransform(options) {
|
|
400
|
+
const managed = this.canvases.get(options.canvasId);
|
|
401
|
+
if (!managed)
|
|
402
|
+
throw new Error("Canvas not found");
|
|
403
|
+
managed.transform = options.transform;
|
|
404
|
+
}
|
|
405
|
+
async resetTransform(options) {
|
|
406
|
+
const managed = this.canvases.get(options.canvasId);
|
|
407
|
+
if (!managed)
|
|
408
|
+
throw new Error("Canvas not found");
|
|
409
|
+
managed.transform = {};
|
|
410
|
+
managed.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
411
|
+
}
|
|
412
|
+
async setTouchEnabled(options) {
|
|
413
|
+
const managed = this.canvases.get(options.canvasId);
|
|
414
|
+
if (!managed)
|
|
415
|
+
throw new Error("Canvas not found");
|
|
416
|
+
managed.touchEnabled = options.enabled;
|
|
417
|
+
}
|
|
418
|
+
// ---- Web View Methods ----
|
|
419
|
+
async navigate(options) {
|
|
420
|
+
const placement = options.placement || "inline";
|
|
421
|
+
// Clean up any existing web view
|
|
422
|
+
this.destroyWebView();
|
|
423
|
+
// Intercept eliza:// deep links immediately
|
|
424
|
+
if (options.url.startsWith("eliza://")) {
|
|
425
|
+
const parsed = new URL(options.url);
|
|
426
|
+
const params = {};
|
|
427
|
+
parsed.searchParams.forEach((value, key) => {
|
|
428
|
+
params[key] = value;
|
|
429
|
+
});
|
|
430
|
+
this.notifyListeners("deepLink", {
|
|
431
|
+
url: options.url,
|
|
432
|
+
path: parsed.pathname,
|
|
433
|
+
params,
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (placement === "popup") {
|
|
438
|
+
const popup = window.open(options.url, "_blank", "width=800,height=600,menubar=no,toolbar=no");
|
|
439
|
+
if (!popup) {
|
|
440
|
+
this.notifyListeners("navigationError", {
|
|
441
|
+
url: options.url,
|
|
442
|
+
code: -1,
|
|
443
|
+
message: "Popup blocked by browser",
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.webViewPopup = popup;
|
|
448
|
+
// Poll to detect when popup loads (cross-origin limits apply)
|
|
449
|
+
const checkReady = setInterval(() => {
|
|
450
|
+
try {
|
|
451
|
+
if (popup.closed) {
|
|
452
|
+
clearInterval(checkReady);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Same-origin: can read title
|
|
456
|
+
const title = popup.document?.title || "";
|
|
457
|
+
clearInterval(checkReady);
|
|
458
|
+
this.notifyListeners("webViewReady", { url: options.url, title });
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// Cross-origin: fire ready without title
|
|
462
|
+
clearInterval(checkReady);
|
|
463
|
+
this.notifyListeners("webViewReady", { url: options.url, title: "" });
|
|
464
|
+
}
|
|
465
|
+
}, 200);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Inline or fullscreen: use an iframe
|
|
469
|
+
const iframe = document.createElement("iframe");
|
|
470
|
+
iframe.style.border = "none";
|
|
471
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups");
|
|
472
|
+
if (placement === "fullscreen") {
|
|
473
|
+
iframe.style.position = "fixed";
|
|
474
|
+
iframe.style.top = "0";
|
|
475
|
+
iframe.style.left = "0";
|
|
476
|
+
iframe.style.width = "100vw";
|
|
477
|
+
iframe.style.height = "100vh";
|
|
478
|
+
iframe.style.zIndex = "999999";
|
|
479
|
+
iframe.style.backgroundColor = "#fff";
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
iframe.style.width = "100%";
|
|
483
|
+
iframe.style.height = "100%";
|
|
484
|
+
}
|
|
485
|
+
iframe.addEventListener("load", () => {
|
|
486
|
+
let title = "";
|
|
487
|
+
try {
|
|
488
|
+
title = iframe.contentDocument?.title || "";
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// Cross-origin: title inaccessible
|
|
492
|
+
}
|
|
493
|
+
this.notifyListeners("webViewReady", { url: options.url, title });
|
|
494
|
+
});
|
|
495
|
+
iframe.addEventListener("error", () => {
|
|
496
|
+
this.notifyListeners("navigationError", {
|
|
497
|
+
url: options.url,
|
|
498
|
+
code: -1,
|
|
499
|
+
message: "Failed to load URL in iframe",
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
iframe.src = options.url;
|
|
503
|
+
document.body.appendChild(iframe);
|
|
504
|
+
this.webViewIframe = iframe;
|
|
505
|
+
this.ensureMessageListener();
|
|
506
|
+
}
|
|
507
|
+
async eval(options) {
|
|
508
|
+
const target = this.webViewIframe?.contentWindow ??
|
|
509
|
+
(this.webViewPopup && !this.webViewPopup.closed
|
|
510
|
+
? this.webViewPopup
|
|
511
|
+
: null);
|
|
512
|
+
if (!target) {
|
|
513
|
+
throw new Error("No web view active. Call navigate() first.");
|
|
514
|
+
}
|
|
515
|
+
return this.evalViaPostMessage(target, options.script);
|
|
516
|
+
}
|
|
517
|
+
async snapshot(options) {
|
|
518
|
+
if (!this.webViewIframe) {
|
|
519
|
+
throw new Error("No web view active or web view opened as popup (snapshot requires inline/fullscreen placement)");
|
|
520
|
+
}
|
|
521
|
+
const format = options?.format || "png";
|
|
522
|
+
const quality = (options?.quality || 85) / 100;
|
|
523
|
+
const iframeRect = this.webViewIframe.getBoundingClientRect();
|
|
524
|
+
let width = Math.round(iframeRect.width) || 800;
|
|
525
|
+
let height = Math.round(iframeRect.height) || 600;
|
|
526
|
+
if (options?.maxWidth && width > options.maxWidth) {
|
|
527
|
+
const scale = options.maxWidth / width;
|
|
528
|
+
height = Math.round(height * scale);
|
|
529
|
+
width = options.maxWidth;
|
|
530
|
+
}
|
|
531
|
+
const canvas = document.createElement("canvas");
|
|
532
|
+
canvas.width = width;
|
|
533
|
+
canvas.height = height;
|
|
534
|
+
const ctx = canvas.getContext("2d");
|
|
535
|
+
if (!ctx)
|
|
536
|
+
throw new Error("Failed to create snapshot canvas context");
|
|
537
|
+
// Attempt same-origin capture via DOM serialization + SVG foreignObject
|
|
538
|
+
let captured = false;
|
|
539
|
+
try {
|
|
540
|
+
const iframeDoc = this.webViewIframe.contentDocument;
|
|
541
|
+
if (iframeDoc) {
|
|
542
|
+
const serializer = new XMLSerializer();
|
|
543
|
+
const htmlString = serializer.serializeToString(iframeDoc);
|
|
544
|
+
const svgParts = [
|
|
545
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">`,
|
|
546
|
+
`<foreignObject width="100%" height="100%">`,
|
|
547
|
+
htmlString,
|
|
548
|
+
`</foreignObject>`,
|
|
549
|
+
`</svg>`,
|
|
550
|
+
];
|
|
551
|
+
const img = new Image();
|
|
552
|
+
const blob = new Blob([svgParts.join("")], {
|
|
553
|
+
type: "image/svg+xml;charset=utf-8",
|
|
554
|
+
});
|
|
555
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
556
|
+
try {
|
|
557
|
+
await new Promise((resolve, reject) => {
|
|
558
|
+
img.onload = () => resolve();
|
|
559
|
+
img.onerror = () => reject(new Error("SVG render failed"));
|
|
560
|
+
img.src = blobUrl;
|
|
561
|
+
});
|
|
562
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
563
|
+
captured = true;
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
URL.revokeObjectURL(blobUrl);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// Cross-origin or serialization failed — fall through to placeholder
|
|
572
|
+
}
|
|
573
|
+
if (!captured) {
|
|
574
|
+
// Render a placeholder indicating cross-origin limitation
|
|
575
|
+
ctx.fillStyle = "#f5f5f5";
|
|
576
|
+
ctx.fillRect(0, 0, width, height);
|
|
577
|
+
ctx.strokeStyle = "#ccc";
|
|
578
|
+
ctx.strokeRect(0, 0, width, height);
|
|
579
|
+
ctx.fillStyle = "#888";
|
|
580
|
+
ctx.font = "14px -apple-system, BlinkMacSystemFont, sans-serif";
|
|
581
|
+
ctx.textAlign = "center";
|
|
582
|
+
ctx.textBaseline = "middle";
|
|
583
|
+
ctx.fillText("Snapshot unavailable (cross-origin content)", width / 2, height / 2);
|
|
584
|
+
}
|
|
585
|
+
const mimeType = format === "jpeg"
|
|
586
|
+
? "image/jpeg"
|
|
587
|
+
: format === "webp"
|
|
588
|
+
? "image/webp"
|
|
589
|
+
: "image/png";
|
|
590
|
+
const dataUrl = canvas.toDataURL(mimeType, quality);
|
|
591
|
+
const base64 = dataUrl.split(",")[1];
|
|
592
|
+
return { base64, width, height, format };
|
|
593
|
+
}
|
|
594
|
+
async a2uiPush(options) {
|
|
595
|
+
// Try window.elizaA2UI bridge first (set up by the A2UI runtime)
|
|
596
|
+
const bridge = window
|
|
597
|
+
.elizaA2UI;
|
|
598
|
+
if (bridge?.push) {
|
|
599
|
+
bridge.push(options.messages || [], options.jsonl || "", options.payload || null);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// Fall back to postMessage into the web view
|
|
603
|
+
const target = this.webViewIframe?.contentWindow ||
|
|
604
|
+
(this.webViewPopup && !this.webViewPopup.closed
|
|
605
|
+
? this.webViewPopup
|
|
606
|
+
: null);
|
|
607
|
+
if (target) {
|
|
608
|
+
target.postMessage({
|
|
609
|
+
type: "eliza:a2uiPush",
|
|
610
|
+
messages: options.messages || [],
|
|
611
|
+
jsonl: options.jsonl || "",
|
|
612
|
+
payload: options.payload || null,
|
|
613
|
+
}, "*");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
throw new Error("No A2UI bridge or web view available");
|
|
617
|
+
}
|
|
618
|
+
async a2uiReset() {
|
|
619
|
+
// Try window.elizaA2UI bridge first
|
|
620
|
+
const bridge = window
|
|
621
|
+
.elizaA2UI;
|
|
622
|
+
if (bridge?.reset) {
|
|
623
|
+
bridge.reset();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// Fall back to postMessage into the web view
|
|
627
|
+
const target = this.webViewIframe?.contentWindow ||
|
|
628
|
+
(this.webViewPopup && !this.webViewPopup.closed
|
|
629
|
+
? this.webViewPopup
|
|
630
|
+
: null);
|
|
631
|
+
if (target) {
|
|
632
|
+
target.postMessage({ type: "eliza:a2uiReset" }, "*");
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
throw new Error("No A2UI bridge or web view available");
|
|
636
|
+
}
|
|
637
|
+
// ---- Web View Helpers ----
|
|
638
|
+
destroyWebView() {
|
|
639
|
+
if (this.webViewIframe) {
|
|
640
|
+
this.webViewIframe.remove();
|
|
641
|
+
this.webViewIframe = null;
|
|
642
|
+
}
|
|
643
|
+
if (this.webViewPopup && !this.webViewPopup.closed) {
|
|
644
|
+
this.webViewPopup.close();
|
|
645
|
+
}
|
|
646
|
+
this.webViewPopup = null;
|
|
647
|
+
}
|
|
648
|
+
evalViaPostMessage(target, script) {
|
|
649
|
+
return new Promise((resolve, reject) => {
|
|
650
|
+
const timeoutMs = 5000;
|
|
651
|
+
const timeout = setTimeout(() => {
|
|
652
|
+
window.removeEventListener("message", handler);
|
|
653
|
+
reject(new Error("eval timed out waiting for response from web view"));
|
|
654
|
+
}, timeoutMs);
|
|
655
|
+
const handler = (event) => {
|
|
656
|
+
const msg = event.data;
|
|
657
|
+
if (msg?.type === "eliza:evalResult" && msg.result !== undefined) {
|
|
658
|
+
clearTimeout(timeout);
|
|
659
|
+
window.removeEventListener("message", handler);
|
|
660
|
+
resolve({ result: String(msg.result) });
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
window.addEventListener("message", handler);
|
|
664
|
+
target.postMessage({ type: "eliza:eval", script }, "*");
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
ensureMessageListener() {
|
|
668
|
+
if (this.messageListenerBound)
|
|
669
|
+
return;
|
|
670
|
+
this.messageListenerBound = true;
|
|
671
|
+
window.addEventListener("message", (event) => {
|
|
672
|
+
// Only accept messages from our web view
|
|
673
|
+
const iframeSrc = this.webViewIframe?.contentWindow;
|
|
674
|
+
const popupSrc = this.webViewPopup;
|
|
675
|
+
if (event.source !== iframeSrc && event.source !== popupSrc)
|
|
676
|
+
return;
|
|
677
|
+
const msg = event.data;
|
|
678
|
+
if (!msg || typeof msg.type !== "string")
|
|
679
|
+
return;
|
|
680
|
+
if (msg.type === "eliza:deepLink" && msg.url && msg.path) {
|
|
681
|
+
this.notifyListeners("deepLink", {
|
|
682
|
+
url: msg.url,
|
|
683
|
+
path: msg.path,
|
|
684
|
+
params: msg.params || {},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (msg.type === "eliza:a2uiAction" && msg.action) {
|
|
688
|
+
this.notifyListeners("a2uiAction", {
|
|
689
|
+
action: msg.action,
|
|
690
|
+
data: msg.data || {},
|
|
691
|
+
messageId: msg.messageId,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
// ---- Drawing Helpers ----
|
|
697
|
+
getContext(canvasId, layerId) {
|
|
698
|
+
const managed = this.canvases.get(canvasId);
|
|
699
|
+
if (!managed)
|
|
700
|
+
throw new Error("Canvas not found");
|
|
701
|
+
if (layerId) {
|
|
702
|
+
const layer = managed.layers.get(layerId);
|
|
703
|
+
if (!layer)
|
|
704
|
+
throw new Error("Layer not found");
|
|
705
|
+
return layer.ctx;
|
|
706
|
+
}
|
|
707
|
+
return managed.ctx;
|
|
708
|
+
}
|
|
709
|
+
colorToString(color) {
|
|
710
|
+
if (typeof color === "string")
|
|
711
|
+
return color;
|
|
712
|
+
const a = color.a !== undefined ? color.a : 1;
|
|
713
|
+
return `rgba(${color.r}, ${color.g}, ${color.b}, ${a})`;
|
|
714
|
+
}
|
|
715
|
+
createFillStyle(ctx, fill) {
|
|
716
|
+
if ("type" in fill) {
|
|
717
|
+
return this.createGradient(ctx, fill);
|
|
718
|
+
}
|
|
719
|
+
return this.colorToString(fill.color);
|
|
720
|
+
}
|
|
721
|
+
createGradient(ctx, gradient) {
|
|
722
|
+
let grad;
|
|
723
|
+
if (gradient.type === "linear") {
|
|
724
|
+
grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
grad = ctx.createRadialGradient(gradient.x0, gradient.y0, gradient.r0, gradient.x1, gradient.y1, gradient.r1);
|
|
728
|
+
}
|
|
729
|
+
for (const stop of gradient.stops) {
|
|
730
|
+
grad.addColorStop(stop.offset, this.colorToString(stop.color));
|
|
731
|
+
}
|
|
732
|
+
return grad;
|
|
733
|
+
}
|
|
734
|
+
applyStrokeStyle(ctx, stroke) {
|
|
735
|
+
ctx.strokeStyle = this.colorToString(stroke.color);
|
|
736
|
+
ctx.lineWidth = stroke.width;
|
|
737
|
+
ctx.lineCap = stroke.lineCap || "butt";
|
|
738
|
+
ctx.lineJoin = stroke.lineJoin || "miter";
|
|
739
|
+
if (stroke.dashPattern) {
|
|
740
|
+
ctx.setLineDash(stroke.dashPattern);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
ctx.setLineDash([]);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
applyDrawOptions(ctx, canvasId, options) {
|
|
747
|
+
ctx.save();
|
|
748
|
+
// Apply canvas-level transform first (from setTransform)
|
|
749
|
+
const managed = this.canvases.get(canvasId);
|
|
750
|
+
if (managed && Object.keys(managed.transform).length > 0) {
|
|
751
|
+
this.applyTransform(ctx, managed.transform);
|
|
752
|
+
}
|
|
753
|
+
if (options?.opacity !== undefined) {
|
|
754
|
+
ctx.globalAlpha = options.opacity;
|
|
755
|
+
}
|
|
756
|
+
if (options?.blendMode) {
|
|
757
|
+
const blendMap = {
|
|
758
|
+
normal: "source-over",
|
|
759
|
+
multiply: "multiply",
|
|
760
|
+
screen: "screen",
|
|
761
|
+
overlay: "overlay",
|
|
762
|
+
darken: "darken",
|
|
763
|
+
lighten: "lighten",
|
|
764
|
+
"color-dodge": "color-dodge",
|
|
765
|
+
"color-burn": "color-burn",
|
|
766
|
+
};
|
|
767
|
+
ctx.globalCompositeOperation =
|
|
768
|
+
blendMap[options.blendMode] ?? "source-over";
|
|
769
|
+
}
|
|
770
|
+
if (options?.shadow) {
|
|
771
|
+
ctx.shadowColor = this.colorToString(options.shadow.color);
|
|
772
|
+
ctx.shadowBlur = options.shadow.blur;
|
|
773
|
+
ctx.shadowOffsetX = options.shadow.offsetX;
|
|
774
|
+
ctx.shadowOffsetY = options.shadow.offsetY;
|
|
775
|
+
}
|
|
776
|
+
// Apply draw-specific transform on top of canvas transform
|
|
777
|
+
if (options?.transform) {
|
|
778
|
+
this.applyTransform(ctx, options.transform);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
applyTransform(ctx, transform) {
|
|
782
|
+
if (transform.translateX || transform.translateY) {
|
|
783
|
+
ctx.translate(transform.translateX || 0, transform.translateY || 0);
|
|
784
|
+
}
|
|
785
|
+
if (transform.rotation) {
|
|
786
|
+
ctx.rotate(transform.rotation);
|
|
787
|
+
}
|
|
788
|
+
if (transform.scaleX !== undefined || transform.scaleY !== undefined) {
|
|
789
|
+
ctx.scale(transform.scaleX ?? 1, transform.scaleY ?? 1);
|
|
790
|
+
}
|
|
791
|
+
if (transform.skewX || transform.skewY) {
|
|
792
|
+
ctx.transform(1, transform.skewY || 0, transform.skewX || 0, 1, 0, 0);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
setupTouchHandlers(managed) {
|
|
796
|
+
const getScaledCoords = (clientX, clientY) => {
|
|
797
|
+
const rect = managed.canvas.getBoundingClientRect();
|
|
798
|
+
return {
|
|
799
|
+
x: (clientX - rect.left) * (managed.size.width / rect.width),
|
|
800
|
+
y: (clientY - rect.top) * (managed.size.height / rect.height),
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
const emitTouch = (type, touches) => {
|
|
804
|
+
this.notifyListeners("touch", { type, touches, timestamp: Date.now() });
|
|
805
|
+
};
|
|
806
|
+
const handleTouchEvent = (e, type) => {
|
|
807
|
+
if (!managed.touchEnabled)
|
|
808
|
+
return;
|
|
809
|
+
const touches = Array.from(e.touches).map((t) => ({
|
|
810
|
+
id: t.identifier,
|
|
811
|
+
...getScaledCoords(t.clientX, t.clientY),
|
|
812
|
+
force: t.force || undefined,
|
|
813
|
+
}));
|
|
814
|
+
emitTouch(type, touches);
|
|
815
|
+
};
|
|
816
|
+
managed.canvas.addEventListener("touchstart", (e) => handleTouchEvent(e, "start"));
|
|
817
|
+
managed.canvas.addEventListener("touchmove", (e) => handleTouchEvent(e, "move"));
|
|
818
|
+
managed.canvas.addEventListener("touchend", (e) => handleTouchEvent(e, "end"));
|
|
819
|
+
managed.canvas.addEventListener("touchcancel", (e) => handleTouchEvent(e, "cancel"));
|
|
820
|
+
managed.canvas.addEventListener("mousedown", (e) => {
|
|
821
|
+
if (!managed.touchEnabled)
|
|
822
|
+
return;
|
|
823
|
+
emitTouch("start", [{ id: 0, ...getScaledCoords(e.clientX, e.clientY) }]);
|
|
824
|
+
});
|
|
825
|
+
managed.canvas.addEventListener("mousemove", (e) => {
|
|
826
|
+
if (!managed.touchEnabled || e.buttons !== 1)
|
|
827
|
+
return;
|
|
828
|
+
emitTouch("move", [{ id: 0, ...getScaledCoords(e.clientX, e.clientY) }]);
|
|
829
|
+
});
|
|
830
|
+
managed.canvas.addEventListener("mouseup", () => {
|
|
831
|
+
if (!managed.touchEnabled)
|
|
832
|
+
return;
|
|
833
|
+
emitTouch("end", []);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
async addListener(eventName, listenerFunc) {
|
|
837
|
+
const entry = { eventName, callback: listenerFunc };
|
|
838
|
+
this.pluginListeners.push(entry);
|
|
839
|
+
return {
|
|
840
|
+
remove: async () => {
|
|
841
|
+
const i = this.pluginListeners.indexOf(entry);
|
|
842
|
+
if (i >= 0)
|
|
843
|
+
this.pluginListeners.splice(i, 1);
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async removeAllListeners() {
|
|
848
|
+
this.pluginListeners = [];
|
|
849
|
+
}
|
|
850
|
+
notifyListeners(eventName, data) {
|
|
851
|
+
this.pluginListeners
|
|
852
|
+
.filter((l) => l.eventName === eventName)
|
|
853
|
+
.forEach((l) => {
|
|
854
|
+
l.callback(data);
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|