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