@twick/canvas 0.15.14 → 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
@@ -53,7 +53,11 @@ const CANVAS_OPERATIONS = {
53
53
  /** Caption properties have been updated */
54
54
  CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED",
55
55
  /** Watermark has been updated */
56
- WATERMARK_UPDATED: "WATERMARK_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"
57
61
  };
58
62
  const ELEMENT_TYPES = {
59
63
  /** Text element type */
@@ -163,6 +167,49 @@ const reorderElementsByZIndex = (canvas) => {
163
167
  objects.forEach((obj) => canvas.add(obj));
164
168
  canvas.renderAll();
165
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
+ };
166
213
  const getCanvasContext = (canvas) => {
167
214
  var _a, _b, _c, _d;
168
215
  if (!canvas || !((_b = (_a = canvas.elements) == null ? void 0 : _a.lower) == null ? void 0 : _b.ctx)) return;
@@ -183,12 +230,31 @@ const convertToCanvasPosition = (x, y, canvasMetadata) => {
183
230
  y: y * canvasMetadata.scaleY + canvasMetadata.height / 2
184
231
  };
185
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
+ };
186
246
  const convertToVideoPosition = (x, y, canvasMetadata, videoSize) => {
187
247
  return {
188
248
  x: Number((x / canvasMetadata.scaleX - videoSize.width / 2).toFixed(2)),
189
249
  y: Number((y / canvasMetadata.scaleY - videoSize.height / 2).toFixed(2))
190
250
  };
191
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
+ };
192
258
  const getCurrentFrameEffect = (item, seekTime) => {
193
259
  var _a;
194
260
  let currentFrameEffect;
@@ -722,7 +788,8 @@ const setImageProps = ({
722
788
  img,
723
789
  element,
724
790
  index,
725
- canvasMetadata
791
+ canvasMetadata,
792
+ lockAspectRatio = true
726
793
  }) => {
727
794
  var _a, _b, _c, _d, _e;
728
795
  const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
@@ -742,13 +809,15 @@ const setImageProps = ({
742
809
  img.set("selectable", true);
743
810
  img.set("hasControls", true);
744
811
  img.set("touchAction", "all");
812
+ img.set("lockUniScaling", lockAspectRatio);
745
813
  };
746
814
  const addCaptionElement = ({
747
815
  element,
748
816
  index,
749
817
  canvas,
750
818
  captionProps,
751
- canvasMetadata
819
+ canvasMetadata,
820
+ lockAspectRatio = false
752
821
  }) => {
753
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;
754
823
  const { x, y } = convertToCanvasPosition(
@@ -787,6 +856,7 @@ const addCaptionElement = ({
787
856
  });
788
857
  caption.set("id", element.id);
789
858
  caption.set("zIndex", index);
859
+ caption.set("lockUniScaling", lockAspectRatio);
790
860
  caption.controls.mt = disabledControl;
791
861
  caption.controls.mb = disabledControl;
792
862
  caption.controls.ml = disabledControl;
@@ -834,7 +904,8 @@ const addImageElement = async ({
834
904
  index,
835
905
  canvas,
836
906
  canvasMetadata,
837
- currentFrameEffect
907
+ currentFrameEffect,
908
+ lockAspectRatio = true
838
909
  }) => {
839
910
  try {
840
911
  const img = await fabric.FabricImage.fromURL(imageUrl || element.props.src || "");
@@ -843,7 +914,7 @@ const addImageElement = async ({
843
914
  originY: "center",
844
915
  lockMovementX: false,
845
916
  lockMovementY: false,
846
- lockUniScaling: true,
917
+ lockUniScaling: lockAspectRatio,
847
918
  hasControls: false,
848
919
  selectable: false
849
920
  });
@@ -854,10 +925,11 @@ const addImageElement = async ({
854
925
  index,
855
926
  canvas,
856
927
  canvasMetadata,
857
- currentFrameEffect
928
+ currentFrameEffect,
929
+ lockAspectRatio
858
930
  });
859
931
  } else {
860
- setImageProps({ img, element, index, canvasMetadata });
932
+ setImageProps({ img, element, index, canvasMetadata, lockAspectRatio });
861
933
  canvas.add(img);
862
934
  return img;
863
935
  }
@@ -870,7 +942,8 @@ const addMediaGroup = ({
870
942
  index,
871
943
  canvas,
872
944
  canvasMetadata,
873
- currentFrameEffect
945
+ currentFrameEffect,
946
+ lockAspectRatio = true
874
947
  }) => {
875
948
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
876
949
  let frameSize;
@@ -959,6 +1032,7 @@ const addMediaGroup = ({
959
1032
  group.controls.mtr = rotateControl;
960
1033
  group.set("id", element.id);
961
1034
  group.set("zIndex", index);
1035
+ group.set("lockUniScaling", lockAspectRatio);
962
1036
  canvas.add(group);
963
1037
  return group;
964
1038
  };
@@ -966,7 +1040,8 @@ const addRectElement = ({
966
1040
  element,
967
1041
  index,
968
1042
  canvas,
969
- canvasMetadata
1043
+ canvasMetadata,
1044
+ lockAspectRatio = false
970
1045
  }) => {
971
1046
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
972
1047
  const { x, y } = convertToCanvasPosition(
@@ -1004,6 +1079,7 @@ const addRectElement = ({
1004
1079
  });
1005
1080
  rect.set("id", element.id);
1006
1081
  rect.set("zIndex", index);
1082
+ rect.set("lockUniScaling", lockAspectRatio);
1007
1083
  rect.controls.mtr = rotateControl;
1008
1084
  canvas.add(rect);
1009
1085
  return rect;
@@ -1012,9 +1088,10 @@ const addCircleElement = ({
1012
1088
  element,
1013
1089
  index,
1014
1090
  canvas,
1015
- canvasMetadata
1091
+ canvasMetadata,
1092
+ lockAspectRatio = true
1016
1093
  }) => {
1017
- var _a, _b, _c, _d, _e, _f;
1094
+ var _a, _b, _c, _d, _e, _f, _g;
1018
1095
  const { x, y } = convertToCanvasPosition(
1019
1096
  ((_a = element.props) == null ? void 0 : _a.x) || 0,
1020
1097
  ((_b = element.props) == null ? void 0 : _b.y) || 0,
@@ -1030,7 +1107,9 @@ const addCircleElement = ({
1030
1107
  stroke: ((_e = element.props) == null ? void 0 : _e.stroke) || "#000000",
1031
1108
  strokeWidth: (((_f = element.props) == null ? void 0 : _f.lineWidth) || 0) * canvasMetadata.scaleX,
1032
1109
  originX: "center",
1033
- 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
1034
1113
  });
1035
1114
  circle.controls.mt = disabledControl;
1036
1115
  circle.controls.mb = disabledControl;
@@ -1039,6 +1118,7 @@ const addCircleElement = ({
1039
1118
  circle.controls.mtr = disabledControl;
1040
1119
  circle.set("id", element.id);
1041
1120
  circle.set("zIndex", index);
1121
+ circle.set("lockUniScaling", lockAspectRatio);
1042
1122
  canvas.add(circle);
1043
1123
  return circle;
1044
1124
  };
@@ -1108,19 +1188,23 @@ const VideoElement = {
1108
1188
  }
1109
1189
  },
1110
1190
  updateFromFabricObject(object, element, context) {
1191
+ const canvasCenter = getObjectCanvasCenter(object);
1111
1192
  const { x, y } = convertToVideoPosition(
1112
- object.left,
1113
- object.top,
1193
+ canvasCenter.x,
1194
+ canvasCenter.y,
1114
1195
  context.canvasMetadata,
1115
1196
  context.videoSize
1116
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];
1117
1206
  const currentFrameEffect = context.elementFrameMapRef.current[element.id];
1118
- let updatedFrameSize;
1119
1207
  if (currentFrameEffect) {
1120
- updatedFrameSize = [
1121
- currentFrameEffect.props.frameSize[0] * object.scaleX,
1122
- currentFrameEffect.props.frameSize[1] * object.scaleY
1123
- ];
1124
1208
  context.elementFrameMapRef.current[element.id] = {
1125
1209
  ...currentFrameEffect,
1126
1210
  props: {
@@ -1146,16 +1230,12 @@ const VideoElement = {
1146
1230
  };
1147
1231
  }
1148
1232
  const frame = element.frame;
1149
- updatedFrameSize = [
1150
- (frame.size[0] ?? 0) * object.scaleX,
1151
- (frame.size[1] ?? 0) * object.scaleY
1152
- ];
1153
1233
  return {
1154
1234
  element: {
1155
1235
  ...element,
1156
1236
  frame: {
1157
1237
  ...frame,
1158
- rotation: object.angle,
1238
+ rotation: getObjectCanvasAngle(object),
1159
1239
  size: updatedFrameSize,
1160
1240
  x,
1161
1241
  y
@@ -1167,12 +1247,14 @@ const VideoElement = {
1167
1247
  const ImageElement = {
1168
1248
  name: ELEMENT_TYPES.IMAGE,
1169
1249
  async add(params) {
1170
- const { element, index, canvas, canvasMetadata } = params;
1250
+ var _a;
1251
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
1171
1252
  await addImageElement({
1172
1253
  element,
1173
1254
  index,
1174
1255
  canvas,
1175
- canvasMetadata
1256
+ canvasMetadata,
1257
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1176
1258
  });
1177
1259
  if (element.timelineType === "scene") {
1178
1260
  await addBackgroundColor({
@@ -1184,21 +1266,24 @@ const ImageElement = {
1184
1266
  }
1185
1267
  },
1186
1268
  updateFromFabricObject(object, element, context) {
1187
- var _a, _b;
1269
+ const canvasCenter = getObjectCanvasCenter(object);
1188
1270
  const { x, y } = convertToVideoPosition(
1189
- object.left,
1190
- object.top,
1271
+ canvasCenter.x,
1272
+ canvasCenter.y,
1191
1273
  context.canvasMetadata,
1192
1274
  context.videoSize
1193
1275
  );
1194
1276
  const currentFrameEffect = context.elementFrameMapRef.current[element.id];
1195
1277
  if (object.type === "group") {
1196
- let updatedFrameSize;
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];
1197
1286
  if (currentFrameEffect) {
1198
- updatedFrameSize = [
1199
- currentFrameEffect.props.frameSize[0] * object.scaleX,
1200
- currentFrameEffect.props.frameSize[1] * object.scaleY
1201
- ];
1202
1287
  context.elementFrameMapRef.current[element.id] = {
1203
1288
  ...currentFrameEffect,
1204
1289
  props: {
@@ -1210,6 +1295,15 @@ const ImageElement = {
1210
1295
  return {
1211
1296
  element: {
1212
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,
1213
1307
  frameEffects: (element.frameEffects || []).map(
1214
1308
  (fe) => fe.id === (currentFrameEffect == null ? void 0 : currentFrameEffect.id) ? {
1215
1309
  ...fe,
@@ -1224,16 +1318,12 @@ const ImageElement = {
1224
1318
  };
1225
1319
  }
1226
1320
  const frame = element.frame;
1227
- updatedFrameSize = [
1228
- (frame.size[0] ?? 0) * object.scaleX,
1229
- (frame.size[1] ?? 0) * object.scaleY
1230
- ];
1231
1321
  return {
1232
1322
  element: {
1233
1323
  ...element,
1234
1324
  frame: {
1235
1325
  ...frame,
1236
- rotation: object.angle,
1326
+ rotation: getObjectCanvasAngle(object),
1237
1327
  size: updatedFrameSize,
1238
1328
  x,
1239
1329
  y
@@ -1241,14 +1331,21 @@ const ImageElement = {
1241
1331
  }
1242
1332
  };
1243
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
+ );
1244
1341
  return {
1245
1342
  element: {
1246
1343
  ...element,
1247
1344
  props: {
1248
1345
  ...element.props,
1249
- rotation: object.angle,
1250
- width: (((_a = element.props) == null ? void 0 : _a.width) ?? 0) * object.scaleX,
1251
- height: (((_b = element.props) == null ? void 0 : _b.height) ?? 0) * object.scaleY,
1346
+ rotation: getObjectCanvasAngle(object),
1347
+ width,
1348
+ height,
1252
1349
  x,
1253
1350
  y
1254
1351
  }
@@ -1259,19 +1356,22 @@ const ImageElement = {
1259
1356
  const RectElement = {
1260
1357
  name: ELEMENT_TYPES.RECT,
1261
1358
  async add(params) {
1262
- const { element, index, canvas, canvasMetadata } = params;
1359
+ var _a;
1360
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
1263
1361
  await addRectElement({
1264
1362
  element,
1265
1363
  index,
1266
1364
  canvas,
1267
- canvasMetadata
1365
+ canvasMetadata,
1366
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1268
1367
  });
1269
1368
  },
1270
1369
  updateFromFabricObject(object, element, context) {
1271
1370
  var _a, _b;
1371
+ const canvasCenter = getObjectCanvasCenter(object);
1272
1372
  const { x, y } = convertToVideoPosition(
1273
- object.left,
1274
- object.top,
1373
+ canvasCenter.x,
1374
+ canvasCenter.y,
1275
1375
  context.canvasMetadata,
1276
1376
  context.videoSize
1277
1377
  );
@@ -1280,7 +1380,7 @@ const RectElement = {
1280
1380
  ...element,
1281
1381
  props: {
1282
1382
  ...element.props,
1283
- rotation: object.angle,
1383
+ rotation: getObjectCanvasAngle(object),
1284
1384
  width: (((_a = element.props) == null ? void 0 : _a.width) ?? 0) * object.scaleX,
1285
1385
  height: (((_b = element.props) == null ? void 0 : _b.height) ?? 0) * object.scaleY,
1286
1386
  x,
@@ -1293,36 +1393,41 @@ const RectElement = {
1293
1393
  const CircleElement = {
1294
1394
  name: ELEMENT_TYPES.CIRCLE,
1295
1395
  async add(params) {
1296
- const { element, index, canvas, canvasMetadata } = params;
1396
+ var _a;
1397
+ const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
1297
1398
  await addCircleElement({
1298
1399
  element,
1299
1400
  index,
1300
1401
  canvas,
1301
- canvasMetadata
1402
+ canvasMetadata,
1403
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1302
1404
  });
1303
1405
  },
1304
1406
  updateFromFabricObject(object, element, context) {
1305
- var _a;
1407
+ var _a, _b;
1408
+ const canvasCenter = getObjectCanvasCenter(object);
1306
1409
  const { x, y } = convertToVideoPosition(
1307
- object.left,
1308
- object.top,
1410
+ canvasCenter.x,
1411
+ canvasCenter.y,
1309
1412
  context.canvasMetadata,
1310
1413
  context.videoSize
1311
1414
  );
1312
1415
  const radius = Number(
1313
1416
  ((((_a = element.props) == null ? void 0 : _a.radius) ?? 0) * object.scaleX).toFixed(2)
1314
1417
  );
1418
+ const opacity = object.opacity != null ? object.opacity : (_b = element.props) == null ? void 0 : _b.opacity;
1315
1419
  return {
1316
1420
  element: {
1317
1421
  ...element,
1318
1422
  props: {
1319
1423
  ...element.props,
1320
- rotation: object.angle,
1424
+ rotation: getObjectCanvasAngle(object),
1321
1425
  radius,
1322
1426
  height: radius * 2,
1323
1427
  width: radius * 2,
1324
1428
  x,
1325
- y
1429
+ y,
1430
+ ...opacity != null && { opacity }
1326
1431
  }
1327
1432
  }
1328
1433
  };
@@ -1340,9 +1445,10 @@ const TextElement = {
1340
1445
  });
1341
1446
  },
1342
1447
  updateFromFabricObject(object, element, context) {
1448
+ const canvasCenter = getObjectCanvasCenter(object);
1343
1449
  const { x, y } = convertToVideoPosition(
1344
- object.left,
1345
- object.top,
1450
+ canvasCenter.x,
1451
+ canvasCenter.y,
1346
1452
  context.canvasMetadata,
1347
1453
  context.videoSize
1348
1454
  );
@@ -1351,7 +1457,7 @@ const TextElement = {
1351
1457
  ...element,
1352
1458
  props: {
1353
1459
  ...element.props,
1354
- rotation: object.angle,
1460
+ rotation: getObjectCanvasAngle(object),
1355
1461
  x,
1356
1462
  y
1357
1463
  }
@@ -1362,20 +1468,23 @@ const TextElement = {
1362
1468
  const CaptionElement = {
1363
1469
  name: ELEMENT_TYPES.CAPTION,
1364
1470
  async add(params) {
1365
- const { element, index, canvas, captionProps, canvasMetadata } = params;
1471
+ var _a;
1472
+ const { element, index, canvas, captionProps, canvasMetadata, lockAspectRatio } = params;
1366
1473
  await addCaptionElement({
1367
1474
  element,
1368
1475
  index,
1369
1476
  canvas,
1370
1477
  captionProps: captionProps ?? {},
1371
- canvasMetadata
1478
+ canvasMetadata,
1479
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1372
1480
  });
1373
1481
  },
1374
1482
  updateFromFabricObject(object, element, context) {
1375
1483
  var _a;
1484
+ const canvasCenter = getObjectCanvasCenter(object);
1376
1485
  const { x, y } = convertToVideoPosition(
1377
- object.left,
1378
- object.top,
1486
+ canvasCenter.x,
1487
+ canvasCenter.y,
1379
1488
  context.canvasMetadata,
1380
1489
  context.videoSize
1381
1490
  );
@@ -1481,7 +1590,15 @@ function registerElements() {
1481
1590
  registerElements();
1482
1591
  const useTwickCanvas = ({
1483
1592
  onCanvasReady,
1484
- 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
1485
1602
  }) => {
1486
1603
  const [twickCanvas, setTwickCanvas] = react.useState(null);
1487
1604
  const elementMap = react.useRef({});
@@ -1491,6 +1608,7 @@ const useTwickCanvas = ({
1491
1608
  const videoSizeRef = react.useRef({ width: 1, height: 1 });
1492
1609
  const canvasResolutionRef = react.useRef({ width: 1, height: 1 });
1493
1610
  const captionPropsRef = react.useRef(null);
1611
+ const axisLockStateRef = react.useRef(null);
1494
1612
  const canvasMetadataRef = react.useRef({
1495
1613
  width: 0,
1496
1614
  height: 0,
@@ -1505,6 +1623,57 @@ const useTwickCanvas = ({
1505
1623
  canvasMetadataRef.current.scaleY = canvasMetadataRef.current.height / videoSize.height;
1506
1624
  }
1507
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
+ };
1508
1677
  const buildCanvas = ({
1509
1678
  videoSize,
1510
1679
  canvasSize,
@@ -1524,6 +1693,9 @@ const useTwickCanvas = ({
1524
1693
  if (twickCanvasRef.current) {
1525
1694
  twickCanvasRef.current.off("mouse:up", handleMouseUp);
1526
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);
1527
1699
  twickCanvasRef.current.dispose();
1528
1700
  }
1529
1701
  const { canvas, canvasMetadata } = createCanvas({
@@ -1541,6 +1713,9 @@ const useTwickCanvas = ({
1541
1713
  videoSizeRef.current = videoSize;
1542
1714
  canvas == null ? void 0 : canvas.on("mouse:up", handleMouseUp);
1543
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);
1544
1719
  canvasResolutionRef.current = canvasSize;
1545
1720
  setTwickCanvas(canvas);
1546
1721
  twickCanvasRef.current = canvas;
@@ -1570,7 +1745,8 @@ const useTwickCanvas = ({
1570
1745
  if (event.target) {
1571
1746
  const object = event.target;
1572
1747
  const elementId = object.get("id");
1573
- 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") {
1574
1750
  const original = event.transform.original;
1575
1751
  if (object.left === original.left && object.top === original.top) {
1576
1752
  onCanvasOperation == null ? void 0 : onCanvasOperation(
@@ -1580,7 +1756,38 @@ const useTwickCanvas = ({
1580
1756
  return;
1581
1757
  }
1582
1758
  }
1583
- 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) {
1584
1791
  case "drag":
1585
1792
  case "scale":
1586
1793
  case "scaleX":
@@ -1590,13 +1797,7 @@ const useTwickCanvas = ({
1590
1797
  const handler = elementController.get(
1591
1798
  elementId === "e-watermark" ? "watermark" : currentElement == null ? void 0 : currentElement.type
1592
1799
  );
1593
- const result = (_c = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _c.call(handler, object, currentElement ?? { id: elementId, type: "text", props: {} }, {
1594
- canvasMetadata: canvasMetadataRef.current,
1595
- videoSize: videoSizeRef.current,
1596
- elementFrameMapRef: elementFrameMap,
1597
- captionPropsRef,
1598
- watermarkPropsRef
1599
- });
1800
+ const result = (_c = handler == null ? void 0 : handler.updateFromFabricObject) == null ? void 0 : _c.call(handler, object, currentElement ?? { id: elementId, type: "text", props: {} }, context);
1600
1801
  if (result) {
1601
1802
  elementMap.current[elementId] = result.element;
1602
1803
  onCanvasOperation == null ? void 0 : onCanvasOperation(
@@ -1614,7 +1815,8 @@ const useTwickCanvas = ({
1614
1815
  watermark,
1615
1816
  seekTime = 0,
1616
1817
  captionProps,
1617
- cleanAndAdd = false
1818
+ cleanAndAdd = false,
1819
+ lockAspectRatio
1618
1820
  }) => {
1619
1821
  if (!twickCanvas || !getCanvasContext(twickCanvas)) return;
1620
1822
  try {
@@ -1627,16 +1829,26 @@ const useTwickCanvas = ({
1627
1829
  }
1628
1830
  }
1629
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
+ }
1630
1840
  await Promise.all(
1631
- elements.map(async (element, index) => {
1841
+ uniqueElements.map(async (element, index) => {
1632
1842
  try {
1633
1843
  if (!element) return;
1844
+ const zOrder = element.zIndex ?? index;
1634
1845
  await addElementToCanvas({
1635
1846
  element,
1636
- index,
1847
+ index: zOrder,
1637
1848
  reorder: false,
1638
1849
  seekTime,
1639
- captionProps
1850
+ captionProps,
1851
+ lockAspectRatio
1640
1852
  });
1641
1853
  } catch {
1642
1854
  }
@@ -1656,8 +1868,10 @@ const useTwickCanvas = ({
1656
1868
  index,
1657
1869
  reorder = true,
1658
1870
  seekTime,
1659
- captionProps
1871
+ captionProps,
1872
+ lockAspectRatio
1660
1873
  }) => {
1874
+ var _a;
1661
1875
  if (!twickCanvas) return;
1662
1876
  const handler = elementController.get(element.type);
1663
1877
  if (handler) {
@@ -1669,10 +1883,11 @@ const useTwickCanvas = ({
1669
1883
  seekTime,
1670
1884
  captionProps: captionProps ?? null,
1671
1885
  elementFrameMapRef: elementFrameMap,
1672
- getCurrentFrameEffect
1886
+ getCurrentFrameEffect,
1887
+ lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio)
1673
1888
  });
1674
1889
  }
1675
- elementMap.current[element.id] = element;
1890
+ elementMap.current[element.id] = { ...element, zIndex: element.zIndex ?? index };
1676
1891
  if (reorder) {
1677
1892
  reorderElementsByZIndex(twickCanvas);
1678
1893
  }
@@ -1693,13 +1908,30 @@ const useTwickCanvas = ({
1693
1908
  elementMap.current[element.id] = element;
1694
1909
  }
1695
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");
1696
1924
  return {
1697
1925
  twickCanvas,
1698
1926
  buildCanvas,
1699
1927
  onVideoSizeChange,
1700
1928
  addWatermarkToCanvas,
1701
1929
  addElementToCanvas,
1702
- setCanvasElements
1930
+ setCanvasElements,
1931
+ bringToFront,
1932
+ sendToBack,
1933
+ bringForward,
1934
+ sendBackward
1703
1935
  };
1704
1936
  };
1705
1937
  exports.CANVAS_OPERATIONS = CANVAS_OPERATIONS;