@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.
- package/.history/package_20251226051014.json +164 -0
- package/.history/package_20251226164045.json +164 -0
- package/dist/fabric.d.ts +2 -0
- package/dist/fabric.d.ts.map +1 -1
- package/dist/fabric.min.mjs +1 -1
- package/dist/fabric.mjs +2 -0
- package/dist/fabric.mjs.map +1 -1
- package/dist/index.js +1760 -368
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.min.mjs.map +1 -1
- package/dist/index.mjs +1759 -369
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +1760 -368
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +1759 -369
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.min.mjs +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts +31 -0
- package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts.map +1 -0
- package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.min.mjs +2 -0
- package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.min.mjs.map +1 -0
- package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.mjs +81 -0
- package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.mjs.map +1 -0
- package/dist/src/LayoutManager/index.d.ts +1 -0
- package/dist/src/LayoutManager/index.d.ts.map +1 -1
- package/dist/src/controls/Control.d.ts.map +1 -1
- package/dist/src/controls/Control.min.mjs +1 -1
- package/dist/src/controls/Control.min.mjs.map +1 -1
- package/dist/src/controls/Control.mjs +19 -1
- package/dist/src/controls/Control.mjs.map +1 -1
- package/dist/src/controls/commonControls.d.ts.map +1 -1
- package/dist/src/controls/commonControls.min.mjs +1 -1
- package/dist/src/controls/commonControls.min.mjs.map +1 -1
- package/dist/src/controls/commonControls.mjs +25 -6
- package/dist/src/controls/commonControls.mjs.map +1 -1
- package/dist/src/controls/controlRendering.d.ts +20 -0
- package/dist/src/controls/controlRendering.d.ts.map +1 -1
- package/dist/src/controls/controlRendering.min.mjs +1 -1
- package/dist/src/controls/controlRendering.min.mjs.map +1 -1
- package/dist/src/controls/controlRendering.mjs +63 -1
- package/dist/src/controls/controlRendering.mjs.map +1 -1
- package/dist/src/shapes/Frame.d.ts +298 -0
- package/dist/src/shapes/Frame.d.ts.map +1 -0
- package/dist/src/shapes/Frame.min.mjs +2 -0
- package/dist/src/shapes/Frame.min.mjs.map +1 -0
- package/dist/src/shapes/Frame.mjs +1236 -0
- package/dist/src/shapes/Frame.mjs.map +1 -0
- package/dist/src/shapes/Object/defaultValues.d.ts.map +1 -1
- package/dist/src/shapes/Object/defaultValues.min.mjs +1 -1
- package/dist/src/shapes/Object/defaultValues.min.mjs.map +1 -1
- package/dist/src/shapes/Object/defaultValues.mjs +8 -7
- package/dist/src/shapes/Object/defaultValues.mjs.map +1 -1
- package/dist-extensions/fabric.d.ts +2 -0
- package/dist-extensions/fabric.d.ts.map +1 -1
- package/dist-extensions/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts +31 -0
- package/dist-extensions/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts.map +1 -0
- package/dist-extensions/src/LayoutManager/index.d.ts +1 -0
- package/dist-extensions/src/LayoutManager/index.d.ts.map +1 -1
- package/dist-extensions/src/controls/Control.d.ts.map +1 -1
- package/dist-extensions/src/controls/commonControls.d.ts.map +1 -1
- package/dist-extensions/src/controls/controlRendering.d.ts +20 -0
- package/dist-extensions/src/controls/controlRendering.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Frame.d.ts +298 -0
- package/dist-extensions/src/shapes/Frame.d.ts.map +1 -0
- package/dist-extensions/src/shapes/Object/defaultValues.d.ts.map +1 -1
- package/fabric.ts +8 -0
- package/package.json +1 -1
- package/src/LayoutManager/LayoutStrategies/FrameLayout.ts +80 -0
- package/src/LayoutManager/index.ts +1 -0
- package/src/controls/Control.ts +40 -1
- package/src/controls/commonControls.ts +22 -0
- package/src/controls/controlRendering.ts +83 -0
- package/src/shapes/Frame.ts +1361 -0
- 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-
|
|
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
|
-
|
|
5128
|
+
// Modern Canva-style controls
|
|
5129
|
+
cornerSize: 10,
|
|
5129
5130
|
touchCornerSize: 24,
|
|
5130
|
-
transparentCorners:
|
|
5131
|
-
cornerColor: '
|
|
5132
|
-
cornerStrokeColor: '',
|
|
5133
|
-
cornerStyle: '
|
|
5131
|
+
transparentCorners: false,
|
|
5132
|
+
cornerColor: '#ffffff',
|
|
5133
|
+
cornerStrokeColor: '#0d99ff',
|
|
5134
|
+
cornerStyle: 'circle',
|
|
5134
5135
|
cornerDashArray: null,
|
|
5135
5136
|
hasControls: true,
|
|
5136
|
-
borderColor: '
|
|
5137
|
+
borderColor: '#0d99ff',
|
|
5137
5138
|
borderDashArray: null,
|
|
5138
|
-
borderOpacityWhenMoving: 0.
|
|
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
|