@twick/canvas 0.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,972 @@
1
+ import { Canvas, Control, controlsUtils, FabricImage, Rect, FabricText, Shadow, Group } from "fabric";
2
+ import { useState, useRef } from "react";
3
+ const isBrowser = typeof window !== "undefined";
4
+ const isCanvasSupported = isBrowser && !!window.HTMLCanvasElement;
5
+ function assertBrowser() {
6
+ if (!isBrowser) {
7
+ throw new Error("This code can only run in a browser environment");
8
+ }
9
+ }
10
+ function assertCanvasSupport() {
11
+ if (!isCanvasSupported) {
12
+ throw new Error("Canvas is not supported in this environment");
13
+ }
14
+ }
15
+ function createCanvas({
16
+ videoSize,
17
+ canvasSize,
18
+ canvasRef,
19
+ backgroundColor = "#000000",
20
+ selectionBorderColor = "#2563eb",
21
+ selectionLineWidth = 2,
22
+ uniScaleTransform = true,
23
+ enableRetinaScaling = true,
24
+ touchZoomThreshold = 10
25
+ }) {
26
+ assertBrowser();
27
+ assertCanvasSupport();
28
+ const canvasMetadata = {
29
+ width: canvasSize.width,
30
+ height: canvasSize.height,
31
+ aspectRatio: canvasSize.width / canvasSize.height,
32
+ scaleX: canvasSize.width / videoSize.width,
33
+ scaleY: canvasSize.height / videoSize.height
34
+ };
35
+ const canvas = new Canvas(canvasRef, {
36
+ backgroundColor,
37
+ width: canvasSize.width,
38
+ height: canvasSize.height,
39
+ preserveObjectStacking: true,
40
+ enableRetinaScaling,
41
+ selectionBorderColor,
42
+ selectionLineWidth,
43
+ uniScaleTransform,
44
+ touchZoomThreshold,
45
+ renderOnAddRemove: false,
46
+ stateful: false,
47
+ selection: true,
48
+ skipTargetFind: false,
49
+ controlsAboveOverlay: true
50
+ });
51
+ if (canvasRef) {
52
+ canvas.setDimensions({
53
+ width: canvasMetadata.width,
54
+ height: canvasMetadata.height
55
+ });
56
+ canvas.renderAll();
57
+ }
58
+ return {
59
+ canvas,
60
+ canvasMetadata
61
+ };
62
+ }
63
+ function reorderElementsByZIndex(canvas) {
64
+ if (!canvas) return;
65
+ let backgroundColor = canvas.backgroundColor;
66
+ const objects = canvas.getObjects();
67
+ objects.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
68
+ canvas.clear();
69
+ canvas.backgroundColor = backgroundColor;
70
+ objects.forEach((obj) => canvas.add(obj));
71
+ canvas.renderAll();
72
+ }
73
+ function clearCanvas(canvas) {
74
+ if (!canvas) return;
75
+ canvas.clear();
76
+ canvas.renderAll();
77
+ }
78
+ function convertToCanvasPosition(x, y, canvasMetadata) {
79
+ return {
80
+ x: x * canvasMetadata.scaleX + canvasMetadata.width / 2,
81
+ y: y * canvasMetadata.scaleY + canvasMetadata.height / 2
82
+ };
83
+ }
84
+ function convertToVideoPosition(x, y, canvasMetadata, videoSize) {
85
+ return {
86
+ x: x / canvasMetadata.scaleX - videoSize.width / 2,
87
+ y: y / canvasMetadata.scaleY - videoSize.height / 2
88
+ };
89
+ }
90
+ function getCurrentFrameEffect(item, seekTime) {
91
+ var _a;
92
+ let currentFrameEffect;
93
+ for (let i = 0; i < ((_a = item == null ? void 0 : item.frameEffects) == null ? void 0 : _a.length); i++) {
94
+ if (item.frameEffects[i].s <= seekTime && item.frameEffects[i].e >= seekTime) {
95
+ currentFrameEffect = item.frameEffects[i];
96
+ break;
97
+ }
98
+ }
99
+ return currentFrameEffect;
100
+ }
101
+ const DEFAULT_TEXT_PROPS = {
102
+ family: "Poppins",
103
+ size: 48,
104
+ fill: "#FFFFFF",
105
+ stroke: "#000000",
106
+ lineWidth: 0
107
+ };
108
+ const DEFAULT_CAPTION_PROPS = {
109
+ family: "Poppins",
110
+ size: 48,
111
+ fill: "#FFFFFF",
112
+ fontWeight: 600,
113
+ stroke: "#000000",
114
+ lineWidth: 0.2,
115
+ shadowColor: "#000000",
116
+ shadowBlur: 2,
117
+ shadowOffset: [0, 0]
118
+ };
119
+ const CANVAS_OPERATIONS = {
120
+ ITEM_SELECTED: "ITEM_SELECTED",
121
+ ITEM_UPDATED: "ITEM_UPDATED",
122
+ ITEM_DELETED: "ITEM_DELETED",
123
+ ITEM_ADDED: "ITEM_ADDED",
124
+ ITEM_GROUPED: "ITEM_GROUPED",
125
+ ITEM_UNGROUPED: "ITEM_UNGROUPED"
126
+ };
127
+ const ELEMENT_TYPES = {
128
+ TEXT: "text",
129
+ CAPTION: "caption",
130
+ IMAGE: "image",
131
+ VIDEO: "video",
132
+ RECT: "rect"
133
+ };
134
+ const disabledControl = new Control({
135
+ x: 0,
136
+ y: -0.5,
137
+ offsetY: 0,
138
+ cursorStyle: "pointer",
139
+ actionHandler: () => {
140
+ return true;
141
+ },
142
+ actionName: "scale",
143
+ render: function(ctx, left, top) {
144
+ const size = 0;
145
+ ctx.save();
146
+ ctx.translate(left, top);
147
+ ctx.fillStyle = "#red";
148
+ ctx.fillRect(-0 / 2, -0 / 2, size, size);
149
+ ctx.restore();
150
+ }
151
+ });
152
+ const rotateControl = new Control({
153
+ x: 0,
154
+ y: -0.5,
155
+ offsetY: -25,
156
+ cursorStyle: "crosshair",
157
+ actionHandler: controlsUtils.rotationWithSnapping,
158
+ actionName: "rotate",
159
+ withConnection: true
160
+ });
161
+ async function getThumbnail(videoUrl, seekTime = 0.1, playbackRate = 1) {
162
+ return new Promise((resolve, reject) => {
163
+ const video = document.createElement("video");
164
+ video.crossOrigin = "anonymous";
165
+ video.muted = true;
166
+ video.playsInline = true;
167
+ video.autoplay = false;
168
+ video.preload = "auto";
169
+ video.playbackRate = playbackRate;
170
+ let timeoutId;
171
+ const cleanup = () => {
172
+ if (video.parentNode) video.remove();
173
+ if (timeoutId) clearTimeout(timeoutId);
174
+ };
175
+ const handleError = () => {
176
+ var _a;
177
+ cleanup();
178
+ reject(new Error(`Failed to load video: ${((_a = video.error) == null ? void 0 : _a.message) || "Unknown error"}`));
179
+ };
180
+ const handleSeeked = () => {
181
+ try {
182
+ video.pause();
183
+ const canvas = document.createElement("canvas");
184
+ const width = video.videoWidth || 640;
185
+ const height = video.videoHeight || 360;
186
+ canvas.width = width;
187
+ canvas.height = height;
188
+ const ctx = canvas.getContext("2d");
189
+ if (!ctx) {
190
+ cleanup();
191
+ reject(new Error("Failed to get canvas context"));
192
+ return;
193
+ }
194
+ ctx.drawImage(video, 0, 0, width, height);
195
+ try {
196
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.8);
197
+ cleanup();
198
+ resolve(dataUrl);
199
+ } catch {
200
+ canvas.toBlob((blob) => {
201
+ if (!blob) {
202
+ cleanup();
203
+ reject(new Error("Failed to create Blob"));
204
+ return;
205
+ }
206
+ const blobUrl = URL.createObjectURL(blob);
207
+ cleanup();
208
+ resolve(blobUrl);
209
+ }, "image/jpeg", 0.8);
210
+ }
211
+ } catch (err) {
212
+ cleanup();
213
+ reject(new Error(`Error creating thumbnail: ${err}`));
214
+ }
215
+ };
216
+ video.addEventListener("error", handleError, { once: true });
217
+ video.addEventListener("seeked", handleSeeked, { once: true });
218
+ video.addEventListener("loadedmetadata", () => {
219
+ const playPromise = video.play();
220
+ if (playPromise !== void 0) {
221
+ playPromise.then(() => {
222
+ video.currentTime = seekTime;
223
+ }).catch(() => {
224
+ video.currentTime = seekTime;
225
+ });
226
+ } else {
227
+ video.currentTime = seekTime;
228
+ }
229
+ }, { once: true });
230
+ timeoutId = window.setTimeout(() => {
231
+ cleanup();
232
+ reject(new Error("Video loading timed out"));
233
+ }, 5e3);
234
+ video.src = videoUrl;
235
+ document.body.appendChild(video);
236
+ });
237
+ }
238
+ function getObjectFitSize(objectFit, elementSize, containerSize) {
239
+ const elementAspectRatio = elementSize.width / elementSize.height;
240
+ const containerAspectRatio = containerSize.width / containerSize.height;
241
+ switch (objectFit) {
242
+ case "contain":
243
+ if (elementAspectRatio > containerAspectRatio) {
244
+ return {
245
+ width: containerSize.width,
246
+ height: containerSize.width / elementAspectRatio
247
+ };
248
+ } else {
249
+ return {
250
+ width: containerSize.height * elementAspectRatio,
251
+ height: containerSize.height
252
+ };
253
+ }
254
+ case "cover":
255
+ if (elementAspectRatio > containerAspectRatio) {
256
+ return {
257
+ width: containerSize.height * elementAspectRatio,
258
+ height: containerSize.height
259
+ };
260
+ } else {
261
+ return {
262
+ width: containerSize.width,
263
+ height: containerSize.width / elementAspectRatio
264
+ };
265
+ }
266
+ case "fill":
267
+ return {
268
+ width: containerSize.width,
269
+ height: containerSize.height
270
+ };
271
+ default:
272
+ return {
273
+ width: elementSize.width,
274
+ height: elementSize.height
275
+ };
276
+ }
277
+ }
278
+ const addTextElement = ({
279
+ element,
280
+ index,
281
+ canvas,
282
+ canvasMetadata
283
+ }) => {
284
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
285
+ const { x, y } = convertToCanvasPosition(
286
+ ((_a = element.props) == null ? void 0 : _a.x) || 0,
287
+ ((_b = element.props) == null ? void 0 : _b.y) || 0,
288
+ canvasMetadata
289
+ );
290
+ const text = new FabricText(((_c = element.props) == null ? void 0 : _c.text) || "", {
291
+ left: x,
292
+ top: y,
293
+ originX: "center",
294
+ originY: "center",
295
+ angle: ((_d = element.props) == null ? void 0 : _d.rotation) || 0,
296
+ fontSize: Math.round(
297
+ (((_e = element.props) == null ? void 0 : _e.fontSize) || DEFAULT_TEXT_PROPS.size) * canvasMetadata.scaleX
298
+ ),
299
+ fontFamily: ((_f = element.props) == null ? void 0 : _f.fontFamily) || DEFAULT_TEXT_PROPS.family,
300
+ fontStyle: ((_g = element.props) == null ? void 0 : _g.fontStyle) || "normal",
301
+ fontWeight: ((_h = element.props) == null ? void 0 : _h.fontWeight) || "normal",
302
+ fill: ((_i = element.props) == null ? void 0 : _i.fill) || DEFAULT_TEXT_PROPS.fill,
303
+ stroke: ((_j = element.props) == null ? void 0 : _j.stroke) || DEFAULT_TEXT_PROPS.stroke,
304
+ strokeWidth: ((_k = element.props) == null ? void 0 : _k.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
305
+ shadow: ((_l = element.props) == null ? void 0 : _l.shadowColor) ? new Shadow({
306
+ offsetX: ((_n = (_m = element.props) == null ? void 0 : _m.shadowOffset) == null ? void 0 : _n.length) && ((_p = (_o = element.props) == null ? void 0 : _o.shadowOffset) == null ? void 0 : _p.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
307
+ offsetY: ((_r = (_q = element.props) == null ? void 0 : _q.shadowOffset) == null ? void 0 : _r.length) && ((_s = element.props) == null ? void 0 : _s.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
308
+ blur: (((_t = element.props) == null ? void 0 : _t.shadowBlur) || 2) / 2,
309
+ color: (_u = element.props) == null ? void 0 : _u.shadowColor
310
+ }) : void 0
311
+ });
312
+ text.set("id", element.id);
313
+ text.set("zIndex", index);
314
+ text.controls.mt = disabledControl;
315
+ text.controls.mb = disabledControl;
316
+ text.controls.ml = disabledControl;
317
+ text.controls.mr = disabledControl;
318
+ text.controls.bl = disabledControl;
319
+ text.controls.br = disabledControl;
320
+ text.controls.tl = disabledControl;
321
+ text.controls.tr = disabledControl;
322
+ text.controls.mtr = rotateControl;
323
+ canvas.add(text);
324
+ return text;
325
+ };
326
+ const setImageProps = ({
327
+ img,
328
+ element,
329
+ index,
330
+ canvasMetadata
331
+ }) => {
332
+ var _a, _b, _c, _d;
333
+ const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
334
+ const height = (((_b = element.props) == null ? void 0 : _b.height) || 0) * canvasMetadata.scaleY || canvasMetadata.height;
335
+ const { x, y } = convertToCanvasPosition(
336
+ ((_c = element.props) == null ? void 0 : _c.x) || 0,
337
+ ((_d = element.props) == null ? void 0 : _d.y) || 0,
338
+ canvasMetadata
339
+ );
340
+ console.log(width, height, x, y);
341
+ img.set("id", element.id);
342
+ img.set("zIndex", index);
343
+ img.set("width", width);
344
+ img.set("height", height);
345
+ img.set("left", x);
346
+ img.set("top", y);
347
+ img.set("selectable", true);
348
+ img.set("hasControls", true);
349
+ img.set("touchAction", "all");
350
+ };
351
+ const addCaptionElement = ({
352
+ element,
353
+ index,
354
+ canvas,
355
+ captionProps,
356
+ canvasMetadata
357
+ }) => {
358
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
359
+ const { x, y } = convertToCanvasPosition(
360
+ ((_b = (_a = element.props) == null ? void 0 : _a.pos) == null ? void 0 : _b.x) || ((_c = captionProps == null ? void 0 : captionProps.pos) == null ? void 0 : _c.x) || 0,
361
+ ((_e = (_d = element.props) == null ? void 0 : _d.pos) == null ? void 0 : _e.y) || ((_f = captionProps == null ? void 0 : captionProps.pos) == null ? void 0 : _f.y) || 0,
362
+ canvasMetadata
363
+ );
364
+ const caption = new FabricText(((_g = element.props) == null ? void 0 : _g.text) || "", {
365
+ left: x,
366
+ top: y,
367
+ originX: "center",
368
+ originY: "center",
369
+ angle: ((_h = element.props) == null ? void 0 : _h.rotation) || 0,
370
+ fontSize: Math.round(
371
+ (((_j = (_i = element.props) == null ? void 0 : _i.font) == null ? void 0 : _j.size) || ((_k = captionProps.font) == null ? void 0 : _k.size) || DEFAULT_CAPTION_PROPS.size) * canvasMetadata.scaleX
372
+ ),
373
+ fontFamily: ((_m = (_l = element.props) == null ? void 0 : _l.font) == null ? void 0 : _m.family) || ((_n = captionProps.font) == null ? void 0 : _n.family) || DEFAULT_CAPTION_PROPS.family,
374
+ fill: ((_o = element.props) == null ? void 0 : _o.fill) || ((_p = captionProps.color) == null ? void 0 : _p.text) || DEFAULT_CAPTION_PROPS.fill,
375
+ fontWeight: DEFAULT_CAPTION_PROPS.fontWeight,
376
+ stroke: ((_q = element.props) == null ? void 0 : _q.stroke) || DEFAULT_CAPTION_PROPS.stroke,
377
+ shadow: new Shadow({
378
+ offsetX: ((_s = (_r = element.props) == null ? void 0 : _r.shadowOffset) == null ? void 0 : _s[0]) || ((_t = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _t[0]),
379
+ offsetY: ((_v = (_u = element.props) == null ? void 0 : _u.shadowOffset) == null ? void 0 : _v[1]) || ((_w = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _w[1]),
380
+ blur: ((_x = element.props) == null ? void 0 : _x.shadowBlur) || DEFAULT_CAPTION_PROPS.shadowBlur,
381
+ color: ((_y = element.props) == null ? void 0 : _y.shadowColor) || DEFAULT_CAPTION_PROPS.shadowColor
382
+ }),
383
+ strokeWidth: ((_z = element.props) == null ? void 0 : _z.lineWidth) || DEFAULT_CAPTION_PROPS.lineWidth
384
+ });
385
+ caption.set("id", element.id);
386
+ caption.set("zIndex", index);
387
+ caption.controls.mt = disabledControl;
388
+ caption.controls.mb = disabledControl;
389
+ caption.controls.ml = disabledControl;
390
+ caption.controls.mr = disabledControl;
391
+ caption.controls.bl = disabledControl;
392
+ caption.controls.br = disabledControl;
393
+ caption.controls.tl = disabledControl;
394
+ caption.controls.tr = disabledControl;
395
+ caption.controls.mtr = disabledControl;
396
+ canvas.add(caption);
397
+ return caption;
398
+ };
399
+ const addVideoElement = async ({
400
+ element,
401
+ index,
402
+ canvas,
403
+ snapTime,
404
+ canvasMetadata,
405
+ currentFrameEffect
406
+ }) => {
407
+ var _a;
408
+ try {
409
+ const thumbnailUrl = await getThumbnail(
410
+ ((_a = element == null ? void 0 : element.props) == null ? void 0 : _a.src) || "",
411
+ snapTime
412
+ );
413
+ if (!thumbnailUrl) {
414
+ console.error("Failed to get thumbnail");
415
+ return;
416
+ }
417
+ return addImageElement({
418
+ imageUrl: thumbnailUrl,
419
+ element,
420
+ index,
421
+ canvas,
422
+ canvasMetadata,
423
+ currentFrameEffect
424
+ });
425
+ } catch (error) {
426
+ console.error("Error loading image:", error);
427
+ }
428
+ };
429
+ const addImageElement = async ({
430
+ imageUrl,
431
+ element,
432
+ index,
433
+ canvas,
434
+ canvasMetadata,
435
+ currentFrameEffect
436
+ }) => {
437
+ try {
438
+ const img = await FabricImage.fromURL(imageUrl || element.props.src || "");
439
+ img.set({
440
+ originX: "center",
441
+ originY: "center",
442
+ lockMovementX: false,
443
+ lockMovementY: false,
444
+ lockUniScaling: true,
445
+ hasControls: false,
446
+ selectable: false
447
+ });
448
+ if (element.frame) {
449
+ return addMediaGroup({
450
+ element,
451
+ img,
452
+ index,
453
+ canvas,
454
+ canvasMetadata,
455
+ currentFrameEffect
456
+ });
457
+ } else {
458
+ setImageProps({ img, element, index, canvasMetadata });
459
+ canvas.add(img);
460
+ return img;
461
+ }
462
+ } catch (error) {
463
+ console.error("Error loading image:", error);
464
+ }
465
+ };
466
+ const addMediaGroup = ({
467
+ element,
468
+ img,
469
+ index,
470
+ canvas,
471
+ canvasMetadata,
472
+ currentFrameEffect
473
+ }) => {
474
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
475
+ let frameSize;
476
+ let angle;
477
+ let framePosition;
478
+ let frameRadius = 0;
479
+ if (currentFrameEffect) {
480
+ frameSize = {
481
+ width: (((_a = currentFrameEffect.props.frameSize) == null ? void 0 : _a[0]) || 0) * canvasMetadata.scaleX || canvasMetadata.width,
482
+ height: (((_b = currentFrameEffect.props.frameSize) == null ? void 0 : _b[1]) || 0) * canvasMetadata.scaleY || canvasMetadata.height
483
+ };
484
+ angle = currentFrameEffect.props.rotation || 0;
485
+ framePosition = currentFrameEffect.props.framePosition;
486
+ if (currentFrameEffect.props.shape === "circle") {
487
+ frameRadius = frameSize.width / 2;
488
+ } else {
489
+ frameRadius = ((_c = currentFrameEffect == null ? void 0 : currentFrameEffect.props) == null ? void 0 : _c.radius) || 0;
490
+ }
491
+ } else {
492
+ frameRadius = ((_d = element == null ? void 0 : element.frame) == null ? void 0 : _d.radius) || 0;
493
+ frameSize = {
494
+ width: (((_f = (_e = element == null ? void 0 : element.frame) == null ? void 0 : _e.size) == null ? void 0 : _f[0]) || 0) * canvasMetadata.scaleX || canvasMetadata.width,
495
+ height: (((_h = (_g = element == null ? void 0 : element.frame) == null ? void 0 : _g.size) == null ? void 0 : _h[1]) || 0) * canvasMetadata.scaleY || canvasMetadata.height
496
+ };
497
+ angle = ((_i = element == null ? void 0 : element.frame) == null ? void 0 : _i.rotation) || 0;
498
+ framePosition = {
499
+ x: ((_j = element == null ? void 0 : element.frame) == null ? void 0 : _j.x) || 0,
500
+ y: ((_k = element == null ? void 0 : element.frame) == null ? void 0 : _k.y) || 0
501
+ };
502
+ }
503
+ const newSize = getObjectFitSize(
504
+ element.objectFit,
505
+ { width: img.width, height: img.height },
506
+ frameSize
507
+ );
508
+ const frameRect = new Rect({
509
+ originX: "center",
510
+ originY: "center",
511
+ lockMovementX: false,
512
+ lockMovementY: false,
513
+ lockUniScaling: true,
514
+ hasControls: false,
515
+ selectable: false,
516
+ width: frameSize.width,
517
+ height: frameSize.height,
518
+ stroke: ((_l = element == null ? void 0 : element.frame) == null ? void 0 : _l.stroke) || "#ffffff",
519
+ strokeWidth: ((_m = element == null ? void 0 : element.frame) == null ? void 0 : _m.lineWidth) || 0,
520
+ hasRotatingPoint: true,
521
+ rx: frameRadius || 0,
522
+ ry: frameRadius || 0
523
+ });
524
+ img.set({
525
+ lockUniScaling: true,
526
+ originX: "center",
527
+ originY: "center",
528
+ width: newSize.width,
529
+ height: newSize.height
530
+ });
531
+ const { x, y } = convertToCanvasPosition(
532
+ (framePosition == null ? void 0 : framePosition.x) || 0,
533
+ (framePosition == null ? void 0 : framePosition.y) || 0,
534
+ canvasMetadata
535
+ );
536
+ const groupProps = {
537
+ left: x,
538
+ top: y,
539
+ width: frameSize.width,
540
+ height: frameSize.height,
541
+ angle
542
+ };
543
+ const group = new Group([frameRect, img], {
544
+ ...groupProps,
545
+ originX: "center",
546
+ originY: "center",
547
+ angle: groupProps.angle,
548
+ selectable: true,
549
+ hasControls: true,
550
+ hasBorders: true,
551
+ clipPath: frameRect
552
+ });
553
+ group.controls.mt = disabledControl;
554
+ group.controls.mb = disabledControl;
555
+ group.controls.ml = disabledControl;
556
+ group.controls.mr = disabledControl;
557
+ group.controls.mtr = rotateControl;
558
+ group.set("id", element.id);
559
+ group.set("zIndex", index);
560
+ canvas.add(group);
561
+ return group;
562
+ };
563
+ const addRectElement = ({
564
+ element,
565
+ index,
566
+ canvas,
567
+ canvasMetadata
568
+ }) => {
569
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
570
+ const { x, y } = convertToCanvasPosition(
571
+ ((_a = element.props) == null ? void 0 : _a.x) || 0,
572
+ ((_b = element.props) == null ? void 0 : _b.y) || 0,
573
+ canvasMetadata
574
+ );
575
+ const rect = new Rect({
576
+ left: x,
577
+ // X-coordinate on the canvas
578
+ top: y,
579
+ // Y-coordinate on the canvas
580
+ originX: "center",
581
+ // Center the rectangle based on its position
582
+ originY: "center",
583
+ // Center the rectangle based on its position
584
+ angle: ((_c = element.props) == null ? void 0 : _c.rotation) || 0,
585
+ // Rotation angle
586
+ rx: (((_d = element.props) == null ? void 0 : _d.radius) || 0) * canvasMetadata.scaleX,
587
+ // Horizontal radius for rounded corners
588
+ ry: (((_e = element.props) == null ? void 0 : _e.radius) || 0) * canvasMetadata.scaleY,
589
+ // Vertical radius for rounded corners
590
+ stroke: ((_f = element.props) == null ? void 0 : _f.stroke) || "#000000",
591
+ // Stroke color
592
+ strokeWidth: (((_g = element.props) == null ? void 0 : _g.lineWidth) || 0) * canvasMetadata.scaleX,
593
+ // Scaled stroke width
594
+ fill: ((_h = element.props) == null ? void 0 : _h.fill) || "#000000",
595
+ // Fill color
596
+ opacity: ((_i = element.props) == null ? void 0 : _i.opacity) || 1,
597
+ // Opacity level
598
+ width: (((_j = element.props) == null ? void 0 : _j.width) || 0) * canvasMetadata.scaleX,
599
+ // Scaled width
600
+ height: (((_k = element.props) == null ? void 0 : _k.height) || 0) * canvasMetadata.scaleY
601
+ // Scaled height
602
+ });
603
+ rect.set("id", element.id);
604
+ rect.set("zIndex", index);
605
+ rect.controls.mtr = rotateControl;
606
+ canvas.add(rect);
607
+ return rect;
608
+ };
609
+ const addBackgroundColor = ({
610
+ element,
611
+ index,
612
+ canvas,
613
+ canvasMetadata
614
+ }) => {
615
+ const bgRect = new Rect({
616
+ width: canvasMetadata.width,
617
+ height: canvasMetadata.height,
618
+ left: canvasMetadata.width / 2,
619
+ top: canvasMetadata.height / 2,
620
+ fill: element.backgoundColor ?? "#000000",
621
+ originX: "center",
622
+ originY: "center",
623
+ hasControls: false,
624
+ hasBorders: false,
625
+ selectable: false
626
+ });
627
+ bgRect.controls.mt = disabledControl;
628
+ bgRect.controls.mb = disabledControl;
629
+ bgRect.controls.ml = disabledControl;
630
+ bgRect.controls.mr = disabledControl;
631
+ bgRect.controls.bl = disabledControl;
632
+ bgRect.controls.br = disabledControl;
633
+ bgRect.controls.tl = disabledControl;
634
+ bgRect.controls.tr = disabledControl;
635
+ bgRect.controls.mtr = disabledControl;
636
+ bgRect.set("zIndex", index - 0.5);
637
+ canvas.add(bgRect);
638
+ return bgRect;
639
+ };
640
+ const useTwickCanvas = ({
641
+ onCanvasReady,
642
+ onCanvasOperation
643
+ }) => {
644
+ const [twickCanvas, setTwickCanvas] = useState(null);
645
+ const elementMap = useRef({});
646
+ const elementFrameMap = useRef({});
647
+ const twickCanvasRef = useRef(null);
648
+ const videoSizeRef = useRef({ width: 1, height: 1 });
649
+ const canvasMetadataRef = useRef({
650
+ width: 0,
651
+ height: 0,
652
+ aspectRatio: 0,
653
+ scaleX: 1,
654
+ scaleY: 1
655
+ });
656
+ const onVideoSizeChange = (videoSize) => {
657
+ if (videoSize) {
658
+ videoSizeRef.current = videoSize;
659
+ canvasMetadataRef.current.scaleX = canvasMetadataRef.current.width / videoSize.width;
660
+ canvasMetadataRef.current.scaleY = canvasMetadataRef.current.height / videoSize.height;
661
+ }
662
+ };
663
+ const buildCanvas = ({
664
+ videoSize,
665
+ canvasSize,
666
+ canvasRef,
667
+ backgroundColor = "#000000",
668
+ selectionBorderColor = "#2563eb",
669
+ selectionLineWidth = 2,
670
+ uniScaleTransform = true,
671
+ enableRetinaScaling = true,
672
+ touchZoomThreshold = 10
673
+ }) => {
674
+ if (!canvasRef) return;
675
+ if (twickCanvasRef.current) {
676
+ console.log("Destroying twickCanvas");
677
+ twickCanvasRef.current.off("mouse:up", handleMouseUp);
678
+ twickCanvasRef.current.dispose();
679
+ }
680
+ const { canvas, canvasMetadata } = createCanvas({
681
+ videoSize,
682
+ canvasSize,
683
+ canvasRef,
684
+ backgroundColor,
685
+ selectionBorderColor,
686
+ selectionLineWidth,
687
+ uniScaleTransform,
688
+ enableRetinaScaling,
689
+ touchZoomThreshold
690
+ });
691
+ canvasMetadataRef.current = canvasMetadata;
692
+ videoSizeRef.current = videoSize;
693
+ canvas == null ? void 0 : canvas.on("mouse:up", handleMouseUp);
694
+ setTwickCanvas(canvas);
695
+ twickCanvasRef.current = canvas;
696
+ if (onCanvasReady) {
697
+ onCanvasReady(canvas);
698
+ }
699
+ };
700
+ const handleMouseUp = (event) => {
701
+ var _a, _b;
702
+ if (event.target) {
703
+ const object = event.target;
704
+ const elementId = object.get("id");
705
+ if (((_a = event.transform) == null ? void 0 : _a.action) === "drag") {
706
+ const original = event.transform.original;
707
+ if (object.left === original.left && object.top === original.top) {
708
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
709
+ CANVAS_OPERATIONS.ITEM_SELECTED,
710
+ elementMap.current[elementId]
711
+ );
712
+ return;
713
+ }
714
+ }
715
+ switch ((_b = event.transform) == null ? void 0 : _b.action) {
716
+ case "drag":
717
+ case "scale":
718
+ case "scaleX":
719
+ case "scaleY":
720
+ case "rotate":
721
+ const { x, y } = convertToVideoPosition(
722
+ object.left,
723
+ object.top,
724
+ canvasMetadataRef.current,
725
+ videoSizeRef.current
726
+ );
727
+ if (elementMap.current[elementId].type === "caption") {
728
+ elementMap.current[elementId] = {
729
+ ...elementMap.current[elementId],
730
+ props: {
731
+ ...elementMap.current[elementId].props,
732
+ pos: {
733
+ x,
734
+ y
735
+ }
736
+ }
737
+ };
738
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
739
+ CANVAS_OPERATIONS.ITEM_UPDATED,
740
+ elementMap.current[elementId]
741
+ );
742
+ } else {
743
+ if ((object == null ? void 0 : object.type) === "group") {
744
+ const currentFrameEffect = elementFrameMap.current[elementId];
745
+ let updatedFrameSize;
746
+ if (currentFrameEffect) {
747
+ updatedFrameSize = [
748
+ currentFrameEffect.props.frameSize[0] * object.scaleX,
749
+ currentFrameEffect.props.frameSize[1] * object.scaleY
750
+ ];
751
+ } else {
752
+ updatedFrameSize = [
753
+ elementMap.current[elementId].frame.size[0] * object.scaleX,
754
+ elementMap.current[elementId].frame.size[1] * object.scaleY
755
+ ];
756
+ }
757
+ if (currentFrameEffect) {
758
+ elementMap.current[elementId] = {
759
+ ...elementMap.current[elementId],
760
+ frameEffects: (elementMap.current[elementId].frameEffects || []).map(
761
+ (frameEffect) => frameEffect.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
762
+ ...frameEffect,
763
+ props: {
764
+ ...frameEffect.props,
765
+ framePosition: {
766
+ x,
767
+ y
768
+ },
769
+ frameSize: updatedFrameSize
770
+ }
771
+ } : frameEffect
772
+ )
773
+ };
774
+ elementFrameMap.current[elementId] = {
775
+ ...elementFrameMap.current[elementId],
776
+ framePosition: {
777
+ x,
778
+ y
779
+ },
780
+ frameSize: updatedFrameSize
781
+ };
782
+ } else {
783
+ elementMap.current[elementId] = {
784
+ ...elementMap.current[elementId],
785
+ frame: {
786
+ ...elementMap.current[elementId].frame,
787
+ rotation: object.angle,
788
+ size: updatedFrameSize,
789
+ x,
790
+ y
791
+ }
792
+ };
793
+ }
794
+ } else {
795
+ if ((object == null ? void 0 : object.type) === "text") {
796
+ elementMap.current[elementId] = {
797
+ ...elementMap.current[elementId],
798
+ props: {
799
+ ...elementMap.current[elementId].props,
800
+ rotation: object.angle,
801
+ x,
802
+ y
803
+ }
804
+ };
805
+ } else {
806
+ elementMap.current[elementId] = {
807
+ ...elementMap.current[elementId],
808
+ props: {
809
+ ...elementMap.current[elementId].props,
810
+ rotation: object.angle,
811
+ width: elementMap.current[elementId].props.width * object.scaleX,
812
+ height: elementMap.current[elementId].props.height * object.scaleY,
813
+ x,
814
+ y
815
+ }
816
+ };
817
+ }
818
+ }
819
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
820
+ CANVAS_OPERATIONS.ITEM_UPDATED,
821
+ elementMap.current[elementId]
822
+ );
823
+ }
824
+ break;
825
+ }
826
+ }
827
+ };
828
+ const setCanvasElements = async ({
829
+ elements,
830
+ seekTime = 0,
831
+ captionProps,
832
+ cleanAndAdd = false
833
+ }) => {
834
+ if (!twickCanvas) {
835
+ console.warn("Canvas not initialized");
836
+ return;
837
+ }
838
+ try {
839
+ if (cleanAndAdd) {
840
+ clearCanvas(twickCanvas);
841
+ }
842
+ await Promise.all(
843
+ elements.map(async (element, index) => {
844
+ try {
845
+ if (!element) {
846
+ console.warn("Element not found");
847
+ return;
848
+ }
849
+ await addElementToCanvas({
850
+ element,
851
+ index,
852
+ reorder: false,
853
+ seekTime,
854
+ captionProps
855
+ });
856
+ } catch (error) {
857
+ console.error(`Error adding element ${element.id}:`, error);
858
+ }
859
+ })
860
+ );
861
+ reorderElementsByZIndex(twickCanvas);
862
+ } catch (error) {
863
+ console.error("Error in setCanvasElements:", error);
864
+ }
865
+ };
866
+ const addElementToCanvas = async ({
867
+ element,
868
+ index,
869
+ reorder = true,
870
+ seekTime,
871
+ captionProps
872
+ }) => {
873
+ var _a, _b;
874
+ if (!twickCanvas) {
875
+ console.warn("Canvas not initialized");
876
+ return;
877
+ }
878
+ switch (element.type) {
879
+ case ELEMENT_TYPES.VIDEO:
880
+ const currentFrameEffect = getCurrentFrameEffect(element, seekTime || 0);
881
+ elementFrameMap.current[element.id] = currentFrameEffect;
882
+ const snapTime = ((seekTime || 0) - ((element == null ? void 0 : element.s) || 0)) * (((_a = element == null ? void 0 : element.props) == null ? void 0 : _a.playbackRate) || 1) + (((_b = element == null ? void 0 : element.props) == null ? void 0 : _b.time) || 0);
883
+ await addVideoElement({
884
+ element,
885
+ index,
886
+ canvas: twickCanvas,
887
+ canvasMetadata: canvasMetadataRef.current,
888
+ currentFrameEffect,
889
+ snapTime
890
+ });
891
+ if (element.timelineType === "scene") {
892
+ await addBackgroundColor({
893
+ element,
894
+ index,
895
+ canvas: twickCanvas,
896
+ canvasMetadata: canvasMetadataRef.current
897
+ });
898
+ }
899
+ break;
900
+ case ELEMENT_TYPES.IMAGE:
901
+ await addImageElement({
902
+ element,
903
+ index,
904
+ canvas: twickCanvas,
905
+ canvasMetadata: canvasMetadataRef.current
906
+ });
907
+ if (element.timelineType === "scene") {
908
+ await addBackgroundColor({
909
+ element,
910
+ index,
911
+ canvas: twickCanvas,
912
+ canvasMetadata: canvasMetadataRef.current
913
+ });
914
+ }
915
+ break;
916
+ case ELEMENT_TYPES.RECT:
917
+ await addRectElement({
918
+ element,
919
+ index,
920
+ canvas: twickCanvas,
921
+ canvasMetadata: canvasMetadataRef.current
922
+ });
923
+ break;
924
+ case ELEMENT_TYPES.TEXT:
925
+ await addTextElement({
926
+ element,
927
+ index,
928
+ canvas: twickCanvas,
929
+ canvasMetadata: canvasMetadataRef.current
930
+ });
931
+ break;
932
+ case ELEMENT_TYPES.CAPTION:
933
+ await addCaptionElement({
934
+ element,
935
+ index,
936
+ canvas: twickCanvas,
937
+ captionProps,
938
+ canvasMetadata: canvasMetadataRef.current
939
+ });
940
+ break;
941
+ }
942
+ elementMap.current[element.id] = element;
943
+ if (reorder) {
944
+ reorderElementsByZIndex(twickCanvas);
945
+ }
946
+ };
947
+ return {
948
+ twickCanvas,
949
+ buildCanvas,
950
+ onVideoSizeChange,
951
+ addElementToCanvas,
952
+ setCanvasElements
953
+ };
954
+ };
955
+ export {
956
+ CANVAS_OPERATIONS,
957
+ addBackgroundColor,
958
+ addCaptionElement,
959
+ addImageElement,
960
+ addRectElement,
961
+ addTextElement,
962
+ addVideoElement,
963
+ convertToCanvasPosition,
964
+ convertToVideoPosition,
965
+ createCanvas,
966
+ disabledControl,
967
+ getCurrentFrameEffect,
968
+ reorderElementsByZIndex,
969
+ rotateControl,
970
+ useTwickCanvas
971
+ };
972
+ //# sourceMappingURL=index.mjs.map