@elizaos/capacitor-canvas 1.0.0 → 2.0.0-beta.1

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.
@@ -33,7 +33,7 @@ android {
33
33
  repositories {
34
34
  google()
35
35
  maven {
36
- url = uri(rootProject.ext.mavenCentralMirrorUrl)
36
+ url = uri(rootProject.ext.has('mavenCentralMirrorUrl') ? rootProject.ext.mavenCentralMirrorUrl : 'https://repo.maven.apache.org/maven2')
37
37
  }
38
38
  mavenCentral()
39
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elizaos/capacitor-canvas",
3
- "version": "1.0.0",
3
+ "version": "2.0.0-beta.1",
4
4
  "description": "Creates interactive canvases with layers, drawing primitives, and A2UI integration.",
5
5
  "keywords": [
6
6
  "canvas",
@@ -26,8 +26,8 @@
26
26
  "dist/",
27
27
  "ios/Sources/",
28
28
  "ios/Plugin.xcodeproj/",
29
- "electrobun/",
30
- "*.podspec"
29
+ "*.podspec",
30
+ "dist"
31
31
  ],
32
32
  "author": "elizaOS",
33
33
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "url": "https://github.com/elizaOS/eliza"
37
37
  },
38
38
  "scripts": {
39
- "build": "npm run clean && tsc && rollup -c rollup.config.mjs",
40
- "clean": "rimraf ./dist",
41
- "prepublishOnly": "npm run build",
39
+ "build": "bun run clean && tsc && bun --bun rollup -c rollup.config.mjs",
40
+ "clean": "node ../../../scripts/rm-path-recursive.mjs dist",
41
+ "prepublishOnly": "bun run build",
42
42
  "watch": "tsc --watch"
43
43
  },
44
44
  "devDependencies": {
@@ -48,7 +48,7 @@
48
48
  "@rollup/plugin-node-resolve": "^16.0.0",
49
49
  "rimraf": "^6.0.0",
50
50
  "rollup": "^4.60.2",
51
- "typescript": "^6.0.0"
51
+ "typescript": "^6.0.3"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@capacitor/core": "^8.3.1"
@@ -1,872 +0,0 @@
1
- /**
2
- * Canvas Plugin for Electrobun
3
- *
4
- * Provides HTML5 Canvas rendering capabilities on desktop platforms.
5
- * This is essentially the same as the web implementation since Canvas
6
- * is fully supported in the desktop Chromium renderer.
7
- */
8
-
9
- import type { PluginListenerHandle } from "@capacitor/core";
10
- import type {
11
- EventCallback,
12
- ListenerEntry as BaseListenerEntry,
13
- } from "../../../shared-types.js";
14
- import type {
15
- CanvasColor,
16
- CanvasDrawBatchCommand,
17
- CanvasDrawOptions,
18
- CanvasFillStyle,
19
- CanvasGradient,
20
- CanvasImageData,
21
- CanvasLayer,
22
- CanvasPath,
23
- CanvasPlugin,
24
- CanvasPoint,
25
- CanvasRect,
26
- CanvasRenderEvent,
27
- CanvasSize,
28
- CanvasStrokeStyle,
29
- CanvasTextStyle,
30
- CanvasTouchEvent,
31
- CanvasTransform,
32
- } from "../../src/definitions";
33
-
34
- type CanvasEvent = CanvasTouchEvent | CanvasRenderEvent;
35
-
36
- type ListenerEntry = BaseListenerEntry<string, CanvasEvent>;
37
-
38
- interface CanvasInstance {
39
- element: HTMLCanvasElement;
40
- context: CanvasRenderingContext2D;
41
- layers: Map<string, CanvasLayer>;
42
- attachedElement: HTMLElement | null;
43
- touchEnabled: boolean;
44
- }
45
-
46
- function colorToString(color: CanvasColor | string): string {
47
- if (typeof color === "string") return color;
48
- const { r, g, b, a = 1 } = color;
49
- return `rgba(${r}, ${g}, ${b}, ${a})`;
50
- }
51
-
52
- function createGradient(
53
- ctx: CanvasRenderingContext2D,
54
- gradient: CanvasGradient,
55
- ): CanvasGradient2D {
56
- let canvasGradient: CanvasGradient2D;
57
- if (gradient.type === "linear") {
58
- canvasGradient = ctx.createLinearGradient(
59
- gradient.x0,
60
- gradient.y0,
61
- gradient.x1,
62
- gradient.y1,
63
- );
64
- } else {
65
- canvasGradient = ctx.createRadialGradient(
66
- gradient.x0,
67
- gradient.y0,
68
- gradient.r0,
69
- gradient.x1,
70
- gradient.y1,
71
- gradient.r1,
72
- );
73
- }
74
- for (const stop of gradient.stops) {
75
- canvasGradient.addColorStop(stop.offset, colorToString(stop.color));
76
- }
77
- return canvasGradient;
78
- }
79
-
80
- type CanvasGradient2D = ReturnType<
81
- CanvasRenderingContext2D["createLinearGradient"]
82
- >;
83
-
84
- /**
85
- * Canvas Plugin implementation for Electrobun
86
- */
87
- export class CanvasElectrobun implements CanvasPlugin {
88
- private canvases: Map<string, CanvasInstance> = new Map();
89
- private listeners: ListenerEntry[] = [];
90
- private canvasIdCounter = 0;
91
- private layerIdCounter = 0;
92
-
93
- // MARK: - Canvas Lifecycle
94
-
95
- async create(options: {
96
- size: CanvasSize;
97
- backgroundColor?: CanvasColor | string;
98
- }): Promise<{ canvasId: string }> {
99
- const canvasId = `canvas_${++this.canvasIdCounter}`;
100
-
101
- const canvas = document.createElement("canvas");
102
- canvas.id = canvasId;
103
- canvas.width = options.size.width;
104
- canvas.height = options.size.height;
105
-
106
- const ctx = canvas.getContext("2d");
107
- if (!ctx) {
108
- throw new Error("Failed to get 2D context");
109
- }
110
-
111
- if (options.backgroundColor) {
112
- ctx.fillStyle = colorToString(options.backgroundColor);
113
- ctx.fillRect(0, 0, options.size.width, options.size.height);
114
- }
115
-
116
- const instance: CanvasInstance = {
117
- element: canvas,
118
- context: ctx,
119
- layers: new Map(),
120
- attachedElement: null,
121
- touchEnabled: false,
122
- };
123
-
124
- this.canvases.set(canvasId, instance);
125
- return { canvasId };
126
- }
127
-
128
- async destroy(options: { canvasId: string }): Promise<void> {
129
- const instance = this.canvases.get(options.canvasId);
130
- if (instance) {
131
- if (instance.attachedElement) {
132
- instance.attachedElement.removeChild(instance.element);
133
- }
134
- this.canvases.delete(options.canvasId);
135
- }
136
- }
137
-
138
- async attach(options: {
139
- canvasId: string;
140
- element: HTMLElement;
141
- }): Promise<void> {
142
- const instance = this.canvases.get(options.canvasId);
143
- if (!instance) {
144
- throw new Error(`Canvas not found: ${options.canvasId}`);
145
- }
146
-
147
- if (instance.attachedElement) {
148
- instance.attachedElement.removeChild(instance.element);
149
- }
150
-
151
- options.element.appendChild(instance.element);
152
- instance.attachedElement = options.element;
153
- }
154
-
155
- async detach(options: { canvasId: string }): Promise<void> {
156
- const instance = this.canvases.get(options.canvasId);
157
- if (!instance) {
158
- throw new Error(`Canvas not found: ${options.canvasId}`);
159
- }
160
-
161
- if (instance.attachedElement) {
162
- instance.attachedElement.removeChild(instance.element);
163
- instance.attachedElement = null;
164
- }
165
- }
166
-
167
- async resize(options: { canvasId: string; size: CanvasSize }): Promise<void> {
168
- const instance = this.canvases.get(options.canvasId);
169
- if (!instance) {
170
- throw new Error(`Canvas not found: ${options.canvasId}`);
171
- }
172
-
173
- instance.element.width = options.size.width;
174
- instance.element.height = options.size.height;
175
- }
176
-
177
- async clear(options: {
178
- canvasId: string;
179
- rect?: CanvasRect;
180
- layerId?: string;
181
- }): Promise<void> {
182
- const instance = this.canvases.get(options.canvasId);
183
- if (!instance) {
184
- throw new Error(`Canvas not found: ${options.canvasId}`);
185
- }
186
-
187
- const ctx = instance.context;
188
- if (options.rect) {
189
- ctx.clearRect(
190
- options.rect.x,
191
- options.rect.y,
192
- options.rect.width,
193
- options.rect.height,
194
- );
195
- } else {
196
- ctx.clearRect(0, 0, instance.element.width, instance.element.height);
197
- }
198
- }
199
-
200
- // MARK: - Layer Management
201
-
202
- async createLayer(options: {
203
- canvasId: string;
204
- layer: Omit<CanvasLayer, "id">;
205
- }): Promise<{ layerId: string }> {
206
- const instance = this.canvases.get(options.canvasId);
207
- if (!instance) {
208
- throw new Error(`Canvas not found: ${options.canvasId}`);
209
- }
210
-
211
- const layerId = `layer_${++this.layerIdCounter}`;
212
- const layer: CanvasLayer = {
213
- ...options.layer,
214
- id: layerId,
215
- };
216
-
217
- instance.layers.set(layerId, layer);
218
- return { layerId };
219
- }
220
-
221
- async updateLayer(options: {
222
- canvasId: string;
223
- layerId: string;
224
- layer: Partial<CanvasLayer>;
225
- }): Promise<void> {
226
- const instance = this.canvases.get(options.canvasId);
227
- if (!instance) {
228
- throw new Error(`Canvas not found: ${options.canvasId}`);
229
- }
230
-
231
- const layer = instance.layers.get(options.layerId);
232
- if (!layer) {
233
- throw new Error(`Layer not found: ${options.layerId}`);
234
- }
235
-
236
- Object.assign(layer, options.layer);
237
- }
238
-
239
- async deleteLayer(options: {
240
- canvasId: string;
241
- layerId: string;
242
- }): Promise<void> {
243
- const instance = this.canvases.get(options.canvasId);
244
- if (!instance) {
245
- throw new Error(`Canvas not found: ${options.canvasId}`);
246
- }
247
-
248
- instance.layers.delete(options.layerId);
249
- }
250
-
251
- async getLayers(options: {
252
- canvasId: string;
253
- }): Promise<{ layers: CanvasLayer[] }> {
254
- const instance = this.canvases.get(options.canvasId);
255
- if (!instance) {
256
- throw new Error(`Canvas not found: ${options.canvasId}`);
257
- }
258
-
259
- return { layers: Array.from(instance.layers.values()) };
260
- }
261
-
262
- // MARK: - Drawing Operations
263
-
264
- private applyDrawOptions(
265
- ctx: CanvasRenderingContext2D,
266
- drawOptions?: CanvasDrawOptions,
267
- ): void {
268
- if (!drawOptions) return;
269
-
270
- if (drawOptions.opacity !== undefined) {
271
- ctx.globalAlpha = drawOptions.opacity;
272
- }
273
-
274
- if (drawOptions.blendMode) {
275
- ctx.globalCompositeOperation = drawOptions.blendMode;
276
- }
277
-
278
- if (drawOptions.shadow) {
279
- ctx.shadowColor = colorToString(drawOptions.shadow.color);
280
- ctx.shadowBlur = drawOptions.shadow.blur;
281
- ctx.shadowOffsetX = drawOptions.shadow.offsetX;
282
- ctx.shadowOffsetY = drawOptions.shadow.offsetY;
283
- }
284
-
285
- if (drawOptions.transform) {
286
- this.applyTransform(ctx, drawOptions.transform);
287
- }
288
- }
289
-
290
- private applyTransform(
291
- ctx: CanvasRenderingContext2D,
292
- transform: CanvasTransform,
293
- ): void {
294
- if (transform.translateX || transform.translateY) {
295
- ctx.translate(transform.translateX || 0, transform.translateY || 0);
296
- }
297
- if (transform.rotation) {
298
- ctx.rotate(transform.rotation);
299
- }
300
- if (transform.scaleX || transform.scaleY) {
301
- ctx.scale(transform.scaleX || 1, transform.scaleY || 1);
302
- }
303
- if (transform.skewX || transform.skewY) {
304
- ctx.transform(1, transform.skewY || 0, transform.skewX || 0, 1, 0, 0);
305
- }
306
- }
307
-
308
- private applyStroke(
309
- ctx: CanvasRenderingContext2D,
310
- stroke: CanvasStrokeStyle,
311
- ): void {
312
- ctx.strokeStyle = colorToString(stroke.color);
313
- ctx.lineWidth = stroke.width;
314
- if (stroke.lineCap) ctx.lineCap = stroke.lineCap;
315
- if (stroke.lineJoin) ctx.lineJoin = stroke.lineJoin;
316
- if (stroke.dashPattern) ctx.setLineDash(stroke.dashPattern);
317
- }
318
-
319
- private applyFill(
320
- ctx: CanvasRenderingContext2D,
321
- fill: CanvasFillStyle | CanvasGradient,
322
- ): void {
323
- if ("type" in fill) {
324
- ctx.fillStyle = createGradient(ctx, fill);
325
- } else {
326
- ctx.fillStyle = colorToString(fill.color);
327
- }
328
- }
329
-
330
- async drawRect(options: {
331
- canvasId: string;
332
- rect: CanvasRect;
333
- fill?: CanvasFillStyle | CanvasGradient;
334
- stroke?: CanvasStrokeStyle;
335
- cornerRadius?: number;
336
- drawOptions?: CanvasDrawOptions;
337
- }): Promise<void> {
338
- const instance = this.canvases.get(options.canvasId);
339
- if (!instance) {
340
- throw new Error(`Canvas not found: ${options.canvasId}`);
341
- }
342
-
343
- const ctx = instance.context;
344
- ctx.save();
345
- this.applyDrawOptions(ctx, options.drawOptions);
346
-
347
- const { x, y, width, height } = options.rect;
348
- const radius = options.cornerRadius || 0;
349
-
350
- ctx.beginPath();
351
- if (radius > 0) {
352
- ctx.moveTo(x + radius, y);
353
- ctx.lineTo(x + width - radius, y);
354
- ctx.arcTo(x + width, y, x + width, y + radius, radius);
355
- ctx.lineTo(x + width, y + height - radius);
356
- ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
357
- ctx.lineTo(x + radius, y + height);
358
- ctx.arcTo(x, y + height, x, y + height - radius, radius);
359
- ctx.lineTo(x, y + radius);
360
- ctx.arcTo(x, y, x + radius, y, radius);
361
- } else {
362
- ctx.rect(x, y, width, height);
363
- }
364
- ctx.closePath();
365
-
366
- if (options.fill) {
367
- this.applyFill(ctx, options.fill);
368
- ctx.fill();
369
- }
370
- if (options.stroke) {
371
- this.applyStroke(ctx, options.stroke);
372
- ctx.stroke();
373
- }
374
-
375
- ctx.restore();
376
- }
377
-
378
- async drawEllipse(options: {
379
- canvasId: string;
380
- center: CanvasPoint;
381
- radiusX: number;
382
- radiusY: number;
383
- fill?: CanvasFillStyle | CanvasGradient;
384
- stroke?: CanvasStrokeStyle;
385
- drawOptions?: CanvasDrawOptions;
386
- }): Promise<void> {
387
- const instance = this.canvases.get(options.canvasId);
388
- if (!instance) {
389
- throw new Error(`Canvas not found: ${options.canvasId}`);
390
- }
391
-
392
- const ctx = instance.context;
393
- ctx.save();
394
- this.applyDrawOptions(ctx, options.drawOptions);
395
-
396
- ctx.beginPath();
397
- ctx.ellipse(
398
- options.center.x,
399
- options.center.y,
400
- options.radiusX,
401
- options.radiusY,
402
- 0,
403
- 0,
404
- Math.PI * 2,
405
- );
406
- ctx.closePath();
407
-
408
- if (options.fill) {
409
- this.applyFill(ctx, options.fill);
410
- ctx.fill();
411
- }
412
- if (options.stroke) {
413
- this.applyStroke(ctx, options.stroke);
414
- ctx.stroke();
415
- }
416
-
417
- ctx.restore();
418
- }
419
-
420
- async drawLine(options: {
421
- canvasId: string;
422
- from: CanvasPoint;
423
- to: CanvasPoint;
424
- stroke: CanvasStrokeStyle;
425
- drawOptions?: CanvasDrawOptions;
426
- }): Promise<void> {
427
- const instance = this.canvases.get(options.canvasId);
428
- if (!instance) {
429
- throw new Error(`Canvas not found: ${options.canvasId}`);
430
- }
431
-
432
- const ctx = instance.context;
433
- ctx.save();
434
- this.applyDrawOptions(ctx, options.drawOptions);
435
- this.applyStroke(ctx, options.stroke);
436
-
437
- ctx.beginPath();
438
- ctx.moveTo(options.from.x, options.from.y);
439
- ctx.lineTo(options.to.x, options.to.y);
440
- ctx.stroke();
441
-
442
- ctx.restore();
443
- }
444
-
445
- async drawPath(options: {
446
- canvasId: string;
447
- path: CanvasPath;
448
- fill?: CanvasFillStyle | CanvasGradient;
449
- stroke?: CanvasStrokeStyle;
450
- drawOptions?: CanvasDrawOptions;
451
- }): Promise<void> {
452
- const instance = this.canvases.get(options.canvasId);
453
- if (!instance) {
454
- throw new Error(`Canvas not found: ${options.canvasId}`);
455
- }
456
-
457
- const ctx = instance.context;
458
- ctx.save();
459
- this.applyDrawOptions(ctx, options.drawOptions);
460
-
461
- ctx.beginPath();
462
- for (const cmd of options.path.commands) {
463
- switch (cmd.type) {
464
- case "moveTo":
465
- ctx.moveTo(cmd.args[0], cmd.args[1]);
466
- break;
467
- case "lineTo":
468
- ctx.lineTo(cmd.args[0], cmd.args[1]);
469
- break;
470
- case "quadraticCurveTo":
471
- ctx.quadraticCurveTo(
472
- cmd.args[0],
473
- cmd.args[1],
474
- cmd.args[2],
475
- cmd.args[3],
476
- );
477
- break;
478
- case "bezierCurveTo":
479
- ctx.bezierCurveTo(
480
- cmd.args[0],
481
- cmd.args[1],
482
- cmd.args[2],
483
- cmd.args[3],
484
- cmd.args[4],
485
- cmd.args[5],
486
- );
487
- break;
488
- case "arcTo":
489
- ctx.arcTo(
490
- cmd.args[0],
491
- cmd.args[1],
492
- cmd.args[2],
493
- cmd.args[3],
494
- cmd.args[4],
495
- );
496
- break;
497
- case "arc":
498
- ctx.arc(
499
- cmd.args[0],
500
- cmd.args[1],
501
- cmd.args[2],
502
- cmd.args[3],
503
- cmd.args[4],
504
- cmd.args[5] === 1,
505
- );
506
- break;
507
- case "ellipse":
508
- ctx.ellipse(
509
- cmd.args[0],
510
- cmd.args[1],
511
- cmd.args[2],
512
- cmd.args[3],
513
- cmd.args[4],
514
- cmd.args[5],
515
- cmd.args[6],
516
- cmd.args[7] === 1,
517
- );
518
- break;
519
- case "rect":
520
- ctx.rect(cmd.args[0], cmd.args[1], cmd.args[2], cmd.args[3]);
521
- break;
522
- case "closePath":
523
- ctx.closePath();
524
- break;
525
- }
526
- }
527
-
528
- if (options.fill) {
529
- this.applyFill(ctx, options.fill);
530
- ctx.fill();
531
- }
532
- if (options.stroke) {
533
- this.applyStroke(ctx, options.stroke);
534
- ctx.stroke();
535
- }
536
-
537
- ctx.restore();
538
- }
539
-
540
- async drawText(options: {
541
- canvasId: string;
542
- text: string;
543
- position: CanvasPoint;
544
- style: CanvasTextStyle;
545
- drawOptions?: CanvasDrawOptions;
546
- }): Promise<void> {
547
- const instance = this.canvases.get(options.canvasId);
548
- if (!instance) {
549
- throw new Error(`Canvas not found: ${options.canvasId}`);
550
- }
551
-
552
- const ctx = instance.context;
553
- ctx.save();
554
- this.applyDrawOptions(ctx, options.drawOptions);
555
-
556
- ctx.font = `${options.style.size}px ${options.style.font}`;
557
- ctx.fillStyle = colorToString(options.style.color);
558
- if (options.style.align) ctx.textAlign = options.style.align;
559
- if (options.style.baseline) ctx.textBaseline = options.style.baseline;
560
-
561
- if (options.style.maxWidth) {
562
- ctx.fillText(
563
- options.text,
564
- options.position.x,
565
- options.position.y,
566
- options.style.maxWidth,
567
- );
568
- } else {
569
- ctx.fillText(options.text, options.position.x, options.position.y);
570
- }
571
-
572
- ctx.restore();
573
- }
574
-
575
- async drawImage(options: {
576
- canvasId: string;
577
- image: CanvasImageData | string;
578
- destRect: CanvasRect;
579
- srcRect?: CanvasRect;
580
- drawOptions?: CanvasDrawOptions;
581
- }): Promise<void> {
582
- const instance = this.canvases.get(options.canvasId);
583
- if (!instance) {
584
- throw new Error(`Canvas not found: ${options.canvasId}`);
585
- }
586
-
587
- const ctx = instance.context;
588
-
589
- return new Promise((resolve, reject) => {
590
- const img = new Image();
591
- img.onload = () => {
592
- ctx.save();
593
- this.applyDrawOptions(ctx, options.drawOptions);
594
-
595
- if (options.srcRect) {
596
- ctx.drawImage(
597
- img,
598
- options.srcRect.x,
599
- options.srcRect.y,
600
- options.srcRect.width,
601
- options.srcRect.height,
602
- options.destRect.x,
603
- options.destRect.y,
604
- options.destRect.width,
605
- options.destRect.height,
606
- );
607
- } else {
608
- ctx.drawImage(
609
- img,
610
- options.destRect.x,
611
- options.destRect.y,
612
- options.destRect.width,
613
- options.destRect.height,
614
- );
615
- }
616
-
617
- ctx.restore();
618
- resolve();
619
- };
620
- img.onerror = () => reject(new Error("Failed to load image"));
621
-
622
- if (typeof options.image === "string") {
623
- img.src = options.image;
624
- } else {
625
- img.src = `data:image/${options.image.format};base64,${options.image.base64}`;
626
- }
627
- });
628
- }
629
-
630
- async drawBatch(options: {
631
- canvasId: string;
632
- commands: CanvasDrawBatchCommand[];
633
- }): Promise<void> {
634
- const base = { canvasId: options.canvasId };
635
- for (const cmd of options.commands) {
636
- switch (cmd.type) {
637
- case "rect":
638
- await this.drawRect({ ...base, ...cmd.args });
639
- break;
640
- case "ellipse":
641
- await this.drawEllipse({ ...base, ...cmd.args });
642
- break;
643
- case "line":
644
- await this.drawLine({ ...base, ...cmd.args });
645
- break;
646
- case "path":
647
- await this.drawPath({ ...base, ...cmd.args });
648
- break;
649
- case "text":
650
- await this.drawText({ ...base, ...cmd.args });
651
- break;
652
- case "image":
653
- await this.drawImage({ ...base, ...cmd.args });
654
- break;
655
- case "clear":
656
- await this.clear({ ...base, ...cmd.args });
657
- break;
658
- }
659
- }
660
- }
661
-
662
- // MARK: - Pixel Data
663
-
664
- async getPixelData(options: {
665
- canvasId: string;
666
- rect?: CanvasRect;
667
- }): Promise<{
668
- data: Uint8ClampedArray;
669
- width: number;
670
- height: number;
671
- }> {
672
- const instance = this.canvases.get(options.canvasId);
673
- if (!instance) {
674
- throw new Error(`Canvas not found: ${options.canvasId}`);
675
- }
676
-
677
- const ctx = instance.context;
678
- const rect = options.rect || {
679
- x: 0,
680
- y: 0,
681
- width: instance.element.width,
682
- height: instance.element.height,
683
- };
684
-
685
- const imageData = ctx.getImageData(rect.x, rect.y, rect.width, rect.height);
686
- return {
687
- data: imageData.data,
688
- width: imageData.width,
689
- height: imageData.height,
690
- };
691
- }
692
-
693
- // MARK: - Export
694
-
695
- async toImage(options: {
696
- canvasId: string;
697
- format?: "png" | "jpeg" | "webp";
698
- quality?: number;
699
- layerIds?: string[];
700
- }): Promise<CanvasImageData> {
701
- const instance = this.canvases.get(options.canvasId);
702
- if (!instance) {
703
- throw new Error(`Canvas not found: ${options.canvasId}`);
704
- }
705
-
706
- const format = options.format || "png";
707
- const mimeType =
708
- format === "jpeg"
709
- ? "image/jpeg"
710
- : format === "webp"
711
- ? "image/webp"
712
- : "image/png";
713
- const quality =
714
- options.quality !== undefined ? options.quality / 100 : 0.92;
715
-
716
- const dataUrl = instance.element.toDataURL(mimeType, quality);
717
- const base64 = dataUrl.split(",")[1];
718
-
719
- return {
720
- base64,
721
- format,
722
- width: instance.element.width,
723
- height: instance.element.height,
724
- };
725
- }
726
-
727
- // MARK: - Transform
728
-
729
- async setTransform(options: {
730
- canvasId: string;
731
- transform: CanvasTransform;
732
- }): Promise<void> {
733
- const instance = this.canvases.get(options.canvasId);
734
- if (!instance) {
735
- throw new Error(`Canvas not found: ${options.canvasId}`);
736
- }
737
-
738
- const ctx = instance.context;
739
- ctx.resetTransform();
740
- this.applyTransform(ctx, options.transform);
741
- }
742
-
743
- async resetTransform(options: { canvasId: string }): Promise<void> {
744
- const instance = this.canvases.get(options.canvasId);
745
- if (!instance) {
746
- throw new Error(`Canvas not found: ${options.canvasId}`);
747
- }
748
-
749
- instance.context.resetTransform();
750
- }
751
-
752
- // MARK: - Touch Input
753
-
754
- async setTouchEnabled(options: {
755
- canvasId: string;
756
- enabled: boolean;
757
- }): Promise<void> {
758
- const instance = this.canvases.get(options.canvasId);
759
- if (!instance) {
760
- throw new Error(`Canvas not found: ${options.canvasId}`);
761
- }
762
-
763
- instance.touchEnabled = options.enabled;
764
-
765
- if (options.enabled) {
766
- this.setupTouchListeners(instance);
767
- } else {
768
- this.removeTouchListeners(instance);
769
- }
770
- }
771
-
772
- private setupTouchListeners(instance: CanvasInstance): void {
773
- const canvas = instance.element;
774
-
775
- const createTouchEvent = (
776
- e: TouchEvent,
777
- type: "start" | "move" | "end" | "cancel",
778
- ): CanvasTouchEvent => {
779
- const rect = canvas.getBoundingClientRect();
780
- const touches = Array.from(e.touches).map((t) => ({
781
- id: t.identifier,
782
- x: t.clientX - rect.left,
783
- y: t.clientY - rect.top,
784
- force: t.force,
785
- }));
786
- return { type, touches, timestamp: Date.now() };
787
- };
788
-
789
- const handlers = {
790
- touchstart: (e: TouchEvent) => {
791
- e.preventDefault();
792
- this.notifyListeners("touch", createTouchEvent(e, "start"));
793
- },
794
- touchmove: (e: TouchEvent) => {
795
- e.preventDefault();
796
- this.notifyListeners("touch", createTouchEvent(e, "move"));
797
- },
798
- touchend: (e: TouchEvent) => {
799
- e.preventDefault();
800
- this.notifyListeners("touch", createTouchEvent(e, "end"));
801
- },
802
- touchcancel: (e: TouchEvent) => {
803
- e.preventDefault();
804
- this.notifyListeners("touch", createTouchEvent(e, "cancel"));
805
- },
806
- };
807
-
808
- for (const [event, handler] of Object.entries(handlers)) {
809
- canvas.addEventListener(event, handler as EventListener);
810
- }
811
-
812
- // Store handlers for removal
813
- (
814
- canvas as HTMLCanvasElement & { _touchHandlers?: typeof handlers }
815
- )._touchHandlers = handlers;
816
- }
817
-
818
- private removeTouchListeners(instance: CanvasInstance): void {
819
- const canvas = instance.element as HTMLCanvasElement & {
820
- _touchHandlers?: Record<string, EventListener>;
821
- };
822
- const handlers = canvas._touchHandlers;
823
- if (handlers) {
824
- for (const [event, handler] of Object.entries(handlers)) {
825
- canvas.removeEventListener(event, handler);
826
- }
827
- delete canvas._touchHandlers;
828
- }
829
- }
830
-
831
- // MARK: - Event Listeners
832
-
833
- private notifyListeners<T>(eventName: string, data: T): void {
834
- for (const listener of this.listeners) {
835
- if (listener.eventName === eventName) {
836
- (listener.callback as EventCallback<T>)(data);
837
- }
838
- }
839
- }
840
-
841
- async addListener(
842
- eventName: "touch",
843
- listenerFunc: (event: CanvasTouchEvent) => void,
844
- ): Promise<PluginListenerHandle>;
845
- async addListener(
846
- eventName: "render",
847
- listenerFunc: (event: CanvasRenderEvent) => void,
848
- ): Promise<PluginListenerHandle>;
849
- async addListener(
850
- eventName: string,
851
- listenerFunc: EventCallback<CanvasEvent>,
852
- ): Promise<PluginListenerHandle> {
853
- const entry: ListenerEntry = { eventName, callback: listenerFunc };
854
- this.listeners.push(entry);
855
-
856
- return {
857
- remove: async () => {
858
- const idx = this.listeners.indexOf(entry);
859
- if (idx >= 0) {
860
- this.listeners.splice(idx, 1);
861
- }
862
- },
863
- };
864
- }
865
-
866
- async removeAllListeners(): Promise<void> {
867
- this.listeners = [];
868
- }
869
- }
870
-
871
- // Export the plugin instance
872
- export const Canvas = new CanvasElectrobun();
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2020", "DOM"],
7
- "declaration": true,
8
- "strict": true,
9
- "noUnusedLocals": true,
10
- "noUnusedParameters": true,
11
- "esModuleInterop": true,
12
- "skipLibCheck": true,
13
- "outDir": "./dist",
14
- "rootDir": "./src"
15
- },
16
- "include": ["src/**/*.ts"],
17
- "exclude": ["node_modules", "dist"]
18
- }