From 877f4ff87c39e3484ae2e7e6c920fc7fb8c04c23 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 28 Aug 2023 21:14:20 +0800
Subject: ...

---
 FrontEnd/src/components/ImageCropper.tsx | 262 +++++++++++--------------------
 1 file changed, 95 insertions(+), 167 deletions(-)

(limited to 'FrontEnd/src/components/ImageCropper.tsx')

diff --git a/FrontEnd/src/components/ImageCropper.tsx b/FrontEnd/src/components/ImageCropper.tsx
index 1f530004..acbbbe0c 100644
--- a/FrontEnd/src/components/ImageCropper.tsx
+++ b/FrontEnd/src/components/ImageCropper.tsx
@@ -1,29 +1,20 @@
-import {
-  useState,
-  useRef,
-  SyntheticEvent,
-  PointerEvent,
-  useMemo,
-  MutableRefObject,
-} from "react";
+import { useState, useRef, PointerEvent } from "react";
 import classnames from "classnames";
 
-import { UiLogicError } from "./common";
+import { UiLogicError, geometry } from "./common";
+
 import BlobImage from "./BlobImage";
 
 import "./ImageCropper.css";
 
-// All in natural size of image.
-export interface Clip {
-  left: number;
-  top: number;
-  width: number;
-  height: number;
-}
+const { Rect } = geometry;
+
+type Rect = geometry.Rect;
+type Movement = geometry.Movement;
 
-export function applyClipToImage(
+export function crop(
   image: HTMLImageElement,
-  clip: Clip,
+  clip: Rect,
   mimeType: string,
 ): Promise<Blob> {
   return new Promise((resolve, reject) => {
@@ -56,17 +47,13 @@ export function applyClipToImage(
   });
 }
 
-interface Movement {
-  x: number;
-  y: number;
-}
-
 interface ImageInfo {
   element: HTMLImageElement;
   width: number;
   height: number;
   ratio: number;
   landscape: boolean;
+  rect: Rect;
 }
 
 export interface CropConstraint {
@@ -77,11 +64,7 @@ export interface CropConstraint {
   //   maxClipHeight?: number;
 }
 
-function generateImageInfo(
-  imageElement: HTMLImageElement | null,
-): ImageInfo | null {
-  if (imageElement == null) return null;
-
+function generateImageInfo(imageElement: HTMLImageElement): ImageInfo {
   const { naturalWidth, naturalHeight } = imageElement;
   const imageRatio = naturalHeight / naturalWidth;
 
@@ -91,108 +74,15 @@ function generateImageInfo(
     height: naturalHeight,
     ratio: imageRatio,
     landscape: imageRatio < 1,
+    rect: new Rect(0, 0, naturalWidth, naturalHeight),
   };
 }
 
-const emptyClip: Clip = {
-  left: 0,
-  top: 0,
-  width: 0,
-  height: 0,
-};
-
-const allClip : Clip = {
-  left: 0,
-  top: 0,
-  width: Number.MAX_VALUE,
-  height: Number.MAX_VALUE,
-}
-
-// TODO: Continue here... mode...
-function adjustClip(
-  clip: Clip,
-  mode: "move" | "resize" | "both",
-  imageSize: { width: number; height: number },
-  targetRatio?: number | null | undefined,
-): Clip {
-  class ClipGeometry {
-    constructor(
-      public left: number,
-      public top: number,
-      public width: number,
-      public height: number,
-    ) {}
-
-    get right(): number {
-      return this.left + this.width;
-    }
-
-    set right(value: number) {
-      this.width = this.left + value;
-    }
-
-    get bottom(): number {
-      return this.top + this.height;
-    }
-
-    set bottom(value: number) {
-      this.height = this.top + value;
-    }
-
-    get ratio(): number {
-      return this.height / this.width;
-    }
-
-    toClip(): Clip {
-      return {
-        left: this.left,
-        top: this.top,
-        width: this.width,
-        height: this.height,
-      };
-    }
-  }
-
-  const clipGeometry = new ClipGeometry(
-    clip.left,
-    clip.top,
-    clip.width,
-    clip.height,
-  );
-
-  // Make clip in image.
-  clipGeometry.left = Math.max(clipGeometry.left, 0);
-  clipGeometry.top = Math.max(clipGeometry.top, 0);
-  clipGeometry.right = Math.min(clipGeometry.right, imageSize.width);
-  clipGeometry.bottom = Math.min(clipGeometry.bottom, imageSize.height);
-
-  // Make image "positive"
-  if (clipGeometry.right < clipGeometry.left) {
-    clipGeometry.right = clipGeometry.left;
-  }
-  if (clipGeometry.bottom < clipGeometry.top) {
-    clipGeometry.bottom = clipGeometry.top;
-  }
-
-  // Now correct ratio
-  const currentRatio = clipGeometry.ratio;
-  if (targetRatio != null && targetRatio > 0 && currentRatio !== targetRatio) {
-    if (currentRatio < targetRatio) {
-      // too wide
-      clipGeometry.width = clipGeometry.height / targetRatio;
-    } else {
-      clipGeometry.height = clipGeometry.width * targetRatio;
-    }
-  }
-
-  return clipGeometry.toClip();
-}
-
 interface ImageCropperProps {
-  clip: Clip;
+  clip: Rect;
   image: Blob | string | null;
-  imageElementRef: MutableRefObject<HTMLImageElement | null>;
-  onImageLoad: (event: SyntheticEvent<HTMLImageElement>) => void;
+  imageElementCallback: (element: HTMLImageElement | null) => void;
+  onImageLoad: () => void;
   onMove: (movement: Movement) => void;
   onResize: (movement: Movement) => void;
   containerClassName?: string;
@@ -204,28 +94,26 @@ export function useImageCrop(
     constraint?: CropConstraint;
   },
 ): {
-  clip: Clip;
-  setClip: (clip: Clip) => void;
+  clip: Rect;
+  setClip: (clip: Rect) => void;
   canCrop: boolean;
   crop: () => Promise<Blob>;
   imageCropperProps: ImageCropperProps;
 } {
   const targetRatio = options?.constraint?.ratio;
 
-  const imageElementRef = useRef<HTMLImageElement | null>(null);
-  const [image, setImage] = useState<ImageInfo | null>(null);
-  const [clip, setClip] = useState<Clip>(emptyClip  );
+  const [imageElement, setImageElement] = useState<HTMLImageElement | null>(
+    null,
+  );
+  const [imageInfo, setImageInfo] = useState<ImageInfo | null>(null);
+  const [clip, setClip] = useState<Rect>(Rect.empty);
 
-  if (imageElementRef.current == null && image != null) {
-    setImage(null);
-    setClip(emptyClip);
+  if (imageElement == null && imageInfo != null) {
+    setImageInfo(null);
+    setClip(Rect.empty);
   }
 
-  const canCrop = file != null && image != null;
-
-  const adjustedClip = useMemo(() => {
-    return image == null ? emptyClip : adjustClip(clip, image, targetRatio);
-  }, [clip, image, targetRatio]);
+  const canCrop = file != null && imageElement != null && imageInfo != null;
 
   return {
     clip,
@@ -233,19 +121,43 @@ export function useImageCrop(
     canCrop,
     crop() {
       if (!canCrop) throw new UiLogicError();
-      return applyClipToImage(image.element, adjustedClip, file.type);
+      return crop(imageElement, clip, file.type);
     },
     imageCropperProps: {
-      clip: adjustedClip,
+      clip,
       image: file,
-      imageElementRef: imageElementRef,
-      // TODO: Continue here...
-      onMove: ,
-      onResize: ,
+      imageElementCallback: setImageElement,
+      onMove: (movement) => {
+        if (imageInfo == null) return;
+        const newClip = geometry.adjustRectToContainer(
+          clip.copy().move(movement),
+          imageInfo.rect,
+          "move",
+          {
+            targetRatio,
+          },
+        );
+        setClip(newClip);
+      },
+      onResize: (movement) => {
+        if (imageInfo == null) return;
+        const newClip = geometry.adjustRectToContainer(
+          clip.copy().expand(movement),
+          imageInfo.rect,
+          "resize",
+          { targetRatio, resizeNoFlip: true, ratioCorrectBasedOn: "width" },
+        );
+        setClip(newClip);
+      },
       onImageLoad: () => {
-        const image = generateImageInfo(imageElementRef.current);
-        setImage(image);
-        setClip(adjustClip(allClip, "both", image, targetRatio));
+        if (imageElement == null) throw new UiLogicError();
+        const image = generateImageInfo(imageElement);
+        setImageInfo(image);
+        setClip(
+          geometry.adjustRectToContainer(Rect.max, image.rect, "both", {
+            targetRatio,
+          }),
+        );
       },
     },
   };
@@ -261,17 +173,17 @@ const imageCropperHandlerSize = 15;
 
 export function ImageCropper(props: ImageCropperProps) {
   function convertClipToElement(
-    clip: Clip,
+    clip: Rect,
     imageElement: HTMLImageElement,
-  ): Clip {
+  ): Rect {
     const xRatio = imageElement.clientWidth / imageElement.naturalWidth;
     const yRatio = imageElement.clientHeight / imageElement.naturalHeight;
-    return {
+    return Rect.from({
       left: xRatio * clip.left,
       top: yRatio * clip.top,
       width: xRatio * clip.width,
       height: yRatio * clip.height,
-    };
+    });
   }
 
   function convertMovementFromElement(
@@ -289,7 +201,7 @@ export function ImageCropper(props: ImageCropperProps) {
   const {
     clip,
     image,
-    imageElementRef,
+    imageElementCallback,
     onImageLoad,
     onMove,
     onResize,
@@ -297,18 +209,21 @@ export function ImageCropper(props: ImageCropperProps) {
   } = props;
 
   const pointerStateRef = useRef<PointerState | null>(null);
+  const [imageElement, setImageElement] = useState<HTMLImageElement | null>(
+    null,
+  );
 
-  const clipInElement =
-    imageElementRef.current != null
-      ? convertClipToElement(clip, imageElementRef.current)
-      : emptyClip;
+  const clipInElement: Rect =
+    imageElement != null
+      ? convertClipToElement(clip, imageElement)
+      : Rect.empty;
 
   const actOnMovement = (
     e: PointerEvent,
     change: (movement: Movement) => void,
   ) => {
     if (
-      imageElementRef.current == null ||
+      imageElement == null ||
       pointerStateRef.current == null ||
       pointerStateRef.current.pointerId != e.pointerId
     ) {
@@ -322,12 +237,14 @@ export function ImageCropper(props: ImageCropperProps) {
       y: e.clientY - y,
     };
 
-    change(movement);
+    pointerStateRef.current.x = e.clientX;
+    pointerStateRef.current.y = e.clientY;
+
+    change(convertMovementFromElement(movement, imageElement));
   };
 
   const onPointerDown = (e: PointerEvent) => {
-    if (imageElementRef.current == null || pointerStateRef.current != null)
-      return;
+    if (imageElement == null || pointerStateRef.current != null) return;
 
     e.currentTarget.setPointerCapture(e.pointerId);
 
@@ -362,16 +279,27 @@ export function ImageCropper(props: ImageCropperProps) {
     <div
       className={classnames("cru-image-cropper-container", containerClassName)}
     >
-      <BlobImage imgRef={imageElementRef} src={image} onLoad={onImageLoad} />
+      <BlobImage
+        imgRef={(element) => {
+          setImageElement(element);
+          imageElementCallback(element);
+        }}
+        src={image}
+        onLoad={onImageLoad}
+      />
       <div className="cru-image-cropper-mask-container">
         <div
           className="cru-image-cropper-mask"
-          style={{
-            left: clipInElement.left,
-            top: clipInElement.top,
-            width: clipInElement.width,
-            height: clipInElement.height,
-          }}
+          style={
+            clipInElement == null
+              ? undefined
+              : {
+                  left: clipInElement.left,
+                  top: clipInElement.top,
+                  width: clipInElement.width,
+                  height: clipInElement.height,
+                }
+          }
           onPointerMove={onMaskPointerMove}
           onPointerDown={onPointerDown}
           onPointerUp={onPointerUp}
-- 
cgit v1.2.3

