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

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