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