simplify crop properties

This commit is contained in:
Ryan Di 2024-09-24 15:59:09 +08:00
parent 997fec6c75
commit 064bede0c5
7 changed files with 174 additions and 157 deletions

View File

@ -7604,6 +7604,11 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
) { ) {
return withBatchedUpdatesThrottled((event: PointerEvent) => { 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 // We need to initialize dragOffsetXY only after we've updated
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
// event handler should hopefully ensure we're already working with // event handler should hopefully ensure we're already working with
@ -7626,8 +7631,6 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
if (isEraserActive(this.state)) { if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords); this.handleEraser(event, pointerDownState, pointerCoords);
return; return;
@ -7852,53 +7855,55 @@ class App extends React.Component<AppProps, AppState> {
selectedElements[0].crop !== null selectedElements[0].crop !== null
) { ) {
const crop = selectedElements[0].crop; const crop = selectedElements[0].crop;
const image = selectedElements[0]; const image =
isInitializedImageElement(selectedElements[0]) &&
this.imageCache.get(selectedElements[0].fileId)?.image;
const lastPointerCoords = if (image && !(image instanceof Promise)) {
this.lastPointerMoveCoords ?? pointerDownState.origin; const instantDragOffset = {
x: pointerCoords.x - lastPointerCoords.x,
y: pointerCoords.y - lastPointerCoords.y,
};
const instantDragOffset = { // current offset is based on the element's width and height
x: pointerCoords.x - lastPointerCoords.x, const uncroppedWidth =
y: pointerCoords.y - lastPointerCoords.y, 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 SENSITIVITY_FACTOR = 3;
const uncroppedWidth = image.widthAtCreation * image.resizedFactorX;
const uncroppedHeight =
image.heightAtCreation * image.resizedFactorY;
const SENSITIVITY_FACTOR = 3; const adjustedOffset = {
x:
instantDragOffset.x *
(uncroppedWidth / image.naturalWidth) *
SENSITIVITY_FACTOR,
y:
instantDragOffset.y *
(uncroppedHeight / image.naturalHeight) *
SENSITIVITY_FACTOR,
};
const adjustedOffset = { const nextCrop = {
x: ...crop,
instantDragOffset.x * x: clamp(
(uncroppedWidth / image.naturalWidth) * crop.x - adjustedOffset.x,
SENSITIVITY_FACTOR, 0,
y: image.naturalWidth - crop.width,
instantDragOffset.y * ),
(uncroppedHeight / image.naturalHeight) * y: clamp(
SENSITIVITY_FACTOR, crop.y - adjustedOffset.y,
}; 0,
image.naturalHeight - crop.height,
),
};
const nextCrop = { mutateElement(selectedElements[0], {
...crop, crop: nextCrop,
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;
return; return;
} }
@ -8037,7 +8042,6 @@ class App extends React.Component<AppProps, AppState> {
this.maybeCacheReferenceSnapPoints(event, selectedElements, true); this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
} }
this.lastPointerMoveCoords = pointerCoords;
return; return;
} }
} }
@ -9478,12 +9482,14 @@ class App extends React.Component<AppProps, AppState> {
y, y,
width, width,
height, height,
widthAtCreation: width, initialWidth: width,
heightAtCreation: height, initialHeight: height,
naturalWidth: image.naturalWidth, crop: {
naturalHeight: image.naturalHeight, x: 0,
resizedFactorX: 1, y: 0,
resizedFactorY: 1, width: image.naturalWidth,
height: image.naturalHeight,
},
}); });
} }
}; };
@ -10053,6 +10059,7 @@ class App extends React.Component<AppProps, AppState> {
cropElement( cropElement(
elementToCrop, elementToCrop,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.imageCache,
transformHandleType, transformHandleType,
x, x,
y, y,

View File

@ -255,13 +255,9 @@ const restoreElement = (
fileId: element.fileId, fileId: element.fileId,
scale: element.scale || [1, 1], scale: element.scale || [1, 1],
crop: element.crop ?? null, crop: element.crop ?? null,
// TODO: restore properly initialWidth: element.initialWidth ?? element.width,
widthAtCreation: element.widthAtCreation, initialHeight: element.initialHeight ?? element.height,
heightAtCreation: element.heightAtCreation, resizeFactors: element.resizeFactors ?? [1, 1],
naturalWidth: element.naturalWidth,
naturalHeight: element.naturalHeight,
resizedFactorX: element.resizedFactorX,
resizedFactorY: element.resizedFactorY,
}); });
case "line": case "line":
// @ts-ignore LEGACY type // @ts-ignore LEGACY type

View File

@ -26,19 +26,22 @@ import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getResizedElementAbsoluteCoords, getResizedElementAbsoluteCoords,
} from "./bounds"; } 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 _cropElement = (
const cropElementInternal = (
element: ExcalidrawImageElement, element: ExcalidrawImageElement,
transformHandle: TransformHandleType, transformHandle: TransformHandleType,
naturalWidth: number,
naturalHeight: number,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const uncroppedWidth = element.widthAtCreation * element.resizedFactorX; const uncroppedWidth = element.initialWidth * element.resizeFactors[0];
const uncroppedHeight = element.heightAtCreation * element.resizedFactorY; const uncroppedHeight = element.initialHeight * element.resizeFactors[1];
const naturalWidthToUncropped = element.naturalWidth / uncroppedWidth; const naturalWidthToUncropped = naturalWidth / uncroppedWidth;
const naturalHeightToUncropped = element.naturalHeight / uncroppedHeight; const naturalHeightToUncropped = naturalHeight / uncroppedHeight;
const croppedLeft = (element.crop?.x ?? 0) / naturalWidthToUncropped; const croppedLeft = (element.crop?.x ?? 0) / naturalWidthToUncropped;
const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped; const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped;
@ -71,8 +74,8 @@ const cropElementInternal = (
const crop = element.crop ?? { const crop = element.crop ?? {
x: 0, x: 0,
y: 0, y: 0,
width: element.naturalWidth, width: naturalWidth,
height: element.naturalHeight, height: naturalHeight,
}; };
if (transformHandle.includes("n")) { if (transformHandle.includes("n")) {
@ -84,9 +87,8 @@ const cropElementInternal = (
const pointerDeltaY = pointerY - element.y; const pointerDeltaY = pointerY - element.y;
nextHeight = element.height - pointerDeltaY; nextHeight = element.height - pointerDeltaY;
crop.y = crop.y = ((pointerDeltaY + croppedTop) / uncroppedHeight) * naturalHeight;
((pointerDeltaY + croppedTop) / uncroppedHeight) * element.naturalHeight; crop.height = (nextHeight / uncroppedHeight) * naturalHeight;
crop.height = (nextHeight / uncroppedHeight) * element.naturalHeight;
} }
if (transformHandle.includes("s")) { if (transformHandle.includes("s")) {
@ -96,7 +98,7 @@ const cropElementInternal = (
pointerY = clamp(pointerY, northBound, southBound); pointerY = clamp(pointerY, northBound, southBound);
nextHeight = pointerY - element.y; nextHeight = pointerY - element.y;
crop.height = (nextHeight / uncroppedHeight) * element.naturalHeight; crop.height = (nextHeight / uncroppedHeight) * naturalHeight;
} }
if (transformHandle.includes("w")) { if (transformHandle.includes("w")) {
@ -108,9 +110,8 @@ const cropElementInternal = (
const pointerDeltaX = pointerX - element.x; const pointerDeltaX = pointerX - element.x;
nextWidth = element.width - pointerDeltaX; nextWidth = element.width - pointerDeltaX;
crop.x = crop.x = ((pointerDeltaX + croppedLeft) / uncroppedWidth) * naturalWidth;
((pointerDeltaX + croppedLeft) / uncroppedWidth) * element.naturalWidth; crop.width = (nextWidth / uncroppedWidth) * naturalWidth;
crop.width = (nextWidth / uncroppedWidth) * element.naturalWidth;
} }
if (transformHandle.includes("e")) { if (transformHandle.includes("e")) {
@ -120,7 +121,7 @@ const cropElementInternal = (
pointerX = clamp(pointerX, westBound, eastBound); pointerX = clamp(pointerX, westBound, eastBound);
nextWidth = pointerX - element.x; nextWidth = pointerX - element.x;
crop.width = (nextWidth / uncroppedWidth) * element.naturalWidth; crop.width = (nextWidth / uncroppedWidth) * naturalWidth;
} }
const newOrigin = recomputeOrigin( const newOrigin = recomputeOrigin(
@ -142,22 +143,30 @@ const cropElementInternal = (
export const cropElement = ( export const cropElement = (
element: ExcalidrawImageElement, element: ExcalidrawImageElement,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
imageCache: AppClassProperties["imageCache"],
transformHandle: TransformHandleType, transformHandle: TransformHandleType,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const mutation = cropElementInternal( const image =
element, isInitializedImageElement(element) && imageCache.get(element.fileId)?.image;
transformHandle,
pointerX,
pointerY,
);
mutateElement(element, mutation); if (image && !(image instanceof Promise)) {
const mutation = _cropElement(
element,
transformHandle,
image.naturalWidth,
image.naturalHeight,
pointerX,
pointerY,
);
updateBoundElements(element, elementsMap, { mutateElement(element, mutation);
oldSize: { width: element.width, height: element.height },
}); updateBoundElements(element, elementsMap, {
oldSize: { width: element.width, height: element.height },
});
}
}; };
// TODO: replace with the refactored resizeSingleElement // TODO: replace with the refactored resizeSingleElement
@ -222,71 +231,77 @@ const recomputeOrigin = (
}; };
export const getUncroppedImageElement = ( export const getUncroppedImageElement = (
image: ExcalidrawImageElement, element: ExcalidrawImageElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
imageCache: AppClassProperties["imageCache"],
) => { ) => {
if (image.crop) { const image =
const width = image.widthAtCreation * image.resizedFactorX; isInitializedImageElement(element) && imageCache.get(element.fileId)?.image;
const height = image.heightAtCreation * image.resizedFactorY;
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( if (image && !(image instanceof Promise)) {
image, if (element.crop) {
elementsMap, const width = element.initialWidth * element.resizeFactors[0];
); const height = element.initialHeight * element.resizeFactors[1];
const topLeftVector = vectorFromPoint( const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
pointRotateRads(point(x1, y1), point(cx, cy), image.angle), element,
); elementsMap,
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 rotatedTopLeft = vectorAdd( const topLeftVector = vectorFromPoint(
vectorAdd( pointRotateRads(point(x1, y1), point(cx, cy), element.angle),
topLeftVector, );
vectorScale( const topRightVector = vectorFromPoint(
topEdgeNormalized, pointRotateRads(point(x2, y1), point(cx, cy), element.angle),
(-image.crop.x * width) / image.naturalWidth, );
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(
vectorScale( leftEdgeNormalized,
leftEdgeNormalized, (-element.crop.y * height) / image.naturalHeight,
(-image.crop.y * height) / image.naturalHeight, ),
), );
);
const center = pointFromVector( const center = pointFromVector(
vectorAdd( vectorAdd(
vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)), vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)),
vectorScale(leftEdgeNormalized, height / 2), vectorScale(leftEdgeNormalized, height / 2),
), ),
); );
const unrotatedTopLeft = pointRotateRads( const unrotatedTopLeft = pointRotateRads(
pointFromVector(rotatedTopLeft), pointFromVector(rotatedTopLeft),
center, center,
-image.angle as Radians, -element.angle as Radians,
); );
const uncroppedElement: ExcalidrawImageElement = { const uncroppedElement: ExcalidrawImageElement = {
...image, ...element,
x: unrotatedTopLeft[0], x: unrotatedTopLeft[0],
y: unrotatedTopLeft[1], y: unrotatedTopLeft[1],
width, width,
height, height,
crop: null, crop: null,
}; };
return uncroppedElement; return uncroppedElement;
}
} }
return image; return element;
}; };

View File

@ -489,13 +489,10 @@ export const newImageElement = (
status: opts.status ?? "pending", status: opts.status ?? "pending",
fileId: opts.fileId ?? null, fileId: opts.fileId ?? null,
scale: opts.scale ?? [1, 1], scale: opts.scale ?? [1, 1],
widthAtCreation: 0, initialWidth: opts.width ?? 0,
heightAtCreation: 0, initialHeight: opts.height ?? 0,
naturalWidth: 0, resizeFactors: [1, 1],
naturalHeight: 0,
crop: opts.crop ?? null, crop: opts.crop ?? null,
resizedFactorX: 1,
resizedFactorY: 1,
}; };
}; };

View File

@ -782,8 +782,10 @@ const updateInternalScale = (
scaleY = Math.abs(scaleY); scaleY = Math.abs(scaleY);
mutateElement(element, { mutateElement(element, {
resizedFactorX: element.resizedFactorX * scaleX, resizeFactors: [
resizedFactorY: element.resizedFactorY * scaleY, element.resizeFactors[0] * scaleX,
element.resizeFactors[1] * scaleY,
],
}); });
}; };

View File

@ -141,15 +141,11 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
/** X and Y scale factors <-1, 1>, used for image axis flipping */ /** X and Y scale factors <-1, 1>, used for image axis flipping */
scale: [number, number]; scale: [number, number];
/** the image's dimension after adjustment at creation */ /** the image's dimension after initialization */
widthAtCreation: number; initialWidth: number;
heightAtCreation: number; initialHeight: number;
/** how much the image has been resized with respect the dimension at creation */ /** how much the image has been resized with respect the dimension at creation */
resizedFactorX: number; resizeFactors: [number, number];
resizedFactorY: number;
naturalWidth: number;
naturalHeight: number;
crop: { crop: {
x: number; x: number;

View File

@ -446,8 +446,8 @@ const drawElementOnCanvas = (
: { : {
x: 0, x: 0,
y: 0, y: 0,
width: element.naturalWidth, width: img.naturalWidth,
height: element.naturalHeight, height: img.naturalHeight,
}; };
context.drawImage( context.drawImage(
@ -950,7 +950,11 @@ export const renderElement = (
context.globalAlpha = 0.1; context.globalAlpha = 0.1;
const uncroppedElementCanvas = generateElementCanvas( const uncroppedElementCanvas = generateElementCanvas(
getUncroppedImageElement(elementWithCanvas.element, elementsMap), getUncroppedImageElement(
elementWithCanvas.element,
elementsMap,
renderConfig.imageCache,
),
allElementsMap, allElementsMap,
appState.zoom, appState.zoom,
renderConfig, renderConfig,