415 lines
10 KiB
TypeScript
415 lines
10 KiB
TypeScript
import { useStore } from "@/lib/states"
|
|
import { ExtenderDirection } from "@/lib/types"
|
|
import { cn } from "@/lib/utils"
|
|
import React, { useEffect, useState } from "react"
|
|
import { twMerge } from "tailwind-merge"
|
|
|
|
const DOC_MOVE_OPTS = { capture: true, passive: false }
|
|
|
|
const DRAG_HANDLE_BORDER = 2
|
|
|
|
interface EVData {
|
|
initX: number
|
|
initY: number
|
|
initHeight: number
|
|
initWidth: number
|
|
startResizeX: number
|
|
startResizeY: number
|
|
ord: string // top/right/bottom/left
|
|
}
|
|
|
|
interface Props {
|
|
scale: number
|
|
minHeight: number
|
|
minWidth: number
|
|
show: boolean
|
|
}
|
|
|
|
const clamp = (
|
|
newPos: number,
|
|
newLength: number,
|
|
oldPos: number,
|
|
minLength: number
|
|
) => {
|
|
if (newLength < minLength) {
|
|
if (newPos === oldPos) {
|
|
return [newPos, minLength]
|
|
}
|
|
return [newPos + newLength - minLength, minLength]
|
|
}
|
|
|
|
return [newPos, newLength]
|
|
}
|
|
|
|
const Extender = (props: Props) => {
|
|
const { minHeight, minWidth, scale, show } = props
|
|
|
|
const [
|
|
isInpainting,
|
|
imageHeight,
|
|
imageWdith,
|
|
isSD,
|
|
{ x, y, width, height },
|
|
setX,
|
|
setY,
|
|
setWidth,
|
|
setHeight,
|
|
extenderDirection,
|
|
isResizing,
|
|
setIsResizing,
|
|
] = useStore((state) => [
|
|
state.isInpainting,
|
|
state.imageHeight,
|
|
state.imageWidth,
|
|
state.isSD(),
|
|
state.extenderState,
|
|
state.setExtenderX,
|
|
state.setExtenderY,
|
|
state.setExtenderWidth,
|
|
state.setExtenderHeight,
|
|
state.settings.extenderDirection,
|
|
state.isCropperExtenderResizing,
|
|
state.setIsCropperExtenderResizing,
|
|
])
|
|
|
|
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, minWidth)
|
|
}
|
|
|
|
const clampTopBottom = (newY: number, newHeight: number) => {
|
|
return clamp(newY, newHeight, y, minHeight)
|
|
}
|
|
|
|
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
|
|
let clampedY = newY
|
|
let clampedHeight = newHeight
|
|
if (extenderDirection === ExtenderDirection.xy) {
|
|
if (clampedY > 0) {
|
|
clampedY = 0
|
|
clampedHeight = evData.initHeight - Math.abs(evData.initY)
|
|
}
|
|
} else {
|
|
const clamped = clampTopBottom(newY, newHeight)
|
|
clampedY = clamped[0]
|
|
clampedHeight = clamped[1]
|
|
}
|
|
setHeight(clampedHeight)
|
|
setY(clampedY)
|
|
}
|
|
|
|
const moveBottom = () => {
|
|
const newHeight = evData.initHeight + offsetY
|
|
let [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
|
|
if (extenderDirection === ExtenderDirection.xy) {
|
|
if (clampedHeight < Math.abs(clampedY) + imageHeight) {
|
|
clampedHeight = Math.abs(clampedY) + imageHeight
|
|
}
|
|
}
|
|
setHeight(clampedHeight)
|
|
setY(clampedY)
|
|
}
|
|
|
|
const moveLeft = () => {
|
|
const newWidth = evData.initWidth - offsetX
|
|
const newX = evData.initX + offsetX
|
|
let clampedX = newX
|
|
let clampedWidth = newWidth
|
|
if (extenderDirection === ExtenderDirection.xy) {
|
|
if (clampedX > 0) {
|
|
clampedX = 0
|
|
clampedWidth = evData.initWidth - Math.abs(evData.initX)
|
|
}
|
|
} else {
|
|
const clamped = clampLeftRight(newX, newWidth)
|
|
clampedX = clamped[0]
|
|
clampedWidth = clamped[1]
|
|
}
|
|
setWidth(clampedWidth)
|
|
setX(clampedX)
|
|
}
|
|
|
|
const moveRight = () => {
|
|
const newWidth = evData.initWidth + offsetX
|
|
let [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
|
|
if (extenderDirection === ExtenderDirection.xy) {
|
|
if (clampedWidth < Math.abs(clampedX) + imageWdith) {
|
|
clampedWidth = Math.abs(clampedX) + imageWdith
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
const onPointerDone = () => {
|
|
if (isResizing) {
|
|
setIsResizing(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (isResizing) {
|
|
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, 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 createDragHandle = (cursor: string, side1: string, side2: string) => {
|
|
const sideLength = 12
|
|
const halfSideLength = sideLength / 2
|
|
const draghandleCls = `w-[${sideLength}px] h-[${sideLength}px] z-[4] absolute content-[''] block border-2 border-primary borde pointer-events-auto hover:bg-primary`
|
|
|
|
let xTrans = "0"
|
|
let yTrans = "0"
|
|
|
|
let side2Key = side2
|
|
let side2Val = `${-halfSideLength}px`
|
|
if (side2 === "") {
|
|
side2Val = "50%"
|
|
if (side1 === "left" || side1 === "right") {
|
|
side2Key = "top"
|
|
yTrans = "-50%"
|
|
} else {
|
|
side2Key = "left"
|
|
xTrans = "-50%"
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(draghandleCls, cursor)}
|
|
style={{
|
|
[side1]: -halfSideLength,
|
|
[side2Key]: side2Val,
|
|
transform: `translate(${xTrans}, ${yTrans}) scale(${1 / scale})`,
|
|
}}
|
|
data-ord={side1 + side2}
|
|
aria-label={side1 + side2}
|
|
tabIndex={-1}
|
|
role="button"
|
|
/>
|
|
)
|
|
}
|
|
|
|
const createCropSelection = () => {
|
|
return (
|
|
<div
|
|
onFocus={onDragFocus}
|
|
onPointerDown={onCropPointerDown}
|
|
className="absolute top-0 h-full w-full"
|
|
>
|
|
{[ExtenderDirection.y, ExtenderDirection.xy].includes(
|
|
extenderDirection
|
|
) ? (
|
|
<>
|
|
<div
|
|
className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]"
|
|
data-ord="top"
|
|
/>
|
|
<div
|
|
className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]"
|
|
data-ord="bottom"
|
|
/>
|
|
{createDragHandle("cursor-ns-resize", "top", "")}
|
|
{createDragHandle("cursor-ns-resize", "bottom", "")}
|
|
</>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
{[ExtenderDirection.x, ExtenderDirection.xy].includes(
|
|
extenderDirection
|
|
) ? (
|
|
<>
|
|
<div
|
|
className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
|
|
data-ord="right"
|
|
/>
|
|
<div
|
|
className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]"
|
|
data-ord="left"
|
|
/>
|
|
{createDragHandle("cursor-ew-resize", "left", "")}
|
|
{createDragHandle("cursor-ew-resize", "right", "")}
|
|
</>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
{extenderDirection === ExtenderDirection.xy ? (
|
|
<>
|
|
{createDragHandle("cursor-nw-resize", "top", "left")}
|
|
{createDragHandle("cursor-ne-resize", "top", "right")}
|
|
{createDragHandle("cursor-sw-resize", "bottom", "left")}
|
|
{createDragHandle("cursor-se-resize", "bottom", "right")}
|
|
</>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
setEVData({
|
|
initX: x,
|
|
initY: y,
|
|
initHeight: height,
|
|
initWidth: width,
|
|
startResizeX: e.clientX,
|
|
startResizeY: e.clientY,
|
|
ord: "",
|
|
})
|
|
}
|
|
|
|
const createInfoBar = () => {
|
|
return (
|
|
<div
|
|
className={twMerge(
|
|
"border absolute pointer-events-auto px-2 py-1 rounded-full bg-background",
|
|
"origin-top-left top-0 left-0"
|
|
)}
|
|
style={{
|
|
transform: `scale(${(1 / scale) * 0.8})`,
|
|
}}
|
|
onPointerDown={onInfoBarPointerDown}
|
|
>
|
|
{/* TODO: 移动的时候会显示 brush */}
|
|
{width} x {height}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const createBorder = () => {
|
|
return (
|
|
<div
|
|
className={cn("outline-dashed outline-primary")}
|
|
style={{
|
|
height,
|
|
width,
|
|
outlineWidth: `${(DRAG_HANDLE_BORDER / scale) * 1.3}px`,
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (show === false || !isSD) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="absolute h-full w-full pointer-events-none z-[2]">
|
|
<div
|
|
className="relative pointer-events-none z-[2] [box-shadow:0_0_0_9999px_rgba(0,_0,_0,_0.5)]"
|
|
style={{ height, width, left: x, top: y }}
|
|
>
|
|
{createBorder()}
|
|
{createInfoBar()}
|
|
{createCropSelection()}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Extender
|