Spaces:
Configuration error
Configuration error
| import React, { useEffect, useState } from 'react' | |
| import { useRecoilState, useRecoilValue } from 'recoil' | |
| import { | |
| croperHeight, | |
| croperWidth, | |
| croperX, | |
| croperY, | |
| isInpaintingState, | |
| } from '../../store/Atoms' | |
| const DOC_MOVE_OPTS = { capture: true, passive: false } | |
| const DRAG_HANDLE_BORDER = 2 | |
| const DRAG_HANDLE_SHORT = 12 | |
| const DRAG_HANDLE_LONG = 40 | |
| interface EVData { | |
| initX: number | |
| initY: number | |
| initHeight: number | |
| initWidth: number | |
| startResizeX: number | |
| startResizeY: number | |
| ord: string // top/right/bottom/left | |
| } | |
| interface Props { | |
| maxHeight: number | |
| maxWidth: number | |
| scale: number | |
| minHeight: number | |
| minWidth: number | |
| show: boolean | |
| } | |
| const clamp = ( | |
| newPos: number, | |
| newLength: number, | |
| oldPos: number, | |
| oldLength: number, | |
| minLength: number, | |
| maxLength: number | |
| ) => { | |
| if (newPos !== oldPos && newLength === oldLength) { | |
| if (newPos < 0) { | |
| return [0, oldLength] | |
| } | |
| if (newPos + newLength > maxLength) { | |
| return [maxLength - oldLength, oldLength] | |
| } | |
| } else { | |
| if (newLength < minLength) { | |
| if (newPos === oldPos) { | |
| return [newPos, minLength] | |
| } | |
| return [newPos + newLength - minLength, minLength] | |
| } | |
| if (newPos < 0) { | |
| return [0, newPos + newLength] | |
| } | |
| if (newPos + newLength > maxLength) { | |
| return [newPos, maxLength - newPos] | |
| } | |
| } | |
| return [newPos, newLength] | |
| } | |
| const Croper = (props: Props) => { | |
| const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props | |
| const [x, setX] = useRecoilState(croperX) | |
| const [y, setY] = useRecoilState(croperY) | |
| const [height, setHeight] = useRecoilState(croperHeight) | |
| const [width, setWidth] = useRecoilState(croperWidth) | |
| const isInpainting = useRecoilValue(isInpaintingState) | |
| const [isResizing, setIsResizing] = useState(false) | |
| const [isMoving, setIsMoving] = useState(false) | |
| useEffect(() => { | |
| setX(Math.round((maxWidth - 512) / 2)) | |
| setY(Math.round((maxHeight - 512) / 2)) | |
| }, [maxHeight, maxWidth]) | |
| const [evData, setEVData] = useState<EVData>({ | |
| initX: 0, | |
| initY: 0, | |
| initHeight: 0, | |
| initWidth: 0, | |
| startResizeX: 0, | |
| startResizeY: 0, | |
| ord: 'top', | |
| }) | |
| const onDragFocus = () => { | |
| console.log('focus') | |
| } | |
| const clampLeftRight = (newX: number, newWidth: number) => { | |
| return clamp(newX, newWidth, x, width, minWidth, maxWidth) | |
| } | |
| const clampTopBottom = (newY: number, newHeight: number) => { | |
| return clamp(newY, newHeight, y, height, minHeight, maxHeight) | |
| } | |
| const onPointerMove = (e: PointerEvent) => { | |
| if (isInpainting) { | |
| return | |
| } | |
| const curX = e.clientX | |
| const curY = e.clientY | |
| const offsetY = Math.round((curY - evData.startResizeY) / scale) | |
| const offsetX = Math.round((curX - evData.startResizeX) / scale) | |
| const moveTop = () => { | |
| const newHeight = evData.initHeight - offsetY | |
| const newY = evData.initY + offsetY | |
| const [clampedY, clampedHeight] = clampTopBottom(newY, newHeight) | |
| setHeight(clampedHeight) | |
| setY(clampedY) | |
| } | |
| const moveBottom = () => { | |
| const newHeight = evData.initHeight + offsetY | |
| const [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight) | |
| setHeight(clampedHeight) | |
| setY(clampedY) | |
| } | |
| const moveLeft = () => { | |
| const newWidth = evData.initWidth - offsetX | |
| const newX = evData.initX + offsetX | |
| const [clampedX, clampedWidth] = clampLeftRight(newX, newWidth) | |
| setWidth(clampedWidth) | |
| setX(clampedX) | |
| } | |
| const moveRight = () => { | |
| const newWidth = evData.initWidth + offsetX | |
| const [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth) | |
| setWidth(clampedWidth) | |
| setX(clampedX) | |
| } | |
| if (isResizing) { | |
| switch (evData.ord) { | |
| case 'topleft': { | |
| moveTop() | |
| moveLeft() | |
| break | |
| } | |
| case 'topright': { | |
| moveTop() | |
| moveRight() | |
| break | |
| } | |
| case 'bottomleft': { | |
| moveBottom() | |
| moveLeft() | |
| break | |
| } | |
| case 'bottomright': { | |
| moveBottom() | |
| moveRight() | |
| break | |
| } | |
| case 'top': { | |
| moveTop() | |
| break | |
| } | |
| case 'right': { | |
| moveRight() | |
| break | |
| } | |
| case 'bottom': { | |
| moveBottom() | |
| break | |
| } | |
| case 'left': { | |
| moveLeft() | |
| break | |
| } | |
| default: | |
| break | |
| } | |
| } | |
| if (isMoving) { | |
| const newX = evData.initX + offsetX | |
| const newY = evData.initY + offsetY | |
| const [clampedX, clampedWidth] = clampLeftRight(newX, evData.initWidth) | |
| const [clampedY, clampedHeight] = clampTopBottom(newY, evData.initHeight) | |
| setWidth(clampedWidth) | |
| setHeight(clampedHeight) | |
| setX(clampedX) | |
| setY(clampedY) | |
| } | |
| } | |
| const onPointerDone = (e: PointerEvent) => { | |
| if (isResizing) { | |
| setIsResizing(false) | |
| } | |
| if (isMoving) { | |
| setIsMoving(false) | |
| } | |
| } | |
| useEffect(() => { | |
| if (isResizing || isMoving) { | |
| document.addEventListener('pointermove', onPointerMove, DOC_MOVE_OPTS) | |
| document.addEventListener('pointerup', onPointerDone, DOC_MOVE_OPTS) | |
| document.addEventListener('pointercancel', onPointerDone, DOC_MOVE_OPTS) | |
| return () => { | |
| document.removeEventListener( | |
| 'pointermove', | |
| onPointerMove, | |
| DOC_MOVE_OPTS | |
| ) | |
| document.removeEventListener('pointerup', onPointerDone, DOC_MOVE_OPTS) | |
| document.removeEventListener( | |
| 'pointercancel', | |
| onPointerDone, | |
| DOC_MOVE_OPTS | |
| ) | |
| } | |
| } | |
| }, [isResizing, isMoving, width, height, evData]) | |
| const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { | |
| const { ord } = (e.target as HTMLElement).dataset | |
| if (ord) { | |
| setIsResizing(true) | |
| setEVData({ | |
| initX: x, | |
| initY: y, | |
| initHeight: height, | |
| initWidth: width, | |
| startResizeX: e.clientX, | |
| startResizeY: e.clientY, | |
| ord, | |
| }) | |
| } | |
| } | |
| const createCropSelection = () => { | |
| return ( | |
| <div | |
| className="drag-elements" | |
| onFocus={onDragFocus} | |
| onPointerDown={onCropPointerDown} | |
| > | |
| <div | |
| className="drag-bar ord-top" | |
| data-ord="top" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-bar ord-right" | |
| data-ord="right" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-bar ord-bottom" | |
| data-ord="bottom" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-bar ord-left" | |
| data-ord="left" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-topleft" | |
| data-ord="topleft" | |
| aria-label="topleft" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-topright" | |
| data-ord="topright" | |
| aria-label="topright" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-bottomleft" | |
| data-ord="bottomleft" | |
| aria-label="bottomleft" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-bottomright" | |
| data-ord="bottomright" | |
| aria-label="bottomright" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-top" | |
| data-ord="top" | |
| aria-label="top" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-right" | |
| data-ord="right" | |
| aria-label="right" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-bottom" | |
| data-ord="bottom" | |
| aria-label="bottom" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| <div | |
| className="drag-handle ord-left" | |
| data-ord="left" | |
| aria-label="left" | |
| tabIndex={0} | |
| role="button" | |
| style={{ transform: `scale(${1 / scale})` }} | |
| /> | |
| </div> | |
| ) | |
| } | |
| const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { | |
| setIsMoving(true) | |
| setEVData({ | |
| initX: x, | |
| initY: y, | |
| initHeight: height, | |
| initWidth: width, | |
| startResizeX: e.clientX, | |
| startResizeY: e.clientY, | |
| ord: '', | |
| }) | |
| } | |
| const createInfoBar = () => { | |
| return ( | |
| <div | |
| className="info-bar" | |
| onPointerDown={onInfoBarPointerDown} | |
| style={{ | |
| transform: `scale(${1 / scale})`, | |
| top: `${10 / scale}px`, | |
| left: `${10 / scale}px`, | |
| }} | |
| > | |
| <div className="crop-size"> | |
| {width} x {height} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| const createBorder = () => { | |
| return ( | |
| <div | |
| className="crop-border" | |
| style={{ | |
| height, | |
| width, | |
| outlineWidth: `${DRAG_HANDLE_BORDER / scale}px`, | |
| }} | |
| /> | |
| ) | |
| } | |
| return ( | |
| <div | |
| className="croper-wrapper" | |
| style={{ visibility: show ? 'visible' : 'hidden' }} | |
| > | |
| <div className="croper" style={{ height, width, left: x, top: y }}> | |
| {createBorder()} | |
| {createInfoBar()} | |
| {createCropSelection()} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default Croper | |