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

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