diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 565821e4e5..2b654bbbc4 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -9531,12 +9531,7 @@ class App extends React.Component { y, width, height, - crop: { - x: 0, - y: 0, - width: image.naturalWidth, - height: image.naturalHeight, - }, + crop: null, }); } }; diff --git a/packages/excalidraw/element/cropElement.ts b/packages/excalidraw/element/cropElement.ts index 4806c738bf..fece3f6ceb 100644 --- a/packages/excalidraw/element/cropElement.ts +++ b/packages/excalidraw/element/cropElement.ts @@ -32,19 +32,14 @@ import { isInitializedImageElement } from "./typeChecks"; const _cropElement = ( element: ExcalidrawImageElement, - image: HTMLImageElement, transformHandle: TransformHandleType, naturalWidth: number, naturalHeight: number, pointerX: number, pointerY: number, ) => { - const uncroppedWidth = - element.width / - (element.crop ? element.crop.width / image.naturalWidth : 1); - const uncroppedHeight = - element.height / - (element.crop ? element.crop.height / image.naturalHeight : 1); + const { width: uncroppedWidth, height: uncroppedHeight } = + getUncroppedWidthAndHeight(element); const naturalWidthToUncropped = naturalWidth / uncroppedWidth; const naturalHeightToUncropped = naturalHeight / uncroppedHeight; @@ -53,7 +48,7 @@ const _cropElement = ( const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped; /** - * uncropped width + * uncropped width * *––––––––––––––––––––––––* * | (x,y) (natural) | * | *–––––––* | @@ -79,6 +74,7 @@ const _cropElement = ( y: 0, width: naturalWidth, height: naturalHeight, + naturalDimension: [naturalWidth, naturalHeight], }; const previousCropHeight = crop.height; @@ -168,25 +164,24 @@ export const cropElement = ( isInitializedImageElement(element) && imageCache.get(element.fileId)?.image; if (image && !(image instanceof Promise)) { - const mutation = _cropElement( + mutateElement( element, - image, - transformHandle, - image.naturalWidth, - image.naturalHeight, - pointerX, - pointerY, + _cropElement( + element, + transformHandle, + image.naturalWidth, + image.naturalHeight, + pointerX, + pointerY, + ), ); - mutateElement(element, mutation); - updateBoundElements(element, elementsMap, { oldSize: { width: element.width, height: element.height }, }); } }; -// TODO: replace with the refactored resizeSingleElement const recomputeOrigin = ( stateAtCropStart: NonDeleted, transformHandle: TransformHandleType, @@ -250,87 +245,96 @@ const recomputeOrigin = ( export const getUncroppedImageElement = ( element: ExcalidrawImageElement, elementsMap: ElementsMap, - imageCache: AppClassProperties["imageCache"], ) => { - const image = - isInitializedImageElement(element) && imageCache.get(element.fileId)?.image; + if (element.crop) { + const { width, height } = getUncroppedWidthAndHeight(element); - if (image && !(image instanceof Promise)) { - if (element.crop) { - const width = element.width / (element.crop.width / image.naturalWidth); - const height = - element.height / (element.crop.height / image.naturalHeight); + const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + ); - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( - element, - elementsMap, - ); - - const topLeftVector = vectorFromPoint( - pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle), - ); - const topRightVector = vectorFromPoint( - pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle), - ); - const topEdgeNormalized = vectorNormalize( - vectorSubtract(topRightVector, topLeftVector), - ); - const bottomLeftVector = vectorFromPoint( - pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle), - ); - 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, (-cropX * width) / image.naturalWidth), - ), + const topLeftVector = vectorFromPoint( + pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle), + ); + const topRightVector = vectorFromPoint( + pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle), + ); + const topEdgeNormalized = vectorNormalize( + vectorSubtract(topRightVector, topLeftVector), + ); + const bottomLeftVector = vectorFromPoint( + pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle), + ); + const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector); + const leftEdgeNormalized = vectorNormalize(leftEdgeVector); + + const { cropX, cropY } = adjustCropPosition(element.crop, element.scale); + + const rotatedTopLeft = vectorAdd( + vectorAdd( + topLeftVector, vectorScale( - leftEdgeNormalized, - (-cropY * height) / image.naturalHeight, + topEdgeNormalized, + (-cropX * width) / element.crop.naturalDimension[0], ), - ); + ), + vectorScale( + leftEdgeNormalized, + (-cropY * height) / element.crop.naturalDimension[1], + ), + ); - const center = pointFromVector( - vectorAdd( - vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)), - vectorScale(leftEdgeNormalized, height / 2), - ), - ); - - const unrotatedTopLeft = pointRotateRads( - pointFromVector(rotatedTopLeft), - center, - -element.angle as Radians, - ); - - const uncroppedElement: ExcalidrawImageElement = { - ...element, - x: unrotatedTopLeft[0], - y: unrotatedTopLeft[1], - width, - height, - crop: null, - }; - - return uncroppedElement; - } + const center = pointFromVector( + vectorAdd( + vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)), + vectorScale(leftEdgeNormalized, height / 2), + ), + ); + + const unrotatedTopLeft = pointRotateRads( + pointFromVector(rotatedTopLeft), + center, + -element.angle as Radians, + ); + + const uncroppedElement: ExcalidrawImageElement = { + ...element, + x: unrotatedTopLeft[0], + y: unrotatedTopLeft[1], + width, + height, + crop: null, + }; + + return uncroppedElement; } return element; }; +export const getUncroppedWidthAndHeight = (element: ExcalidrawImageElement) => { + if (element.crop) { + const width = + element.width / (element.crop.width / element.crop.naturalDimension[0]); + const height = + element.height / (element.crop.height / element.crop.naturalDimension[1]); + + return { + width, + height, + }; + } + + return { + width: element.width, + height: element.height, + }; +}; + const adjustCropPosition = ( crop: ImageCrop, scale: ExcalidrawImageElement["scale"], - image: HTMLImageElement, ) => { let cropX = crop.x; let cropY = crop.y; @@ -339,11 +343,11 @@ const adjustCropPosition = ( const flipY = scale[1] === -1; if (flipX) { - cropX = image.naturalWidth - Math.abs(cropX) - crop.width; + cropX = crop.naturalDimension[0] - Math.abs(cropX) - crop.width; } if (flipY) { - cropY = image.naturalHeight - Math.abs(cropY) - crop.height; + cropY = crop.naturalDimension[1] - Math.abs(cropY) - crop.height; } return { diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index c804d85254..74cee8a300 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -137,6 +137,7 @@ export type ImageCrop = { y: number; width: number; height: number; + naturalDimension: [number, number]; }; export type ExcalidrawImageElement = _ExcalidrawElementBase & @@ -147,7 +148,7 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase & status: "pending" | "saved" | "error"; /** X and Y scale factors <-1, 1>, used for image axis flipping */ scale: [number, number]; - + /** whether an element is cropped */ crop: ImageCrop | null; }>; diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index e087e09015..ebc5e5ac40 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -950,11 +950,7 @@ export const renderElement = ( context.globalAlpha = 0.1; const uncroppedElementCanvas = generateElementCanvas( - getUncroppedImageElement( - elementWithCanvas.element, - elementsMap, - renderConfig.imageCache, - ), + getUncroppedImageElement(elementWithCanvas.element, elementsMap), allElementsMap, appState.zoom, renderConfig, diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index f0bf989670..7926e81bc9 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -37,6 +37,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getVerticalOffset } from "../fonts"; import { getCornerRadius, isPathALoop } from "../shapes"; +import { getUncroppedWidthAndHeight } from "../element/cropElement"; const roughSVGDrawWithPrecision = ( rsvg: RoughSVG, @@ -417,12 +418,30 @@ const renderElementToSvg = ( symbol.id = symbolId; const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image"); - - image.setAttribute("width", "100%"); - image.setAttribute("height", "100%"); image.setAttribute("href", fileData.dataURL); image.setAttribute("preserveAspectRatio", "none"); + if (element.crop) { + const { width: uncroppedWidth, height: uncroppedHeight } = + getUncroppedWidthAndHeight(element); + + symbol.setAttribute( + "viewBox", + `${ + element.crop.x / + (element.crop.naturalDimension[0] / uncroppedWidth) + } ${ + element.crop.y / + (element.crop.naturalDimension[1] / uncroppedHeight) + } ${width} ${height}`, + ); + image.setAttribute("width", `${uncroppedWidth}`); + image.setAttribute("height", `${uncroppedHeight}`); + } else { + image.setAttribute("width", "100%"); + image.setAttribute("height", "100%"); + } + symbol.appendChild(image); root.prepend(symbol);