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