@nasser-sw/fabric 7.0.1-beta37 → 7.0.1-beta39
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_20251227203809.json +164 -0
- package/.history/package_20251227220608.json +164 -0
- package/dist/index.js +756 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +756 -14
- package/dist/index.mjs.map +1 -1
- package/dist/index.node.cjs +756 -14
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +756 -14
- package/dist/index.node.mjs.map +1 -1
- package/dist/package.json.mjs +1 -1
- package/dist/src/controls/imageCropControls.d.ts +40 -0
- package/dist/src/controls/imageCropControls.d.ts.map +1 -0
- package/dist/src/controls/imageCropControls.mjs +480 -0
- package/dist/src/controls/imageCropControls.mjs.map +1 -0
- package/dist/src/controls/index.d.ts +1 -0
- package/dist/src/controls/index.d.ts.map +1 -1
- package/dist/src/controls/index.mjs +1 -0
- package/dist/src/controls/index.mjs.map +1 -1
- package/dist/src/shapes/Frame.d.ts +5 -0
- package/dist/src/shapes/Frame.d.ts.map +1 -1
- package/dist/src/shapes/Frame.mjs +20 -0
- package/dist/src/shapes/Frame.mjs.map +1 -1
- package/dist/src/shapes/Image.d.ts +60 -0
- package/dist/src/shapes/Image.d.ts.map +1 -1
- package/dist/src/shapes/Image.mjs +249 -2
- package/dist/src/shapes/Image.mjs.map +1 -1
- package/dist/src/shapes/Object/InteractiveObject.d.ts.map +1 -1
- package/dist/src/shapes/Object/InteractiveObject.mjs +9 -11
- package/dist/src/shapes/Object/InteractiveObject.mjs.map +1 -1
- package/dist-extensions/src/controls/imageCropControls.d.ts +40 -0
- package/dist-extensions/src/controls/imageCropControls.d.ts.map +1 -0
- package/dist-extensions/src/controls/index.d.ts +1 -0
- package/dist-extensions/src/controls/index.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Frame.d.ts +5 -0
- package/dist-extensions/src/shapes/Frame.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Image.d.ts +60 -0
- package/dist-extensions/src/shapes/Image.d.ts.map +1 -1
- package/dist-extensions/src/shapes/Object/InteractiveObject.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/controls/imageCropControls.ts +656 -0
- package/src/controls/index.ts +1 -0
- package/src/shapes/Frame.ts +21 -0
- package/src/shapes/Image.ts +267 -2
- package/src/shapes/Object/InteractiveObject.ts +9 -15
package/dist/index.js
CHANGED
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
}
|
|
361
361
|
const cache = new Cache();
|
|
362
362
|
|
|
363
|
-
var version = "7.0.1-
|
|
363
|
+
var version = "7.0.1-beta38";
|
|
364
364
|
|
|
365
365
|
// use this syntax so babel plugin see this import here
|
|
366
366
|
const VERSION = version;
|
|
@@ -10392,21 +10392,19 @@
|
|
|
10392
10392
|
* This ensures correct behavior with zoom and grouped objects.
|
|
10393
10393
|
*/
|
|
10394
10394
|
drawExpandPreview(ctx, size, options) {
|
|
10395
|
-
var _this$canvas6, _this$canvas7;
|
|
10396
10395
|
if (!this.expandMode) return;
|
|
10397
10396
|
|
|
10398
|
-
// Use
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
// 1. Viewport zoom
|
|
10402
|
-
// 2. Parent group's scale (Frame's scale)
|
|
10403
|
-
// 3. Object's own scale
|
|
10404
|
-
const scaleX = options !== null && options !== void 0 && options.scaleX ? Math.abs(options.scaleX) : (((_this$canvas6 = this.canvas) === null || _this$canvas6 === void 0 ? void 0 : _this$canvas6.getZoom()) || 1) * Math.abs(this.scaleX || 1);
|
|
10405
|
-
const scaleY = options !== null && options !== void 0 && options.scaleY ? Math.abs(options.scaleY) : (((_this$canvas7 = this.canvas) === null || _this$canvas7 === void 0 ? void 0 : _this$canvas7.getZoom()) || 1) * Math.abs(this.scaleY || 1);
|
|
10397
|
+
// Use size but subtract borderScaleFactor and padding to match control positioning
|
|
10398
|
+
const borderOffset = this.borderScaleFactor || 1;
|
|
10399
|
+
const paddingOffset = (this.padding || 0) * 2;
|
|
10406
10400
|
const dim = {
|
|
10407
|
-
x:
|
|
10408
|
-
y:
|
|
10401
|
+
x: size.x - borderOffset - paddingOffset,
|
|
10402
|
+
y: size.y - borderOffset - paddingOffset
|
|
10409
10403
|
};
|
|
10404
|
+
|
|
10405
|
+
// Calculate scale factor from dim for expansion scaling
|
|
10406
|
+
const scaleX = dim.x / (this.width || 1);
|
|
10407
|
+
const scaleY = dim.y / (this.height || 1);
|
|
10410
10408
|
const {
|
|
10411
10409
|
expandLeft,
|
|
10412
10410
|
expandRight,
|
|
@@ -29821,6 +29819,474 @@
|
|
|
29821
29819
|
_defineProperty(Textbox, "ownDefaults", textboxDefaultValues);
|
|
29822
29820
|
classRegistry.setClass(Textbox);
|
|
29823
29821
|
|
|
29822
|
+
/**
|
|
29823
|
+
* Minimum size for cropped images (in pixels)
|
|
29824
|
+
*/
|
|
29825
|
+
const MIN_SIZE = 20;
|
|
29826
|
+
|
|
29827
|
+
/**
|
|
29828
|
+
* Get the original element dimensions for an image
|
|
29829
|
+
*/
|
|
29830
|
+
function getOriginalDimensions(target) {
|
|
29831
|
+
const element = target.getElement();
|
|
29832
|
+
if (!element) {
|
|
29833
|
+
return {
|
|
29834
|
+
width: target.width || 100,
|
|
29835
|
+
height: target.height || 100
|
|
29836
|
+
};
|
|
29837
|
+
}
|
|
29838
|
+
return {
|
|
29839
|
+
width: element.naturalWidth || element.width || target.width || 100,
|
|
29840
|
+
height: element.naturalHeight || element.height || target.height || 100
|
|
29841
|
+
};
|
|
29842
|
+
}
|
|
29843
|
+
|
|
29844
|
+
/**
|
|
29845
|
+
* Handler for resizing from RIGHT edge (mr control) - Canva style
|
|
29846
|
+
* Crops from right side, anchor on left
|
|
29847
|
+
*/
|
|
29848
|
+
const resizeFromRightHandler = (eventData, transform, x, y) => {
|
|
29849
|
+
const target = transform.target;
|
|
29850
|
+
const original = getOriginalDimensions(target);
|
|
29851
|
+
const currentScale = target.scaleX || 1;
|
|
29852
|
+
const cropX = target.cropX || 0;
|
|
29853
|
+
const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
|
|
29854
|
+
const requestedVisualWidth = Math.max(MIN_SIZE, Math.abs(localPoint.x));
|
|
29855
|
+
const maxAvailableWidth = (original.width - cropX) * currentScale;
|
|
29856
|
+
if (requestedVisualWidth <= maxAvailableWidth) {
|
|
29857
|
+
// Within bounds - just change visible width, cropX stays same (crops from right)
|
|
29858
|
+
target.width = requestedVisualWidth / currentScale;
|
|
29859
|
+
} else {
|
|
29860
|
+
// Beyond bounds - scale uniformly
|
|
29861
|
+
target.width = original.width - cropX;
|
|
29862
|
+
const newScale = requestedVisualWidth / target.width;
|
|
29863
|
+
target.scaleX = newScale;
|
|
29864
|
+
target.scaleY = newScale;
|
|
29865
|
+
const currentVisualHeight = (target.height || original.height) * currentScale;
|
|
29866
|
+
target.height = currentVisualHeight / newScale;
|
|
29867
|
+
}
|
|
29868
|
+
target.setCoords();
|
|
29869
|
+
return true;
|
|
29870
|
+
};
|
|
29871
|
+
|
|
29872
|
+
/**
|
|
29873
|
+
* Handler for resizing from LEFT edge (ml control) - Canva style
|
|
29874
|
+
* Crops from left side, anchor on right
|
|
29875
|
+
*/
|
|
29876
|
+
const resizeFromLeftHandler = (eventData, transform, x, y) => {
|
|
29877
|
+
const target = transform.target;
|
|
29878
|
+
const original = getOriginalDimensions(target);
|
|
29879
|
+
const currentScale = target.scaleX || 1;
|
|
29880
|
+
const currentCropX = target.cropX || 0;
|
|
29881
|
+
const currentWidth = target.width || original.width;
|
|
29882
|
+
const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
|
|
29883
|
+
const requestedVisualWidth = Math.max(MIN_SIZE, Math.abs(localPoint.x));
|
|
29884
|
+
|
|
29885
|
+
// Right edge position in original image coords (stays fixed)
|
|
29886
|
+
const rightEdgeInOriginal = currentCropX + currentWidth;
|
|
29887
|
+
|
|
29888
|
+
// Maximum we can expand to the left (cropX can go to 0)
|
|
29889
|
+
const maxAvailableWidth = rightEdgeInOriginal * currentScale;
|
|
29890
|
+
if (requestedVisualWidth <= maxAvailableWidth) {
|
|
29891
|
+
// Within bounds - adjust cropX and width (crops from left)
|
|
29892
|
+
const newWidthUnscaled = requestedVisualWidth / currentScale;
|
|
29893
|
+
const newCropX = rightEdgeInOriginal - newWidthUnscaled;
|
|
29894
|
+
if (newCropX >= 0) {
|
|
29895
|
+
target.cropX = newCropX;
|
|
29896
|
+
target.width = newWidthUnscaled;
|
|
29897
|
+
} else {
|
|
29898
|
+
// Hit left boundary
|
|
29899
|
+
target.cropX = 0;
|
|
29900
|
+
target.width = rightEdgeInOriginal;
|
|
29901
|
+
}
|
|
29902
|
+
} else {
|
|
29903
|
+
// Beyond bounds - scale uniformly
|
|
29904
|
+
target.cropX = 0;
|
|
29905
|
+
target.width = rightEdgeInOriginal;
|
|
29906
|
+
const newScale = requestedVisualWidth / target.width;
|
|
29907
|
+
target.scaleX = newScale;
|
|
29908
|
+
target.scaleY = newScale;
|
|
29909
|
+
const currentVisualHeight = (target.height || original.height) * currentScale;
|
|
29910
|
+
target.height = currentVisualHeight / newScale;
|
|
29911
|
+
}
|
|
29912
|
+
target.setCoords();
|
|
29913
|
+
return true;
|
|
29914
|
+
};
|
|
29915
|
+
|
|
29916
|
+
/**
|
|
29917
|
+
* Handler for cropping from the right edge (mr control) - for crop mode
|
|
29918
|
+
* - Drag inward: decrease width (crop right side)
|
|
29919
|
+
* - Drag outward: increase width until hitting boundary, then scale
|
|
29920
|
+
*/
|
|
29921
|
+
const cropFromRightHandler = (eventData, transform, x, y) => {
|
|
29922
|
+
const target = transform.target;
|
|
29923
|
+
const localPoint = getLocalPoint(transform, 'left', 'center', x, y);
|
|
29924
|
+
const original = getOriginalDimensions(target);
|
|
29925
|
+
const currentCropX = target.cropX || 0;
|
|
29926
|
+
const currentScale = target.scaleX || 1;
|
|
29927
|
+
|
|
29928
|
+
// Maximum visible width at current scale (from current cropX to right edge of original)
|
|
29929
|
+
const maxVisibleWidth = (original.width - currentCropX) * currentScale;
|
|
29930
|
+
|
|
29931
|
+
// Requested width based on mouse position
|
|
29932
|
+
const requestedWidth = Math.max(MIN_SIZE, localPoint.x);
|
|
29933
|
+
if (requestedWidth <= maxVisibleWidth) {
|
|
29934
|
+
// Within bounds - just change visible width (crop/uncrop)
|
|
29935
|
+
// Convert to unscaled width for the width property
|
|
29936
|
+
target.width = requestedWidth / currentScale;
|
|
29937
|
+
} else {
|
|
29938
|
+
// Beyond bounds - need to scale
|
|
29939
|
+
// First, set width to maximum possible
|
|
29940
|
+
target.width = original.width - currentCropX;
|
|
29941
|
+
// Calculate new scale to reach requested size
|
|
29942
|
+
const newScale = requestedWidth / target.width;
|
|
29943
|
+
target.scaleX = newScale;
|
|
29944
|
+
target.scaleY = newScale; // Uniform scaling
|
|
29945
|
+
}
|
|
29946
|
+
target.setCoords();
|
|
29947
|
+
return true;
|
|
29948
|
+
};
|
|
29949
|
+
|
|
29950
|
+
/**
|
|
29951
|
+
* Handler for resizing from BOTTOM edge (mb control) - Canva style
|
|
29952
|
+
* Crops from bottom side, anchor on top
|
|
29953
|
+
*/
|
|
29954
|
+
const resizeFromBottomHandler = (eventData, transform, x, y) => {
|
|
29955
|
+
const target = transform.target;
|
|
29956
|
+
const original = getOriginalDimensions(target);
|
|
29957
|
+
const currentScale = target.scaleY || 1;
|
|
29958
|
+
const cropY = target.cropY || 0;
|
|
29959
|
+
const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
|
|
29960
|
+
const requestedVisualHeight = Math.max(MIN_SIZE, Math.abs(localPoint.y));
|
|
29961
|
+
const maxAvailableHeight = (original.height - cropY) * currentScale;
|
|
29962
|
+
if (requestedVisualHeight <= maxAvailableHeight) {
|
|
29963
|
+
// Within bounds - just change visible height, cropY stays same (crops from bottom)
|
|
29964
|
+
target.height = requestedVisualHeight / currentScale;
|
|
29965
|
+
} else {
|
|
29966
|
+
// Beyond bounds - scale uniformly
|
|
29967
|
+
target.height = original.height - cropY;
|
|
29968
|
+
const newScale = requestedVisualHeight / target.height;
|
|
29969
|
+
target.scaleX = newScale;
|
|
29970
|
+
target.scaleY = newScale;
|
|
29971
|
+
const currentVisualWidth = (target.width || original.width) * currentScale;
|
|
29972
|
+
target.width = currentVisualWidth / newScale;
|
|
29973
|
+
}
|
|
29974
|
+
target.setCoords();
|
|
29975
|
+
return true;
|
|
29976
|
+
};
|
|
29977
|
+
|
|
29978
|
+
/**
|
|
29979
|
+
* Handler for resizing from TOP edge (mt control) - Canva style
|
|
29980
|
+
* Crops from top side, anchor on bottom
|
|
29981
|
+
*/
|
|
29982
|
+
const resizeFromTopHandler = (eventData, transform, x, y) => {
|
|
29983
|
+
const target = transform.target;
|
|
29984
|
+
const original = getOriginalDimensions(target);
|
|
29985
|
+
const currentScale = target.scaleY || 1;
|
|
29986
|
+
const currentCropY = target.cropY || 0;
|
|
29987
|
+
const currentHeight = target.height || original.height;
|
|
29988
|
+
const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
|
|
29989
|
+
const requestedVisualHeight = Math.max(MIN_SIZE, Math.abs(localPoint.y));
|
|
29990
|
+
|
|
29991
|
+
// Bottom edge position in original image coords (stays fixed)
|
|
29992
|
+
const bottomEdgeInOriginal = currentCropY + currentHeight;
|
|
29993
|
+
|
|
29994
|
+
// Maximum we can expand to the top (cropY can go to 0)
|
|
29995
|
+
const maxAvailableHeight = bottomEdgeInOriginal * currentScale;
|
|
29996
|
+
if (requestedVisualHeight <= maxAvailableHeight) {
|
|
29997
|
+
// Within bounds - adjust cropY and height (crops from top)
|
|
29998
|
+
const newHeightUnscaled = requestedVisualHeight / currentScale;
|
|
29999
|
+
const newCropY = bottomEdgeInOriginal - newHeightUnscaled;
|
|
30000
|
+
if (newCropY >= 0) {
|
|
30001
|
+
target.cropY = newCropY;
|
|
30002
|
+
target.height = newHeightUnscaled;
|
|
30003
|
+
} else {
|
|
30004
|
+
// Hit top boundary
|
|
30005
|
+
target.cropY = 0;
|
|
30006
|
+
target.height = bottomEdgeInOriginal;
|
|
30007
|
+
}
|
|
30008
|
+
} else {
|
|
30009
|
+
// Beyond bounds - scale uniformly
|
|
30010
|
+
target.cropY = 0;
|
|
30011
|
+
target.height = bottomEdgeInOriginal;
|
|
30012
|
+
const newScale = requestedVisualHeight / target.height;
|
|
30013
|
+
target.scaleX = newScale;
|
|
30014
|
+
target.scaleY = newScale;
|
|
30015
|
+
const currentVisualWidth = (target.width || original.width) * currentScale;
|
|
30016
|
+
target.width = currentVisualWidth / newScale;
|
|
30017
|
+
}
|
|
30018
|
+
target.setCoords();
|
|
30019
|
+
return true;
|
|
30020
|
+
};
|
|
30021
|
+
|
|
30022
|
+
// Wrapped resize handlers with fixed anchor
|
|
30023
|
+
const resizeFromRight = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(resizeFromRightHandler));
|
|
30024
|
+
const resizeFromLeft = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(resizeFromLeftHandler));
|
|
30025
|
+
const resizeFromBottom = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(resizeFromBottomHandler));
|
|
30026
|
+
const resizeFromTop = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(resizeFromTopHandler));
|
|
30027
|
+
|
|
30028
|
+
/**
|
|
30029
|
+
* Handler for cropping from the left edge (ml control)
|
|
30030
|
+
* - Drag inward: increase cropX, decrease width (crop left side)
|
|
30031
|
+
* - Drag outward: decrease cropX until 0, then scale
|
|
30032
|
+
*/
|
|
30033
|
+
const cropFromLeftHandler = (eventData, transform, x, y) => {
|
|
30034
|
+
const target = transform.target;
|
|
30035
|
+
const localPoint = getLocalPoint(transform, 'right', 'center', x, y);
|
|
30036
|
+
getOriginalDimensions(target);
|
|
30037
|
+
const currentCropX = target.cropX || 0;
|
|
30038
|
+
const currentWidth = target.width || 100;
|
|
30039
|
+
const currentScale = target.scaleX || 1;
|
|
30040
|
+
|
|
30041
|
+
// Requested width based on mouse position (localPoint.x is negative from right origin)
|
|
30042
|
+
const requestedWidth = Math.max(MIN_SIZE, Math.abs(localPoint.x));
|
|
30043
|
+
|
|
30044
|
+
// Current right edge position in original image coordinates
|
|
30045
|
+
const rightEdge = currentCropX + currentWidth;
|
|
30046
|
+
|
|
30047
|
+
// Maximum possible width at current scale (if cropX goes to 0)
|
|
30048
|
+
const maxVisibleWidth = rightEdge * currentScale;
|
|
30049
|
+
if (requestedWidth <= maxVisibleWidth) {
|
|
30050
|
+
// Within bounds - adjust cropX and width
|
|
30051
|
+
const newWidthUnscaled = requestedWidth / currentScale;
|
|
30052
|
+
const newCropX = rightEdge - newWidthUnscaled;
|
|
30053
|
+
if (newCropX >= 0) {
|
|
30054
|
+
target.cropX = newCropX;
|
|
30055
|
+
target.width = newWidthUnscaled;
|
|
30056
|
+
} else {
|
|
30057
|
+
// Would go past left edge - clamp cropX to 0
|
|
30058
|
+
target.cropX = 0;
|
|
30059
|
+
target.width = rightEdge;
|
|
30060
|
+
}
|
|
30061
|
+
} else {
|
|
30062
|
+
// Beyond bounds - need to scale
|
|
30063
|
+
target.cropX = 0;
|
|
30064
|
+
target.width = rightEdge;
|
|
30065
|
+
const newScale = requestedWidth / target.width;
|
|
30066
|
+
target.scaleX = newScale;
|
|
30067
|
+
target.scaleY = newScale;
|
|
30068
|
+
}
|
|
30069
|
+
target.setCoords();
|
|
30070
|
+
return true;
|
|
30071
|
+
};
|
|
30072
|
+
|
|
30073
|
+
/**
|
|
30074
|
+
* Handler for cropping from the bottom edge (mb control)
|
|
30075
|
+
*/
|
|
30076
|
+
const cropFromBottomHandler = (eventData, transform, x, y) => {
|
|
30077
|
+
const target = transform.target;
|
|
30078
|
+
const localPoint = getLocalPoint(transform, 'center', 'top', x, y);
|
|
30079
|
+
const original = getOriginalDimensions(target);
|
|
30080
|
+
const currentCropY = target.cropY || 0;
|
|
30081
|
+
const currentScale = target.scaleY || 1;
|
|
30082
|
+
const maxVisibleHeight = (original.height - currentCropY) * currentScale;
|
|
30083
|
+
const requestedHeight = Math.max(MIN_SIZE, localPoint.y);
|
|
30084
|
+
if (requestedHeight <= maxVisibleHeight) {
|
|
30085
|
+
target.height = requestedHeight / currentScale;
|
|
30086
|
+
} else {
|
|
30087
|
+
target.height = original.height - currentCropY;
|
|
30088
|
+
const newScale = requestedHeight / target.height;
|
|
30089
|
+
target.scaleX = newScale;
|
|
30090
|
+
target.scaleY = newScale;
|
|
30091
|
+
}
|
|
30092
|
+
target.setCoords();
|
|
30093
|
+
return true;
|
|
30094
|
+
};
|
|
30095
|
+
|
|
30096
|
+
/**
|
|
30097
|
+
* Handler for cropping from the top edge (mt control)
|
|
30098
|
+
*/
|
|
30099
|
+
const cropFromTopHandler = (eventData, transform, x, y) => {
|
|
30100
|
+
const target = transform.target;
|
|
30101
|
+
const localPoint = getLocalPoint(transform, 'center', 'bottom', x, y);
|
|
30102
|
+
getOriginalDimensions(target);
|
|
30103
|
+
const currentCropY = target.cropY || 0;
|
|
30104
|
+
const currentHeight = target.height || 100;
|
|
30105
|
+
const currentScale = target.scaleY || 1;
|
|
30106
|
+
const requestedHeight = Math.max(MIN_SIZE, Math.abs(localPoint.y));
|
|
30107
|
+
const bottomEdge = currentCropY + currentHeight;
|
|
30108
|
+
const maxVisibleHeight = bottomEdge * currentScale;
|
|
30109
|
+
if (requestedHeight <= maxVisibleHeight) {
|
|
30110
|
+
const newHeightUnscaled = requestedHeight / currentScale;
|
|
30111
|
+
const newCropY = bottomEdge - newHeightUnscaled;
|
|
30112
|
+
if (newCropY >= 0) {
|
|
30113
|
+
target.cropY = newCropY;
|
|
30114
|
+
target.height = newHeightUnscaled;
|
|
30115
|
+
} else {
|
|
30116
|
+
target.cropY = 0;
|
|
30117
|
+
target.height = bottomEdge;
|
|
30118
|
+
}
|
|
30119
|
+
} else {
|
|
30120
|
+
target.cropY = 0;
|
|
30121
|
+
target.height = bottomEdge;
|
|
30122
|
+
const newScale = requestedHeight / target.height;
|
|
30123
|
+
target.scaleX = newScale;
|
|
30124
|
+
target.scaleY = newScale;
|
|
30125
|
+
}
|
|
30126
|
+
target.setCoords();
|
|
30127
|
+
return true;
|
|
30128
|
+
};
|
|
30129
|
+
|
|
30130
|
+
/**
|
|
30131
|
+
* Handler for cropping from corners (tl, tr, bl, br controls)
|
|
30132
|
+
* Handles both dimensions simultaneously
|
|
30133
|
+
*/
|
|
30134
|
+
const createCornerCropHandler = (xDirection, yDirection) => {
|
|
30135
|
+
const xHandler = xDirection === 'left' ? cropFromLeftHandler : cropFromRightHandler;
|
|
30136
|
+
const yHandler = yDirection === 'top' ? cropFromTopHandler : cropFromBottomHandler;
|
|
30137
|
+
return (eventData, transform, x, y) => {
|
|
30138
|
+
// Apply both handlers
|
|
30139
|
+
const xChanged = xHandler(eventData, transform, x, y);
|
|
30140
|
+
const yChanged = yHandler(eventData, transform, x, y);
|
|
30141
|
+
return xChanged || yChanged;
|
|
30142
|
+
};
|
|
30143
|
+
};
|
|
30144
|
+
|
|
30145
|
+
// Wrapped handlers with fixed anchor and fire event
|
|
30146
|
+
const cropFromRight = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(cropFromRightHandler));
|
|
30147
|
+
const cropFromLeft = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(cropFromLeftHandler));
|
|
30148
|
+
const cropFromBottom = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(cropFromBottomHandler));
|
|
30149
|
+
const cropFromTop = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(cropFromTopHandler));
|
|
30150
|
+
const cropFromTopLeft = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(createCornerCropHandler('left', 'top')));
|
|
30151
|
+
const cropFromTopRight = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(createCornerCropHandler('right', 'top')));
|
|
30152
|
+
const cropFromBottomLeft = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(createCornerCropHandler('left', 'bottom')));
|
|
30153
|
+
const cropFromBottomRight = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(createCornerCropHandler('right', 'bottom')));
|
|
30154
|
+
|
|
30155
|
+
/**
|
|
30156
|
+
* Creates Canva-like controls for FabricImage
|
|
30157
|
+
* - Side handles crop/resize visible area (Canva style)
|
|
30158
|
+
* - Corner handles scale uniformly
|
|
30159
|
+
* - Double-click enters crop mode for fine-tuning
|
|
30160
|
+
*/
|
|
30161
|
+
const createImageCropControls = () => ({
|
|
30162
|
+
// Side controls - crop from each side
|
|
30163
|
+
ml: new Control({
|
|
30164
|
+
x: -0.5,
|
|
30165
|
+
y: 0,
|
|
30166
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30167
|
+
actionHandler: resizeFromLeft,
|
|
30168
|
+
actionName: RESIZING,
|
|
30169
|
+
render: renderHorizontalPillControl,
|
|
30170
|
+
sizeX: 6,
|
|
30171
|
+
sizeY: 20
|
|
30172
|
+
}),
|
|
30173
|
+
mr: new Control({
|
|
30174
|
+
x: 0.5,
|
|
30175
|
+
y: 0,
|
|
30176
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30177
|
+
actionHandler: resizeFromRight,
|
|
30178
|
+
actionName: RESIZING,
|
|
30179
|
+
render: renderHorizontalPillControl,
|
|
30180
|
+
sizeX: 6,
|
|
30181
|
+
sizeY: 20
|
|
30182
|
+
}),
|
|
30183
|
+
mb: new Control({
|
|
30184
|
+
x: 0,
|
|
30185
|
+
y: 0.5,
|
|
30186
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30187
|
+
actionHandler: resizeFromBottom,
|
|
30188
|
+
actionName: RESIZING,
|
|
30189
|
+
render: renderVerticalPillControl,
|
|
30190
|
+
sizeX: 20,
|
|
30191
|
+
sizeY: 6
|
|
30192
|
+
}),
|
|
30193
|
+
mt: new Control({
|
|
30194
|
+
x: 0,
|
|
30195
|
+
y: -0.5,
|
|
30196
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30197
|
+
actionHandler: resizeFromTop,
|
|
30198
|
+
actionName: RESIZING,
|
|
30199
|
+
render: renderVerticalPillControl,
|
|
30200
|
+
sizeX: 20,
|
|
30201
|
+
sizeY: 6
|
|
30202
|
+
}),
|
|
30203
|
+
// Corner controls - uniform scaling (like Canva)
|
|
30204
|
+
tl: new Control({
|
|
30205
|
+
x: -0.5,
|
|
30206
|
+
y: -0.5,
|
|
30207
|
+
cursorStyleHandler: scaleCursorStyleHandler,
|
|
30208
|
+
actionHandler: scalingEqually
|
|
30209
|
+
}),
|
|
30210
|
+
tr: new Control({
|
|
30211
|
+
x: 0.5,
|
|
30212
|
+
y: -0.5,
|
|
30213
|
+
cursorStyleHandler: scaleCursorStyleHandler,
|
|
30214
|
+
actionHandler: scalingEqually
|
|
30215
|
+
}),
|
|
30216
|
+
bl: new Control({
|
|
30217
|
+
x: -0.5,
|
|
30218
|
+
y: 0.5,
|
|
30219
|
+
cursorStyleHandler: scaleCursorStyleHandler,
|
|
30220
|
+
actionHandler: scalingEqually
|
|
30221
|
+
}),
|
|
30222
|
+
br: new Control({
|
|
30223
|
+
x: 0.5,
|
|
30224
|
+
y: 0.5,
|
|
30225
|
+
cursorStyleHandler: scaleCursorStyleHandler,
|
|
30226
|
+
actionHandler: scalingEqually
|
|
30227
|
+
}),
|
|
30228
|
+
mtr: new Control({
|
|
30229
|
+
x: 0,
|
|
30230
|
+
y: -0.5,
|
|
30231
|
+
actionHandler: rotationWithSnapping,
|
|
30232
|
+
cursorStyleHandler: rotationStyleHandler,
|
|
30233
|
+
offsetY: -40,
|
|
30234
|
+
withConnection: true,
|
|
30235
|
+
actionName: ROTATE
|
|
30236
|
+
})
|
|
30237
|
+
});
|
|
30238
|
+
|
|
30239
|
+
/**
|
|
30240
|
+
* Creates crop mode controls for FabricImage (used in crop mode after double-click)
|
|
30241
|
+
* - Side handles crop/uncrop single axis
|
|
30242
|
+
* - Corner handles crop/uncrop both axes
|
|
30243
|
+
*/
|
|
30244
|
+
const createImageCropModeControls = () => ({
|
|
30245
|
+
ml: new Control({
|
|
30246
|
+
x: -0.5,
|
|
30247
|
+
y: 0,
|
|
30248
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30249
|
+
actionHandler: cropFromLeft,
|
|
30250
|
+
actionName: RESIZING,
|
|
30251
|
+
render: renderHorizontalPillControl,
|
|
30252
|
+
sizeX: 6,
|
|
30253
|
+
sizeY: 20
|
|
30254
|
+
}),
|
|
30255
|
+
mr: new Control({
|
|
30256
|
+
x: 0.5,
|
|
30257
|
+
y: 0,
|
|
30258
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30259
|
+
actionHandler: cropFromRight,
|
|
30260
|
+
actionName: RESIZING,
|
|
30261
|
+
render: renderHorizontalPillControl,
|
|
30262
|
+
sizeX: 6,
|
|
30263
|
+
sizeY: 20
|
|
30264
|
+
}),
|
|
30265
|
+
mb: new Control({
|
|
30266
|
+
x: 0,
|
|
30267
|
+
y: 0.5,
|
|
30268
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30269
|
+
actionHandler: cropFromBottom,
|
|
30270
|
+
actionName: RESIZING,
|
|
30271
|
+
render: renderVerticalPillControl,
|
|
30272
|
+
sizeX: 20,
|
|
30273
|
+
sizeY: 6
|
|
30274
|
+
}),
|
|
30275
|
+
mt: new Control({
|
|
30276
|
+
x: 0,
|
|
30277
|
+
y: -0.5,
|
|
30278
|
+
cursorStyleHandler: scaleSkewCursorStyleHandler,
|
|
30279
|
+
actionHandler: cropFromTop,
|
|
30280
|
+
actionName: RESIZING,
|
|
30281
|
+
render: renderVerticalPillControl,
|
|
30282
|
+
sizeX: 20,
|
|
30283
|
+
sizeY: 6
|
|
30284
|
+
})
|
|
30285
|
+
|
|
30286
|
+
// No corner controls in crop mode - or could add corner crop handlers
|
|
30287
|
+
// No rotation in crop mode
|
|
30288
|
+
});
|
|
30289
|
+
|
|
29824
30290
|
/**
|
|
29825
30291
|
* Canvas 2D filter backend.
|
|
29826
30292
|
*/
|
|
@@ -30231,7 +30697,8 @@
|
|
|
30231
30697
|
minimumScaleTrigger: 0.5,
|
|
30232
30698
|
cropX: 0,
|
|
30233
30699
|
cropY: 0,
|
|
30234
|
-
imageSmoothing: true
|
|
30700
|
+
imageSmoothing: true,
|
|
30701
|
+
cropMode: false
|
|
30235
30702
|
};
|
|
30236
30703
|
const IMAGE_PROPS = ['cropX', 'cropY'];
|
|
30237
30704
|
|
|
@@ -30245,6 +30712,154 @@
|
|
|
30245
30712
|
...FabricImage.ownDefaults
|
|
30246
30713
|
};
|
|
30247
30714
|
}
|
|
30715
|
+
|
|
30716
|
+
/**
|
|
30717
|
+
* Creates Canva-like controls for images
|
|
30718
|
+
* - All handles scale uniformly
|
|
30719
|
+
* - Double-click to enter crop mode
|
|
30720
|
+
*/
|
|
30721
|
+
static createControls() {
|
|
30722
|
+
return {
|
|
30723
|
+
controls: createImageCropControls()
|
|
30724
|
+
};
|
|
30725
|
+
}
|
|
30726
|
+
|
|
30727
|
+
/**
|
|
30728
|
+
* Enter crop mode - switches to crop controls
|
|
30729
|
+
* Call this on double-click
|
|
30730
|
+
*/
|
|
30731
|
+
enterCropMode() {
|
|
30732
|
+
var _this$canvas;
|
|
30733
|
+
if (this.cropMode) return;
|
|
30734
|
+
this.cropMode = true;
|
|
30735
|
+
// Backup current controls
|
|
30736
|
+
this._normalControls = {
|
|
30737
|
+
...this.controls
|
|
30738
|
+
};
|
|
30739
|
+
// Switch to crop mode controls
|
|
30740
|
+
this.controls = createImageCropModeControls();
|
|
30741
|
+
// Dirty cache to force re-render with full image visible
|
|
30742
|
+
this.dirty = true;
|
|
30743
|
+
// Reset drag state
|
|
30744
|
+
this._cropModeDragActive = false;
|
|
30745
|
+
this.setCoords();
|
|
30746
|
+
(_this$canvas = this.canvas) === null || _this$canvas === void 0 || _this$canvas.requestRenderAll();
|
|
30747
|
+
}
|
|
30748
|
+
|
|
30749
|
+
/**
|
|
30750
|
+
* Exit crop mode - restores normal controls
|
|
30751
|
+
* Call this on click outside or escape
|
|
30752
|
+
*/
|
|
30753
|
+
exitCropMode() {
|
|
30754
|
+
var _this$canvas2;
|
|
30755
|
+
if (!this.cropMode) return;
|
|
30756
|
+
this.cropMode = false;
|
|
30757
|
+
// Restore normal controls
|
|
30758
|
+
if (this._normalControls) {
|
|
30759
|
+
this.controls = this._normalControls;
|
|
30760
|
+
this._normalControls = undefined;
|
|
30761
|
+
} else {
|
|
30762
|
+
this.controls = createImageCropControls();
|
|
30763
|
+
}
|
|
30764
|
+
// Dirty cache to force re-render with cropped image only
|
|
30765
|
+
this.dirty = true;
|
|
30766
|
+
this.setCoords();
|
|
30767
|
+
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.requestRenderAll();
|
|
30768
|
+
}
|
|
30769
|
+
|
|
30770
|
+
/**
|
|
30771
|
+
* Toggle crop mode
|
|
30772
|
+
*/
|
|
30773
|
+
toggleCropMode() {
|
|
30774
|
+
if (this.cropMode) {
|
|
30775
|
+
this.exitCropMode();
|
|
30776
|
+
} else {
|
|
30777
|
+
this.enterCropMode();
|
|
30778
|
+
}
|
|
30779
|
+
}
|
|
30780
|
+
|
|
30781
|
+
/**
|
|
30782
|
+
* Override set to intercept movement in crop mode
|
|
30783
|
+
* In crop mode, dragging adjusts cropX/cropY instead of left/top
|
|
30784
|
+
*/
|
|
30785
|
+
// @ts-ignore - override set with different signature for crop mode handling
|
|
30786
|
+
set(key, value) {
|
|
30787
|
+
// Only intercept in crop mode when actually dragging (isMoving is true)
|
|
30788
|
+
if (this.cropMode && this.isMoving && typeof key === 'string') {
|
|
30789
|
+
if (key === 'left' && typeof value === 'number') {
|
|
30790
|
+
return this._setCropModePosition('left', value);
|
|
30791
|
+
}
|
|
30792
|
+
if (key === 'top' && typeof value === 'number') {
|
|
30793
|
+
return this._setCropModePosition('top', value);
|
|
30794
|
+
}
|
|
30795
|
+
}
|
|
30796
|
+
return super.set(key, value);
|
|
30797
|
+
}
|
|
30798
|
+
|
|
30799
|
+
/**
|
|
30800
|
+
* Handle position changes in crop mode - converts to cropX/cropY changes
|
|
30801
|
+
* @private
|
|
30802
|
+
*/
|
|
30803
|
+
_setCropModePosition(axis, newPos) {
|
|
30804
|
+
const element = this._element;
|
|
30805
|
+
if (!element) {
|
|
30806
|
+
return super.set(axis, newPos);
|
|
30807
|
+
}
|
|
30808
|
+
|
|
30809
|
+
// Capture baseline values at the start of each new drag
|
|
30810
|
+
if (!this._cropModeDragActive) {
|
|
30811
|
+
this._cropModeDragActive = true;
|
|
30812
|
+
this._cropModeOriginalLeft = this.left;
|
|
30813
|
+
this._cropModeOriginalTop = this.top;
|
|
30814
|
+
this._cropModeOriginalCropX = this.cropX;
|
|
30815
|
+
this._cropModeOriginalCropY = this.cropY;
|
|
30816
|
+
}
|
|
30817
|
+
const scale = axis === 'left' ? this.scaleX || 1 : this.scaleY || 1;
|
|
30818
|
+
const basePos = axis === 'left' ? this._cropModeOriginalLeft : this._cropModeOriginalTop;
|
|
30819
|
+
const baseCrop = axis === 'left' ? this._cropModeOriginalCropX : this._cropModeOriginalCropY;
|
|
30820
|
+
const cropProp = axis === 'left' ? 'cropX' : 'cropY';
|
|
30821
|
+
const sizeProp = axis === 'left' ? 'width' : 'height';
|
|
30822
|
+
const elSize = axis === 'left' ? element.naturalWidth || element.width : element.naturalHeight || element.height;
|
|
30823
|
+
if (basePos === undefined || baseCrop === undefined) {
|
|
30824
|
+
return super.set(axis, newPos);
|
|
30825
|
+
}
|
|
30826
|
+
|
|
30827
|
+
// Calculate total delta from drag start position
|
|
30828
|
+
const totalDelta = newPos - basePos;
|
|
30829
|
+
|
|
30830
|
+
// Convert screen delta to source image pixels
|
|
30831
|
+
// Dragging right (positive delta) should move the visible content right
|
|
30832
|
+
// This means showing content from further LEFT in the source = cropX decreases
|
|
30833
|
+
// So we negate the delta. Use Math.abs(scale) to handle flipped images.
|
|
30834
|
+
const cropOffset = -totalDelta / Math.abs(scale);
|
|
30835
|
+
|
|
30836
|
+
// Calculate new crop value from baseline crop + offset
|
|
30837
|
+
const currentSize = this[sizeProp] || elSize;
|
|
30838
|
+
let newCrop = baseCrop + cropOffset;
|
|
30839
|
+
|
|
30840
|
+
// Clamp to valid range: 0 to (elSize - visible size)
|
|
30841
|
+
const maxCrop = Math.max(0, elSize - currentSize);
|
|
30842
|
+
newCrop = Math.max(0, Math.min(maxCrop, newCrop));
|
|
30843
|
+
|
|
30844
|
+
// Update crop value
|
|
30845
|
+
super.set(cropProp, newCrop);
|
|
30846
|
+
// Keep position fixed at baseline
|
|
30847
|
+
super.set(axis, basePos);
|
|
30848
|
+
// Mark as dirty for re-render
|
|
30849
|
+
this.dirty = true;
|
|
30850
|
+
return this;
|
|
30851
|
+
}
|
|
30852
|
+
|
|
30853
|
+
/**
|
|
30854
|
+
* Reset crop mode drag state when drag ends
|
|
30855
|
+
* Called by canvas on mouse up
|
|
30856
|
+
*/
|
|
30857
|
+
_onMouseUp() {
|
|
30858
|
+
if (this.cropMode) {
|
|
30859
|
+
this._cropModeDragActive = false;
|
|
30860
|
+
}
|
|
30861
|
+
}
|
|
30862
|
+
|
|
30248
30863
|
/**
|
|
30249
30864
|
* Constructor
|
|
30250
30865
|
* Image can be initialized with any canvas drawable or a string.
|
|
@@ -30290,6 +30905,19 @@
|
|
|
30290
30905
|
* @type Number
|
|
30291
30906
|
*/
|
|
30292
30907
|
_defineProperty(this, "_filterScalingY", 1);
|
|
30908
|
+
/**
|
|
30909
|
+
* Backup of normal controls when entering crop mode
|
|
30910
|
+
*/
|
|
30911
|
+
_defineProperty(this, "_normalControls", void 0);
|
|
30912
|
+
/**
|
|
30913
|
+
* Original position and crop values for drag-to-reposition in crop mode
|
|
30914
|
+
* Updated at the start of each drag
|
|
30915
|
+
*/
|
|
30916
|
+
_defineProperty(this, "_cropModeOriginalLeft", void 0);
|
|
30917
|
+
_defineProperty(this, "_cropModeOriginalTop", void 0);
|
|
30918
|
+
_defineProperty(this, "_cropModeOriginalCropX", void 0);
|
|
30919
|
+
_defineProperty(this, "_cropModeOriginalCropY", void 0);
|
|
30920
|
+
_defineProperty(this, "_cropModeDragActive", void 0);
|
|
30293
30921
|
this.filters = [];
|
|
30294
30922
|
Object.assign(this, FabricImage.ownDefaults);
|
|
30295
30923
|
this.setOptions(options);
|
|
@@ -30648,6 +31276,10 @@
|
|
|
30648
31276
|
* @return {Boolean}
|
|
30649
31277
|
*/
|
|
30650
31278
|
shouldCache() {
|
|
31279
|
+
// Don't cache in crop mode - we need to render the full image
|
|
31280
|
+
if (this.cropMode) {
|
|
31281
|
+
return false;
|
|
31282
|
+
}
|
|
30651
31283
|
return this.needsItsOwnCache();
|
|
30652
31284
|
}
|
|
30653
31285
|
_renderFill(ctx) {
|
|
@@ -30673,7 +31305,87 @@
|
|
|
30673
31305
|
y = -h / 2,
|
|
30674
31306
|
maxDestW = Math.min(w, elWidth / scaleX - cropX),
|
|
30675
31307
|
maxDestH = Math.min(h, elHeight / scaleY - cropY);
|
|
30676
|
-
|
|
31308
|
+
if (this.cropMode) {
|
|
31309
|
+
// In crop mode: show full image with crop area highlighted
|
|
31310
|
+
this._renderCropMode(ctx, elementToDraw, elWidth, elHeight, scaleX, scaleY);
|
|
31311
|
+
} else {
|
|
31312
|
+
// Normal mode: just draw the cropped portion
|
|
31313
|
+
elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH);
|
|
31314
|
+
}
|
|
31315
|
+
}
|
|
31316
|
+
|
|
31317
|
+
/**
|
|
31318
|
+
* Render the image in crop mode - shows full image dimmed with crop area highlighted
|
|
31319
|
+
* @private
|
|
31320
|
+
*/
|
|
31321
|
+
_renderCropMode(ctx, elementToDraw, elWidth, elHeight, scaleX, scaleY) {
|
|
31322
|
+
const w = this.width,
|
|
31323
|
+
h = this.height,
|
|
31324
|
+
cropX = Math.max(this.cropX, 0),
|
|
31325
|
+
cropY = Math.max(this.cropY, 0);
|
|
31326
|
+
|
|
31327
|
+
// Calculate full image dimensions at current scale
|
|
31328
|
+
const fullW = elWidth / scaleX;
|
|
31329
|
+
const fullH = elHeight / scaleY;
|
|
31330
|
+
|
|
31331
|
+
// Position of the full image (crop area is centered at 0,0)
|
|
31332
|
+
// The crop window starts at (cropX, cropY) in the original image
|
|
31333
|
+
// We want the crop window to be at (-w/2, -h/2) to (w/2, h/2)
|
|
31334
|
+
// So the full image starts at (-w/2 - cropX, -h/2 - cropY)
|
|
31335
|
+
const fullX = -w / 2 - cropX;
|
|
31336
|
+
const fullY = -h / 2 - cropY;
|
|
31337
|
+
|
|
31338
|
+
// Draw the FULL image dimmed (outside crop area)
|
|
31339
|
+
ctx.save();
|
|
31340
|
+
ctx.globalAlpha = 0.3;
|
|
31341
|
+
ctx.drawImage(elementToDraw, 0, 0, elWidth, elHeight,
|
|
31342
|
+
// source: full image
|
|
31343
|
+
fullX, fullY, fullW, fullH // dest: positioned so crop area is centered
|
|
31344
|
+
);
|
|
31345
|
+
ctx.restore();
|
|
31346
|
+
|
|
31347
|
+
// Draw dark overlay on the dimmed parts (outside crop area)
|
|
31348
|
+
ctx.save();
|
|
31349
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
31350
|
+
// Left side
|
|
31351
|
+
if (cropX > 0) {
|
|
31352
|
+
ctx.fillRect(fullX, fullY, cropX, fullH);
|
|
31353
|
+
}
|
|
31354
|
+
// Right side
|
|
31355
|
+
const rightStart = -w / 2 + w;
|
|
31356
|
+
const rightWidth = fullW - cropX - w;
|
|
31357
|
+
if (rightWidth > 0) {
|
|
31358
|
+
ctx.fillRect(rightStart, fullY, rightWidth, fullH);
|
|
31359
|
+
}
|
|
31360
|
+
// Top side (between left and right)
|
|
31361
|
+
if (cropY > 0) {
|
|
31362
|
+
ctx.fillRect(-w / 2, fullY, w, cropY);
|
|
31363
|
+
}
|
|
31364
|
+
// Bottom side (between left and right)
|
|
31365
|
+
const bottomStart = -h / 2 + h;
|
|
31366
|
+
const bottomHeight = fullH - cropY - h;
|
|
31367
|
+
if (bottomHeight > 0) {
|
|
31368
|
+
ctx.fillRect(-w / 2, bottomStart, w, bottomHeight);
|
|
31369
|
+
}
|
|
31370
|
+
ctx.restore();
|
|
31371
|
+
|
|
31372
|
+
// Draw the crop area at FULL opacity
|
|
31373
|
+
const sX = cropX * scaleX,
|
|
31374
|
+
sY = cropY * scaleY,
|
|
31375
|
+
sW = Math.min(w * scaleX, elWidth - sX),
|
|
31376
|
+
sH = Math.min(h * scaleY, elHeight - sY),
|
|
31377
|
+
x = -w / 2,
|
|
31378
|
+
y = -h / 2,
|
|
31379
|
+
maxDestW = Math.min(w, elWidth / scaleX - cropX),
|
|
31380
|
+
maxDestH = Math.min(h, elHeight / scaleY - cropY);
|
|
31381
|
+
ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH);
|
|
31382
|
+
|
|
31383
|
+
// Draw a border around the crop area
|
|
31384
|
+
ctx.save();
|
|
31385
|
+
ctx.strokeStyle = '#fff';
|
|
31386
|
+
ctx.lineWidth = 2;
|
|
31387
|
+
ctx.strokeRect(-w / 2, -h / 2, w, h);
|
|
31388
|
+
ctx.restore();
|
|
30677
31389
|
}
|
|
30678
31390
|
|
|
30679
31391
|
/**
|
|
@@ -31032,6 +31744,11 @@
|
|
|
31032
31744
|
* @private
|
|
31033
31745
|
*/
|
|
31034
31746
|
_defineProperty(this, "_editModeObjectCaching", void 0);
|
|
31747
|
+
/**
|
|
31748
|
+
* Stored original controls of content image before edit mode
|
|
31749
|
+
* @private
|
|
31750
|
+
*/
|
|
31751
|
+
_defineProperty(this, "_editModeOriginalControls", void 0);
|
|
31035
31752
|
/**
|
|
31036
31753
|
* Bound constraint handler references for cleanup
|
|
31037
31754
|
* @private
|
|
@@ -31654,6 +32371,15 @@
|
|
|
31654
32371
|
});
|
|
31655
32372
|
this._contentImage.dirty = true;
|
|
31656
32373
|
|
|
32374
|
+
// Save original controls and replace with corner-only controls for scaling
|
|
32375
|
+
this._editModeOriginalControls = this._contentImage.controls;
|
|
32376
|
+
this._contentImage.controls = {
|
|
32377
|
+
tl: this._contentImage.controls.tl,
|
|
32378
|
+
tr: this._contentImage.controls.tr,
|
|
32379
|
+
bl: this._contentImage.controls.bl,
|
|
32380
|
+
br: this._contentImage.controls.br
|
|
32381
|
+
};
|
|
32382
|
+
|
|
31657
32383
|
// Store and remove clipPath to show full image
|
|
31658
32384
|
// We must actually remove it (not just skip rendering) because Fabric's
|
|
31659
32385
|
// rendering pipeline checks clipPath existence in multiple places
|
|
@@ -31951,6 +32677,12 @@
|
|
|
31951
32677
|
contentScaleY: contentScaleY
|
|
31952
32678
|
};
|
|
31953
32679
|
|
|
32680
|
+
// Restore original controls before making non-interactive
|
|
32681
|
+
if (this._editModeOriginalControls) {
|
|
32682
|
+
this._contentImage.controls = this._editModeOriginalControls;
|
|
32683
|
+
this._editModeOriginalControls = undefined;
|
|
32684
|
+
}
|
|
32685
|
+
|
|
31954
32686
|
// Make content non-interactive again and restore caching
|
|
31955
32687
|
this._contentImage.set({
|
|
31956
32688
|
selectable: false,
|
|
@@ -33504,6 +34236,8 @@
|
|
|
33504
34236
|
changeWidth: changeWidth,
|
|
33505
34237
|
createDefaultExpansion: createDefaultExpansion,
|
|
33506
34238
|
createExpandControls: createExpandControls,
|
|
34239
|
+
createImageCropControls: createImageCropControls,
|
|
34240
|
+
createImageCropModeControls: createImageCropModeControls,
|
|
33507
34241
|
createObjectDefaultControls: createObjectDefaultControls,
|
|
33508
34242
|
createPathControls: createPathControls,
|
|
33509
34243
|
createPolyActionHandler: createPolyActionHandler,
|
|
@@ -33511,6 +34245,14 @@
|
|
|
33511
34245
|
createPolyPositionHandler: createPolyPositionHandler,
|
|
33512
34246
|
createResizeControls: createResizeControls,
|
|
33513
34247
|
createTextboxDefaultControls: createTextboxDefaultControls,
|
|
34248
|
+
cropFromBottom: cropFromBottom,
|
|
34249
|
+
cropFromBottomLeft: cropFromBottomLeft,
|
|
34250
|
+
cropFromBottomRight: cropFromBottomRight,
|
|
34251
|
+
cropFromLeft: cropFromLeft,
|
|
34252
|
+
cropFromRight: cropFromRight,
|
|
34253
|
+
cropFromTop: cropFromTop,
|
|
34254
|
+
cropFromTopLeft: cropFromTopLeft,
|
|
34255
|
+
cropFromTopRight: cropFromTopRight,
|
|
33514
34256
|
dragHandler: dragHandler,
|
|
33515
34257
|
expandBottomHandler: expandBottomHandler,
|
|
33516
34258
|
expandLeftHandler: expandLeftHandler,
|