wip
This commit is contained in:
126
lama_cleaner/app/src/components/Croper/Croper.scss
Normal file
126
lama_cleaner/app/src/components/Croper/Croper.scss
Normal file
@@ -0,0 +1,126 @@
|
||||
@use 'sass:math';
|
||||
|
||||
$drag-handle-shortside: 12px;
|
||||
$drag-handle-longside: 40px;
|
||||
|
||||
$half-handle-shortside: math.div($drag-handle-shortside, 2);
|
||||
$half-handle-longside: math.div($drag-handle-longside, 2);
|
||||
|
||||
.crop-border {
|
||||
outline-color: var(--yellow-accent);
|
||||
outline-style: dashed;
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
font-size: 1rem;
|
||||
padding: 0.2rem 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--text-color);
|
||||
background-color: var(--page-bg);
|
||||
border-radius: 9999px;
|
||||
|
||||
border: var(--editor-toolkit-panel-border);
|
||||
box-shadow: 0 0 0 1px #0000001a, 0 3px 16px #00000014, 0 2px 6px 1px #00000017;
|
||||
|
||||
&:hover {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.croper-wrapper {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.croper {
|
||||
position: relative;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
width: $drag-handle-shortside;
|
||||
height: $drag-handle-shortside;
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
border: 2px solid var(--yellow-accent);
|
||||
background-color: var(--yellow-accent-light);
|
||||
pointer-events: auto;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--yellow-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.ord-topleft {
|
||||
cursor: nw-resize;
|
||||
top: (-$half-handle-shortside)-1px;
|
||||
left: (-$half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-topright {
|
||||
cursor: ne-resize;
|
||||
top: -($half-handle-shortside)-1px;
|
||||
right: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-bottomright {
|
||||
cursor: se-resize;
|
||||
bottom: -($half-handle-shortside)-1px;
|
||||
right: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-bottomleft {
|
||||
cursor: sw-resize;
|
||||
bottom: -($half-handle-shortside)-1px;
|
||||
left: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-top,
|
||||
.ord-bottom {
|
||||
left: calc(50% - $half-handle-shortside);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ord-top {
|
||||
top: (-$half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-bottom {
|
||||
bottom: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-left,
|
||||
.ord-right {
|
||||
top: calc(50% - $half-handle-shortside);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.ord-left {
|
||||
left: (-$half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-right {
|
||||
right: -($half-handle-shortside)-1px;
|
||||
}
|
||||
326
lama_cleaner/app/src/components/Croper/Croper.tsx
Normal file
326
lama_cleaner/app/src/components/Croper/Croper.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'
|
||||
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
|
||||
}
|
||||
|
||||
const Croper = (props: Props) => {
|
||||
const { minHeight, minWidth, maxHeight, maxWidth, scale } = 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, minHeight, minWidth])
|
||||
|
||||
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 checkTopBottomLimit = (newY: number, newHeight: number) => {
|
||||
if (newY > 0 && newHeight > minHeight && newY + newHeight <= maxHeight) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkLeftRightLimit = (newX: number, newWidth: number) => {
|
||||
if (newX > 0 && newWidth > minWidth && newX + newWidth <= maxWidth) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
const curX = e.clientX
|
||||
const curY = e.clientY
|
||||
if (isResizing) {
|
||||
switch (evData.ord) {
|
||||
case 'top': {
|
||||
// TODO: 添加四个角以及 drag bar handle
|
||||
const offset = Math.round((curY - evData.startResizeY) / scale)
|
||||
const newHeight = evData.initHeight - offset
|
||||
const newY = evData.initY + offset
|
||||
if (checkTopBottomLimit(newY, newHeight)) {
|
||||
setHeight(newHeight)
|
||||
setY(newY)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'right': {
|
||||
const offset = Math.round((curX - evData.startResizeX) / scale)
|
||||
const newWidth = evData.initWidth + offset
|
||||
if (checkLeftRightLimit(evData.initX, newWidth)) {
|
||||
setWidth(newWidth)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'bottom': {
|
||||
const offset = Math.round((curY - evData.startResizeY) / scale)
|
||||
const newHeight = evData.initHeight + offset
|
||||
if (checkTopBottomLimit(evData.initY, newHeight)) {
|
||||
setHeight(newHeight)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'left': {
|
||||
const offset = Math.round((curX - evData.startResizeX) / scale)
|
||||
const newWidth = evData.initWidth - offset
|
||||
const newX = evData.initX + offset
|
||||
if (checkLeftRightLimit(newX, newWidth)) {
|
||||
setWidth(newWidth)
|
||||
setX(newX)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (isMoving) {
|
||||
const offsetX = Math.round((curX - evData.startResizeX) / scale)
|
||||
const offsetY = Math.round((curY - evData.startResizeY) / scale)
|
||||
const newX = evData.initX + offsetX
|
||||
const newY = evData.initY + offsetY
|
||||
if (
|
||||
checkLeftRightLimit(newX, evData.initWidth) &&
|
||||
checkTopBottomLimit(newY, evData.initHeight)
|
||||
) {
|
||||
setX(newX)
|
||||
setY(newY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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-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: `${-28 / scale - 14}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">
|
||||
<div className="croper" style={{ height, width, left: x, top: y }}>
|
||||
{createBorder()}
|
||||
{createInfoBar()}
|
||||
{createCropSelection()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Croper
|
||||
Reference in New Issue
Block a user