UNPKG

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
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