Parcourir la source

shift to crop with initial aspect ratio

Ryan Di il y a 11 mois
Parent
commit
467a4a2a6a

+ 13 - 1
packages/excalidraw/components/App.tsx

@@ -10161,11 +10161,20 @@ class App extends React.Component<AppProps, AppState> {
       croppingElement &&
       isImageElement(croppingElement)
     ) {
+      const croppingAtStateStart = pointerDownState.originalElements.get(
+        croppingElement.id,
+      );
+
       const image =
         isInitializedImageElement(croppingElement) &&
         this.imageCache.get(croppingElement.fileId)?.image;
 
-      if (image && !(image instanceof Promise)) {
+      if (
+        croppingAtStateStart &&
+        isImageElement(croppingAtStateStart) &&
+        image &&
+        !(image instanceof Promise)
+      ) {
         mutateElement(
           croppingElement,
           cropElement(
@@ -10175,6 +10184,9 @@ class App extends React.Component<AppProps, AppState> {
             image.naturalHeight,
             x,
             y,
+            event.shiftKey
+              ? croppingAtStateStart.width / croppingAtStateStart.height
+              : undefined,
           ),
         );
 

+ 286 - 29
packages/excalidraw/element/cropElement.ts

@@ -12,6 +12,7 @@ import {
   pointFromVector,
   clamp,
   isCloseTo,
+  round,
 } from "../../math";
 import type { TransformHandleType } from "./transformHandles";
 import type {
@@ -35,6 +36,7 @@ export const cropElement = (
   naturalHeight: number,
   pointerX: number,
   pointerY: number,
+  widthAspectRatio?: number,
 ) => {
   const { width: uncroppedWidth, height: uncroppedHeight } =
     getUncroppedWidthAndHeight(element);
@@ -83,57 +85,296 @@ export const cropElement = (
   const isFlippedByX = element.scale[0] === -1;
   const isFlippedByY = element.scale[1] === -1;
 
+  let changeInHeight = pointerY - element.y;
+  let changeInWidth = pointerX - element.x;
+
   if (transformHandle.includes("n")) {
-    const pointerDeltaY = pointerY - element.y;
     nextHeight = clamp(
-      element.height - pointerDeltaY,
+      element.height - changeInHeight,
       MINIMAL_CROP_SIZE,
       isFlippedByY ? uncroppedHeight - croppedTop : element.height + croppedTop,
     );
-    crop.height = (nextHeight / uncroppedHeight) * naturalHeight;
+  }
 
-    if (!isFlippedByY) {
-      crop.y = crop.y + (previousCropHeight - crop.height);
-    }
-  } else if (transformHandle.includes("s")) {
+  if (transformHandle.includes("s")) {
+    changeInHeight = pointerY - element.y - element.height;
     nextHeight = clamp(
-      pointerY - element.y,
+      element.height + changeInHeight,
       MINIMAL_CROP_SIZE,
       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 pointerDeltaX = pointerX - element.x;
+  if (transformHandle.includes("e")) {
+    changeInWidth = pointerX - element.x - element.width;
 
     nextWidth = clamp(
-      element.width - pointerDeltaX,
+      element.width + changeInWidth,
       MINIMAL_CROP_SIZE,
-      isFlippedByX ? uncroppedWidth - croppedLeft : element.width + croppedLeft,
+      isFlippedByX ? element.width + croppedLeft : uncroppedWidth - croppedLeft,
     );
+  }
 
-    crop.width = (nextWidth / uncroppedWidth) * naturalWidth;
-
-    if (!isFlippedByX) {
-      crop.x += previousCropWidth - crop.width;
-    }
-  } else if (transformHandle.includes("e")) {
+  if (transformHandle.includes("w")) {
     nextWidth = clamp(
-      pointerX - element.x,
+      element.width - changeInWidth,
       MINIMAL_CROP_SIZE,
-      isFlippedByX ? element.width + croppedLeft : uncroppedWidth - croppedLeft,
+      isFlippedByX ? uncroppedWidth - croppedLeft : element.width + croppedLeft,
     );
+  }
+
+  const updateCropWidthAndHeight = (crop: ImageCrop) => {
+    crop.height = nextHeight * naturalHeightToUncropped;
     crop.width = nextWidth * naturalWidthToUncropped;
-    if (isFlippedByX) {
-      const changeInCropWidth = previousCropWidth - crop.width;
-      crop.x += changeInCropWidth;
+  };
+
+  updateCropWidthAndHeight(crop);
+
+  const adjustFlipForHandle = (
+    handle: TransformHandleType,
+    crop: ImageCrop,
+  ) => {
+    updateCropWidthAndHeight(crop);
+    if (handle.includes("n")) {
+      if (!isFlippedByY) {
+        crop.y += previousCropHeight - crop.height;
+      }
+    }
+    if (handle.includes("s")) {
+      if (isFlippedByY) {
+        crop.y += previousCropHeight - crop.height;
+      }
+    }
+    if (handle.includes("e")) {
+      if (isFlippedByX) {
+        crop.x += previousCropWidth - crop.width;
+      }
+    }
+    if (handle.includes("w")) {
+      if (!isFlippedByX) {
+        crop.x += previousCropWidth - crop.width;
+      }
+    }
+  };
+
+  switch (transformHandle) {
+    case "n": {
+      if (widthAspectRatio) {
+        const distanceToLeft = croppedLeft + element.width / 2;
+        const distanceToRight =
+          uncroppedWidth - croppedLeft - element.width / 2;
+
+        const MAX_WIDTH = Math.min(distanceToLeft, distanceToRight) * 2;
+
+        nextWidth = clamp(
+          nextHeight * widthAspectRatio,
+          MINIMAL_CROP_SIZE,
+          MAX_WIDTH,
+        );
+        nextHeight = nextWidth / widthAspectRatio;
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+
+      if (widthAspectRatio) {
+        crop.x += (previousCropWidth - crop.width) / 2;
+      }
+
+      break;
+    }
+    case "s": {
+      if (widthAspectRatio) {
+        const distanceToLeft = croppedLeft + element.width / 2;
+        const distanceToRight =
+          uncroppedWidth - croppedLeft - element.width / 2;
+
+        const MAX_WIDTH = Math.min(distanceToLeft, distanceToRight) * 2;
+
+        nextWidth = clamp(
+          nextHeight * widthAspectRatio,
+          MINIMAL_CROP_SIZE,
+          MAX_WIDTH,
+        );
+        nextHeight = nextWidth / widthAspectRatio;
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+
+      if (widthAspectRatio) {
+        crop.x += (previousCropWidth - crop.width) / 2;
+      }
+
+      break;
+    }
+    case "w": {
+      if (widthAspectRatio) {
+        const distanceToTop = croppedTop + element.height / 2;
+        const distanceToBottom =
+          uncroppedHeight - croppedTop - element.height / 2;
+
+        const MAX_HEIGHT = Math.min(distanceToTop, distanceToBottom) * 2;
+
+        nextHeight = clamp(
+          nextWidth / widthAspectRatio,
+          MINIMAL_CROP_SIZE,
+          MAX_HEIGHT,
+        );
+        nextWidth = nextHeight * widthAspectRatio;
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+
+      if (widthAspectRatio) {
+        crop.y += (previousCropHeight - crop.height) / 2;
+      }
+
+      break;
+    }
+    case "e": {
+      if (widthAspectRatio) {
+        const distanceToTop = croppedTop + element.height / 2;
+        const distanceToBottom =
+          uncroppedHeight - croppedTop - element.height / 2;
+
+        const MAX_HEIGHT = Math.min(distanceToTop, distanceToBottom) * 2;
+
+        nextHeight = clamp(
+          nextWidth / widthAspectRatio,
+          MINIMAL_CROP_SIZE,
+          MAX_HEIGHT,
+        );
+        nextWidth = nextHeight * widthAspectRatio;
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+
+      if (widthAspectRatio) {
+        crop.y += (previousCropHeight - crop.height) / 2;
+      }
+
+      break;
+    }
+    case "ne": {
+      if (widthAspectRatio) {
+        if (changeInWidth > -changeInHeight) {
+          const MAX_HEIGHT = isFlippedByY
+            ? uncroppedHeight - croppedTop
+            : croppedTop + element.height;
+
+          nextHeight = clamp(
+            nextWidth / widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_HEIGHT,
+          );
+          nextWidth = nextHeight * widthAspectRatio;
+        } else {
+          const MAX_WIDTH = isFlippedByX
+            ? croppedLeft + element.width
+            : uncroppedWidth - croppedLeft;
+
+          nextWidth = clamp(
+            nextHeight * widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_WIDTH,
+          );
+          nextHeight = nextWidth / widthAspectRatio;
+        }
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+      break;
+    }
+    case "nw": {
+      if (widthAspectRatio) {
+        if (changeInWidth < changeInHeight) {
+          const MAX_HEIGHT = isFlippedByY
+            ? uncroppedHeight - croppedTop
+            : croppedTop + element.height;
+          nextHeight = clamp(
+            nextWidth / widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_HEIGHT,
+          );
+          nextWidth = nextHeight * widthAspectRatio;
+        } else {
+          const MAX_WIDTH = isFlippedByX
+            ? uncroppedWidth - croppedLeft
+            : croppedLeft + element.width;
+
+          nextWidth = clamp(
+            nextHeight * widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_WIDTH,
+          );
+          nextHeight = nextWidth / widthAspectRatio;
+        }
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+      break;
     }
+    case "se": {
+      if (widthAspectRatio) {
+        if (changeInWidth > changeInHeight) {
+          const MAX_HEIGHT = isFlippedByY
+            ? croppedTop + element.height
+            : uncroppedHeight - croppedTop;
+
+          nextHeight = clamp(
+            nextWidth / widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_HEIGHT,
+          );
+          nextWidth = nextHeight * widthAspectRatio;
+        } else {
+          const MAX_WIDTH = isFlippedByX
+            ? croppedLeft + element.width
+            : uncroppedWidth - croppedLeft;
+
+          nextWidth = clamp(
+            nextHeight * widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_WIDTH,
+          );
+          nextHeight = nextWidth / widthAspectRatio;
+        }
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+      break;
+    }
+    case "sw": {
+      if (widthAspectRatio) {
+        if (-changeInWidth > changeInHeight) {
+          const MAX_HEIGHT = isFlippedByY
+            ? croppedTop + element.height
+            : uncroppedHeight - croppedTop;
+
+          nextHeight = clamp(
+            nextWidth / widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_HEIGHT,
+          );
+          nextWidth = nextHeight * widthAspectRatio;
+        } else {
+          const MAX_WIDTH = isFlippedByX
+            ? uncroppedWidth - croppedLeft
+            : croppedLeft + element.width;
+
+          nextWidth = clamp(
+            nextHeight * widthAspectRatio,
+            MINIMAL_CROP_SIZE,
+            MAX_WIDTH,
+          );
+          nextHeight = nextWidth / widthAspectRatio;
+        }
+      }
+
+      adjustFlipForHandle(transformHandle, crop);
+      break;
+    }
+    default:
+      break;
   }
 
   const newOrigin = recomputeOrigin(
@@ -141,8 +382,14 @@ export const cropElement = (
     transformHandle,
     nextWidth,
     nextHeight,
+    !!widthAspectRatio,
   );
 
+  crop.x = round(crop.x, 6);
+  crop.y = round(crop.y, 6);
+  crop.width = round(crop.width, 6);
+  crop.height = round(crop.height, 6);
+
   // reset crop to null if we're back to orig size
   if (
     isCloseTo(crop.width, crop.naturalWidth) &&
@@ -165,6 +412,7 @@ const recomputeOrigin = (
   transformHandle: TransformHandleType,
   width: number,
   height: number,
+  shouldMaintainAspectRatio?: boolean,
 ) => {
   const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
     stateAtCropStart,
@@ -199,6 +447,15 @@ const recomputeOrigin = (
     newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
   }
 
+  if (shouldMaintainAspectRatio) {
+    if (["s", "n"].includes(transformHandle)) {
+      newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
+    }
+    if (["e", "w"].includes(transformHandle)) {
+      newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
+    }
+  }
+
   // adjust topLeft to new rotation point
   const angle = stateAtCropStart.angle;
   const rotatedTopLeft = pointRotateRads(newTopLeft, startCenter, angle);

+ 1 - 1
packages/excalidraw/renderer/interactiveScene.ts

@@ -781,7 +781,7 @@ const _renderInteractiveScene = ({
   }
 
   // Paint selection element
-  if (appState.selectionElement) {
+  if (appState.selectionElement && !appState.isCropping) {
     try {
       renderSelectionElement(
         appState.selectionElement,