@nasser-sw/fabric 7.0.1-beta17 → 7.0.1-beta19

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.
Files changed (78) hide show
  1. package/.history/package_20251226051014.json +164 -0
  2. package/.history/package_20251226164045.json +164 -0
  3. package/dist/fabric.d.ts +2 -0
  4. package/dist/fabric.d.ts.map +1 -1
  5. package/dist/fabric.min.mjs +1 -1
  6. package/dist/fabric.mjs +2 -0
  7. package/dist/fabric.mjs.map +1 -1
  8. package/dist/index.js +1760 -368
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.js +1 -1
  11. package/dist/index.min.js.map +1 -1
  12. package/dist/index.min.mjs +1 -1
  13. package/dist/index.min.mjs.map +1 -1
  14. package/dist/index.mjs +1759 -369
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.node.cjs +1760 -368
  17. package/dist/index.node.cjs.map +1 -1
  18. package/dist/index.node.mjs +1759 -369
  19. package/dist/index.node.mjs.map +1 -1
  20. package/dist/package.json.min.mjs +1 -1
  21. package/dist/package.json.mjs +1 -1
  22. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts +31 -0
  23. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts.map +1 -0
  24. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.min.mjs +2 -0
  25. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.min.mjs.map +1 -0
  26. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.mjs +81 -0
  27. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.mjs.map +1 -0
  28. package/dist/src/LayoutManager/index.d.ts +1 -0
  29. package/dist/src/LayoutManager/index.d.ts.map +1 -1
  30. package/dist/src/controls/Control.d.ts.map +1 -1
  31. package/dist/src/controls/Control.min.mjs +1 -1
  32. package/dist/src/controls/Control.min.mjs.map +1 -1
  33. package/dist/src/controls/Control.mjs +19 -1
  34. package/dist/src/controls/Control.mjs.map +1 -1
  35. package/dist/src/controls/commonControls.d.ts.map +1 -1
  36. package/dist/src/controls/commonControls.min.mjs +1 -1
  37. package/dist/src/controls/commonControls.min.mjs.map +1 -1
  38. package/dist/src/controls/commonControls.mjs +25 -6
  39. package/dist/src/controls/commonControls.mjs.map +1 -1
  40. package/dist/src/controls/controlRendering.d.ts +20 -0
  41. package/dist/src/controls/controlRendering.d.ts.map +1 -1
  42. package/dist/src/controls/controlRendering.min.mjs +1 -1
  43. package/dist/src/controls/controlRendering.min.mjs.map +1 -1
  44. package/dist/src/controls/controlRendering.mjs +63 -1
  45. package/dist/src/controls/controlRendering.mjs.map +1 -1
  46. package/dist/src/shapes/Frame.d.ts +298 -0
  47. package/dist/src/shapes/Frame.d.ts.map +1 -0
  48. package/dist/src/shapes/Frame.min.mjs +2 -0
  49. package/dist/src/shapes/Frame.min.mjs.map +1 -0
  50. package/dist/src/shapes/Frame.mjs +1236 -0
  51. package/dist/src/shapes/Frame.mjs.map +1 -0
  52. package/dist/src/shapes/Object/defaultValues.d.ts.map +1 -1
  53. package/dist/src/shapes/Object/defaultValues.min.mjs +1 -1
  54. package/dist/src/shapes/Object/defaultValues.min.mjs.map +1 -1
  55. package/dist/src/shapes/Object/defaultValues.mjs +8 -7
  56. package/dist/src/shapes/Object/defaultValues.mjs.map +1 -1
  57. package/dist-extensions/fabric.d.ts +2 -0
  58. package/dist-extensions/fabric.d.ts.map +1 -1
  59. package/dist-extensions/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts +31 -0
  60. package/dist-extensions/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts.map +1 -0
  61. package/dist-extensions/src/LayoutManager/index.d.ts +1 -0
  62. package/dist-extensions/src/LayoutManager/index.d.ts.map +1 -1
  63. package/dist-extensions/src/controls/Control.d.ts.map +1 -1
  64. package/dist-extensions/src/controls/commonControls.d.ts.map +1 -1
  65. package/dist-extensions/src/controls/controlRendering.d.ts +20 -0
  66. package/dist-extensions/src/controls/controlRendering.d.ts.map +1 -1
  67. package/dist-extensions/src/shapes/Frame.d.ts +298 -0
  68. package/dist-extensions/src/shapes/Frame.d.ts.map +1 -0
  69. package/dist-extensions/src/shapes/Object/defaultValues.d.ts.map +1 -1
  70. package/fabric.ts +8 -0
  71. package/package.json +1 -1
  72. package/src/LayoutManager/LayoutStrategies/FrameLayout.ts +80 -0
  73. package/src/LayoutManager/index.ts +1 -0
  74. package/src/controls/Control.ts +40 -1
  75. package/src/controls/commonControls.ts +22 -0
  76. package/src/controls/controlRendering.ts +83 -0
  77. package/src/shapes/Frame.ts +1361 -0
  78. package/src/shapes/Object/defaultValues.ts +8 -7
package/dist/index.js CHANGED
@@ -360,7 +360,7 @@
360
360
  }
361
361
  const cache = new Cache();
362
362
 
363
- var version = "7.0.1-beta16";
363
+ var version = "7.0.1-beta19";
364
364
 
365
365
  // use this syntax so babel plugin see this import here
366
366
  const VERSION = version;
@@ -5131,17 +5131,18 @@
5131
5131
  lockSkewingX: false,
5132
5132
  lockSkewingY: false,
5133
5133
  lockScalingFlip: false,
5134
- cornerSize: 13,
5134
+ // Modern Canva-style controls
5135
+ cornerSize: 10,
5135
5136
  touchCornerSize: 24,
5136
- transparentCorners: true,
5137
- cornerColor: 'rgb(178,204,255)',
5138
- cornerStrokeColor: '',
5139
- cornerStyle: 'rect',
5137
+ transparentCorners: false,
5138
+ cornerColor: '#ffffff',
5139
+ cornerStrokeColor: '#0d99ff',
5140
+ cornerStyle: 'circle',
5140
5141
  cornerDashArray: null,
5141
5142
  hasControls: true,
5142
- borderColor: 'rgb(178,204,255)',
5143
+ borderColor: '#0d99ff',
5143
5144
  borderDashArray: null,
5144
- borderOpacityWhenMoving: 0.4,
5145
+ borderOpacityWhenMoving: 0.6,
5145
5146
  borderScaleFactor: 1,
5146
5147
  hasBorders: true,
5147
5148
  selectionBackgroundColor: '',
@@ -8218,6 +8219,12 @@
8218
8219
  };
8219
8220
  const changeWidth = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeObjectWidth));
8220
8221
 
8222
+ /**
8223
+ * Pill dimensions for side controls (Canva-style)
8224
+ */
8225
+ const PILL_WIDTH = 6;
8226
+ const PILL_HEIGHT = 20;
8227
+ const PILL_RADIUS = 3;
8221
8228
  /**
8222
8229
  * Render a round control, as per fabric features.
8223
8230
  * This function is written to respect object properties like transparentCorners, cornerSize
@@ -8300,6 +8307,62 @@
8300
8307
  ctx.restore();
8301
8308
  }
8302
8309
 
8310
+ /**
8311
+ * Render a horizontal pill control (for left/right side handles).
8312
+ * Modern Canva-style appearance.
8313
+ * @param {CanvasRenderingContext2D} ctx context to render on
8314
+ * @param {Number} left x coordinate where the control center should be
8315
+ * @param {Number} top y coordinate where the control center should be
8316
+ * @param {Object} styleOverride override for FabricObject controls style
8317
+ * @param {FabricObject} fabricObject the fabric object for which we are rendering controls
8318
+ */
8319
+ function renderHorizontalPillControl(ctx, left, top, styleOverride, fabricObject) {
8320
+ styleOverride = styleOverride || {};
8321
+ const width = PILL_WIDTH;
8322
+ const height = PILL_HEIGHT;
8323
+ const radius = PILL_RADIUS;
8324
+ ctx.save();
8325
+ ctx.translate(left, top);
8326
+ const angle = fabricObject.getTotalAngle();
8327
+ ctx.rotate(degreesToRadians(angle));
8328
+ ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor || '#ffffff';
8329
+ ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor || '#0d99ff';
8330
+ ctx.lineWidth = 1.5;
8331
+ ctx.beginPath();
8332
+ ctx.roundRect(-width / 2, -height / 2, width, height, radius);
8333
+ ctx.fill();
8334
+ ctx.stroke();
8335
+ ctx.restore();
8336
+ }
8337
+
8338
+ /**
8339
+ * Render a vertical pill control (for top/bottom side handles).
8340
+ * Modern Canva-style appearance.
8341
+ * @param {CanvasRenderingContext2D} ctx context to render on
8342
+ * @param {Number} left x coordinate where the control center should be
8343
+ * @param {Number} top y coordinate where the control center should be
8344
+ * @param {Object} styleOverride override for FabricObject controls style
8345
+ * @param {FabricObject} fabricObject the fabric object for which we are rendering controls
8346
+ */
8347
+ function renderVerticalPillControl(ctx, left, top, styleOverride, fabricObject) {
8348
+ styleOverride = styleOverride || {};
8349
+ const width = PILL_HEIGHT; // Swapped for vertical
8350
+ const height = PILL_WIDTH;
8351
+ const radius = PILL_RADIUS;
8352
+ ctx.save();
8353
+ ctx.translate(left, top);
8354
+ const angle = fabricObject.getTotalAngle();
8355
+ ctx.rotate(degreesToRadians(angle));
8356
+ ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor || '#ffffff';
8357
+ ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor || '#0d99ff';
8358
+ ctx.lineWidth = 1.5;
8359
+ ctx.beginPath();
8360
+ ctx.roundRect(-width / 2, -height / 2, width, height, radius);
8361
+ ctx.fill();
8362
+ ctx.stroke();
8363
+ ctx.restore();
8364
+ }
8365
+
8303
8366
  class Control {
8304
8367
  constructor(options) {
8305
8368
  /**
@@ -8567,6 +8630,24 @@
8567
8630
  */
8568
8631
  render(ctx, left, top, styleOverride, fabricObject) {
8569
8632
  styleOverride = styleOverride || {};
8633
+
8634
+ // Auto-detect side controls by position and use pill renderers
8635
+ // Side controls have one axis at 0: ml/mr have y=0, mt/mb have x=0
8636
+ const isSideControl = (this.x === 0 || this.y === 0) && !(this.x === 0 && this.y === 0);
8637
+ if (isSideControl && !styleOverride.cornerStyle) {
8638
+ // Horizontal pills for left/right (y = 0)
8639
+ if (this.y === 0 && this.x !== 0) {
8640
+ renderHorizontalPillControl.call(this, ctx, left, top, styleOverride, fabricObject);
8641
+ return;
8642
+ }
8643
+ // Vertical pills for top/bottom (x = 0)
8644
+ if (this.x === 0 && this.y !== 0) {
8645
+ renderVerticalPillControl.call(this, ctx, left, top, styleOverride, fabricObject);
8646
+ return;
8647
+ }
8648
+ }
8649
+
8650
+ // Corner controls and rotation use cornerStyle
8570
8651
  switch (styleOverride.cornerStyle || fabricObject.cornerStyle) {
8571
8652
  case 'circle':
8572
8653
  renderCircleControl.call(this, ctx, left, top, styleOverride, fabricObject);
@@ -9083,28 +9164,40 @@
9083
9164
  y: 0,
9084
9165
  cursorStyleHandler: scaleSkewCursorStyleHandler,
9085
9166
  actionHandler: scalingXOrSkewingY,
9086
- getActionName: scaleOrSkewActionName
9167
+ getActionName: scaleOrSkewActionName,
9168
+ render: renderHorizontalPillControl,
9169
+ sizeX: 6,
9170
+ sizeY: 20
9087
9171
  }),
9088
9172
  mr: new Control({
9089
9173
  x: 0.5,
9090
9174
  y: 0,
9091
9175
  cursorStyleHandler: scaleSkewCursorStyleHandler,
9092
9176
  actionHandler: scalingXOrSkewingY,
9093
- getActionName: scaleOrSkewActionName
9177
+ getActionName: scaleOrSkewActionName,
9178
+ render: renderHorizontalPillControl,
9179
+ sizeX: 6,
9180
+ sizeY: 20
9094
9181
  }),
9095
9182
  mb: new Control({
9096
9183
  x: 0,
9097
9184
  y: 0.5,
9098
9185
  cursorStyleHandler: scaleSkewCursorStyleHandler,
9099
9186
  actionHandler: scalingYOrSkewingX,
9100
- getActionName: scaleOrSkewActionName
9187
+ getActionName: scaleOrSkewActionName,
9188
+ render: renderVerticalPillControl,
9189
+ sizeX: 20,
9190
+ sizeY: 6
9101
9191
  }),
9102
9192
  mt: new Control({
9103
9193
  x: 0,
9104
9194
  y: -0.5,
9105
9195
  cursorStyleHandler: scaleSkewCursorStyleHandler,
9106
9196
  actionHandler: scalingYOrSkewingX,
9107
- getActionName: scaleOrSkewActionName
9197
+ getActionName: scaleOrSkewActionName,
9198
+ render: renderVerticalPillControl,
9199
+ sizeX: 20,
9200
+ sizeY: 6
9108
9201
  }),
9109
9202
  tl: new Control({
9110
9203
  x: -0.5,
@@ -9146,14 +9239,20 @@
9146
9239
  y: 0,
9147
9240
  actionHandler: changeWidth,
9148
9241
  cursorStyleHandler: scaleSkewCursorStyleHandler,
9149
- actionName: RESIZING
9242
+ actionName: RESIZING,
9243
+ render: renderHorizontalPillControl,
9244
+ sizeX: 6,
9245
+ sizeY: 20
9150
9246
  }),
9151
9247
  ml: new Control({
9152
9248
  x: -0.5,
9153
9249
  y: 0,
9154
9250
  actionHandler: changeWidth,
9155
9251
  cursorStyleHandler: scaleSkewCursorStyleHandler,
9156
- actionName: RESIZING
9252
+ actionName: RESIZING,
9253
+ render: renderHorizontalPillControl,
9254
+ sizeX: 6,
9255
+ sizeY: 20
9157
9256
  })
9158
9257
  });
9159
9258
  const createTextboxDefaultControls = () => {
@@ -29049,360 +29148,6 @@
29049
29148
  _defineProperty(Textbox, "ownDefaults", textboxDefaultValues);
29050
29149
  classRegistry.setClass(Textbox);
29051
29150
 
29052
- /**
29053
- * Layout will adjust the bounding box to match the clip path bounding box.
29054
- */
29055
- class ClipPathLayout extends LayoutStrategy {
29056
- shouldPerformLayout(context) {
29057
- return !!context.target.clipPath && super.shouldPerformLayout(context);
29058
- }
29059
- shouldLayoutClipPath() {
29060
- return false;
29061
- }
29062
- calcLayoutResult(context, objects) {
29063
- const {
29064
- target
29065
- } = context;
29066
- const {
29067
- clipPath,
29068
- group
29069
- } = target;
29070
- if (!clipPath || !this.shouldPerformLayout(context)) {
29071
- return;
29072
- }
29073
- // TODO: remove stroke calculation from this case
29074
- const {
29075
- width,
29076
- height
29077
- } = makeBoundingBoxFromPoints(getObjectBounds(target, clipPath));
29078
- const size = new Point(width, height);
29079
- if (clipPath.absolutePositioned) {
29080
- // we want the center point to exist in group's containing plane
29081
- const clipPathCenter = sendPointToPlane(clipPath.getRelativeCenterPoint(), undefined, group ? group.calcTransformMatrix() : undefined);
29082
- return {
29083
- center: clipPathCenter,
29084
- size
29085
- };
29086
- } else {
29087
- // we want the center point to exist in group's containing plane, so we send it upwards
29088
- const clipPathCenter = clipPath.getRelativeCenterPoint().transform(target.calcOwnMatrix(), true);
29089
- if (this.shouldPerformLayout(context)) {
29090
- // the clip path is positioned relative to the group's center which is affected by the bbox
29091
- // so we first calculate the bbox
29092
- const {
29093
- center = new Point(),
29094
- correction = new Point()
29095
- } = this.calcBoundingBox(objects, context) || {};
29096
- return {
29097
- center: center.add(clipPathCenter),
29098
- correction: correction.subtract(clipPathCenter),
29099
- size
29100
- };
29101
- } else {
29102
- return {
29103
- center: target.getRelativeCenterPoint().add(clipPathCenter),
29104
- size
29105
- };
29106
- }
29107
- }
29108
- }
29109
- }
29110
- _defineProperty(ClipPathLayout, "type", 'clip-path');
29111
- classRegistry.setClass(ClipPathLayout);
29112
-
29113
- /**
29114
- * Layout will keep target's initial size.
29115
- */
29116
- class FixedLayout extends LayoutStrategy {
29117
- /**
29118
- * @override respect target's initial size
29119
- */
29120
- getInitialSize(_ref, _ref2) {
29121
- let {
29122
- target
29123
- } = _ref;
29124
- let {
29125
- size
29126
- } = _ref2;
29127
- return new Point(target.width || size.x, target.height || size.y);
29128
- }
29129
- }
29130
- _defineProperty(FixedLayout, "type", 'fixed');
29131
- classRegistry.setClass(FixedLayout);
29132
-
29133
- /**
29134
- * Today the LayoutManager class also takes care of subscribing event handlers
29135
- * to update the group layout when the group is interactive and a transform is applied
29136
- * to a child object.
29137
- * The ActiveSelection is never interactive, but it could contain objects from
29138
- * groups that are.
29139
- * The standard LayoutManager would subscribe the children of the activeSelection to
29140
- * perform layout changes to the active selection itself, what we need instead is that
29141
- * the transformation applied to the active selection will trigger changes to the
29142
- * original group of the children ( the one referenced under the parent property )
29143
- * This subclass of the LayoutManager has a single duty to fill the gap of this difference.`
29144
- */
29145
- class ActiveSelectionLayoutManager extends LayoutManager {
29146
- subscribeTargets(context) {
29147
- const activeSelection = context.target;
29148
- const parents = context.targets.reduce((parents, target) => {
29149
- target.parent && parents.add(target.parent);
29150
- return parents;
29151
- }, new Set());
29152
- parents.forEach(parent => {
29153
- parent.layoutManager.subscribeTargets({
29154
- target: parent,
29155
- targets: [activeSelection]
29156
- });
29157
- });
29158
- }
29159
-
29160
- /**
29161
- * unsubscribe from parent only if all its children were deselected
29162
- */
29163
- unsubscribeTargets(context) {
29164
- const activeSelection = context.target;
29165
- const selectedObjects = activeSelection.getObjects();
29166
- const parents = context.targets.reduce((parents, target) => {
29167
- target.parent && parents.add(target.parent);
29168
- return parents;
29169
- }, new Set());
29170
- parents.forEach(parent => {
29171
- !selectedObjects.some(object => object.parent === parent) && parent.layoutManager.unsubscribeTargets({
29172
- target: parent,
29173
- targets: [activeSelection]
29174
- });
29175
- });
29176
- }
29177
- }
29178
-
29179
- const activeSelectionDefaultValues = {
29180
- multiSelectionStacking: 'canvas-stacking'
29181
- };
29182
-
29183
- /**
29184
- * Used by Canvas to manage selection.
29185
- *
29186
- * @example
29187
- * class MyActiveSelection extends ActiveSelection {
29188
- * ...
29189
- * }
29190
- *
29191
- * // override the default `ActiveSelection` class
29192
- * classRegistry.setClass(MyActiveSelection)
29193
- */
29194
- class ActiveSelection extends Group {
29195
- static getDefaults() {
29196
- return {
29197
- ...super.getDefaults(),
29198
- ...ActiveSelection.ownDefaults
29199
- };
29200
- }
29201
-
29202
- /**
29203
- * The ActiveSelection needs to use the ActiveSelectionLayoutManager
29204
- * or selections on interactive groups may be broken
29205
- */
29206
-
29207
- /**
29208
- * controls how selected objects are added during a multiselection event
29209
- * - `canvas-stacking` adds the selected object to the active selection while respecting canvas object stacking order
29210
- * - `selection-order` adds the selected object to the top of the stack,
29211
- * meaning that the stack is ordered by the order in which objects were selected
29212
- * @default `canvas-stacking`
29213
- */
29214
-
29215
- constructor() {
29216
- let objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
29217
- let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
29218
- super();
29219
- Object.assign(this, ActiveSelection.ownDefaults);
29220
- this.setOptions(options);
29221
- const {
29222
- left,
29223
- top,
29224
- layoutManager
29225
- } = options;
29226
- this.groupInit(objects, {
29227
- left,
29228
- top,
29229
- layoutManager: layoutManager !== null && layoutManager !== void 0 ? layoutManager : new ActiveSelectionLayoutManager()
29230
- });
29231
- }
29232
-
29233
- /**
29234
- * @private
29235
- */
29236
- _shouldSetNestedCoords() {
29237
- return true;
29238
- }
29239
-
29240
- /**
29241
- * @private
29242
- * @override we don't want the selection monitor to be active
29243
- */
29244
- __objectSelectionMonitor() {
29245
- // noop
29246
- }
29247
-
29248
- /**
29249
- * Adds objects with respect to {@link multiSelectionStacking}
29250
- * @param targets object to add to selection
29251
- */
29252
- multiSelectAdd() {
29253
- for (var _len = arguments.length, targets = new Array(_len), _key = 0; _key < _len; _key++) {
29254
- targets[_key] = arguments[_key];
29255
- }
29256
- if (this.multiSelectionStacking === 'selection-order') {
29257
- this.add(...targets);
29258
- } else {
29259
- // respect object stacking as it is on canvas
29260
- // perf enhancement for large ActiveSelection: consider a binary search of `isInFrontOf`
29261
- targets.forEach(target => {
29262
- const index = this._objects.findIndex(obj => obj.isInFrontOf(target));
29263
- const insertAt = index === -1 ?
29264
- // `target` is in front of all other objects
29265
- this.size() : index;
29266
- this.insertAt(insertAt, target);
29267
- });
29268
- }
29269
- }
29270
-
29271
- /**
29272
- * @override block ancestors/descendants of selected objects from being selected to prevent a circular object tree
29273
- */
29274
- canEnterGroup(object) {
29275
- if (this.getObjects().some(o => o.isDescendantOf(object) || object.isDescendantOf(o))) {
29276
- // prevent circular object tree
29277
- log('error', 'ActiveSelection: circular object trees are not supported, this call has no effect');
29278
- return false;
29279
- }
29280
- return super.canEnterGroup(object);
29281
- }
29282
-
29283
- /**
29284
- * Change an object so that it can be part of an active selection.
29285
- * this method is called by multiselectAdd from canvas code.
29286
- * @private
29287
- * @param {FabricObject} object
29288
- * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane
29289
- */
29290
- enterGroup(object, removeParentTransform) {
29291
- // This condition check that the object has currently a group, and the group
29292
- // is also its parent, meaning that is not in an active selection, but is
29293
- // in a normal group.
29294
- if (object.parent && object.parent === object.group) {
29295
- // Disconnect the object from the group functionalities, but keep the ref parent intact
29296
- // for later re-enter
29297
- object.parent._exitGroup(object);
29298
- // in this case the object is probably inside an active selection.
29299
- } else if (object.group && object.parent !== object.group) {
29300
- // in this case group.remove will also clear the old parent reference.
29301
- object.group.remove(object);
29302
- }
29303
- // enter the active selection from a render perspective
29304
- // the object will be in the objects array of both the ActiveSelection and the Group
29305
- // but referenced in the group's _activeObjects so that it won't be rendered twice.
29306
- this._enterGroup(object, removeParentTransform);
29307
- }
29308
-
29309
- /**
29310
- * we want objects to retain their canvas ref when exiting instance
29311
- * @private
29312
- * @param {FabricObject} object
29313
- * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it
29314
- */
29315
- exitGroup(object, removeParentTransform) {
29316
- this._exitGroup(object, removeParentTransform);
29317
- // return to parent
29318
- object.parent && object.parent._enterGroup(object, true);
29319
- }
29320
-
29321
- /**
29322
- * @private
29323
- * @param {'added'|'removed'} type
29324
- * @param {FabricObject[]} targets
29325
- */
29326
- _onAfterObjectsChange(type, targets) {
29327
- super._onAfterObjectsChange(type, targets);
29328
- const groups = new Set();
29329
- targets.forEach(object => {
29330
- const {
29331
- parent
29332
- } = object;
29333
- parent && groups.add(parent);
29334
- });
29335
- if (type === LAYOUT_TYPE_REMOVED) {
29336
- // invalidate groups' layout and mark as dirty
29337
- groups.forEach(group => {
29338
- group._onAfterObjectsChange(LAYOUT_TYPE_ADDED, targets);
29339
- });
29340
- } else {
29341
- // mark groups as dirty
29342
- groups.forEach(group => {
29343
- group._set('dirty', true);
29344
- });
29345
- }
29346
- }
29347
-
29348
- /**
29349
- * @override remove all objects
29350
- */
29351
- onDeselect() {
29352
- this.removeAll();
29353
- return false;
29354
- }
29355
-
29356
- /**
29357
- * Returns string representation of a group
29358
- * @return {String}
29359
- */
29360
- toString() {
29361
- return `#<ActiveSelection: (${this.complexity()})>`;
29362
- }
29363
-
29364
- /**
29365
- * Decide if the object should cache or not. The Active selection never caches
29366
- * @return {Boolean}
29367
- */
29368
- shouldCache() {
29369
- return false;
29370
- }
29371
-
29372
- /**
29373
- * Check if this group or its parent group are caching, recursively up
29374
- * @return {Boolean}
29375
- */
29376
- isOnACache() {
29377
- return false;
29378
- }
29379
-
29380
- /**
29381
- * Renders controls and borders for the object
29382
- * @param {CanvasRenderingContext2D} ctx Context to render on
29383
- * @param {Object} [styleOverride] properties to override the object style
29384
- * @param {Object} [childrenOverride] properties to override the children overrides
29385
- */
29386
- _renderControls(ctx, styleOverride, childrenOverride) {
29387
- ctx.save();
29388
- ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
29389
- const options = {
29390
- hasControls: false,
29391
- ...childrenOverride,
29392
- forActiveSelection: true
29393
- };
29394
- for (let i = 0; i < this._objects.length; i++) {
29395
- this._objects[i]._renderControls(ctx, options);
29396
- }
29397
- super._renderControls(ctx, styleOverride);
29398
- ctx.restore();
29399
- }
29400
- }
29401
- _defineProperty(ActiveSelection, "type", 'ActiveSelection');
29402
- _defineProperty(ActiveSelection, "ownDefaults", activeSelectionDefaultValues);
29403
- classRegistry.setClass(ActiveSelection);
29404
- classRegistry.setClass(ActiveSelection, 'activeSelection');
29405
-
29406
29151
  /**
29407
29152
  * Canvas 2D filter backend.
29408
29153
  */
@@ -30446,6 +30191,1651 @@
30446
30191
  classRegistry.setClass(FabricImage);
30447
30192
  classRegistry.setSVGClass(FabricImage);
30448
30193
 
30194
+ /**
30195
+ * FrameLayout is a layout strategy that maintains fixed dimensions
30196
+ * regardless of the content inside the group.
30197
+ *
30198
+ * This is essential for Frame objects where:
30199
+ * - The frame size should never change when images are added/removed
30200
+ * - Content is clipped to the frame boundaries
30201
+ * - The frame acts as a container with fixed dimensions
30202
+ */
30203
+ class FrameLayout extends LayoutStrategy {
30204
+ /**
30205
+ * Override to prevent layout recalculation on content changes.
30206
+ * Only perform layout during initialization or imperative calls.
30207
+ */
30208
+ shouldPerformLayout(_ref) {
30209
+ let {
30210
+ type
30211
+ } = _ref;
30212
+ // Only perform layout during initialization
30213
+ // After that, the frame maintains its fixed size
30214
+ return type === LAYOUT_TYPE_INITIALIZATION;
30215
+ }
30216
+
30217
+ /**
30218
+ * Calculate the bounding box for frame objects.
30219
+ * Returns the fixed frame dimensions instead of calculating from contents.
30220
+ */
30221
+ calcBoundingBox(objects, context) {
30222
+ var _ref2, _frameWidth, _ref3, _frameHeight;
30223
+ const {
30224
+ type,
30225
+ target
30226
+ } = context;
30227
+
30228
+ // Get fixed dimensions from frame properties
30229
+ const frameWidth = (_ref2 = (_frameWidth = target.frameWidth) !== null && _frameWidth !== void 0 ? _frameWidth : target.width) !== null && _ref2 !== void 0 ? _ref2 : 200;
30230
+ const frameHeight = (_ref3 = (_frameHeight = target.frameHeight) !== null && _frameHeight !== void 0 ? _frameHeight : target.height) !== null && _ref3 !== void 0 ? _ref3 : 200;
30231
+ const size = new Point(frameWidth, frameHeight);
30232
+ if (type === LAYOUT_TYPE_INITIALIZATION) {
30233
+ // During initialization, use the frame's position or calculate center
30234
+ const center = new Point(0, 0);
30235
+ return {
30236
+ center,
30237
+ size,
30238
+ relativeCorrection: new Point(0, 0)
30239
+ };
30240
+ }
30241
+
30242
+ // For any other layout triggers, return the fixed size
30243
+ // This shouldn't normally be called due to shouldPerformLayout override
30244
+ const center = target.getRelativeCenterPoint();
30245
+ return {
30246
+ center,
30247
+ size
30248
+ };
30249
+ }
30250
+
30251
+ /**
30252
+ * Override to always return fixed frame dimensions during initialization.
30253
+ */
30254
+ getInitialSize(context, result) {
30255
+ var _ref4, _frameWidth2, _ref5, _frameHeight2;
30256
+ const {
30257
+ target
30258
+ } = context;
30259
+ const frameWidth = (_ref4 = (_frameWidth2 = target.frameWidth) !== null && _frameWidth2 !== void 0 ? _frameWidth2 : target.width) !== null && _ref4 !== void 0 ? _ref4 : 200;
30260
+ const frameHeight = (_ref5 = (_frameHeight2 = target.frameHeight) !== null && _frameHeight2 !== void 0 ? _frameHeight2 : target.height) !== null && _ref5 !== void 0 ? _ref5 : 200;
30261
+ return new Point(frameWidth, frameHeight);
30262
+ }
30263
+ }
30264
+ _defineProperty(FrameLayout, "type", 'frame-layout');
30265
+ classRegistry.setClass(FrameLayout);
30266
+
30267
+ /**
30268
+ * Frame shape types supported out of the box
30269
+ */
30270
+
30271
+ /**
30272
+ * Frame metadata for persistence and state management
30273
+ */
30274
+
30275
+ /**
30276
+ * Frame-specific properties
30277
+ */
30278
+
30279
+ const frameDefaultValues = {
30280
+ frameWidth: 200,
30281
+ frameHeight: 200,
30282
+ frameShape: 'rect',
30283
+ frameBorderRadius: 0,
30284
+ isEditMode: false,
30285
+ placeholderText: 'Drop image here',
30286
+ placeholderColor: '#d0d0d0',
30287
+ frameMeta: {
30288
+ contentScale: 1,
30289
+ contentOffsetX: 0,
30290
+ contentOffsetY: 0
30291
+ }
30292
+ };
30293
+
30294
+ /**
30295
+ * Frame class - A Canva-like frame container for images
30296
+ *
30297
+ * Features:
30298
+ * - Fixed dimensions that don't change when content is added/removed
30299
+ * - Multiple shape types (rect, circle, rounded-rect, custom SVG path)
30300
+ * - Cover scaling: images fill the frame completely, overflow is clipped
30301
+ * - Double-click edit mode: reposition/zoom content within frame
30302
+ * - Drag & drop support for replacing images
30303
+ * - Full serialization/deserialization support
30304
+ *
30305
+ * @example
30306
+ * ```ts
30307
+ * // Create a rectangular frame
30308
+ * const frame = new Frame([], {
30309
+ * frameWidth: 300,
30310
+ * frameHeight: 200,
30311
+ * frameShape: 'rect',
30312
+ * left: 100,
30313
+ * top: 100,
30314
+ * });
30315
+ *
30316
+ * // Add image with cover scaling
30317
+ * await frame.setImage('https://example.com/image.jpg');
30318
+ *
30319
+ * canvas.add(frame);
30320
+ * ```
30321
+ */
30322
+ class Frame extends Group {
30323
+ static getDefaults() {
30324
+ return {
30325
+ ...super.getDefaults(),
30326
+ ...Frame.ownDefaults
30327
+ };
30328
+ }
30329
+
30330
+ /**
30331
+ * Constructor
30332
+ * @param objects - Initial objects (typically empty for frames)
30333
+ * @param options - Frame configuration options
30334
+ */
30335
+ constructor() {
30336
+ var _defaultMeta$contentS, _defaultMeta$contentO, _defaultMeta$contentO2;
30337
+ let objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
30338
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
30339
+ // Set up the frame layout manager before calling super
30340
+ const frameLayoutManager = new LayoutManager(new FrameLayout());
30341
+ super(objects, {
30342
+ ...options,
30343
+ layoutManager: frameLayoutManager
30344
+ });
30345
+
30346
+ // Apply defaults
30347
+ /**
30348
+ * Reference to the content image
30349
+ * @private
30350
+ */
30351
+ _defineProperty(this, "_contentImage", null);
30352
+ /**
30353
+ * Reference to the placeholder object
30354
+ * @private
30355
+ */
30356
+ _defineProperty(this, "_placeholder", null);
30357
+ /**
30358
+ * Bound constraint handler references for cleanup
30359
+ * @private
30360
+ */
30361
+ _defineProperty(this, "_boundConstrainMove", void 0);
30362
+ _defineProperty(this, "_boundConstrainScale", void 0);
30363
+ /**
30364
+ * Stored clip path before edit mode
30365
+ * @private
30366
+ */
30367
+ _defineProperty(this, "_editModeClipPath", void 0);
30368
+ Object.assign(this, Frame.ownDefaults);
30369
+
30370
+ // Apply user options
30371
+ this.setOptions(options);
30372
+
30373
+ // Ensure frameMeta is properly initialized with defaults
30374
+ const defaultMeta = frameDefaultValues.frameMeta || {};
30375
+ this.frameMeta = {
30376
+ contentScale: (_defaultMeta$contentS = defaultMeta.contentScale) !== null && _defaultMeta$contentS !== void 0 ? _defaultMeta$contentS : 1,
30377
+ contentOffsetX: (_defaultMeta$contentO = defaultMeta.contentOffsetX) !== null && _defaultMeta$contentO !== void 0 ? _defaultMeta$contentO : 0,
30378
+ contentOffsetY: (_defaultMeta$contentO2 = defaultMeta.contentOffsetY) !== null && _defaultMeta$contentO2 !== void 0 ? _defaultMeta$contentO2 : 0,
30379
+ ...options.frameMeta
30380
+ };
30381
+
30382
+ // Set fixed dimensions
30383
+ this.set({
30384
+ width: this.frameWidth,
30385
+ height: this.frameHeight
30386
+ });
30387
+
30388
+ // Create clip path based on shape
30389
+ this._updateClipPath();
30390
+
30391
+ // Create placeholder if no content
30392
+ if (objects.length === 0) {
30393
+ this._createPlaceholder();
30394
+ }
30395
+
30396
+ // Set up custom resize controls (instead of scale controls)
30397
+ this._setupResizeControls();
30398
+ }
30399
+
30400
+ /**
30401
+ * Sets up custom controls that resize instead of scale
30402
+ * This is the key to Canva-like behavior - corners resize the frame dimensions
30403
+ * instead of scaling the entire group (which would stretch the image)
30404
+ * @private
30405
+ */
30406
+ _setupResizeControls() {
30407
+ // Helper to change width (like changeObjectWidth but for frames)
30408
+ // Note: wrapWithFixedAnchor sets origin to opposite corner, so localPoint.x IS the new width
30409
+ const changeFrameWidth = (eventData, transform, x, y) => {
30410
+ const target = transform.target;
30411
+ const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
30412
+ const oldWidth = target.frameWidth;
30413
+ // localPoint.x is distance from anchor (opposite side) to mouse = new width
30414
+ const newWidth = Math.max(20, Math.abs(localPoint.x));
30415
+ if (Math.abs(oldWidth - newWidth) < 1) return false;
30416
+ target.frameWidth = newWidth;
30417
+ target.width = newWidth;
30418
+ target._updateClipPath();
30419
+ target._adjustContentAfterResize();
30420
+ return true;
30421
+ };
30422
+
30423
+ // Helper to change height
30424
+ const changeFrameHeight = (eventData, transform, x, y) => {
30425
+ const target = transform.target;
30426
+ const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
30427
+ const oldHeight = target.frameHeight;
30428
+ const newHeight = Math.max(20, Math.abs(localPoint.y));
30429
+ if (Math.abs(oldHeight - newHeight) < 1) return false;
30430
+ target.frameHeight = newHeight;
30431
+ target.height = newHeight;
30432
+ target._updateClipPath();
30433
+ target._adjustContentAfterResize();
30434
+ return true;
30435
+ };
30436
+
30437
+ // Helper to change both width and height (corners)
30438
+ const changeFrameSize = (eventData, transform, x, y) => {
30439
+ const target = transform.target;
30440
+ const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
30441
+ const oldWidth = target.frameWidth;
30442
+ const oldHeight = target.frameHeight;
30443
+ const newWidth = Math.max(20, Math.abs(localPoint.x));
30444
+ const newHeight = Math.max(20, Math.abs(localPoint.y));
30445
+ if (Math.abs(oldWidth - newWidth) < 1 && Math.abs(oldHeight - newHeight) < 1) return false;
30446
+ target.frameWidth = newWidth;
30447
+ target.frameHeight = newHeight;
30448
+ target.width = newWidth;
30449
+ target.height = newHeight;
30450
+ target._updateClipPath();
30451
+ target._adjustContentAfterResize();
30452
+ return true;
30453
+ };
30454
+
30455
+ // Create wrapped handlers
30456
+ const resizeFromCorner = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeFrameSize));
30457
+ const resizeX = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeFrameWidth));
30458
+ const resizeY = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeFrameHeight));
30459
+
30460
+ // Guard: ensure controls exist
30461
+ if (!this.controls) {
30462
+ console.warn('Frame: controls not initialized yet');
30463
+ return;
30464
+ }
30465
+
30466
+ // Override corner controls - use resize instead of scale
30467
+ const cornerControls = ['tl', 'tr', 'bl', 'br'];
30468
+ cornerControls.forEach(corner => {
30469
+ const existing = this.controls[corner];
30470
+ if (existing) {
30471
+ this.controls[corner] = new Control({
30472
+ x: existing.x,
30473
+ y: existing.y,
30474
+ cursorStyleHandler: existing.cursorStyleHandler,
30475
+ actionHandler: resizeFromCorner,
30476
+ actionName: 'resizing'
30477
+ });
30478
+ }
30479
+ });
30480
+
30481
+ // Override side controls for horizontal resize
30482
+ const horizontalControls = ['ml', 'mr'];
30483
+ horizontalControls.forEach(corner => {
30484
+ const existing = this.controls[corner];
30485
+ if (existing) {
30486
+ this.controls[corner] = new Control({
30487
+ x: existing.x,
30488
+ y: existing.y,
30489
+ cursorStyleHandler: existing.cursorStyleHandler,
30490
+ actionHandler: resizeX,
30491
+ actionName: 'resizing',
30492
+ render: existing.render,
30493
+ // Keep the global pill renderer
30494
+ sizeX: existing.sizeX,
30495
+ sizeY: existing.sizeY
30496
+ });
30497
+ }
30498
+ });
30499
+
30500
+ // Override side controls for vertical resize
30501
+ const verticalControls = ['mt', 'mb'];
30502
+ verticalControls.forEach(corner => {
30503
+ const existing = this.controls[corner];
30504
+ if (existing) {
30505
+ this.controls[corner] = new Control({
30506
+ x: existing.x,
30507
+ y: existing.y,
30508
+ cursorStyleHandler: existing.cursorStyleHandler,
30509
+ actionHandler: resizeY,
30510
+ actionName: 'resizing',
30511
+ render: existing.render,
30512
+ // Keep the global pill renderer
30513
+ sizeX: existing.sizeX,
30514
+ sizeY: existing.sizeY
30515
+ });
30516
+ }
30517
+ });
30518
+ }
30519
+
30520
+ /**
30521
+ * Adjusts content after a resize operation (called from set override)
30522
+ * @private
30523
+ */
30524
+ _adjustContentAfterResize() {
30525
+ // Update placeholder if present (simple rect)
30526
+ if (this._placeholder) {
30527
+ this._placeholder.set({
30528
+ width: this.frameWidth,
30529
+ height: this.frameHeight
30530
+ });
30531
+ }
30532
+
30533
+ // Adjust content image (Canva-like behavior)
30534
+ if (this._contentImage) {
30535
+ var _ref, _this$frameMeta$origi, _ref2, _this$frameMeta$origi2, _img$scaleX, _img$left, _img$top;
30536
+ const img = this._contentImage;
30537
+ const originalWidth = (_ref = (_this$frameMeta$origi = this.frameMeta.originalWidth) !== null && _this$frameMeta$origi !== void 0 ? _this$frameMeta$origi : img.width) !== null && _ref !== void 0 ? _ref : 100;
30538
+ const originalHeight = (_ref2 = (_this$frameMeta$origi2 = this.frameMeta.originalHeight) !== null && _this$frameMeta$origi2 !== void 0 ? _this$frameMeta$origi2 : img.height) !== null && _ref2 !== void 0 ? _ref2 : 100;
30539
+
30540
+ // Current image scale and position - preserve user's position
30541
+ let currentScale = (_img$scaleX = img.scaleX) !== null && _img$scaleX !== void 0 ? _img$scaleX : 1;
30542
+ let imgCenterX = (_img$left = img.left) !== null && _img$left !== void 0 ? _img$left : 0;
30543
+ let imgCenterY = (_img$top = img.top) !== null && _img$top !== void 0 ? _img$top : 0;
30544
+
30545
+ // Check if current scale still covers the frame
30546
+ const minScaleForCover = this._calculateCoverScale(originalWidth, originalHeight);
30547
+ if (currentScale < minScaleForCover) {
30548
+ // Image is too small to cover frame - scale up proportionally
30549
+ // But try to keep the same visual center point
30550
+ const scaleRatio = minScaleForCover / currentScale;
30551
+
30552
+ // Scale position proportionally to maintain visual anchor
30553
+ imgCenterX = imgCenterX * scaleRatio;
30554
+ imgCenterY = imgCenterY * scaleRatio;
30555
+ currentScale = minScaleForCover;
30556
+ img.set({
30557
+ scaleX: currentScale,
30558
+ scaleY: currentScale
30559
+ });
30560
+ this.frameMeta = {
30561
+ ...this.frameMeta,
30562
+ contentScale: currentScale
30563
+ };
30564
+ }
30565
+
30566
+ // Now constrain position only if needed to prevent empty space
30567
+ const scaledImgHalfW = originalWidth * currentScale / 2;
30568
+ const scaledImgHalfH = originalHeight * currentScale / 2;
30569
+ const frameHalfW = this.frameWidth / 2;
30570
+ const frameHalfH = this.frameHeight / 2;
30571
+
30572
+ // Calculate how much the image can move while still covering the frame
30573
+ const maxOffsetX = Math.max(0, scaledImgHalfW - frameHalfW);
30574
+ const maxOffsetY = Math.max(0, scaledImgHalfH - frameHalfH);
30575
+
30576
+ // Only constrain if position would show empty space
30577
+ const needsConstraintX = Math.abs(imgCenterX) > maxOffsetX;
30578
+ const needsConstraintY = Math.abs(imgCenterY) > maxOffsetY;
30579
+ if (needsConstraintX) {
30580
+ imgCenterX = Math.max(-maxOffsetX, Math.min(maxOffsetX, imgCenterX));
30581
+ }
30582
+ if (needsConstraintY) {
30583
+ imgCenterY = Math.max(-maxOffsetY, Math.min(maxOffsetY, imgCenterY));
30584
+ }
30585
+ if (needsConstraintX || needsConstraintY) {
30586
+ img.set({
30587
+ left: imgCenterX,
30588
+ top: imgCenterY
30589
+ });
30590
+ this.frameMeta = {
30591
+ ...this.frameMeta,
30592
+ contentOffsetX: imgCenterX,
30593
+ contentOffsetY: imgCenterY
30594
+ };
30595
+ }
30596
+ img.setCoords();
30597
+ }
30598
+ this.setCoords();
30599
+ }
30600
+
30601
+ /**
30602
+ * Updates the clip path based on the current frame shape
30603
+ * @private
30604
+ */
30605
+ _updateClipPath() {
30606
+ let clipPath;
30607
+ switch (this.frameShape) {
30608
+ case 'circle':
30609
+ {
30610
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
30611
+ clipPath = new Circle({
30612
+ radius,
30613
+ originX: 'center',
30614
+ originY: 'center',
30615
+ left: 0,
30616
+ top: 0
30617
+ });
30618
+ break;
30619
+ }
30620
+ case 'rounded-rect':
30621
+ {
30622
+ clipPath = new Rect({
30623
+ width: this.frameWidth,
30624
+ height: this.frameHeight,
30625
+ rx: this.frameBorderRadius,
30626
+ ry: this.frameBorderRadius,
30627
+ originX: 'center',
30628
+ originY: 'center',
30629
+ left: 0,
30630
+ top: 0
30631
+ });
30632
+ break;
30633
+ }
30634
+ case 'custom':
30635
+ {
30636
+ if (this.frameCustomPath) {
30637
+ clipPath = new Path(this.frameCustomPath, {
30638
+ originX: 'center',
30639
+ originY: 'center',
30640
+ left: 0,
30641
+ top: 0
30642
+ });
30643
+ // Scale custom path to fit frame
30644
+ const pathBounds = clipPath.getBoundingRect();
30645
+ const scaleX = this.frameWidth / pathBounds.width;
30646
+ const scaleY = this.frameHeight / pathBounds.height;
30647
+ clipPath.set({
30648
+ scaleX,
30649
+ scaleY
30650
+ });
30651
+ } else {
30652
+ // Fallback to rect if no custom path
30653
+ clipPath = new Rect({
30654
+ width: this.frameWidth,
30655
+ height: this.frameHeight,
30656
+ originX: 'center',
30657
+ originY: 'center',
30658
+ left: 0,
30659
+ top: 0
30660
+ });
30661
+ }
30662
+ break;
30663
+ }
30664
+ case 'rect':
30665
+ default:
30666
+ {
30667
+ clipPath = new Rect({
30668
+ width: this.frameWidth,
30669
+ height: this.frameHeight,
30670
+ originX: 'center',
30671
+ originY: 'center',
30672
+ left: 0,
30673
+ top: 0
30674
+ });
30675
+ break;
30676
+ }
30677
+ }
30678
+ this.clipPath = clipPath;
30679
+ this.set('dirty', true);
30680
+ }
30681
+
30682
+ /**
30683
+ * Creates a placeholder element for empty frames
30684
+ * Shows a colored rectangle - users can customize via placeholderColor
30685
+ * @private
30686
+ */
30687
+ _createPlaceholder() {
30688
+ // Remove existing placeholder if any
30689
+ if (this._placeholder) {
30690
+ super.remove(this._placeholder);
30691
+ this._placeholder = null;
30692
+ }
30693
+
30694
+ // Create placeholder background
30695
+ const placeholder = new Rect({
30696
+ width: this.frameWidth,
30697
+ height: this.frameHeight,
30698
+ fill: this.placeholderColor,
30699
+ originX: 'center',
30700
+ originY: 'center',
30701
+ left: 0,
30702
+ top: 0,
30703
+ selectable: false,
30704
+ evented: false
30705
+ });
30706
+ this._placeholder = placeholder;
30707
+ super.add(placeholder);
30708
+
30709
+ // Ensure dimensions remain fixed
30710
+ this._restoreFixedDimensions();
30711
+ }
30712
+
30713
+ /**
30714
+ * Removes the placeholder element
30715
+ * @private
30716
+ */
30717
+ _removePlaceholder() {
30718
+ if (this._placeholder) {
30719
+ super.remove(this._placeholder);
30720
+ this._placeholder = null;
30721
+ }
30722
+ }
30723
+
30724
+ /**
30725
+ * Restores the fixed frame dimensions
30726
+ * @private
30727
+ */
30728
+ _restoreFixedDimensions() {
30729
+ this.set({
30730
+ width: this.frameWidth,
30731
+ height: this.frameHeight
30732
+ });
30733
+ }
30734
+
30735
+ /**
30736
+ * Sets an image in the frame with cover scaling
30737
+ *
30738
+ * @param src - Image source URL
30739
+ * @param options - Optional loading options
30740
+ * @returns Promise that resolves when the image is loaded and set
30741
+ *
30742
+ * @example
30743
+ * ```ts
30744
+ * await frame.setImage('https://example.com/photo.jpg');
30745
+ * canvas.renderAll();
30746
+ * ```
30747
+ */
30748
+ async setImage(src) {
30749
+ var _image$width, _image$height;
30750
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
30751
+ const {
30752
+ crossOrigin = 'anonymous',
30753
+ signal
30754
+ } = options;
30755
+
30756
+ // Load the image
30757
+ const image = await FabricImage.fromURL(src, {
30758
+ crossOrigin,
30759
+ signal
30760
+ });
30761
+
30762
+ // Get original dimensions
30763
+ const originalWidth = (_image$width = image.width) !== null && _image$width !== void 0 ? _image$width : 100;
30764
+ const originalHeight = (_image$height = image.height) !== null && _image$height !== void 0 ? _image$height : 100;
30765
+
30766
+ // Calculate cover scale
30767
+ const scale = this._calculateCoverScale(originalWidth, originalHeight);
30768
+
30769
+ // Configure image for frame
30770
+ image.set({
30771
+ scaleX: scale,
30772
+ scaleY: scale,
30773
+ originX: 'center',
30774
+ originY: 'center',
30775
+ left: 0,
30776
+ top: 0,
30777
+ selectable: false,
30778
+ evented: false
30779
+ });
30780
+
30781
+ // Remove existing content
30782
+ this._clearContent();
30783
+
30784
+ // Add new image
30785
+ this._contentImage = image;
30786
+ super.add(image);
30787
+
30788
+ // Force re-center the image after adding (layout might have moved it)
30789
+ this._contentImage.set({
30790
+ left: 0,
30791
+ top: 0
30792
+ });
30793
+
30794
+ // Update metadata
30795
+ this.frameMeta = {
30796
+ ...this.frameMeta,
30797
+ contentScale: scale,
30798
+ contentOffsetX: 0,
30799
+ contentOffsetY: 0,
30800
+ imageSrc: src,
30801
+ originalWidth,
30802
+ originalHeight
30803
+ };
30804
+
30805
+ // Restore dimensions (in case Group recalculated them)
30806
+ this._restoreFixedDimensions();
30807
+
30808
+ // Force recalculation of coordinates
30809
+ this.setCoords();
30810
+ this._contentImage.setCoords();
30811
+ this.set('dirty', true);
30812
+ }
30813
+
30814
+ /**
30815
+ * Sets an image from an existing FabricImage object
30816
+ *
30817
+ * @param image - FabricImage instance
30818
+ */
30819
+ setImageObject(image) {
30820
+ var _image$width2, _image$height2;
30821
+ const originalWidth = (_image$width2 = image.width) !== null && _image$width2 !== void 0 ? _image$width2 : 100;
30822
+ const originalHeight = (_image$height2 = image.height) !== null && _image$height2 !== void 0 ? _image$height2 : 100;
30823
+
30824
+ // Calculate cover scale
30825
+ const scale = this._calculateCoverScale(originalWidth, originalHeight);
30826
+
30827
+ // Configure image for frame
30828
+ image.set({
30829
+ scaleX: scale,
30830
+ scaleY: scale,
30831
+ originX: 'center',
30832
+ originY: 'center',
30833
+ left: 0,
30834
+ top: 0,
30835
+ selectable: false,
30836
+ evented: false
30837
+ });
30838
+
30839
+ // Remove existing content
30840
+ this._clearContent();
30841
+
30842
+ // Add new image
30843
+ this._contentImage = image;
30844
+ super.add(image);
30845
+
30846
+ // Update metadata
30847
+ this.frameMeta = {
30848
+ ...this.frameMeta,
30849
+ contentScale: scale,
30850
+ contentOffsetX: 0,
30851
+ contentOffsetY: 0,
30852
+ imageSrc: image.getSrc(),
30853
+ originalWidth,
30854
+ originalHeight
30855
+ };
30856
+
30857
+ // Restore dimensions
30858
+ this._restoreFixedDimensions();
30859
+ this.set('dirty', true);
30860
+ }
30861
+
30862
+ /**
30863
+ * Calculates the cover scale factor for an image
30864
+ * Cover scaling ensures the image fills the frame completely
30865
+ *
30866
+ * @param imageWidth - Original image width
30867
+ * @param imageHeight - Original image height
30868
+ * @returns Scale factor to apply
30869
+ * @private
30870
+ */
30871
+ _calculateCoverScale(imageWidth, imageHeight) {
30872
+ const scaleX = this.frameWidth / imageWidth;
30873
+ const scaleY = this.frameHeight / imageHeight;
30874
+ return Math.max(scaleX, scaleY);
30875
+ }
30876
+
30877
+ /**
30878
+ * Clears all content from the frame
30879
+ * @private
30880
+ */
30881
+ _clearContent() {
30882
+ // Remove placeholder
30883
+ this._removePlaceholder();
30884
+
30885
+ // Remove content image
30886
+ if (this._contentImage) {
30887
+ super.remove(this._contentImage);
30888
+ this._contentImage = null;
30889
+ }
30890
+
30891
+ // Clear any other objects
30892
+ const objects = this.getObjects();
30893
+ objects.forEach(obj => super.remove(obj));
30894
+ }
30895
+
30896
+ /**
30897
+ * Clears the frame content and shows placeholder
30898
+ */
30899
+ clearContent() {
30900
+ this._clearContent();
30901
+ this._createPlaceholder();
30902
+
30903
+ // Reset metadata
30904
+ this.frameMeta = {
30905
+ contentScale: 1,
30906
+ contentOffsetX: 0,
30907
+ contentOffsetY: 0
30908
+ };
30909
+ this.set('dirty', true);
30910
+ }
30911
+
30912
+ /**
30913
+ * Checks if the frame has image content
30914
+ */
30915
+ hasContent() {
30916
+ return this._contentImage !== null;
30917
+ }
30918
+
30919
+ /**
30920
+ * Gets the current content image
30921
+ */
30922
+ getContentImage() {
30923
+ return this._contentImage;
30924
+ }
30925
+
30926
+ /**
30927
+ * Enters edit mode for repositioning content within the frame
30928
+ * In edit mode, the content image can be dragged and scaled
30929
+ */
30930
+ enterEditMode() {
30931
+ var _ref3, _this$frameMeta$origi3, _ref4, _this$frameMeta$origi4;
30932
+ if (!this._contentImage || this.isEditMode) {
30933
+ return;
30934
+ }
30935
+ this.isEditMode = true;
30936
+
30937
+ // Enable sub-target interaction so clicks go through to content
30938
+ this.subTargetCheck = true;
30939
+ this.interactive = true;
30940
+
30941
+ // Calculate minimum scale to cover frame
30942
+ const originalWidth = (_ref3 = (_this$frameMeta$origi3 = this.frameMeta.originalWidth) !== null && _this$frameMeta$origi3 !== void 0 ? _this$frameMeta$origi3 : this._contentImage.width) !== null && _ref3 !== void 0 ? _ref3 : 100;
30943
+ const originalHeight = (_ref4 = (_this$frameMeta$origi4 = this.frameMeta.originalHeight) !== null && _this$frameMeta$origi4 !== void 0 ? _this$frameMeta$origi4 : this._contentImage.height) !== null && _ref4 !== void 0 ? _ref4 : 100;
30944
+ const minScale = this._calculateCoverScale(originalWidth, originalHeight);
30945
+
30946
+ // Make content image interactive with scale constraint
30947
+ this._contentImage.set({
30948
+ selectable: true,
30949
+ evented: true,
30950
+ hasControls: true,
30951
+ hasBorders: true,
30952
+ minScaleLimit: minScale,
30953
+ lockScalingFlip: true
30954
+ });
30955
+
30956
+ // Store clip path but keep rendering it for the overlay effect
30957
+ if (this.clipPath) {
30958
+ this._editModeClipPath = this.clipPath;
30959
+ this.clipPath = undefined;
30960
+ }
30961
+
30962
+ // Add constraint handlers for moving/scaling
30963
+ this._setupEditModeConstraints();
30964
+ this.set('dirty', true);
30965
+
30966
+ // Select the content image on the canvas
30967
+ if (this.canvas) {
30968
+ this.canvas.setActiveObject(this._contentImage);
30969
+ this.canvas.renderAll();
30970
+ }
30971
+
30972
+ // Fire custom event
30973
+ this.fire('frame:editmode:enter', {
30974
+ target: this
30975
+ });
30976
+ }
30977
+ /**
30978
+ * Sets up constraints for edit mode - prevents gaps
30979
+ * @private
30980
+ */
30981
+ _setupEditModeConstraints() {
30982
+ if (!this._contentImage || !this.canvas) return;
30983
+ const frame = this;
30984
+ const img = this._contentImage;
30985
+
30986
+ // Constrain movement to prevent gaps
30987
+ this._boundConstrainMove = e => {
30988
+ var _ref5, _frame$frameMeta$orig, _ref6, _frame$frameMeta$orig2, _img$scaleX2, _img$left2, _img$top2;
30989
+ if (e.target !== img || !frame.isEditMode) return;
30990
+ const originalWidth = (_ref5 = (_frame$frameMeta$orig = frame.frameMeta.originalWidth) !== null && _frame$frameMeta$orig !== void 0 ? _frame$frameMeta$orig : img.width) !== null && _ref5 !== void 0 ? _ref5 : 100;
30991
+ const originalHeight = (_ref6 = (_frame$frameMeta$orig2 = frame.frameMeta.originalHeight) !== null && _frame$frameMeta$orig2 !== void 0 ? _frame$frameMeta$orig2 : img.height) !== null && _ref6 !== void 0 ? _ref6 : 100;
30992
+ const currentScale = (_img$scaleX2 = img.scaleX) !== null && _img$scaleX2 !== void 0 ? _img$scaleX2 : 1;
30993
+ const scaledImgHalfW = originalWidth * currentScale / 2;
30994
+ const scaledImgHalfH = originalHeight * currentScale / 2;
30995
+ const frameHalfW = frame.frameWidth / 2;
30996
+ const frameHalfH = frame.frameHeight / 2;
30997
+ const maxOffsetX = Math.max(0, scaledImgHalfW - frameHalfW);
30998
+ const maxOffsetY = Math.max(0, scaledImgHalfH - frameHalfH);
30999
+ let left = (_img$left2 = img.left) !== null && _img$left2 !== void 0 ? _img$left2 : 0;
31000
+ let top = (_img$top2 = img.top) !== null && _img$top2 !== void 0 ? _img$top2 : 0;
31001
+
31002
+ // Constrain position
31003
+ left = Math.max(-maxOffsetX, Math.min(maxOffsetX, left));
31004
+ top = Math.max(-maxOffsetY, Math.min(maxOffsetY, top));
31005
+ img.set({
31006
+ left,
31007
+ top
31008
+ });
31009
+ };
31010
+
31011
+ // Constrain scaling to prevent gaps
31012
+ this._boundConstrainScale = e => {
31013
+ var _ref7, _frame$frameMeta$orig3, _ref8, _frame$frameMeta$orig4, _img$scaleX3, _img$scaleY, _frame$_boundConstrai;
31014
+ if (e.target !== img || !frame.isEditMode) return;
31015
+ const originalWidth = (_ref7 = (_frame$frameMeta$orig3 = frame.frameMeta.originalWidth) !== null && _frame$frameMeta$orig3 !== void 0 ? _frame$frameMeta$orig3 : img.width) !== null && _ref7 !== void 0 ? _ref7 : 100;
31016
+ const originalHeight = (_ref8 = (_frame$frameMeta$orig4 = frame.frameMeta.originalHeight) !== null && _frame$frameMeta$orig4 !== void 0 ? _frame$frameMeta$orig4 : img.height) !== null && _ref8 !== void 0 ? _ref8 : 100;
31017
+ const minScale = frame._calculateCoverScale(originalWidth, originalHeight);
31018
+ let scaleX = (_img$scaleX3 = img.scaleX) !== null && _img$scaleX3 !== void 0 ? _img$scaleX3 : 1;
31019
+ let scaleY = (_img$scaleY = img.scaleY) !== null && _img$scaleY !== void 0 ? _img$scaleY : 1;
31020
+
31021
+ // Ensure uniform scaling and minimum scale
31022
+ const scale = Math.max(minScale, Math.max(scaleX, scaleY));
31023
+ img.set({
31024
+ scaleX: scale,
31025
+ scaleY: scale
31026
+ });
31027
+
31028
+ // Also constrain position after scale
31029
+ (_frame$_boundConstrai = frame._boundConstrainMove) === null || _frame$_boundConstrai === void 0 || _frame$_boundConstrai.call(frame, e);
31030
+ };
31031
+ this.canvas.on('object:moving', this._boundConstrainMove);
31032
+ this.canvas.on('object:scaling', this._boundConstrainScale);
31033
+ }
31034
+
31035
+ /**
31036
+ * Removes edit mode constraint handlers
31037
+ * @private
31038
+ */
31039
+ _removeEditModeConstraints() {
31040
+ if (!this.canvas) return;
31041
+ if (this._boundConstrainMove) {
31042
+ this.canvas.off('object:moving', this._boundConstrainMove);
31043
+ this._boundConstrainMove = undefined;
31044
+ }
31045
+ if (this._boundConstrainScale) {
31046
+ this.canvas.off('object:scaling', this._boundConstrainScale);
31047
+ this._boundConstrainScale = undefined;
31048
+ }
31049
+ }
31050
+ /**
31051
+ * Custom render to show edit mode overlay
31052
+ * @override
31053
+ */
31054
+ render(ctx) {
31055
+ super.render(ctx);
31056
+
31057
+ // Draw edit mode overlay if in edit mode
31058
+ if (this.isEditMode && this._editModeClipPath) {
31059
+ this._renderEditModeOverlay(ctx);
31060
+ }
31061
+ }
31062
+
31063
+ /**
31064
+ * Renders the edit mode overlay - dims area outside frame, shows frame border
31065
+ * @private
31066
+ */
31067
+ _renderEditModeOverlay(ctx) {
31068
+ ctx.save();
31069
+
31070
+ // Apply the group's transform
31071
+ const m = this.calcTransformMatrix();
31072
+ ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
31073
+
31074
+ // Draw semi-transparent overlay on the OUTSIDE of the frame
31075
+ // We do this by drawing a large rect and cutting out the frame shape
31076
+ ctx.beginPath();
31077
+
31078
+ // Large outer rectangle (covers the whole image area)
31079
+ const padding = 2000; // Large enough to cover any overflow
31080
+ ctx.rect(-padding, -padding, padding * 2, padding * 2);
31081
+
31082
+ // Cut out the frame shape (counter-clockwise to create hole)
31083
+ if (this.frameShape === 'circle') {
31084
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
31085
+ ctx.moveTo(radius, 0);
31086
+ ctx.arc(0, 0, radius, 0, Math.PI * 2, true);
31087
+ } else if (this.frameShape === 'rounded-rect') {
31088
+ const w = this.frameWidth / 2;
31089
+ const h = this.frameHeight / 2;
31090
+ const r = Math.min(this.frameBorderRadius, w, h);
31091
+ ctx.moveTo(w, h - r);
31092
+ ctx.arcTo(w, -h, w - r, -h, r);
31093
+ ctx.arcTo(-w, -h, -w, -h + r, r);
31094
+ ctx.arcTo(-w, h, -w + r, h, r);
31095
+ ctx.arcTo(w, h, w, h - r, r);
31096
+ ctx.closePath();
31097
+ } else {
31098
+ // Rectangle
31099
+ const w = this.frameWidth / 2;
31100
+ const h = this.frameHeight / 2;
31101
+ ctx.moveTo(w, -h);
31102
+ ctx.lineTo(-w, -h);
31103
+ ctx.lineTo(-w, h);
31104
+ ctx.lineTo(w, h);
31105
+ ctx.closePath();
31106
+ }
31107
+
31108
+ // Fill with semi-transparent dark overlay
31109
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
31110
+ ctx.fill('evenodd');
31111
+
31112
+ // Draw frame border
31113
+ ctx.beginPath();
31114
+ if (this.frameShape === 'circle') {
31115
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
31116
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
31117
+ } else if (this.frameShape === 'rounded-rect') {
31118
+ const w = this.frameWidth / 2;
31119
+ const h = this.frameHeight / 2;
31120
+ const r = Math.min(this.frameBorderRadius, w, h);
31121
+ ctx.moveTo(w - r, -h);
31122
+ ctx.arcTo(w, -h, w, -h + r, r);
31123
+ ctx.arcTo(w, h, w - r, h, r);
31124
+ ctx.arcTo(-w, h, -w, h - r, r);
31125
+ ctx.arcTo(-w, -h, -w + r, -h, r);
31126
+ ctx.closePath();
31127
+ } else {
31128
+ const w = this.frameWidth / 2;
31129
+ const h = this.frameHeight / 2;
31130
+ ctx.rect(-w, -h, this.frameWidth, this.frameHeight);
31131
+ }
31132
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
31133
+ ctx.lineWidth = 2;
31134
+ ctx.stroke();
31135
+
31136
+ // Draw subtle dashed line for frame boundary
31137
+ ctx.setLineDash([5, 5]);
31138
+ ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
31139
+ ctx.lineWidth = 1;
31140
+ ctx.stroke();
31141
+ ctx.restore();
31142
+ }
31143
+
31144
+ /**
31145
+ * Exits edit mode and saves the content position
31146
+ */
31147
+ exitEditMode() {
31148
+ var _this$_contentImage$l, _this$_contentImage$t, _this$_contentImage$s, _this$_contentImage$s2, _ref9, _this$frameMeta$origi5, _ref0, _this$frameMeta$origi6;
31149
+ if (!this._contentImage || !this.isEditMode) {
31150
+ return;
31151
+ }
31152
+ this.isEditMode = false;
31153
+
31154
+ // Remove constraint handlers
31155
+ this._removeEditModeConstraints();
31156
+
31157
+ // Disable sub-target interaction
31158
+ this.subTargetCheck = false;
31159
+ this.interactive = false;
31160
+
31161
+ // Get the current position of the content
31162
+ const contentLeft = (_this$_contentImage$l = this._contentImage.left) !== null && _this$_contentImage$l !== void 0 ? _this$_contentImage$l : 0;
31163
+ const contentTop = (_this$_contentImage$t = this._contentImage.top) !== null && _this$_contentImage$t !== void 0 ? _this$_contentImage$t : 0;
31164
+ const contentScaleX = (_this$_contentImage$s = this._contentImage.scaleX) !== null && _this$_contentImage$s !== void 0 ? _this$_contentImage$s : 1;
31165
+ const contentScaleY = (_this$_contentImage$s2 = this._contentImage.scaleY) !== null && _this$_contentImage$s2 !== void 0 ? _this$_contentImage$s2 : 1;
31166
+
31167
+ // Constrain position so image always covers the frame
31168
+ const originalWidth = (_ref9 = (_this$frameMeta$origi5 = this.frameMeta.originalWidth) !== null && _this$frameMeta$origi5 !== void 0 ? _this$frameMeta$origi5 : this._contentImage.width) !== null && _ref9 !== void 0 ? _ref9 : 100;
31169
+ const originalHeight = (_ref0 = (_this$frameMeta$origi6 = this.frameMeta.originalHeight) !== null && _this$frameMeta$origi6 !== void 0 ? _this$frameMeta$origi6 : this._contentImage.height) !== null && _ref0 !== void 0 ? _ref0 : 100;
31170
+ const currentScale = Math.max(contentScaleX, contentScaleY);
31171
+ const scaledImgHalfW = originalWidth * currentScale / 2;
31172
+ const scaledImgHalfH = originalHeight * currentScale / 2;
31173
+ const frameHalfW = this.frameWidth / 2;
31174
+ const frameHalfH = this.frameHeight / 2;
31175
+
31176
+ // Ensure image covers frame (constrain position)
31177
+ const maxOffsetX = Math.max(0, scaledImgHalfW - frameHalfW);
31178
+ const maxOffsetY = Math.max(0, scaledImgHalfH - frameHalfH);
31179
+ const constrainedLeft = Math.max(-maxOffsetX, Math.min(maxOffsetX, contentLeft));
31180
+ const constrainedTop = Math.max(-maxOffsetY, Math.min(maxOffsetY, contentTop));
31181
+
31182
+ // Apply constrained position
31183
+ this._contentImage.set({
31184
+ left: constrainedLeft,
31185
+ top: constrainedTop
31186
+ });
31187
+
31188
+ // Update metadata with new offsets and scale
31189
+ this.frameMeta = {
31190
+ ...this.frameMeta,
31191
+ contentOffsetX: constrainedLeft,
31192
+ contentOffsetY: constrainedTop,
31193
+ contentScale: currentScale
31194
+ };
31195
+
31196
+ // Make content non-interactive again
31197
+ this._contentImage.set({
31198
+ selectable: false,
31199
+ evented: false,
31200
+ hasControls: false,
31201
+ hasBorders: false
31202
+ });
31203
+
31204
+ // Restore clip path
31205
+ if (this._editModeClipPath) {
31206
+ this.clipPath = this._editModeClipPath;
31207
+ this._editModeClipPath = undefined;
31208
+ } else {
31209
+ this._updateClipPath();
31210
+ }
31211
+ this.set('dirty', true);
31212
+
31213
+ // Re-select the frame itself
31214
+ if (this.canvas) {
31215
+ this.canvas.setActiveObject(this);
31216
+ this.canvas.renderAll();
31217
+ }
31218
+
31219
+ // Fire custom event
31220
+ this.fire('frame:editmode:exit', {
31221
+ target: this
31222
+ });
31223
+ }
31224
+
31225
+ /**
31226
+ * Toggles edit mode
31227
+ */
31228
+ toggleEditMode() {
31229
+ if (this.isEditMode) {
31230
+ this.exitEditMode();
31231
+ } else {
31232
+ this.enterEditMode();
31233
+ }
31234
+ }
31235
+
31236
+ /**
31237
+ * Resizes the frame to new dimensions (Canva-like behavior)
31238
+ *
31239
+ * Canva behavior:
31240
+ * - When frame shrinks: crops more of image (no scale change)
31241
+ * - When frame grows: uncrops to show more, preserving position
31242
+ * - Only scales up when image can't cover the frame anymore
31243
+ *
31244
+ * @param width - New frame width
31245
+ * @param height - New frame height
31246
+ * @param options - Resize options
31247
+ */
31248
+ resizeFrame(width, height) {
31249
+ let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
31250
+ const {
31251
+ maintainAspect = false
31252
+ } = options;
31253
+ if (maintainAspect) {
31254
+ const currentAspect = this.frameWidth / this.frameHeight;
31255
+ const newAspect = width / height;
31256
+ if (newAspect > currentAspect) {
31257
+ height = width / currentAspect;
31258
+ } else {
31259
+ width = height * currentAspect;
31260
+ }
31261
+ }
31262
+ this.frameWidth = width;
31263
+ this.frameHeight = height;
31264
+
31265
+ // Update dimensions using super.set to avoid re-triggering conversion
31266
+ super.set({
31267
+ width: this.frameWidth,
31268
+ height: this.frameHeight
31269
+ });
31270
+
31271
+ // Update clip path
31272
+ this._updateClipPath();
31273
+
31274
+ // Canva-like content adjustment
31275
+ this._adjustContentAfterResize();
31276
+ this.set('dirty', true);
31277
+ this.setCoords();
31278
+ }
31279
+
31280
+ /**
31281
+ * Sets the frame shape
31282
+ *
31283
+ * @param shape - Shape type
31284
+ * @param customPath - Custom SVG path for 'custom' shape type
31285
+ */
31286
+ setFrameShape(shape, customPath) {
31287
+ this.frameShape = shape;
31288
+ if (customPath) {
31289
+ this.frameCustomPath = customPath;
31290
+ }
31291
+ this._updateClipPath();
31292
+ this.set('dirty', true);
31293
+ }
31294
+
31295
+ /**
31296
+ * Sets the border radius for rounded-rect shape
31297
+ *
31298
+ * @param radius - Border radius in pixels
31299
+ */
31300
+ setBorderRadius(radius) {
31301
+ this.frameBorderRadius = radius;
31302
+ if (this.frameShape === 'rounded-rect') {
31303
+ this._updateClipPath();
31304
+ this.set('dirty', true);
31305
+ }
31306
+ }
31307
+
31308
+ /**
31309
+ * Override add to maintain fixed dimensions
31310
+ */
31311
+ add() {
31312
+ const size = super.add(...arguments);
31313
+ this._restoreFixedDimensions();
31314
+ return size;
31315
+ }
31316
+
31317
+ /**
31318
+ * Override remove to maintain fixed dimensions
31319
+ */
31320
+ remove() {
31321
+ const removed = super.remove(...arguments);
31322
+ this._restoreFixedDimensions();
31323
+ return removed;
31324
+ }
31325
+
31326
+ /**
31327
+ * Override insertAt to maintain fixed dimensions
31328
+ */
31329
+ insertAt(index) {
31330
+ for (var _len = arguments.length, objects = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
31331
+ objects[_key - 1] = arguments[_key];
31332
+ }
31333
+ const size = super.insertAt(index, ...objects);
31334
+ this._restoreFixedDimensions();
31335
+ return size;
31336
+ }
31337
+
31338
+ /**
31339
+ * Serializes the frame to a plain object
31340
+ */
31341
+ // @ts-ignore - Frame extends Group's toObject with additional properties
31342
+ toObject() {
31343
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
31344
+ return {
31345
+ ...super.toObject(propertiesToInclude),
31346
+ frameWidth: this.frameWidth,
31347
+ frameHeight: this.frameHeight,
31348
+ frameShape: this.frameShape,
31349
+ frameBorderRadius: this.frameBorderRadius,
31350
+ frameCustomPath: this.frameCustomPath,
31351
+ frameMeta: {
31352
+ ...this.frameMeta
31353
+ },
31354
+ isEditMode: false,
31355
+ // Always serialize as not in edit mode
31356
+ placeholderText: this.placeholderText,
31357
+ placeholderColor: this.placeholderColor
31358
+ };
31359
+ }
31360
+
31361
+ /**
31362
+ * Creates a Frame instance from a serialized object
31363
+ */
31364
+ static fromObject(object, abortable) {
31365
+ const {
31366
+ objects = [],
31367
+ layoutManager,
31368
+ frameWidth,
31369
+ frameHeight,
31370
+ frameShape,
31371
+ frameBorderRadius,
31372
+ frameCustomPath,
31373
+ frameMeta,
31374
+ placeholderText,
31375
+ placeholderColor,
31376
+ ...groupOptions
31377
+ } = object;
31378
+ return Promise.all([enlivenObjects(objects, abortable), enlivenObjectEnlivables(groupOptions, abortable)]).then(_ref1 => {
31379
+ var _frameMeta$contentSca, _frameMeta$contentOff, _frameMeta$contentOff2;
31380
+ let [enlivenedObjects, hydratedOptions] = _ref1;
31381
+ // Create frame with restored options
31382
+ const frame = new Frame([], {
31383
+ ...groupOptions,
31384
+ ...hydratedOptions,
31385
+ frameWidth,
31386
+ frameHeight,
31387
+ frameShape,
31388
+ frameBorderRadius,
31389
+ frameCustomPath,
31390
+ frameMeta: frameMeta ? {
31391
+ contentScale: (_frameMeta$contentSca = frameMeta.contentScale) !== null && _frameMeta$contentSca !== void 0 ? _frameMeta$contentSca : 1,
31392
+ contentOffsetX: (_frameMeta$contentOff = frameMeta.contentOffsetX) !== null && _frameMeta$contentOff !== void 0 ? _frameMeta$contentOff : 0,
31393
+ contentOffsetY: (_frameMeta$contentOff2 = frameMeta.contentOffsetY) !== null && _frameMeta$contentOff2 !== void 0 ? _frameMeta$contentOff2 : 0,
31394
+ ...frameMeta
31395
+ } : undefined,
31396
+ placeholderText,
31397
+ placeholderColor
31398
+ });
31399
+
31400
+ // If there was an image, restore it
31401
+ if (frameMeta !== null && frameMeta !== void 0 && frameMeta.imageSrc) {
31402
+ // Async restoration of image - caller should wait if needed
31403
+ frame.setImage(frameMeta.imageSrc).then(() => {
31404
+ // Restore content position from metadata
31405
+ if (frame._contentImage) {
31406
+ var _frameMeta$contentOff3, _frameMeta$contentOff4, _frameMeta$contentSca2, _frameMeta$contentSca3;
31407
+ frame._contentImage.set({
31408
+ left: (_frameMeta$contentOff3 = frameMeta.contentOffsetX) !== null && _frameMeta$contentOff3 !== void 0 ? _frameMeta$contentOff3 : 0,
31409
+ top: (_frameMeta$contentOff4 = frameMeta.contentOffsetY) !== null && _frameMeta$contentOff4 !== void 0 ? _frameMeta$contentOff4 : 0,
31410
+ scaleX: (_frameMeta$contentSca2 = frameMeta.contentScale) !== null && _frameMeta$contentSca2 !== void 0 ? _frameMeta$contentSca2 : 1,
31411
+ scaleY: (_frameMeta$contentSca3 = frameMeta.contentScale) !== null && _frameMeta$contentSca3 !== void 0 ? _frameMeta$contentSca3 : 1
31412
+ });
31413
+ }
31414
+ frame.set('dirty', true);
31415
+ }).catch(err => {
31416
+ console.warn('Failed to restore frame image:', err);
31417
+ });
31418
+ }
31419
+ return frame;
31420
+ });
31421
+ }
31422
+
31423
+ /**
31424
+ * Creates a Frame with a specific aspect ratio preset
31425
+ *
31426
+ * @param aspect - Aspect ratio preset (e.g., '16:9', '1:1', '4:5', '9:16')
31427
+ * @param size - Base size in pixels
31428
+ * @param options - Additional frame options
31429
+ */
31430
+ static createWithAspect(aspect) {
31431
+ var _defaultMeta$contentS2, _defaultMeta$contentO3, _defaultMeta$contentO4;
31432
+ let size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 200;
31433
+ let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
31434
+ let width;
31435
+ let height;
31436
+ switch (aspect) {
31437
+ case '16:9':
31438
+ width = size;
31439
+ height = size * (9 / 16);
31440
+ break;
31441
+ case '9:16':
31442
+ width = size * (9 / 16);
31443
+ height = size;
31444
+ break;
31445
+ case '4:5':
31446
+ width = size * (4 / 5);
31447
+ height = size;
31448
+ break;
31449
+ case '4:3':
31450
+ width = size;
31451
+ height = size * (3 / 4);
31452
+ break;
31453
+ case '3:4':
31454
+ width = size * (3 / 4);
31455
+ height = size;
31456
+ break;
31457
+ case '1:1':
31458
+ default:
31459
+ width = size;
31460
+ height = size;
31461
+ break;
31462
+ }
31463
+ const defaultMeta = frameDefaultValues.frameMeta || {};
31464
+ return new Frame([], {
31465
+ ...options,
31466
+ frameWidth: width,
31467
+ frameHeight: height,
31468
+ frameMeta: {
31469
+ contentScale: (_defaultMeta$contentS2 = defaultMeta.contentScale) !== null && _defaultMeta$contentS2 !== void 0 ? _defaultMeta$contentS2 : 1,
31470
+ contentOffsetX: (_defaultMeta$contentO3 = defaultMeta.contentOffsetX) !== null && _defaultMeta$contentO3 !== void 0 ? _defaultMeta$contentO3 : 0,
31471
+ contentOffsetY: (_defaultMeta$contentO4 = defaultMeta.contentOffsetY) !== null && _defaultMeta$contentO4 !== void 0 ? _defaultMeta$contentO4 : 0,
31472
+ aspect,
31473
+ ...options.frameMeta
31474
+ }
31475
+ });
31476
+ }
31477
+ }
31478
+
31479
+ // Register the Frame class with the class registry
31480
+ _defineProperty(Frame, "type", 'Frame');
31481
+ _defineProperty(Frame, "ownDefaults", frameDefaultValues);
31482
+ classRegistry.setClass(Frame);
31483
+ classRegistry.setClass(Frame, 'frame');
31484
+
31485
+ /**
31486
+ * Layout will adjust the bounding box to match the clip path bounding box.
31487
+ */
31488
+ class ClipPathLayout extends LayoutStrategy {
31489
+ shouldPerformLayout(context) {
31490
+ return !!context.target.clipPath && super.shouldPerformLayout(context);
31491
+ }
31492
+ shouldLayoutClipPath() {
31493
+ return false;
31494
+ }
31495
+ calcLayoutResult(context, objects) {
31496
+ const {
31497
+ target
31498
+ } = context;
31499
+ const {
31500
+ clipPath,
31501
+ group
31502
+ } = target;
31503
+ if (!clipPath || !this.shouldPerformLayout(context)) {
31504
+ return;
31505
+ }
31506
+ // TODO: remove stroke calculation from this case
31507
+ const {
31508
+ width,
31509
+ height
31510
+ } = makeBoundingBoxFromPoints(getObjectBounds(target, clipPath));
31511
+ const size = new Point(width, height);
31512
+ if (clipPath.absolutePositioned) {
31513
+ // we want the center point to exist in group's containing plane
31514
+ const clipPathCenter = sendPointToPlane(clipPath.getRelativeCenterPoint(), undefined, group ? group.calcTransformMatrix() : undefined);
31515
+ return {
31516
+ center: clipPathCenter,
31517
+ size
31518
+ };
31519
+ } else {
31520
+ // we want the center point to exist in group's containing plane, so we send it upwards
31521
+ const clipPathCenter = clipPath.getRelativeCenterPoint().transform(target.calcOwnMatrix(), true);
31522
+ if (this.shouldPerformLayout(context)) {
31523
+ // the clip path is positioned relative to the group's center which is affected by the bbox
31524
+ // so we first calculate the bbox
31525
+ const {
31526
+ center = new Point(),
31527
+ correction = new Point()
31528
+ } = this.calcBoundingBox(objects, context) || {};
31529
+ return {
31530
+ center: center.add(clipPathCenter),
31531
+ correction: correction.subtract(clipPathCenter),
31532
+ size
31533
+ };
31534
+ } else {
31535
+ return {
31536
+ center: target.getRelativeCenterPoint().add(clipPathCenter),
31537
+ size
31538
+ };
31539
+ }
31540
+ }
31541
+ }
31542
+ }
31543
+ _defineProperty(ClipPathLayout, "type", 'clip-path');
31544
+ classRegistry.setClass(ClipPathLayout);
31545
+
31546
+ /**
31547
+ * Layout will keep target's initial size.
31548
+ */
31549
+ class FixedLayout extends LayoutStrategy {
31550
+ /**
31551
+ * @override respect target's initial size
31552
+ */
31553
+ getInitialSize(_ref, _ref2) {
31554
+ let {
31555
+ target
31556
+ } = _ref;
31557
+ let {
31558
+ size
31559
+ } = _ref2;
31560
+ return new Point(target.width || size.x, target.height || size.y);
31561
+ }
31562
+ }
31563
+ _defineProperty(FixedLayout, "type", 'fixed');
31564
+ classRegistry.setClass(FixedLayout);
31565
+
31566
+ /**
31567
+ * Today the LayoutManager class also takes care of subscribing event handlers
31568
+ * to update the group layout when the group is interactive and a transform is applied
31569
+ * to a child object.
31570
+ * The ActiveSelection is never interactive, but it could contain objects from
31571
+ * groups that are.
31572
+ * The standard LayoutManager would subscribe the children of the activeSelection to
31573
+ * perform layout changes to the active selection itself, what we need instead is that
31574
+ * the transformation applied to the active selection will trigger changes to the
31575
+ * original group of the children ( the one referenced under the parent property )
31576
+ * This subclass of the LayoutManager has a single duty to fill the gap of this difference.`
31577
+ */
31578
+ class ActiveSelectionLayoutManager extends LayoutManager {
31579
+ subscribeTargets(context) {
31580
+ const activeSelection = context.target;
31581
+ const parents = context.targets.reduce((parents, target) => {
31582
+ target.parent && parents.add(target.parent);
31583
+ return parents;
31584
+ }, new Set());
31585
+ parents.forEach(parent => {
31586
+ parent.layoutManager.subscribeTargets({
31587
+ target: parent,
31588
+ targets: [activeSelection]
31589
+ });
31590
+ });
31591
+ }
31592
+
31593
+ /**
31594
+ * unsubscribe from parent only if all its children were deselected
31595
+ */
31596
+ unsubscribeTargets(context) {
31597
+ const activeSelection = context.target;
31598
+ const selectedObjects = activeSelection.getObjects();
31599
+ const parents = context.targets.reduce((parents, target) => {
31600
+ target.parent && parents.add(target.parent);
31601
+ return parents;
31602
+ }, new Set());
31603
+ parents.forEach(parent => {
31604
+ !selectedObjects.some(object => object.parent === parent) && parent.layoutManager.unsubscribeTargets({
31605
+ target: parent,
31606
+ targets: [activeSelection]
31607
+ });
31608
+ });
31609
+ }
31610
+ }
31611
+
31612
+ const activeSelectionDefaultValues = {
31613
+ multiSelectionStacking: 'canvas-stacking'
31614
+ };
31615
+
31616
+ /**
31617
+ * Used by Canvas to manage selection.
31618
+ *
31619
+ * @example
31620
+ * class MyActiveSelection extends ActiveSelection {
31621
+ * ...
31622
+ * }
31623
+ *
31624
+ * // override the default `ActiveSelection` class
31625
+ * classRegistry.setClass(MyActiveSelection)
31626
+ */
31627
+ class ActiveSelection extends Group {
31628
+ static getDefaults() {
31629
+ return {
31630
+ ...super.getDefaults(),
31631
+ ...ActiveSelection.ownDefaults
31632
+ };
31633
+ }
31634
+
31635
+ /**
31636
+ * The ActiveSelection needs to use the ActiveSelectionLayoutManager
31637
+ * or selections on interactive groups may be broken
31638
+ */
31639
+
31640
+ /**
31641
+ * controls how selected objects are added during a multiselection event
31642
+ * - `canvas-stacking` adds the selected object to the active selection while respecting canvas object stacking order
31643
+ * - `selection-order` adds the selected object to the top of the stack,
31644
+ * meaning that the stack is ordered by the order in which objects were selected
31645
+ * @default `canvas-stacking`
31646
+ */
31647
+
31648
+ constructor() {
31649
+ let objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
31650
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
31651
+ super();
31652
+ Object.assign(this, ActiveSelection.ownDefaults);
31653
+ this.setOptions(options);
31654
+ const {
31655
+ left,
31656
+ top,
31657
+ layoutManager
31658
+ } = options;
31659
+ this.groupInit(objects, {
31660
+ left,
31661
+ top,
31662
+ layoutManager: layoutManager !== null && layoutManager !== void 0 ? layoutManager : new ActiveSelectionLayoutManager()
31663
+ });
31664
+ }
31665
+
31666
+ /**
31667
+ * @private
31668
+ */
31669
+ _shouldSetNestedCoords() {
31670
+ return true;
31671
+ }
31672
+
31673
+ /**
31674
+ * @private
31675
+ * @override we don't want the selection monitor to be active
31676
+ */
31677
+ __objectSelectionMonitor() {
31678
+ // noop
31679
+ }
31680
+
31681
+ /**
31682
+ * Adds objects with respect to {@link multiSelectionStacking}
31683
+ * @param targets object to add to selection
31684
+ */
31685
+ multiSelectAdd() {
31686
+ for (var _len = arguments.length, targets = new Array(_len), _key = 0; _key < _len; _key++) {
31687
+ targets[_key] = arguments[_key];
31688
+ }
31689
+ if (this.multiSelectionStacking === 'selection-order') {
31690
+ this.add(...targets);
31691
+ } else {
31692
+ // respect object stacking as it is on canvas
31693
+ // perf enhancement for large ActiveSelection: consider a binary search of `isInFrontOf`
31694
+ targets.forEach(target => {
31695
+ const index = this._objects.findIndex(obj => obj.isInFrontOf(target));
31696
+ const insertAt = index === -1 ?
31697
+ // `target` is in front of all other objects
31698
+ this.size() : index;
31699
+ this.insertAt(insertAt, target);
31700
+ });
31701
+ }
31702
+ }
31703
+
31704
+ /**
31705
+ * @override block ancestors/descendants of selected objects from being selected to prevent a circular object tree
31706
+ */
31707
+ canEnterGroup(object) {
31708
+ if (this.getObjects().some(o => o.isDescendantOf(object) || object.isDescendantOf(o))) {
31709
+ // prevent circular object tree
31710
+ log('error', 'ActiveSelection: circular object trees are not supported, this call has no effect');
31711
+ return false;
31712
+ }
31713
+ return super.canEnterGroup(object);
31714
+ }
31715
+
31716
+ /**
31717
+ * Change an object so that it can be part of an active selection.
31718
+ * this method is called by multiselectAdd from canvas code.
31719
+ * @private
31720
+ * @param {FabricObject} object
31721
+ * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane
31722
+ */
31723
+ enterGroup(object, removeParentTransform) {
31724
+ // This condition check that the object has currently a group, and the group
31725
+ // is also its parent, meaning that is not in an active selection, but is
31726
+ // in a normal group.
31727
+ if (object.parent && object.parent === object.group) {
31728
+ // Disconnect the object from the group functionalities, but keep the ref parent intact
31729
+ // for later re-enter
31730
+ object.parent._exitGroup(object);
31731
+ // in this case the object is probably inside an active selection.
31732
+ } else if (object.group && object.parent !== object.group) {
31733
+ // in this case group.remove will also clear the old parent reference.
31734
+ object.group.remove(object);
31735
+ }
31736
+ // enter the active selection from a render perspective
31737
+ // the object will be in the objects array of both the ActiveSelection and the Group
31738
+ // but referenced in the group's _activeObjects so that it won't be rendered twice.
31739
+ this._enterGroup(object, removeParentTransform);
31740
+ }
31741
+
31742
+ /**
31743
+ * we want objects to retain their canvas ref when exiting instance
31744
+ * @private
31745
+ * @param {FabricObject} object
31746
+ * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it
31747
+ */
31748
+ exitGroup(object, removeParentTransform) {
31749
+ this._exitGroup(object, removeParentTransform);
31750
+ // return to parent
31751
+ object.parent && object.parent._enterGroup(object, true);
31752
+ }
31753
+
31754
+ /**
31755
+ * @private
31756
+ * @param {'added'|'removed'} type
31757
+ * @param {FabricObject[]} targets
31758
+ */
31759
+ _onAfterObjectsChange(type, targets) {
31760
+ super._onAfterObjectsChange(type, targets);
31761
+ const groups = new Set();
31762
+ targets.forEach(object => {
31763
+ const {
31764
+ parent
31765
+ } = object;
31766
+ parent && groups.add(parent);
31767
+ });
31768
+ if (type === LAYOUT_TYPE_REMOVED) {
31769
+ // invalidate groups' layout and mark as dirty
31770
+ groups.forEach(group => {
31771
+ group._onAfterObjectsChange(LAYOUT_TYPE_ADDED, targets);
31772
+ });
31773
+ } else {
31774
+ // mark groups as dirty
31775
+ groups.forEach(group => {
31776
+ group._set('dirty', true);
31777
+ });
31778
+ }
31779
+ }
31780
+
31781
+ /**
31782
+ * @override remove all objects
31783
+ */
31784
+ onDeselect() {
31785
+ this.removeAll();
31786
+ return false;
31787
+ }
31788
+
31789
+ /**
31790
+ * Returns string representation of a group
31791
+ * @return {String}
31792
+ */
31793
+ toString() {
31794
+ return `#<ActiveSelection: (${this.complexity()})>`;
31795
+ }
31796
+
31797
+ /**
31798
+ * Decide if the object should cache or not. The Active selection never caches
31799
+ * @return {Boolean}
31800
+ */
31801
+ shouldCache() {
31802
+ return false;
31803
+ }
31804
+
31805
+ /**
31806
+ * Check if this group or its parent group are caching, recursively up
31807
+ * @return {Boolean}
31808
+ */
31809
+ isOnACache() {
31810
+ return false;
31811
+ }
31812
+
31813
+ /**
31814
+ * Renders controls and borders for the object
31815
+ * @param {CanvasRenderingContext2D} ctx Context to render on
31816
+ * @param {Object} [styleOverride] properties to override the object style
31817
+ * @param {Object} [childrenOverride] properties to override the children overrides
31818
+ */
31819
+ _renderControls(ctx, styleOverride, childrenOverride) {
31820
+ ctx.save();
31821
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
31822
+ const options = {
31823
+ hasControls: false,
31824
+ ...childrenOverride,
31825
+ forActiveSelection: true
31826
+ };
31827
+ for (let i = 0; i < this._objects.length; i++) {
31828
+ this._objects[i]._renderControls(ctx, options);
31829
+ }
31830
+ super._renderControls(ctx, styleOverride);
31831
+ ctx.restore();
31832
+ }
31833
+ }
31834
+ _defineProperty(ActiveSelection, "type", 'ActiveSelection');
31835
+ _defineProperty(ActiveSelection, "ownDefaults", activeSelectionDefaultValues);
31836
+ classRegistry.setClass(ActiveSelection);
31837
+ classRegistry.setClass(ActiveSelection, 'activeSelection');
31838
+
30449
31839
  /**
30450
31840
  * Add a <g> element that envelop all child elements and makes the viewbox transformMatrix descend on all elements
30451
31841
  */
@@ -34187,6 +35577,8 @@ void main() {
34187
35577
  exports.FabricText = FabricText;
34188
35578
  exports.FitContentLayout = FitContentLayout;
34189
35579
  exports.FixedLayout = FixedLayout;
35580
+ exports.Frame = Frame;
35581
+ exports.FrameLayout = FrameLayout;
34190
35582
  exports.Gradient = Gradient;
34191
35583
  exports.Group = Group;
34192
35584
  exports.IText = IText;