aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/components
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-08-28 21:14:20 +0800
committercrupest <crupest@outlook.com>2023-08-28 21:14:20 +0800
commit877f4ff87c39e3484ae2e7e6c920fc7fb8c04c23 (patch)
treec24b04031d7c065c784d4bff6692b98f1b860578 /FrontEnd/src/components
parentb66a57071316434356e77e294ec22181e4db54d5 (diff)
downloadtimeline-877f4ff87c39e3484ae2e7e6c920fc7fb8c04c23.tar.gz
timeline-877f4ff87c39e3484ae2e7e6c920fc7fb8c04c23.tar.bz2
timeline-877f4ff87c39e3484ae2e7e6c920fc7fb8c04c23.zip
...
Diffstat (limited to 'FrontEnd/src/components')
-rw-r--r--FrontEnd/src/components/ImageCropper.tsx262
-rw-r--r--FrontEnd/src/components/common.ts2
2 files changed, 97 insertions, 167 deletions
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}
diff --git a/FrontEnd/src/components/common.ts b/FrontEnd/src/components/common.ts
index b96388ab..171ebc48 100644
--- a/FrontEnd/src/components/common.ts
+++ b/FrontEnd/src/components/common.ts
@@ -11,3 +11,5 @@ export const themeColors = [
export type ThemeColor = (typeof themeColors)[number];
export { breakpoints } from "./breakpoints";
+
+export * as geometry from "~src/utilities/geometry";