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