fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
234 lines (233 loc) • 11.9 kB
JavaScript
import { Point, controlsUtils, util } from "fabric";
//#region extensions/cropping_controls/croppingHandlers.ts
const { wrapWithFixedAnchor, wrapWithFireEvent } = controlsUtils;
/**
* Wraps a handler to swap behavior based on flip state.
*/
const withFlip = (handler, flippedHandler, axis) => {
return (eventData, transform, x, y) => {
if (transform.target[axis]) return flippedHandler(eventData, transform, x, y);
return handler(eventData, transform, x, y);
};
};
/**
* Wraps corner handlers to swap both X and Y behavior based on flip state.
*/
const withCornerFlip = (xHandler, xFlippedHandler, yHandler, yFlippedHandler) => {
return (eventData, transform, x, y) => {
const target = transform.target;
const xResult = (target.flipX ? xFlippedHandler : xHandler)(eventData, transform, x, y);
const yResult = (target.flipY ? yFlippedHandler : yHandler)(eventData, transform, x, y);
return xResult || yResult;
};
};
/**
* Wrap controlsUtils.changeObjectWidth with image constrains
*/
const changeImageWidth = (eventData, transform, x, y) => {
const { target } = transform;
const { width } = target;
const image = target;
const modified = controlsUtils.changeObjectWidth(eventData, transform, x, y);
const availableWidth = image._element.width - image.cropX;
if (modified) {
if (image.width > availableWidth) image.width = availableWidth;
if (image.width < 1) image.width = 1;
}
return width !== image.width;
};
const changeCropWidth = wrapWithFireEvent("CROPPING", wrapWithFixedAnchor(changeImageWidth));
/**
* Wrap controlsUtils.changeObjectHeight with image constrains
*/
const changeImageHeight = (eventData, transform, x, y) => {
const { target } = transform;
const { height } = target;
const image = target;
const modified = controlsUtils.changeObjectHeight(eventData, transform, x, y);
const availableHeight = image._element.height - image.cropY;
if (modified) {
if (image.height > availableHeight) image.height = availableHeight;
if (image.height < 1) image.height = 1;
}
return height !== image.height;
};
const changeCropHeight = wrapWithFireEvent("CROPPING", wrapWithFixedAnchor(changeImageHeight));
const changeImageCropX = (eventData, transform, x, y) => {
const { target } = transform;
const image = target;
const { width, cropX } = image;
const modified = controlsUtils.changeObjectWidth(eventData, transform, x, y);
let newCropX = cropX + width - image.width;
image.width = width;
if (modified) {
if (newCropX < 0) newCropX = 0;
image.cropX = newCropX;
image.width += cropX - newCropX;
}
return newCropX !== cropX;
};
const changeImageCropY = (eventData, transform, x, y) => {
const { target } = transform;
const image = target;
const { height, cropY } = image;
const modified = controlsUtils.changeObjectHeight(eventData, transform, x, y);
let newCropY = cropY + height - image.height;
image.height = height;
if (modified) {
if (newCropY < 0) newCropY = 0;
image.cropY = newCropY;
image.height += cropY - newCropY;
}
return newCropY !== cropY;
};
const changeCropX = wrapWithFireEvent("CROPPING", wrapWithFixedAnchor(changeImageCropX));
const changeCropY = wrapWithFireEvent("CROPPING", wrapWithFixedAnchor(changeImageCropY));
/**
* A function to counter the move action and change cropX/cropY of an image
* Keep the image steady, but moves it inside its own cropping rectangle
*/
const cropPanMoveHandler = ({ transform }) => {
const { target, original } = transform;
const fabricImage = target;
const p = new Point(target.left - original.left, target.top - original.top).transform(util.invertTransform(util.createRotateMatrix({ angle: fabricImage.getTotalAngle() })));
let cropX = original.cropX - p.x / fabricImage.scaleX * (fabricImage.flipX ? -1 : 1);
let cropY = original.cropY - p.y / fabricImage.scaleY * (fabricImage.flipY ? -1 : 1);
const { width, height, _element } = fabricImage;
if (cropX < 0) cropX = 0;
if (cropY < 0) cropY = 0;
if (cropX + width > _element.width) cropX = _element.width - width;
if (cropY + height > _element.height) cropY = _element.height - height;
fabricImage.cropX = cropX;
fabricImage.cropY = cropY;
fabricImage.left = original.left;
fabricImage.top = original.top;
};
/**
* This position handler works only for this specific use case.
* It does not support padding nor offset, and it reduces all possible positions
* to the main 4 corners only.
* Any position that is < 0 is the extreme left/top, the rest are right/bottom
*/
function ghostScalePositionHandler(dim, finalMatrix, fabricObject) {
const matrix = fabricObject.calcTransformMatrix();
const vpt = fabricObject.getViewportTransform();
const _finalMatrix = util.multiplyTransformMatrices(vpt, matrix);
return new Point(this.x < 0 ? -fabricObject.width / 2 - fabricObject.cropX : fabricObject.getElement().width - fabricObject.width / 2 - fabricObject.cropX, this.y < 0 ? -fabricObject.height / 2 - fabricObject.cropY : fabricObject.getElement().height - fabricObject.height / 2 - fabricObject.cropY).transform(_finalMatrix);
}
const calcScale = (currentPoint, height, width) => Math.min(Math.abs(currentPoint.x / width), Math.abs(currentPoint.y / height));
const flipNumericOrigin = (origin, flipped) => flipped ? 1 - origin : origin;
/**
* Reflects pointer position across object center when image is flipped.
* This compensates for the inverted local coordinate system.
*/
/**
* Action handler generator that handles scaling of an image in crop mode.
* The goal is to keep the current bounding box steady.
* So this action handler has its own calculations for a dynamic anchor point
*/
const scaleEquallyCropGenerator = (cx, cy) => (eventData, transform, x, y) => {
const { target } = transform;
const { width: fullWidth, height: fullHeight } = target.getElement();
const remainderX = fullWidth - target.width - target.cropX;
const remainderY = fullHeight - target.height - target.cropY;
const anchorOriginX = flipNumericOrigin(cx < 0 ? 1 + remainderX / target.width : -target.cropX / target.width, target.flipX);
const anchorOriginY = flipNumericOrigin(cy < 0 ? 1 + remainderY / target.height : -target.cropY / target.height, target.flipY);
const constraint = target.translateToOriginPoint(target.getCenterPoint(), anchorOriginX, anchorOriginY);
const scale = calcScale(controlsUtils.getLocalPoint(transform, anchorOriginX, anchorOriginY, x, y), fullHeight, fullWidth);
const scaleChangeX = scale / target.scaleX;
const scaleChangeY = scale / target.scaleY;
const scaledRemainderX = remainderX / scaleChangeX;
const scaledRemainderY = remainderY / scaleChangeY;
const newWidth = target.width / scaleChangeX;
const newHeight = target.height / scaleChangeY;
const newCropX = cx < 0 ? fullWidth - newWidth - scaledRemainderX : target.cropX / scaleChangeX;
const newCropY = cy < 0 ? fullHeight - newHeight - scaledRemainderY : target.cropY / scaleChangeY;
const boundsFailX = (cx < 0 ? scaledRemainderX : newCropX) + newWidth > fullWidth;
const boundsFailY = (cy < 0 ? scaledRemainderY : newCropY) + newHeight > fullHeight;
if (boundsFailX || boundsFailY) return false;
target.scaleX = scale;
target.scaleY = scale;
target.width = newWidth;
target.height = newHeight;
target.cropX = newCropX;
target.cropY = newCropY;
const newAnchorOriginX = flipNumericOrigin(cx < 0 ? 1 + scaledRemainderX / newWidth : -newCropX / newWidth, target.flipX);
const newAnchorOriginY = flipNumericOrigin(cy < 0 ? 1 + scaledRemainderY / newHeight : -newCropY / newHeight, target.flipY);
target.setPositionByOrigin(constraint, newAnchorOriginX, newAnchorOriginY);
return true;
};
function renderGhostImage({ ctx }) {
const element = this._element;
const ghostX = -this.width / 2 - this.cropX;
const ghostY = -this.height / 2 - this.cropY;
const alpha = ctx.globalAlpha;
ctx.globalAlpha *= .5;
ctx.drawImage(element, ghostX, ghostY);
ctx.strokeStyle = this.borderColor;
ctx.lineWidth = this.borderScaleFactor / this.scaleX;
ctx.strokeRect(ghostX, ghostY, element.width, element.height);
ctx.globalAlpha = alpha;
}
const { capValue } = util;
/**
* Those are controls used to resize an image, similar to cropX,cropY,width,height
* But they change the scale of an image to accomodate out of bounds resizing.
* When resize comes back they scale the image back to what was before.
* The memory effect for bounce back works for the same transform.
* Once you mouseup, the bounce back is lost.
*/
const changeImageSizeWithAutoCoverGenerator = (axis) => (_eventData, transform, x, y) => {
var _original$cropX, _original$cropY, _original$cropY2, _original$cropX2;
const image = transform.target;
const original = transform.original;
const isX = axis === "x";
const isFlipped = isX ? image.flipX : image.flipY;
const elementSize = isX ? image._element.width : image._element.height;
const crossElementSize = isX ? image._element.height : image._element.width;
const isNegativeEdge = isX ? transform.originX === "right" : transform.originY === "bottom";
const initialSize = isX ? transform.width : transform.height;
const initialCrossSize = isX ? transform.height : transform.width;
const initialCrop = isX ? (_original$cropX = original.cropX) !== null && _original$cropX !== void 0 ? _original$cropX : 0 : (_original$cropY = original.cropY) !== null && _original$cropY !== void 0 ? _original$cropY : 0;
const initialCrossCrop = isX ? (_original$cropY2 = original.cropY) !== null && _original$cropY2 !== void 0 ? _original$cropY2 : 0 : (_original$cropX2 = original.cropX) !== null && _original$cropX2 !== void 0 ? _original$cropX2 : 0;
const initialScale = isX ? original.scaleX : original.scaleY;
const initialCrossScale = isX ? original.scaleY : original.scaleX;
const localPoint = controlsUtils.getLocalPoint(transform, transform.originX, transform.originY, x, y);
const coordinate = isX ? localPoint.x : localPoint.y;
const rawSize = isNegativeEdge ? -coordinate : coordinate;
const requestedSize = Math.max(10, rawSize / initialScale);
const availableSize = isNegativeEdge !== isFlipped ? initialCrop + initialSize : elementSize - initialCrop;
const setImageProps = (size, crossSize, scale, crop, crossCrop) => {
if (isX) {
image.width = size;
image.height = crossSize;
image.cropX = crop;
image.cropY = crossCrop;
} else {
image.height = size;
image.width = crossSize;
image.cropY = crop;
image.cropX = crossCrop;
}
image.scaleX = scale;
image.scaleY = scale;
};
if (requestedSize <= availableSize) {
const newCrop = isNegativeEdge !== isFlipped ? Math.max(0, initialCrop + initialSize - requestedSize) : initialCrop;
setImageProps(Math.max(1, requestedSize), initialCrossSize, initialScale, newCrop, initialCrossCrop);
} else {
const newScale = requestedSize * initialScale / availableSize;
const crossNaturalInView = initialCrossSize * initialCrossScale / newScale;
const newCrossSize = Math.min(crossNaturalInView, crossElementSize);
const newCrossCrop = capValue(initialCrossCrop + initialCrossSize / 2 - newCrossSize / 2, 0, crossElementSize - newCrossSize);
setImageProps(availableSize, newCrossSize, newScale, isNegativeEdge !== isFlipped ? 0 : initialCrop, newCrossCrop);
}
return true;
};
const changeImageWidthWithAutoCover = changeImageSizeWithAutoCoverGenerator("x");
const changeImageHeightWithAutoCover = changeImageSizeWithAutoCoverGenerator("y");
const changeWidthAndScaleToCover = wrapWithFireEvent("RESIZING", wrapWithFixedAnchor(changeImageWidthWithAutoCover));
const changeHeightAndScaleToCover = wrapWithFireEvent("RESIZING", wrapWithFixedAnchor(changeImageHeightWithAutoCover));
//#endregion
export { changeCropHeight, changeCropWidth, changeCropX, changeCropY, changeHeightAndScaleToCover, changeWidthAndScaleToCover, cropPanMoveHandler, ghostScalePositionHandler, renderGhostImage, scaleEquallyCropGenerator, withCornerFlip, withFlip };
//# sourceMappingURL=croppingHandlers.mjs.map