@twick/canvas 0.15.13 → 0.15.15

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 CHANGED
@@ -1,4 +1,7 @@
1
- import { Canvas, Control, controlsUtils, FabricImage, Rect, Textbox, Shadow, Group, Circle } from "fabric";
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { Canvas, Control, controlsUtils, FabricImage, Rect, Textbox, Shadow, Group, Circle, ActiveSelection } from "fabric";
2
5
  import { useState, useRef } from "react";
3
6
  const DEFAULT_TEXT_PROPS = {
4
7
  /** Font family for text elements */
@@ -46,7 +49,13 @@ const CANVAS_OPERATIONS = {
46
49
  /** Items have been ungrouped */
47
50
  ITEM_UNGROUPED: "ITEM_UNGROUPED",
48
51
  /** Caption properties have been updated */
49
- CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED"
52
+ CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED",
53
+ /** Watermark has been updated */
54
+ WATERMARK_UPDATED: "WATERMARK_UPDATED",
55
+ /** A new element was added via drop on canvas; payload is { element } */
56
+ ADDED_NEW_ELEMENT: "ADDED_NEW_ELEMENT",
57
+ /** Z-order changed (bring to front / send to back). Payload is { elementId, direction }. Timeline should reorder tracks. */
58
+ Z_ORDER_CHANGED: "Z_ORDER_CHANGED"
50
59
  };
51
60
  const ELEMENT_TYPES = {
52
61
  /** Text element type */
@@ -60,7 +69,11 @@ const ELEMENT_TYPES = {
60
69
  /** Rectangle element type */
61
70
  RECT: "rect",
62
71
  /** Circle element type */
63
- CIRCLE: "circle"
72
+ CIRCLE: "circle",
73
+ /** Icon element type */
74
+ ICON: "icon",
75
+ /** Background color element type */
76
+ BACKGROUND_COLOR: "backgroundColor"
64
77
  };
65
78
  const isBrowser = typeof window !== "undefined";
66
79
  const isCanvasSupported = isBrowser && !!window.HTMLCanvasElement;
@@ -122,6 +135,26 @@ const createCanvas = ({
122
135
  canvasMetadata
123
136
  };
124
137
  };
138
+ function measureTextWidth(text, options) {
139
+ if (typeof document === "undefined" || !text) return 0;
140
+ const canvas = document.createElement("canvas");
141
+ const ctx = canvas.getContext("2d");
142
+ if (!ctx) return 0;
143
+ const {
144
+ fontSize,
145
+ fontFamily,
146
+ fontStyle = "normal",
147
+ fontWeight = "normal"
148
+ } = options;
149
+ ctx.font = `${fontStyle} ${String(fontWeight)} ${fontSize}px ${fontFamily}`;
150
+ const lines = text.split("\n");
151
+ let maxWidth = 0;
152
+ for (const line of lines) {
153
+ const { width } = ctx.measureText(line);
154
+ if (width > maxWidth) maxWidth = width;
155
+ }
156
+ return Math.ceil(maxWidth);
157
+ }
125
158
  const reorderElementsByZIndex = (canvas) => {
126
159
  if (!canvas) return;
127
160
  const backgroundColor = canvas.backgroundColor;
@@ -132,6 +165,49 @@ const reorderElementsByZIndex = (canvas) => {
132
165
  objects.forEach((obj) => canvas.add(obj));
133
166
  canvas.renderAll();
134
167
  };
168
+ const changeZOrder = (canvas, elementId, direction) => {
169
+ var _a, _b;
170
+ if (!canvas) return null;
171
+ const objects = canvas.getObjects();
172
+ const sorted = [...objects].sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
173
+ const idx = sorted.findIndex((obj2) => {
174
+ var _a2;
175
+ return ((_a2 = obj2.get) == null ? void 0 : _a2.call(obj2, "id")) === elementId;
176
+ });
177
+ if (idx < 0) return null;
178
+ const minZ = ((_a = sorted[0]) == null ? void 0 : _a.zIndex) ?? 0;
179
+ const maxZ = ((_b = sorted[sorted.length - 1]) == null ? void 0 : _b.zIndex) ?? 0;
180
+ const obj = sorted[idx];
181
+ if (direction === "front") {
182
+ obj.set("zIndex", maxZ + 1);
183
+ reorderElementsByZIndex(canvas);
184
+ return maxZ + 1;
185
+ }
186
+ if (direction === "back") {
187
+ obj.set("zIndex", minZ - 1);
188
+ reorderElementsByZIndex(canvas);
189
+ return minZ - 1;
190
+ }
191
+ if (direction === "forward" && idx < sorted.length - 1) {
192
+ const next = sorted[idx + 1];
193
+ const myZ = obj.zIndex ?? idx;
194
+ const nextZ = next.zIndex ?? idx + 1;
195
+ obj.set("zIndex", nextZ);
196
+ next.set("zIndex", myZ);
197
+ reorderElementsByZIndex(canvas);
198
+ return nextZ;
199
+ }
200
+ if (direction === "backward" && idx > 0) {
201
+ const prev = sorted[idx - 1];
202
+ const myZ = obj.zIndex ?? idx;
203
+ const prevZ = prev.zIndex ?? idx - 1;
204
+ obj.set("zIndex", prevZ);
205
+ prev.set("zIndex", myZ);
206
+ reorderElementsByZIndex(canvas);
207
+ return prevZ;
208
+ }
209
+ return obj.zIndex ?? idx;
210
+ };
135
211
  const getCanvasContext = (canvas) => {
136
212
  var _a, _b, _c, _d;
137
213
  if (!canvas || !((_b = (_a = canvas.elements) == null ? void 0 : _a.lower) == null ? void 0 : _b.ctx)) return;
@@ -152,12 +228,31 @@ const convertToCanvasPosition = (x, y, canvasMetadata) => {
152
228
  y: y * canvasMetadata.scaleY + canvasMetadata.height / 2
153
229
  };
154
230
  };
231
+ const getObjectCanvasCenter = (obj) => {
232
+ if (obj.getCenterPoint) {
233
+ const p = obj.getCenterPoint();
234
+ return { x: p.x, y: p.y };
235
+ }
236
+ return { x: obj.left ?? 0, y: obj.top ?? 0 };
237
+ };
238
+ const getObjectCanvasAngle = (obj) => {
239
+ if (typeof obj.getTotalAngle === "function") {
240
+ return obj.getTotalAngle();
241
+ }
242
+ return obj.angle ?? 0;
243
+ };
155
244
  const convertToVideoPosition = (x, y, canvasMetadata, videoSize) => {
156
245
  return {
157
246
  x: Number((x / canvasMetadata.scaleX - videoSize.width / 2).toFixed(2)),
158
247
  y: Number((y / canvasMetadata.scaleY - videoSize.height / 2).toFixed(2))
159
248
  };
160
249
  };
250
+ const convertToVideoDimensions = (widthCanvas, heightCanvas, canvasMetadata) => {
251
+ return {
252
+ width: Number((widthCanvas / canvasMetadata.scaleX).toFixed(2)),
253
+ height: Number((heightCanvas / canvasMetadata.scaleY).toFixed(2))
254
+ };
255
+ };
161
256
  const getCurrentFrameEffect = (item, seekTime) => {
162
257
  var _a;
163
258
  let currentFrameEffect;
@@ -169,6 +264,17 @@ const getCurrentFrameEffect = (item, seekTime) => {
169
264
  }
170
265
  return currentFrameEffect;
171
266
  };
267
+ const hexToRgba = (hex, alpha) => {
268
+ const color = hex.replace(/^#/, "");
269
+ const fullHex = color.length === 3 ? color.split("").map((c) => c + c).join("") : color;
270
+ if (fullHex.length !== 6) {
271
+ return hex;
272
+ }
273
+ const r = parseInt(fullHex.slice(0, 2), 16);
274
+ const g = parseInt(fullHex.slice(2, 4), 16);
275
+ const b = parseInt(fullHex.slice(4, 6), 16);
276
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
277
+ };
172
278
  const disabledControl = new Control({
173
279
  /** X position offset */
174
280
  x: 0,
@@ -600,40 +706,66 @@ const addTextElement = ({
600
706
  canvas,
601
707
  canvasMetadata
602
708
  }) => {
603
- 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;
709
+ 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, _A, _B, _C;
604
710
  const { x, y } = convertToCanvasPosition(
605
711
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
606
712
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
607
713
  canvasMetadata
608
714
  );
609
- let width = ((_c = element.props) == null ? void 0 : _c.width) ? element.props.width * canvasMetadata.scaleX : canvasMetadata.width - 2 * MARGIN;
610
- if ((_d = element.props) == null ? void 0 : _d.maxWidth) {
611
- width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
715
+ const fontSize = Math.floor(
716
+ (((_c = element.props) == null ? void 0 : _c.fontSize) || DEFAULT_TEXT_PROPS.size) * canvasMetadata.scaleX
717
+ );
718
+ const fontFamily = ((_d = element.props) == null ? void 0 : _d.fontFamily) || DEFAULT_TEXT_PROPS.family;
719
+ const fontStyle = ((_e = element.props) == null ? void 0 : _e.fontStyle) || "normal";
720
+ const fontWeight = ((_f = element.props) == null ? void 0 : _f.fontWeight) || "normal";
721
+ let width;
722
+ if (((_g = element.props) == null ? void 0 : _g.width) != null && element.props.width > 0) {
723
+ width = element.props.width * canvasMetadata.scaleX;
724
+ if ((_h = element.props) == null ? void 0 : _h.maxWidth) {
725
+ width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
726
+ }
727
+ } else {
728
+ const textContent = ((_i = element.props) == null ? void 0 : _i.text) ?? element.t ?? "";
729
+ width = measureTextWidth(textContent, {
730
+ fontSize,
731
+ fontFamily,
732
+ fontStyle,
733
+ fontWeight
734
+ });
735
+ const padding = 4;
736
+ width = width + padding * 2;
737
+ if ((_j = element.props) == null ? void 0 : _j.maxWidth) {
738
+ width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
739
+ }
740
+ if (width <= 0) width = 100;
612
741
  }
613
- const text = new Textbox(((_e = element.props) == null ? void 0 : _e.text) || element.t || "", {
742
+ const backgroundColor = ((_k = element.props) == null ? void 0 : _k.backgroundColor) ? hexToRgba(
743
+ element.props.backgroundColor,
744
+ ((_l = element.props) == null ? void 0 : _l.backgroundOpacity) ?? 1
745
+ ) : void 0;
746
+ const text = new Textbox(((_m = element.props) == null ? void 0 : _m.text) || element.t || "", {
614
747
  left: x,
615
748
  top: y,
616
749
  originX: "center",
617
750
  originY: "center",
618
- angle: ((_f = element.props) == null ? void 0 : _f.rotation) || 0,
619
- fontSize: Math.floor(
620
- (((_g = element.props) == null ? void 0 : _g.fontSize) || DEFAULT_TEXT_PROPS.size) * canvasMetadata.scaleX
621
- ),
622
- fontFamily: ((_h = element.props) == null ? void 0 : _h.fontFamily) || DEFAULT_TEXT_PROPS.family,
623
- fontStyle: ((_i = element.props) == null ? void 0 : _i.fontStyle) || "normal",
624
- fontWeight: ((_j = element.props) == null ? void 0 : _j.fontWeight) || "normal",
625
- fill: ((_k = element.props) == null ? void 0 : _k.fill) || DEFAULT_TEXT_PROPS.fill,
626
- opacity: ((_l = element.props) == null ? void 0 : _l.opacity) ?? 1,
751
+ angle: ((_n = element.props) == null ? void 0 : _n.rotation) || 0,
752
+ fontSize,
753
+ fontFamily,
754
+ fontStyle,
755
+ fontWeight,
756
+ fill: ((_o = element.props) == null ? void 0 : _o.fill) || DEFAULT_TEXT_PROPS.fill,
757
+ opacity: ((_p = element.props) == null ? void 0 : _p.opacity) ?? 1,
627
758
  width,
628
759
  splitByGrapheme: false,
629
- textAlign: ((_m = element.props) == null ? void 0 : _m.textAlign) || "center",
630
- stroke: ((_n = element.props) == null ? void 0 : _n.stroke) || DEFAULT_TEXT_PROPS.stroke,
631
- strokeWidth: ((_o = element.props) == null ? void 0 : _o.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
632
- shadow: ((_p = element.props) == null ? void 0 : _p.shadowColor) ? new Shadow({
633
- offsetX: ((_r = (_q = element.props) == null ? void 0 : _q.shadowOffset) == null ? void 0 : _r.length) && ((_t = (_s = element.props) == null ? void 0 : _s.shadowOffset) == null ? void 0 : _t.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
634
- offsetY: ((_v = (_u = element.props) == null ? void 0 : _u.shadowOffset) == null ? void 0 : _v.length) && ((_w = element.props) == null ? void 0 : _w.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
635
- blur: (((_x = element.props) == null ? void 0 : _x.shadowBlur) || 2) / 2,
636
- color: (_y = element.props) == null ? void 0 : _y.shadowColor
760
+ textAlign: ((_q = element.props) == null ? void 0 : _q.textAlign) || "center",
761
+ stroke: ((_r = element.props) == null ? void 0 : _r.stroke) || DEFAULT_TEXT_PROPS.stroke,
762
+ strokeWidth: ((_s = element.props) == null ? void 0 : _s.lineWidth) || DEFAULT_TEXT_PROPS.lineWidth,
763
+ ...backgroundColor && { backgroundColor },
764
+ shadow: ((_t = element.props) == null ? void 0 : _t.shadowColor) ? new Shadow({
765
+ offsetX: ((_v = (_u = element.props) == null ? void 0 : _u.shadowOffset) == null ? void 0 : _v.length) && ((_x = (_w = element.props) == null ? void 0 : _w.shadowOffset) == null ? void 0 : _x.length) > 1 ? element.props.shadowOffset[0] / 2 : 1,
766
+ offsetY: ((_z = (_y = element.props) == null ? void 0 : _y.shadowOffset) == null ? void 0 : _z.length) && ((_A = element.props) == null ? void 0 : _A.shadowOffset.length) > 1 ? element.props.shadowOffset[1] / 2 : 1,
767
+ blur: (((_B = element.props) == null ? void 0 : _B.shadowBlur) || 2) / 2,
768
+ color: (_C = element.props) == null ? void 0 : _C.shadowColor
637
769
  }) : void 0
638
770
  });
639
771
  text.set("id", element.id);
@@ -654,7 +786,8 @@ const setImageProps = ({
654
786
  img,
655
787
  element,
656
788
  index,
657
- canvasMetadata
789
+ canvasMetadata,
790
+ lockAspectRatio = true
658
791
  }) => {
659
792
  var _a, _b, _c, _d, _e;
660
793
  const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
@@ -674,13 +807,15 @@ const setImageProps = ({
674
807
  img.set("selectable", true);
675
808
  img.set("hasControls", true);
676
809
  img.set("touchAction", "all");
810
+ img.set("lockUniScaling", lockAspectRatio);
677
811
  };
678
812
  const addCaptionElement = ({
679
813
  element,
680
814
  index,
681
815
  canvas,
682
816
  captionProps,
683
- canvasMetadata
817
+ canvasMetadata,
818
+ lockAspectRatio = false
684
819
  }) => {
685
820
  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, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J;
686
821
  const { x, y } = convertToCanvasPosition(
@@ -719,6 +854,7 @@ const addCaptionElement = ({
719
854
  });
720
855
  caption.set("id", element.id);
721
856
  caption.set("zIndex", index);
857
+ caption.set("lockUniScaling", lockAspectRatio);
722
858
  caption.controls.mt = disabledControl;
723
859
  caption.controls.mb = disabledControl;
724
860
  caption.controls.ml = disabledControl;
@@ -766,7 +902,8 @@ const addImageElement = async ({
766
902
  index,
767
903
  canvas,
768
904
  canvasMetadata,
769
- currentFrameEffect
905
+ currentFrameEffect,
906
+ lockAspectRatio = true
770
907
  }) => {
771
908
  try {
772
909
  const img = await FabricImage.fromURL(imageUrl || element.props.src || "");
@@ -775,7 +912,7 @@ const addImageElement = async ({
775
912
  originY: "center",
776
913
  lockMovementX: false,
777
914
  lockMovementY: false,
778
- lockUniScaling: true,
915
+ lockUniScaling: lockAspectRatio,
779
916
  hasControls: false,
780
917
  selectable: false
781
918
  });
@@ -786,10 +923,11 @@ const addImageElement = async ({
786
923
  index,
787
924
  canvas,
788
925
  canvasMetadata,
789
- currentFrameEffect
926
+ currentFrameEffect,
927
+ lockAspectRatio
790
928
  });
791
929
  } else {
792
- setImageProps({ img, element, index, canvasMetadata });
930
+ setImageProps({ img, element, index, canvasMetadata, lockAspectRatio });
793
931
  canvas.add(img);
794
932
  return img;
795
933
  }
@@ -802,7 +940,8 @@ const addMediaGroup = ({
802
940
  index,
803
941
  canvas,
804
942
  canvasMetadata,
805
- currentFrameEffect
943
+ currentFrameEffect,
944
+ lockAspectRatio = true
806
945
  }) => {
807
946
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
808
947
  let frameSize;
@@ -891,6 +1030,7 @@ const addMediaGroup = ({
891
1030
  group.controls.mtr = rotateControl;
892
1031
  group.set("id", element.id);
893
1032
  group.set("zIndex", index);
1033
+ group.set("lockUniScaling", lockAspectRatio);
894
1034
  canvas.add(group);
895
1035
  return group;
896
1036
  };
@@ -898,7 +1038,8 @@ const addRectElement = ({
898
1038
  element,
899
1039
  index,
900
1040
  canvas,
901
- canvasMetadata
1041
+ canvasMetadata,
1042
+ lockAspectRatio = false
902
1043
  }) => {
903
1044
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
904
1045
  const { x, y } = convertToCanvasPosition(
@@ -936,6 +1077,7 @@ const addRectElement = ({
936
1077
  });
937
1078
  rect.set("id", element.id);
938
1079
  rect.set("zIndex", index);
1080
+ rect.set("lockUniScaling", lockAspectRatio);
939
1081
  rect.controls.mtr = rotateControl;
940
1082
  canvas.add(rect);
941
1083
  return rect;
@@ -944,9 +1086,10 @@ const addCircleElement = ({
944
1086
  element,
945
1087
  index,
946
1088
  canvas,
947
- canvasMetadata
1089
+ canvasMetadata,
1090
+ lockAspectRatio = true
948
1091
  }) => {
949
- var _a, _b, _c, _d, _e, _f;
1092
+ var _a, _b, _c, _d, _e, _f, _g;
950
1093
  const { x, y } = convertToCanvasPosition(
951
1094
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
952
1095
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
@@ -962,7 +1105,9 @@ const addCircleElement = ({
962
1105
  stroke: ((_e = element.props) == null ? void 0 : _e.stroke) || "#000000",
963
1106
  strokeWidth: (((_f = element.props) == null ? void 0 : _f.lineWidth) || 0) * canvasMetadata.scaleX,
964
1107
  originX: "center",
965
- originY: "center"
1108
+ originY: "center",
1109
+ // Respect element opacity (0–1). Defaults to fully opaque.
1110
+ opacity: ((_g = element.props) == null ? void 0 : _g.opacity) ?? 1
966
1111
  });
967
1112
  circle.controls.mt = disabledControl;
968
1113
  circle.controls.mb = disabledControl;
@@ -971,6 +1116,7 @@ const addCircleElement = ({
971
1116
  circle.controls.mtr = disabledControl;
972
1117
  circle.set("id", element.id);
973
1118
  circle.set("zIndex", index);
1119
+ circle.set("lockUniScaling", lockAspectRatio);
974
1120
  canvas.add(circle);
975
1121
  return circle;
976
1122
  };
@@ -1005,17 +1151,462 @@ const addBackgroundColor = ({
1005
1151
  canvas.add(bgRect);
1006
1152
  return bgRect;
1007
1153
  };
1154
+ const VideoElement = {
1155
+ name: ELEMENT_TYPES.VIDEO,
1156
+ async add(params) {
1157
+ var _a, _b;
1158
+ const {
1159
+ element,
1160
+ index,
1161
+ canvas,
1162
+ canvasMetadata,
1163
+ seekTime = 0,
1164
+ elementFrameMapRef,
1165
+ getCurrentFrameEffect: getFrameEffect
1166
+ } = params;
1167
+ if (!getFrameEffect || !elementFrameMapRef) return;
1168
+ const currentFrameEffect = getFrameEffect(element, seekTime);
1169
+ elementFrameMapRef.current[element.id] = currentFrameEffect;
1170
+ const snapTime = (seekTime - ((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);
1171
+ await addVideoElement({
1172
+ element,
1173
+ index,
1174
+ canvas,
1175
+ canvasMetadata,
1176
+ currentFrameEffect,
1177
+ snapTime
1178
+ });
1179
+ if (element.timelineType === "scene") {
1180
+ await addBackgroundColor({
1181
+ element,
1182
+ index,
1183
+ canvas,
1184
+ canvasMetadata
1185
+ });
1186
+ }
1187
+ },
1188
+ updateFromFabricObject(object, element, context) {
1189
+ const canvasCenter = getObjectCanvasCenter(object);
1190
+ const { x, y } = convertToVideoPosition(
1191
+ canvasCenter.x,
1192
+ canvasCenter.y,
1193
+ context.canvasMetadata,
1194
+ context.videoSize
1195
+ );
1196
+ const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
1197
+ const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
1198
+ const { width: fw, height: fh } = convertToVideoDimensions(
1199
+ scaledW,
1200
+ scaledH,
1201
+ context.canvasMetadata
1202
+ );
1203
+ const updatedFrameSize = [fw, fh];
1204
+ const currentFrameEffect = context.elementFrameMapRef.current[element.id];
1205
+ if (currentFrameEffect) {
1206
+ context.elementFrameMapRef.current[element.id] = {
1207
+ ...currentFrameEffect,
1208
+ props: {
1209
+ ...currentFrameEffect.props,
1210
+ framePosition: { x, y },
1211
+ frameSize: updatedFrameSize
1212
+ }
1213
+ };
1214
+ return {
1215
+ element: {
1216
+ ...element,
1217
+ frameEffects: (element.frameEffects || []).map(
1218
+ (fe) => fe.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
1219
+ ...fe,
1220
+ props: {
1221
+ ...fe.props,
1222
+ framePosition: { x, y },
1223
+ frameSize: updatedFrameSize
1224
+ }
1225
+ } : fe
1226
+ )
1227
+ }
1228
+ };
1229
+ }
1230
+ const frame = element.frame;
1231
+ return {
1232
+ element: {
1233
+ ...element,
1234
+ frame: {
1235
+ ...frame,
1236
+ rotation: getObjectCanvasAngle(object),
1237
+ size: updatedFrameSize,
1238
+ x,
1239
+ y
1240
+ }
1241
+ }
1242
+ };
1243
+ }
1244
+ };
1245
+ const ImageElement = {
1246
+ name: ELEMENT_TYPES.IMAGE,
1247
+ async add(params) {
1248
+ var _a;
1249
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
1250
+ await addImageElement({
1251
+ element,
1252
+ index,
1253
+ canvas,
1254
+ canvasMetadata,
1255
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1256
+ });
1257
+ if (element.timelineType === "scene") {
1258
+ await addBackgroundColor({
1259
+ element,
1260
+ index,
1261
+ canvas,
1262
+ canvasMetadata
1263
+ });
1264
+ }
1265
+ },
1266
+ updateFromFabricObject(object, element, context) {
1267
+ const canvasCenter = getObjectCanvasCenter(object);
1268
+ const { x, y } = convertToVideoPosition(
1269
+ canvasCenter.x,
1270
+ canvasCenter.y,
1271
+ context.canvasMetadata,
1272
+ context.videoSize
1273
+ );
1274
+ const currentFrameEffect = context.elementFrameMapRef.current[element.id];
1275
+ if (object.type === "group") {
1276
+ const scaledW2 = (object.width ?? 0) * (object.scaleX ?? 1);
1277
+ const scaledH2 = (object.height ?? 0) * (object.scaleY ?? 1);
1278
+ const { width: fw, height: fh } = convertToVideoDimensions(
1279
+ scaledW2,
1280
+ scaledH2,
1281
+ context.canvasMetadata
1282
+ );
1283
+ const updatedFrameSize = [fw, fh];
1284
+ if (currentFrameEffect) {
1285
+ context.elementFrameMapRef.current[element.id] = {
1286
+ ...currentFrameEffect,
1287
+ props: {
1288
+ ...currentFrameEffect.props,
1289
+ framePosition: { x, y },
1290
+ frameSize: updatedFrameSize
1291
+ }
1292
+ };
1293
+ return {
1294
+ element: {
1295
+ ...element,
1296
+ // Keep the base frame in sync with the active frame effect
1297
+ // so visualizer `Rect {...element.frame}` reflects the same size/position.
1298
+ frame: element.frame ? {
1299
+ ...element.frame,
1300
+ rotation: getObjectCanvasAngle(object),
1301
+ size: updatedFrameSize,
1302
+ x,
1303
+ y
1304
+ } : element.frame,
1305
+ frameEffects: (element.frameEffects || []).map(
1306
+ (fe) => fe.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
1307
+ ...fe,
1308
+ props: {
1309
+ ...fe.props,
1310
+ framePosition: { x, y },
1311
+ frameSize: updatedFrameSize
1312
+ }
1313
+ } : fe
1314
+ )
1315
+ }
1316
+ };
1317
+ }
1318
+ const frame = element.frame;
1319
+ return {
1320
+ element: {
1321
+ ...element,
1322
+ frame: {
1323
+ ...frame,
1324
+ rotation: getObjectCanvasAngle(object),
1325
+ size: updatedFrameSize,
1326
+ x,
1327
+ y
1328
+ }
1329
+ }
1330
+ };
1331
+ }
1332
+ const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
1333
+ const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
1334
+ const { width, height } = convertToVideoDimensions(
1335
+ scaledW,
1336
+ scaledH,
1337
+ context.canvasMetadata
1338
+ );
1339
+ return {
1340
+ element: {
1341
+ ...element,
1342
+ props: {
1343
+ ...element.props,
1344
+ rotation: getObjectCanvasAngle(object),
1345
+ width,
1346
+ height,
1347
+ x,
1348
+ y
1349
+ }
1350
+ }
1351
+ };
1352
+ }
1353
+ };
1354
+ const RectElement = {
1355
+ name: ELEMENT_TYPES.RECT,
1356
+ async add(params) {
1357
+ var _a;
1358
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
1359
+ await addRectElement({
1360
+ element,
1361
+ index,
1362
+ canvas,
1363
+ canvasMetadata,
1364
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1365
+ });
1366
+ },
1367
+ updateFromFabricObject(object, element, context) {
1368
+ var _a, _b;
1369
+ const canvasCenter = getObjectCanvasCenter(object);
1370
+ const { x, y } = convertToVideoPosition(
1371
+ canvasCenter.x,
1372
+ canvasCenter.y,
1373
+ context.canvasMetadata,
1374
+ context.videoSize
1375
+ );
1376
+ return {
1377
+ element: {
1378
+ ...element,
1379
+ props: {
1380
+ ...element.props,
1381
+ rotation: getObjectCanvasAngle(object),
1382
+ width: (((_a = element.props) == null ? void 0 : _a.width) ?? 0) * object.scaleX,
1383
+ height: (((_b = element.props) == null ? void 0 : _b.height) ?? 0) * object.scaleY,
1384
+ x,
1385
+ y
1386
+ }
1387
+ }
1388
+ };
1389
+ }
1390
+ };
1391
+ const CircleElement = {
1392
+ name: ELEMENT_TYPES.CIRCLE,
1393
+ async add(params) {
1394
+ var _a;
1395
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
1396
+ await addCircleElement({
1397
+ element,
1398
+ index,
1399
+ canvas,
1400
+ canvasMetadata,
1401
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1402
+ });
1403
+ },
1404
+ updateFromFabricObject(object, element, context) {
1405
+ var _a, _b;
1406
+ const canvasCenter = getObjectCanvasCenter(object);
1407
+ const { x, y } = convertToVideoPosition(
1408
+ canvasCenter.x,
1409
+ canvasCenter.y,
1410
+ context.canvasMetadata,
1411
+ context.videoSize
1412
+ );
1413
+ const radius = Number(
1414
+ ((((_a = element.props) == null ? void 0 : _a.radius) ?? 0) * object.scaleX).toFixed(2)
1415
+ );
1416
+ const opacity = object.opacity != null ? object.opacity : (_b = element.props) == null ? void 0 : _b.opacity;
1417
+ return {
1418
+ element: {
1419
+ ...element,
1420
+ props: {
1421
+ ...element.props,
1422
+ rotation: getObjectCanvasAngle(object),
1423
+ radius,
1424
+ height: radius * 2,
1425
+ width: radius * 2,
1426
+ x,
1427
+ y,
1428
+ ...opacity != null && { opacity }
1429
+ }
1430
+ }
1431
+ };
1432
+ }
1433
+ };
1434
+ const TextElement = {
1435
+ name: ELEMENT_TYPES.TEXT,
1436
+ async add(params) {
1437
+ const { element, index, canvas, canvasMetadata } = params;
1438
+ await addTextElement({
1439
+ element,
1440
+ index,
1441
+ canvas,
1442
+ canvasMetadata
1443
+ });
1444
+ },
1445
+ updateFromFabricObject(object, element, context) {
1446
+ const canvasCenter = getObjectCanvasCenter(object);
1447
+ const { x, y } = convertToVideoPosition(
1448
+ canvasCenter.x,
1449
+ canvasCenter.y,
1450
+ context.canvasMetadata,
1451
+ context.videoSize
1452
+ );
1453
+ return {
1454
+ element: {
1455
+ ...element,
1456
+ props: {
1457
+ ...element.props,
1458
+ rotation: getObjectCanvasAngle(object),
1459
+ x,
1460
+ y
1461
+ }
1462
+ }
1463
+ };
1464
+ }
1465
+ };
1466
+ const CaptionElement = {
1467
+ name: ELEMENT_TYPES.CAPTION,
1468
+ async add(params) {
1469
+ var _a;
1470
+ const { element, index, canvas, captionProps, canvasMetadata, lockAspectRatio } = params;
1471
+ await addCaptionElement({
1472
+ element,
1473
+ index,
1474
+ canvas,
1475
+ captionProps: captionProps ?? {},
1476
+ canvasMetadata,
1477
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1478
+ });
1479
+ },
1480
+ updateFromFabricObject(object, element, context) {
1481
+ var _a;
1482
+ const canvasCenter = getObjectCanvasCenter(object);
1483
+ const { x, y } = convertToVideoPosition(
1484
+ canvasCenter.x,
1485
+ canvasCenter.y,
1486
+ context.canvasMetadata,
1487
+ context.videoSize
1488
+ );
1489
+ if ((_a = context.captionPropsRef.current) == null ? void 0 : _a.applyToAll) {
1490
+ return {
1491
+ element,
1492
+ operation: CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED,
1493
+ payload: {
1494
+ element,
1495
+ props: {
1496
+ ...context.captionPropsRef.current,
1497
+ x,
1498
+ y
1499
+ }
1500
+ }
1501
+ };
1502
+ }
1503
+ return {
1504
+ element: {
1505
+ ...element,
1506
+ props: {
1507
+ ...element.props,
1508
+ x,
1509
+ y
1510
+ }
1511
+ }
1512
+ };
1513
+ }
1514
+ };
1515
+ const WatermarkElement = {
1516
+ name: "watermark",
1517
+ async add(params) {
1518
+ const { element, index, canvas, canvasMetadata, watermarkPropsRef } = params;
1519
+ if (element.type === ELEMENT_TYPES.TEXT) {
1520
+ if (watermarkPropsRef) watermarkPropsRef.current = element.props;
1521
+ await addTextElement({
1522
+ element,
1523
+ index,
1524
+ canvas,
1525
+ canvasMetadata
1526
+ });
1527
+ } else if (element.type === ELEMENT_TYPES.IMAGE) {
1528
+ await addImageElement({
1529
+ element,
1530
+ index,
1531
+ canvas,
1532
+ canvasMetadata
1533
+ });
1534
+ }
1535
+ },
1536
+ updateFromFabricObject(object, element, context) {
1537
+ const { x, y } = convertToVideoPosition(
1538
+ object.left,
1539
+ object.top,
1540
+ context.canvasMetadata,
1541
+ context.videoSize
1542
+ );
1543
+ const rotation = object.angle != null ? object.angle : void 0;
1544
+ const opacity = object.opacity != null ? object.opacity : void 0;
1545
+ const baseProps = element.type === ELEMENT_TYPES.TEXT ? context.watermarkPropsRef.current ?? element.props ?? {} : { ...element.props };
1546
+ const props = element.type === ELEMENT_TYPES.IMAGE && (object.scaleX != null || object.scaleY != null) ? {
1547
+ ...baseProps,
1548
+ width: baseProps.width != null && object.scaleX != null ? baseProps.width * object.scaleX : baseProps.width,
1549
+ height: baseProps.height != null && object.scaleY != null ? baseProps.height * object.scaleY : baseProps.height
1550
+ } : baseProps;
1551
+ const payload = {
1552
+ position: { x, y },
1553
+ ...rotation != null && { rotation },
1554
+ ...opacity != null && { opacity },
1555
+ ...Object.keys(props).length > 0 && { props }
1556
+ };
1557
+ return {
1558
+ element: { ...element, props: { ...element.props, x, y, rotation, opacity, ...props } },
1559
+ operation: CANVAS_OPERATIONS.WATERMARK_UPDATED,
1560
+ payload
1561
+ };
1562
+ }
1563
+ };
1564
+ class ElementController {
1565
+ constructor() {
1566
+ __publicField(this, "elements", /* @__PURE__ */ new Map());
1567
+ }
1568
+ register(handler) {
1569
+ this.elements.set(handler.name, handler);
1570
+ }
1571
+ get(name) {
1572
+ return this.elements.get(name);
1573
+ }
1574
+ list() {
1575
+ return Array.from(this.elements.keys());
1576
+ }
1577
+ }
1578
+ const elementController = new ElementController();
1579
+ function registerElements() {
1580
+ elementController.register(VideoElement);
1581
+ elementController.register(ImageElement);
1582
+ elementController.register(RectElement);
1583
+ elementController.register(CircleElement);
1584
+ elementController.register(TextElement);
1585
+ elementController.register(CaptionElement);
1586
+ elementController.register(WatermarkElement);
1587
+ }
1588
+ registerElements();
1008
1589
  const useTwickCanvas = ({
1009
1590
  onCanvasReady,
1010
- onCanvasOperation
1591
+ onCanvasOperation,
1592
+ /**
1593
+ * When true, holding Shift while dragging an object will lock movement to
1594
+ * the dominant axis (horizontal or vertical). This mirrors behavior in
1595
+ * professional editors and improves precise alignment.
1596
+ *
1597
+ * Default: false (opt‑in to avoid surprising existing consumers).
1598
+ */
1599
+ enableShiftAxisLock = false
1011
1600
  }) => {
1012
1601
  const [twickCanvas, setTwickCanvas] = useState(null);
1013
1602
  const elementMap = useRef({});
1603
+ const watermarkPropsRef = useRef(null);
1014
1604
  const elementFrameMap = useRef({});
1015
1605
  const twickCanvasRef = useRef(null);
1016
1606
  const videoSizeRef = useRef({ width: 1, height: 1 });
1017
1607
  const canvasResolutionRef = useRef({ width: 1, height: 1 });
1018
1608
  const captionPropsRef = useRef(null);
1609
+ const axisLockStateRef = useRef(null);
1019
1610
  const canvasMetadataRef = useRef({
1020
1611
  width: 0,
1021
1612
  height: 0,
@@ -1030,6 +1621,57 @@ const useTwickCanvas = ({
1030
1621
  canvasMetadataRef.current.scaleY = canvasMetadataRef.current.height / videoSize.height;
1031
1622
  }
1032
1623
  };
1624
+ const handleObjectMoving = (event) => {
1625
+ var _a;
1626
+ if (!enableShiftAxisLock) return;
1627
+ const target = event == null ? void 0 : event.target;
1628
+ const transform = event == null ? void 0 : event.transform;
1629
+ const pointerEvent = event == null ? void 0 : event.e;
1630
+ if (!target || !transform || !pointerEvent) {
1631
+ axisLockStateRef.current = null;
1632
+ return;
1633
+ }
1634
+ if (!pointerEvent.shiftKey) {
1635
+ axisLockStateRef.current = null;
1636
+ return;
1637
+ }
1638
+ const original = transform.original;
1639
+ if (!original || typeof target.left !== "number" || typeof target.top !== "number") {
1640
+ axisLockStateRef.current = null;
1641
+ return;
1642
+ }
1643
+ if (!axisLockStateRef.current) {
1644
+ const dx = Math.abs(target.left - original.left);
1645
+ const dy = Math.abs(target.top - original.top);
1646
+ axisLockStateRef.current = {
1647
+ axis: dx >= dy ? "x" : "y"
1648
+ };
1649
+ }
1650
+ if (axisLockStateRef.current.axis === "x") {
1651
+ target.top = original.top;
1652
+ } else {
1653
+ target.left = original.left;
1654
+ }
1655
+ (_a = target.canvas) == null ? void 0 : _a.requestRenderAll();
1656
+ };
1657
+ const applyMarqueeSelectionControls = () => {
1658
+ const canvasInstance = twickCanvasRef.current;
1659
+ if (!canvasInstance) return;
1660
+ const activeObject = canvasInstance.getActiveObject();
1661
+ if (!activeObject) return;
1662
+ if (activeObject instanceof ActiveSelection) {
1663
+ activeObject.controls.mt = disabledControl;
1664
+ activeObject.controls.mb = disabledControl;
1665
+ activeObject.controls.ml = disabledControl;
1666
+ activeObject.controls.mr = disabledControl;
1667
+ activeObject.controls.bl = disabledControl;
1668
+ activeObject.controls.br = disabledControl;
1669
+ activeObject.controls.tl = disabledControl;
1670
+ activeObject.controls.tr = disabledControl;
1671
+ activeObject.controls.mtr = rotateControl;
1672
+ canvasInstance.requestRenderAll();
1673
+ }
1674
+ };
1033
1675
  const buildCanvas = ({
1034
1676
  videoSize,
1035
1677
  canvasSize,
@@ -1049,6 +1691,9 @@ const useTwickCanvas = ({
1049
1691
  if (twickCanvasRef.current) {
1050
1692
  twickCanvasRef.current.off("mouse:up", handleMouseUp);
1051
1693
  twickCanvasRef.current.off("text:editing:exited", onTextEdit);
1694
+ twickCanvasRef.current.off("object:moving", handleObjectMoving);
1695
+ twickCanvasRef.current.off("selection:created", applyMarqueeSelectionControls);
1696
+ twickCanvasRef.current.off("selection:updated", applyMarqueeSelectionControls);
1052
1697
  twickCanvasRef.current.dispose();
1053
1698
  }
1054
1699
  const { canvas, canvasMetadata } = createCanvas({
@@ -1066,6 +1711,9 @@ const useTwickCanvas = ({
1066
1711
  videoSizeRef.current = videoSize;
1067
1712
  canvas == null ? void 0 : canvas.on("mouse:up", handleMouseUp);
1068
1713
  canvas == null ? void 0 : canvas.on("text:editing:exited", onTextEdit);
1714
+ canvas == null ? void 0 : canvas.on("object:moving", handleObjectMoving);
1715
+ canvas == null ? void 0 : canvas.on("selection:created", applyMarqueeSelectionControls);
1716
+ canvas == null ? void 0 : canvas.on("selection:updated", applyMarqueeSelectionControls);
1069
1717
  canvasResolutionRef.current = canvasSize;
1070
1718
  setTwickCanvas(canvas);
1071
1719
  twickCanvasRef.current = canvas;
@@ -1095,7 +1743,8 @@ const useTwickCanvas = ({
1095
1743
  if (event.target) {
1096
1744
  const object = event.target;
1097
1745
  const elementId = object.get("id");
1098
- if (((_a = event.transform) == null ? void 0 : _a.action) === "drag") {
1746
+ const action = (_a = event.transform) == null ? void 0 : _a.action;
1747
+ if (action === "drag") {
1099
1748
  const original = event.transform.original;
1100
1749
  if (object.left === original.left && object.top === original.top) {
1101
1750
  onCanvasOperation == null ? void 0 : onCanvasOperation(
@@ -1105,149 +1754,67 @@ const useTwickCanvas = ({
1105
1754
  return;
1106
1755
  }
1107
1756
  }
1108
- switch ((_b = event.transform) == null ? void 0 : _b.action) {
1757
+ const context = {
1758
+ canvasMetadata: canvasMetadataRef.current,
1759
+ videoSize: videoSizeRef.current,
1760
+ elementFrameMapRef: elementFrameMap,
1761
+ captionPropsRef,
1762
+ watermarkPropsRef
1763
+ };
1764
+ if (object instanceof ActiveSelection && (action === "drag" || action === "rotate")) {
1765
+ const objects = object.getObjects();
1766
+ for (const fabricObj of objects) {
1767
+ const id = fabricObj.get("id");
1768
+ if (!id || id === "e-watermark") continue;
1769
+ const currentElement = elementMap.current[id];
1770
+ if (!currentElement) continue;
1771
+ const handler = elementController.get(currentElement.type);
1772
+ const result = (_b = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _b.call(
1773
+ handler,
1774
+ fabricObj,
1775
+ currentElement,
1776
+ context
1777
+ );
1778
+ if (result) {
1779
+ elementMap.current[id] = result.element;
1780
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
1781
+ result.operation ?? CANVAS_OPERATIONS.ITEM_UPDATED,
1782
+ result.payload ?? result.element
1783
+ );
1784
+ }
1785
+ }
1786
+ return;
1787
+ }
1788
+ switch (action) {
1109
1789
  case "drag":
1110
1790
  case "scale":
1111
1791
  case "scaleX":
1112
1792
  case "scaleY":
1113
- case "rotate":
1114
- const { x, y } = convertToVideoPosition(
1115
- object.left,
1116
- object.top,
1117
- canvasMetadataRef.current,
1118
- videoSizeRef.current
1793
+ case "rotate": {
1794
+ const currentElement = elementMap.current[elementId];
1795
+ const handler = elementController.get(
1796
+ elementId === "e-watermark" ? "watermark" : currentElement == null ? void 0 : currentElement.type
1119
1797
  );
1120
- if (elementMap.current[elementId].type === "caption") {
1121
- if ((_c = captionPropsRef.current) == null ? void 0 : _c.applyToAll) {
1122
- onCanvasOperation == null ? void 0 : onCanvasOperation(CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED, {
1123
- element: elementMap.current[elementId],
1124
- props: {
1125
- ...captionPropsRef.current,
1126
- x,
1127
- y
1128
- }
1129
- });
1130
- } else {
1131
- elementMap.current[elementId] = {
1132
- ...elementMap.current[elementId],
1133
- props: {
1134
- ...elementMap.current[elementId].props,
1135
- x,
1136
- y
1137
- }
1138
- };
1139
- onCanvasOperation == null ? void 0 : onCanvasOperation(
1140
- CANVAS_OPERATIONS.ITEM_UPDATED,
1141
- elementMap.current[elementId]
1142
- );
1143
- }
1144
- } else {
1145
- if ((object == null ? void 0 : object.type) === "group") {
1146
- const currentFrameEffect = elementFrameMap.current[elementId];
1147
- let updatedFrameSize;
1148
- if (currentFrameEffect) {
1149
- updatedFrameSize = [
1150
- currentFrameEffect.props.frameSize[0] * object.scaleX,
1151
- currentFrameEffect.props.frameSize[1] * object.scaleY
1152
- ];
1153
- } else {
1154
- updatedFrameSize = [
1155
- elementMap.current[elementId].frame.size[0] * object.scaleX,
1156
- elementMap.current[elementId].frame.size[1] * object.scaleY
1157
- ];
1158
- }
1159
- if (currentFrameEffect) {
1160
- elementMap.current[elementId] = {
1161
- ...elementMap.current[elementId],
1162
- frameEffects: (elementMap.current[elementId].frameEffects || []).map(
1163
- (frameEffect) => frameEffect.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
1164
- ...frameEffect,
1165
- props: {
1166
- ...frameEffect.props,
1167
- framePosition: {
1168
- x,
1169
- y
1170
- },
1171
- frameSize: updatedFrameSize
1172
- }
1173
- } : frameEffect
1174
- )
1175
- };
1176
- elementFrameMap.current[elementId] = {
1177
- ...elementFrameMap.current[elementId],
1178
- framePosition: {
1179
- x,
1180
- y
1181
- },
1182
- frameSize: updatedFrameSize
1183
- };
1184
- } else {
1185
- elementMap.current[elementId] = {
1186
- ...elementMap.current[elementId],
1187
- frame: {
1188
- ...elementMap.current[elementId].frame,
1189
- rotation: object.angle,
1190
- size: updatedFrameSize,
1191
- x,
1192
- y
1193
- }
1194
- };
1195
- }
1196
- } else {
1197
- if ((object == null ? void 0 : object.type) === "text") {
1198
- elementMap.current[elementId] = {
1199
- ...elementMap.current[elementId],
1200
- props: {
1201
- ...elementMap.current[elementId].props,
1202
- rotation: object.angle,
1203
- x,
1204
- y
1205
- }
1206
- };
1207
- } else if ((object == null ? void 0 : object.type) === "circle") {
1208
- const radius = Number(
1209
- (elementMap.current[elementId].props.radius * object.scaleX).toFixed(2)
1210
- );
1211
- elementMap.current[elementId] = {
1212
- ...elementMap.current[elementId],
1213
- props: {
1214
- ...elementMap.current[elementId].props,
1215
- rotation: object.angle,
1216
- radius,
1217
- height: radius * 2,
1218
- width: radius * 2,
1219
- x,
1220
- y
1221
- }
1222
- };
1223
- } else {
1224
- elementMap.current[elementId] = {
1225
- ...elementMap.current[elementId],
1226
- props: {
1227
- ...elementMap.current[elementId].props,
1228
- rotation: object.angle,
1229
- width: elementMap.current[elementId].props.width * object.scaleX,
1230
- height: elementMap.current[elementId].props.height * object.scaleY,
1231
- x,
1232
- y
1233
- }
1234
- };
1235
- }
1236
- }
1798
+ const result = (_c = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _c.call(handler, object, currentElement ?? { id: elementId, type: "text", props: {} }, context);
1799
+ if (result) {
1800
+ elementMap.current[elementId] = result.element;
1237
1801
  onCanvasOperation == null ? void 0 : onCanvasOperation(
1238
- CANVAS_OPERATIONS.ITEM_UPDATED,
1239
- elementMap.current[elementId]
1802
+ result.operation ?? CANVAS_OPERATIONS.ITEM_UPDATED,
1803
+ result.payload ?? result.element
1240
1804
  );
1241
1805
  }
1242
1806
  break;
1807
+ }
1243
1808
  }
1244
1809
  }
1245
1810
  };
1246
1811
  const setCanvasElements = async ({
1247
1812
  elements,
1813
+ watermark,
1248
1814
  seekTime = 0,
1249
1815
  captionProps,
1250
- cleanAndAdd = false
1816
+ cleanAndAdd = false,
1817
+ lockAspectRatio
1251
1818
  }) => {
1252
1819
  if (!twickCanvas || !getCanvasContext(twickCanvas)) return;
1253
1820
  try {
@@ -1260,21 +1827,36 @@ const useTwickCanvas = ({
1260
1827
  }
1261
1828
  }
1262
1829
  captionPropsRef.current = captionProps;
1830
+ const uniqueElements = [];
1831
+ const seenIds = /* @__PURE__ */ new Set();
1832
+ for (const el of elements) {
1833
+ if (!el || !el.id) continue;
1834
+ if (seenIds.has(el.id)) continue;
1835
+ seenIds.add(el.id);
1836
+ uniqueElements.push(el);
1837
+ }
1263
1838
  await Promise.all(
1264
- elements.map(async (element, index) => {
1839
+ uniqueElements.map(async (element, index) => {
1265
1840
  try {
1266
1841
  if (!element) return;
1842
+ const zOrder = element.zIndex ?? index;
1267
1843
  await addElementToCanvas({
1268
1844
  element,
1269
- index,
1845
+ index: zOrder,
1270
1846
  reorder: false,
1271
1847
  seekTime,
1272
- captionProps
1848
+ captionProps,
1849
+ lockAspectRatio
1273
1850
  });
1274
1851
  } catch {
1275
1852
  }
1276
1853
  })
1277
1854
  );
1855
+ if (watermark) {
1856
+ addWatermarkToCanvas({
1857
+ element: watermark
1858
+ });
1859
+ }
1278
1860
  reorderElementsByZIndex(twickCanvas);
1279
1861
  } catch {
1280
1862
  }
@@ -1284,100 +1866,76 @@ const useTwickCanvas = ({
1284
1866
  index,
1285
1867
  reorder = true,
1286
1868
  seekTime,
1287
- captionProps
1869
+ captionProps,
1870
+ lockAspectRatio
1288
1871
  }) => {
1289
- var _a, _b;
1872
+ var _a;
1290
1873
  if (!twickCanvas) return;
1291
- switch (element.type) {
1292
- case ELEMENT_TYPES.VIDEO:
1293
- const currentFrameEffect = getCurrentFrameEffect(
1294
- element,
1295
- seekTime || 0
1296
- );
1297
- elementFrameMap.current[element.id] = currentFrameEffect;
1298
- 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);
1299
- await addVideoElement({
1300
- element,
1301
- index,
1302
- canvas: twickCanvas,
1303
- canvasMetadata: canvasMetadataRef.current,
1304
- currentFrameEffect,
1305
- snapTime
1306
- });
1307
- if (element.timelineType === "scene") {
1308
- await addBackgroundColor({
1309
- element,
1310
- index,
1311
- canvas: twickCanvas,
1312
- canvasMetadata: canvasMetadataRef.current
1313
- });
1314
- }
1315
- break;
1316
- case ELEMENT_TYPES.IMAGE:
1317
- await addImageElement({
1318
- element,
1319
- index,
1320
- canvas: twickCanvas,
1321
- canvasMetadata: canvasMetadataRef.current
1322
- });
1323
- if (element.timelineType === "scene") {
1324
- await addBackgroundColor({
1325
- element,
1326
- index,
1327
- canvas: twickCanvas,
1328
- canvasMetadata: canvasMetadataRef.current
1329
- });
1330
- }
1331
- break;
1332
- case ELEMENT_TYPES.RECT:
1333
- await addRectElement({
1334
- element,
1335
- index,
1336
- canvas: twickCanvas,
1337
- canvasMetadata: canvasMetadataRef.current
1338
- });
1339
- break;
1340
- case ELEMENT_TYPES.CIRCLE:
1341
- await addCircleElement({
1342
- element,
1343
- index,
1344
- canvas: twickCanvas,
1345
- canvasMetadata: canvasMetadataRef.current
1346
- });
1347
- break;
1348
- case ELEMENT_TYPES.TEXT:
1349
- await addTextElement({
1350
- element,
1351
- index,
1352
- canvas: twickCanvas,
1353
- canvasMetadata: canvasMetadataRef.current
1354
- });
1355
- break;
1356
- case ELEMENT_TYPES.CAPTION:
1357
- await addCaptionElement({
1358
- element,
1359
- index,
1360
- canvas: twickCanvas,
1361
- captionProps,
1362
- canvasMetadata: canvasMetadataRef.current
1363
- });
1364
- break;
1874
+ const handler = elementController.get(element.type);
1875
+ if (handler) {
1876
+ await handler.add({
1877
+ element,
1878
+ index,
1879
+ canvas: twickCanvas,
1880
+ canvasMetadata: canvasMetadataRef.current,
1881
+ seekTime,
1882
+ captionProps: captionProps ?? null,
1883
+ elementFrameMapRef: elementFrameMap,
1884
+ getCurrentFrameEffect,
1885
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1886
+ });
1365
1887
  }
1366
- elementMap.current[element.id] = element;
1888
+ elementMap.current[element.id] = { ...element, zIndex: element.zIndex ?? index };
1367
1889
  if (reorder) {
1368
1890
  reorderElementsByZIndex(twickCanvas);
1369
1891
  }
1370
1892
  };
1893
+ const addWatermarkToCanvas = ({
1894
+ element
1895
+ }) => {
1896
+ if (!twickCanvas) return;
1897
+ const handler = elementController.get("watermark");
1898
+ if (handler) {
1899
+ handler.add({
1900
+ element,
1901
+ index: Object.keys(elementMap.current).length,
1902
+ canvas: twickCanvas,
1903
+ canvasMetadata: canvasMetadataRef.current,
1904
+ watermarkPropsRef
1905
+ });
1906
+ elementMap.current[element.id] = element;
1907
+ }
1908
+ };
1909
+ const applyZOrder = (elementId, direction) => {
1910
+ if (!twickCanvas) return false;
1911
+ const newZIndex = changeZOrder(twickCanvas, elementId, direction);
1912
+ if (newZIndex == null) return false;
1913
+ const element = elementMap.current[elementId];
1914
+ if (element) elementMap.current[elementId] = { ...element, zIndex: newZIndex };
1915
+ onCanvasOperation == null ? void 0 : onCanvasOperation(CANVAS_OPERATIONS.Z_ORDER_CHANGED, { elementId, direction });
1916
+ return true;
1917
+ };
1918
+ const bringToFront = (elementId) => applyZOrder(elementId, "front");
1919
+ const sendToBack = (elementId) => applyZOrder(elementId, "back");
1920
+ const bringForward = (elementId) => applyZOrder(elementId, "forward");
1921
+ const sendBackward = (elementId) => applyZOrder(elementId, "backward");
1371
1922
  return {
1372
1923
  twickCanvas,
1373
1924
  buildCanvas,
1374
1925
  onVideoSizeChange,
1926
+ addWatermarkToCanvas,
1375
1927
  addElementToCanvas,
1376
- setCanvasElements
1928
+ setCanvasElements,
1929
+ bringToFront,
1930
+ sendToBack,
1931
+ bringForward,
1932
+ sendBackward
1377
1933
  };
1378
1934
  };
1379
1935
  export {
1380
1936
  CANVAS_OPERATIONS,
1937
+ ELEMENT_TYPES,
1938
+ ElementController,
1381
1939
  addBackgroundColor,
1382
1940
  addCaptionElement,
1383
1941
  addImageElement,
@@ -1388,6 +1946,7 @@ export {
1388
1946
  convertToVideoPosition,
1389
1947
  createCanvas,
1390
1948
  disabledControl,
1949
+ elementController,
1391
1950
  getCurrentFrameEffect,
1392
1951
  reorderElementsByZIndex,
1393
1952
  rotateControl,