From 064bede0c559058572781255907815824ea6e7a1 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Tue, 24 Sep 2024 15:59:09 +0800 Subject: [PATCH] simplify crop properties --- packages/excalidraw/components/App.tsx | 107 ++++++----- packages/excalidraw/data/restore.ts | 10 +- packages/excalidraw/element/cropElement.ts | 177 ++++++++++-------- packages/excalidraw/element/newElement.ts | 9 +- packages/excalidraw/element/resizeElements.ts | 6 +- packages/excalidraw/element/types.ts | 12 +- packages/excalidraw/renderer/renderElement.ts | 10 +- 7 files changed, 174 insertions(+), 157 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 1a2f8f34e..de8026f22 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -7604,6 +7604,11 @@ class App extends React.Component { pointerDownState: PointerDownState, ) { return withBatchedUpdatesThrottled((event: PointerEvent) => { + const pointerCoords = viewportCoordsToSceneCoords(event, this.state); + const lastPointerCoords = + this.lastPointerMoveCoords ?? pointerDownState.origin; + this.lastPointerMoveCoords = pointerCoords; + // We need to initialize dragOffsetXY only after we've updated // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove // event handler should hopefully ensure we're already working with @@ -7626,8 +7631,6 @@ class App extends React.Component { return; } - const pointerCoords = viewportCoordsToSceneCoords(event, this.state); - if (isEraserActive(this.state)) { this.handleEraser(event, pointerDownState, pointerCoords); return; @@ -7852,53 +7855,55 @@ class App extends React.Component { selectedElements[0].crop !== null ) { const crop = selectedElements[0].crop; - const image = selectedElements[0]; + const image = + isInitializedImageElement(selectedElements[0]) && + this.imageCache.get(selectedElements[0].fileId)?.image; - const lastPointerCoords = - this.lastPointerMoveCoords ?? pointerDownState.origin; + if (image && !(image instanceof Promise)) { + const instantDragOffset = { + x: pointerCoords.x - lastPointerCoords.x, + y: pointerCoords.y - lastPointerCoords.y, + }; - const instantDragOffset = { - x: pointerCoords.x - lastPointerCoords.x, - y: pointerCoords.y - lastPointerCoords.y, - }; + // current offset is based on the element's width and height + const uncroppedWidth = + selectedElements[0].initialWidth * + selectedElements[0].resizeFactors[0]; + const uncroppedHeight = + selectedElements[0].initialHeight * + selectedElements[0].resizeFactors[1]; - // current offset is based on the element's width and height - const uncroppedWidth = image.widthAtCreation * image.resizedFactorX; - const uncroppedHeight = - image.heightAtCreation * image.resizedFactorY; + const SENSITIVITY_FACTOR = 3; - const SENSITIVITY_FACTOR = 3; + const adjustedOffset = { + x: + instantDragOffset.x * + (uncroppedWidth / image.naturalWidth) * + SENSITIVITY_FACTOR, + y: + instantDragOffset.y * + (uncroppedHeight / image.naturalHeight) * + SENSITIVITY_FACTOR, + }; - const adjustedOffset = { - x: - instantDragOffset.x * - (uncroppedWidth / image.naturalWidth) * - SENSITIVITY_FACTOR, - y: - instantDragOffset.y * - (uncroppedHeight / image.naturalHeight) * - SENSITIVITY_FACTOR, - }; + const nextCrop = { + ...crop, + x: clamp( + crop.x - adjustedOffset.x, + 0, + image.naturalWidth - crop.width, + ), + y: clamp( + crop.y - adjustedOffset.y, + 0, + image.naturalHeight - crop.height, + ), + }; - const nextCrop = { - ...crop, - x: clamp( - crop.x - adjustedOffset.x, - 0, - image.naturalWidth - crop.width, - ), - y: clamp( - crop.y - adjustedOffset.y, - 0, - image.naturalHeight - crop.height, - ), - }; - - mutateElement(image, { - crop: nextCrop, - }); - - this.lastPointerMoveCoords = pointerCoords; + mutateElement(selectedElements[0], { + crop: nextCrop, + }); + } return; } @@ -8037,7 +8042,6 @@ class App extends React.Component { this.maybeCacheReferenceSnapPoints(event, selectedElements, true); } - this.lastPointerMoveCoords = pointerCoords; return; } } @@ -9478,12 +9482,14 @@ class App extends React.Component { y, width, height, - widthAtCreation: width, - heightAtCreation: height, - naturalWidth: image.naturalWidth, - naturalHeight: image.naturalHeight, - resizedFactorX: 1, - resizedFactorY: 1, + initialWidth: width, + initialHeight: height, + crop: { + x: 0, + y: 0, + width: image.naturalWidth, + height: image.naturalHeight, + }, }); } }; @@ -10053,6 +10059,7 @@ class App extends React.Component { cropElement( elementToCrop, this.scene.getNonDeletedElementsMap(), + this.imageCache, transformHandleType, x, y, diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index fc11337bf..1fdaf8c85 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -255,13 +255,9 @@ const restoreElement = ( fileId: element.fileId, scale: element.scale || [1, 1], crop: element.crop ?? null, - // TODO: restore properly - widthAtCreation: element.widthAtCreation, - heightAtCreation: element.heightAtCreation, - naturalWidth: element.naturalWidth, - naturalHeight: element.naturalHeight, - resizedFactorX: element.resizedFactorX, - resizedFactorY: element.resizedFactorY, + initialWidth: element.initialWidth ?? element.width, + initialHeight: element.initialHeight ?? element.height, + resizeFactors: element.resizeFactors ?? [1, 1], }); case "line": // @ts-ignore LEGACY type diff --git a/packages/excalidraw/element/cropElement.ts b/packages/excalidraw/element/cropElement.ts index 27c0f15c7..4a1fc601f 100644 --- a/packages/excalidraw/element/cropElement.ts +++ b/packages/excalidraw/element/cropElement.ts @@ -26,19 +26,22 @@ import { getElementAbsoluteCoords, getResizedElementAbsoluteCoords, } from "./bounds"; +import { AppClassProperties } from "../types"; +import { isInitializedImageElement } from "./typeChecks"; -// i split out these 'internal' functions so that this functionality can be easily unit tested -const cropElementInternal = ( +const _cropElement = ( element: ExcalidrawImageElement, transformHandle: TransformHandleType, + naturalWidth: number, + naturalHeight: number, pointerX: number, pointerY: number, ) => { - const uncroppedWidth = element.widthAtCreation * element.resizedFactorX; - const uncroppedHeight = element.heightAtCreation * element.resizedFactorY; + const uncroppedWidth = element.initialWidth * element.resizeFactors[0]; + const uncroppedHeight = element.initialHeight * element.resizeFactors[1]; - const naturalWidthToUncropped = element.naturalWidth / uncroppedWidth; - const naturalHeightToUncropped = element.naturalHeight / uncroppedHeight; + const naturalWidthToUncropped = naturalWidth / uncroppedWidth; + const naturalHeightToUncropped = naturalHeight / uncroppedHeight; const croppedLeft = (element.crop?.x ?? 0) / naturalWidthToUncropped; const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped; @@ -71,8 +74,8 @@ const cropElementInternal = ( const crop = element.crop ?? { x: 0, y: 0, - width: element.naturalWidth, - height: element.naturalHeight, + width: naturalWidth, + height: naturalHeight, }; if (transformHandle.includes("n")) { @@ -84,9 +87,8 @@ const cropElementInternal = ( const pointerDeltaY = pointerY - element.y; nextHeight = element.height - pointerDeltaY; - crop.y = - ((pointerDeltaY + croppedTop) / uncroppedHeight) * element.naturalHeight; - crop.height = (nextHeight / uncroppedHeight) * element.naturalHeight; + crop.y = ((pointerDeltaY + croppedTop) / uncroppedHeight) * naturalHeight; + crop.height = (nextHeight / uncroppedHeight) * naturalHeight; } if (transformHandle.includes("s")) { @@ -96,7 +98,7 @@ const cropElementInternal = ( pointerY = clamp(pointerY, northBound, southBound); nextHeight = pointerY - element.y; - crop.height = (nextHeight / uncroppedHeight) * element.naturalHeight; + crop.height = (nextHeight / uncroppedHeight) * naturalHeight; } if (transformHandle.includes("w")) { @@ -108,9 +110,8 @@ const cropElementInternal = ( const pointerDeltaX = pointerX - element.x; nextWidth = element.width - pointerDeltaX; - crop.x = - ((pointerDeltaX + croppedLeft) / uncroppedWidth) * element.naturalWidth; - crop.width = (nextWidth / uncroppedWidth) * element.naturalWidth; + crop.x = ((pointerDeltaX + croppedLeft) / uncroppedWidth) * naturalWidth; + crop.width = (nextWidth / uncroppedWidth) * naturalWidth; } if (transformHandle.includes("e")) { @@ -120,7 +121,7 @@ const cropElementInternal = ( pointerX = clamp(pointerX, westBound, eastBound); nextWidth = pointerX - element.x; - crop.width = (nextWidth / uncroppedWidth) * element.naturalWidth; + crop.width = (nextWidth / uncroppedWidth) * naturalWidth; } const newOrigin = recomputeOrigin( @@ -142,22 +143,30 @@ const cropElementInternal = ( export const cropElement = ( element: ExcalidrawImageElement, elementsMap: NonDeletedSceneElementsMap, + imageCache: AppClassProperties["imageCache"], transformHandle: TransformHandleType, pointerX: number, pointerY: number, ) => { - const mutation = cropElementInternal( - element, - transformHandle, - pointerX, - pointerY, - ); + const image = + isInitializedImageElement(element) && imageCache.get(element.fileId)?.image; - mutateElement(element, mutation); + if (image && !(image instanceof Promise)) { + const mutation = _cropElement( + element, + transformHandle, + image.naturalWidth, + image.naturalHeight, + pointerX, + pointerY, + ); - updateBoundElements(element, elementsMap, { - oldSize: { width: element.width, height: element.height }, - }); + mutateElement(element, mutation); + + updateBoundElements(element, elementsMap, { + oldSize: { width: element.width, height: element.height }, + }); + } }; // TODO: replace with the refactored resizeSingleElement @@ -222,71 +231,77 @@ const recomputeOrigin = ( }; export const getUncroppedImageElement = ( - image: ExcalidrawImageElement, + element: ExcalidrawImageElement, elementsMap: ElementsMap, + imageCache: AppClassProperties["imageCache"], ) => { - if (image.crop) { - const width = image.widthAtCreation * image.resizedFactorX; - const height = image.heightAtCreation * image.resizedFactorY; + const image = + isInitializedImageElement(element) && imageCache.get(element.fileId)?.image; - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( - image, - elementsMap, - ); + if (image && !(image instanceof Promise)) { + if (element.crop) { + const width = element.initialWidth * element.resizeFactors[0]; + const height = element.initialHeight * element.resizeFactors[1]; - const topLeftVector = vectorFromPoint( - pointRotateRads(point(x1, y1), point(cx, cy), image.angle), - ); - const topRightVector = vectorFromPoint( - pointRotateRads(point(x2, y1), point(cx, cy), image.angle), - ); - const topEdgeNormalized = vectorNormalize( - vectorSubtract(topRightVector, topLeftVector), - ); - const bottomLeftVector = vectorFromPoint( - pointRotateRads(point(x1, y2), point(cx, cy), image.angle), - ); - const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector); - const leftEdgeNormalized = vectorNormalize(leftEdgeVector); + const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + ); - const rotatedTopLeft = vectorAdd( - vectorAdd( - topLeftVector, - vectorScale( - topEdgeNormalized, - (-image.crop.x * width) / image.naturalWidth, + const topLeftVector = vectorFromPoint( + pointRotateRads(point(x1, y1), point(cx, cy), element.angle), + ); + const topRightVector = vectorFromPoint( + pointRotateRads(point(x2, y1), point(cx, cy), element.angle), + ); + const topEdgeNormalized = vectorNormalize( + vectorSubtract(topRightVector, topLeftVector), + ); + const bottomLeftVector = vectorFromPoint( + pointRotateRads(point(x1, y2), point(cx, cy), element.angle), + ); + const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector); + const leftEdgeNormalized = vectorNormalize(leftEdgeVector); + + const rotatedTopLeft = vectorAdd( + vectorAdd( + topLeftVector, + vectorScale( + topEdgeNormalized, + (-element.crop.x * width) / image.naturalWidth, + ), ), - ), - vectorScale( - leftEdgeNormalized, - (-image.crop.y * height) / image.naturalHeight, - ), - ); + vectorScale( + leftEdgeNormalized, + (-element.crop.y * height) / image.naturalHeight, + ), + ); - const center = pointFromVector( - vectorAdd( - vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)), - vectorScale(leftEdgeNormalized, height / 2), - ), - ); + const center = pointFromVector( + vectorAdd( + vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)), + vectorScale(leftEdgeNormalized, height / 2), + ), + ); - const unrotatedTopLeft = pointRotateRads( - pointFromVector(rotatedTopLeft), - center, - -image.angle as Radians, - ); + const unrotatedTopLeft = pointRotateRads( + pointFromVector(rotatedTopLeft), + center, + -element.angle as Radians, + ); - const uncroppedElement: ExcalidrawImageElement = { - ...image, - x: unrotatedTopLeft[0], - y: unrotatedTopLeft[1], - width, - height, - crop: null, - }; + const uncroppedElement: ExcalidrawImageElement = { + ...element, + x: unrotatedTopLeft[0], + y: unrotatedTopLeft[1], + width, + height, + crop: null, + }; - return uncroppedElement; + return uncroppedElement; + } } - return image; + return element; }; diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index f20953265..3edb31e00 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -489,13 +489,10 @@ export const newImageElement = ( status: opts.status ?? "pending", fileId: opts.fileId ?? null, scale: opts.scale ?? [1, 1], - widthAtCreation: 0, - heightAtCreation: 0, - naturalWidth: 0, - naturalHeight: 0, + initialWidth: opts.width ?? 0, + initialHeight: opts.height ?? 0, + resizeFactors: [1, 1], crop: opts.crop ?? null, - resizedFactorX: 1, - resizedFactorY: 1, }; }; diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 071a30df3..5f22b7c77 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -782,8 +782,10 @@ const updateInternalScale = ( scaleY = Math.abs(scaleY); mutateElement(element, { - resizedFactorX: element.resizedFactorX * scaleX, - resizedFactorY: element.resizedFactorY * scaleY, + resizeFactors: [ + element.resizeFactors[0] * scaleX, + element.resizeFactors[1] * scaleY, + ], }); }; diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index 967df2bcb..47cc2f6b7 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -141,15 +141,11 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase & /** X and Y scale factors <-1, 1>, used for image axis flipping */ scale: [number, number]; - /** the image's dimension after adjustment at creation */ - widthAtCreation: number; - heightAtCreation: number; + /** the image's dimension after initialization */ + initialWidth: number; + initialHeight: number; /** how much the image has been resized with respect the dimension at creation */ - resizedFactorX: number; - resizedFactorY: number; - - naturalWidth: number; - naturalHeight: number; + resizeFactors: [number, number]; crop: { x: number; diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index c0fdfb49f..e087e0901 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -446,8 +446,8 @@ const drawElementOnCanvas = ( : { x: 0, y: 0, - width: element.naturalWidth, - height: element.naturalHeight, + width: img.naturalWidth, + height: img.naturalHeight, }; context.drawImage( @@ -950,7 +950,11 @@ export const renderElement = ( context.globalAlpha = 0.1; const uncroppedElementCanvas = generateElementCanvas( - getUncroppedImageElement(elementWithCanvas.element, elementsMap), + getUncroppedImageElement( + elementWithCanvas.element, + elementsMap, + renderConfig.imageCache, + ), allElementsMap, appState.zoom, renderConfig,