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

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