From 3a01122093e73c5b4d7ec161ca77cc2d51fba7d1 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Fri, 4 Oct 2024 14:27:13 +0800 Subject: [PATCH] make cropping work with flipping --- packages/excalidraw/components/App.tsx | 6 +- packages/excalidraw/element/cropElement.ts | 117 ++++++++++++++------- packages/excalidraw/element/types.ts | 14 +-- 3 files changed, 90 insertions(+), 47 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index aed95b1680..530d558023 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -7842,12 +7842,14 @@ class App extends React.Component { const nextCrop = { ...crop, x: clamp( - crop.x - instantDragOffset.x, + crop.x - + instantDragOffset.x * Math.sign(croppingElement.scale[0]), 0, image.naturalWidth - crop.width, ), y: clamp( - crop.y - instantDragOffset.y, + crop.y - + instantDragOffset.y * Math.sign(croppingElement.scale[1]), 0, image.naturalHeight - crop.height, ), diff --git a/packages/excalidraw/element/cropElement.ts b/packages/excalidraw/element/cropElement.ts index ee60721f1c..2913e80fae 100644 --- a/packages/excalidraw/element/cropElement.ts +++ b/packages/excalidraw/element/cropElement.ts @@ -19,6 +19,7 @@ import type { ElementsMap, ExcalidrawElement, ExcalidrawImageElement, + ImageCrop, NonDeleted, NonDeletedSceneElementsMap, } from "./types"; @@ -62,9 +63,6 @@ const _cropElement = ( * *––––––––––––––––––––––––* */ - const availableTopCropSpace = croppedTop; - const availableLeftCropSpace = croppedLeft; - const rotatedPointer = pointRotateRads( point(pointerX, pointerY), point(element.x + element.width / 2, element.y + element.height / 2), @@ -83,50 +81,63 @@ const _cropElement = ( height: naturalHeight, }; - if (transformHandle.includes("n")) { - const northBound = element.y - availableTopCropSpace; - const southBound = element.y + element.height; + const previousCropHeight = crop.height; + const previousCropWidth = crop.width; - pointerY = clamp(pointerY, northBound, southBound); + const isFlippedByX = element.scale[0] === -1; + const isFlippedByY = element.scale[1] === -1; + if (transformHandle.includes("n")) { const pointerDeltaY = pointerY - element.y; - nextHeight = Math.max(element.height - pointerDeltaY, 1); - - crop.y = ((pointerDeltaY + croppedTop) / uncroppedHeight) * naturalHeight; + nextHeight = clamp( + element.height - pointerDeltaY, + 1, + isFlippedByY ? uncroppedHeight - croppedTop : element.height + croppedTop, + ); crop.height = (nextHeight / uncroppedHeight) * naturalHeight; - } - - if (transformHandle.includes("s")) { - const northBound = element.y; - const southBound = element.y + (uncroppedHeight - croppedTop); - - pointerY = clamp(pointerY, northBound, southBound); - nextHeight = Math.max(pointerY - element.y, 1); + if (!isFlippedByY) { + crop.y = crop.y + (previousCropHeight - crop.height); + } + } else if (transformHandle.includes("s")) { + nextHeight = clamp( + pointerY - element.y, + 1, + isFlippedByY ? element.height + croppedTop : uncroppedHeight - croppedTop, + ); crop.height = (nextHeight / uncroppedHeight) * naturalHeight; + + if (isFlippedByY) { + const changeInCropHeight = previousCropHeight - crop.height; + crop.y += changeInCropHeight; + } } if (transformHandle.includes("w")) { - const eastBound = element.x + element.width; - const westBound = element.x - availableLeftCropSpace; - - pointerX = clamp(pointerX, westBound, eastBound); - const pointerDeltaX = pointerX - element.x; - nextWidth = Math.max(element.width - pointerDeltaX, 1); - crop.x = ((pointerDeltaX + croppedLeft) / uncroppedWidth) * naturalWidth; - crop.width = (nextWidth / uncroppedWidth) * naturalWidth; - } - - if (transformHandle.includes("e")) { - const eastBound = element.x + (uncroppedWidth - croppedLeft); - const westBound = element.x; - - pointerX = clamp(pointerX, westBound, eastBound); + nextWidth = clamp( + element.width - pointerDeltaX, + 1, + isFlippedByX ? uncroppedWidth - croppedLeft : element.width + croppedLeft, + ); - nextWidth = Math.max(pointerX - element.x, 1); crop.width = (nextWidth / uncroppedWidth) * naturalWidth; + + if (!isFlippedByX) { + crop.x += previousCropWidth - crop.width; + } + } else if (transformHandle.includes("e")) { + nextWidth = clamp( + pointerX - element.x, + 1, + isFlippedByX ? element.width + croppedLeft : uncroppedWidth - croppedLeft, + ); + crop.width = nextWidth * naturalWidthToUncropped; + if (isFlippedByX) { + const changeInCropWidth = previousCropWidth - crop.width; + crop.x += changeInCropWidth; + } } const newOrigin = recomputeOrigin( @@ -270,17 +281,20 @@ export const getUncroppedImageElement = ( const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector); const leftEdgeNormalized = vectorNormalize(leftEdgeVector); + const { cropX, cropY } = adjustCropPosition( + element.crop, + element.scale, + image, + ); + const rotatedTopLeft = vectorAdd( vectorAdd( topLeftVector, - vectorScale( - topEdgeNormalized, - (-element.crop.x * width) / image.naturalWidth, - ), + vectorScale(topEdgeNormalized, (-cropX * width) / image.naturalWidth), ), vectorScale( leftEdgeNormalized, - (-element.crop.y * height) / image.naturalHeight, + (-cropY * height) / image.naturalHeight, ), ); @@ -312,3 +326,28 @@ export const getUncroppedImageElement = ( return element; }; + +const adjustCropPosition = ( + crop: ImageCrop, + scale: ExcalidrawImageElement["scale"], + image: HTMLImageElement, +) => { + let cropX = crop.x; + let cropY = crop.y; + + const flipX = scale[0] === -1; + const flipY = scale[1] === -1; + + if (flipX) { + cropX = image.naturalWidth - Math.abs(cropX) - crop.width; + } + + if (flipY) { + cropY = image.naturalHeight - Math.abs(cropY) - crop.height; + } + + return { + cropX, + cropY, + }; +}; diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index afd5762e9b..98624875b7 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -132,6 +132,13 @@ export type IframeData = | { type: "document"; srcdoc: (theme: Theme) => string } ); +export type ImageCrop = { + x: number; + y: number; + width: number; + height: number; +}; + export type ExcalidrawImageElement = _ExcalidrawElementBase & Readonly<{ type: "image"; @@ -141,12 +148,7 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase & /** X and Y scale factors <-1, 1>, used for image axis flipping */ scale: [number, number]; - crop: { - x: number; - y: number; - width: number; - height: number; - } | null; + crop: ImageCrop | null; }>; export type InitializedExcalidrawImageElement = MarkNonNullable<