2022-02-06 14:40:49 +01:00
|
|
|
import {
|
|
|
|
ArrowsExpandIcon,
|
|
|
|
DownloadIcon,
|
|
|
|
EyeIcon,
|
|
|
|
} from '@heroicons/react/outline'
|
2022-02-06 03:37:22 +01:00
|
|
|
import React, {
|
|
|
|
SyntheticEvent,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react'
|
|
|
|
import {
|
|
|
|
ReactZoomPanPinchRef,
|
|
|
|
TransformComponent,
|
|
|
|
TransformWrapper,
|
|
|
|
} from 'react-zoom-pan-pinch'
|
2022-02-06 12:27:49 +01:00
|
|
|
import {
|
|
|
|
useWindowSize,
|
|
|
|
useLocalStorage,
|
|
|
|
useKey,
|
|
|
|
useKeyPressEvent,
|
|
|
|
} from 'react-use'
|
2021-11-15 08:22:34 +01:00
|
|
|
import inpaint from './adapters/inpainting'
|
|
|
|
import Button from './components/Button'
|
|
|
|
import Slider from './components/Slider'
|
2021-11-27 13:37:37 +01:00
|
|
|
import SizeSelector from './components/SizeSelector'
|
|
|
|
import { downloadImage, loadImage, useImage } from './utils'
|
2021-11-15 08:22:34 +01:00
|
|
|
|
|
|
|
const TOOLBAR_SIZE = 200
|
|
|
|
const BRUSH_COLOR = 'rgba(189, 255, 1, 0.75)'
|
2022-02-06 06:50:26 +01:00
|
|
|
// const NO_COLOR = 'rgba(255,255,255,0)'
|
2021-11-15 08:22:34 +01:00
|
|
|
|
|
|
|
interface EditorProps {
|
|
|
|
file: File
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Line {
|
|
|
|
size?: number
|
|
|
|
pts: { x: number; y: number }[]
|
|
|
|
}
|
|
|
|
|
|
|
|
function drawLines(
|
|
|
|
ctx: CanvasRenderingContext2D,
|
|
|
|
lines: Line[],
|
|
|
|
color = BRUSH_COLOR
|
|
|
|
) {
|
|
|
|
ctx.strokeStyle = color
|
|
|
|
ctx.lineCap = 'round'
|
|
|
|
ctx.lineJoin = 'round'
|
|
|
|
|
|
|
|
lines.forEach(line => {
|
|
|
|
if (!line?.pts.length || !line.size) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.lineWidth = line.size
|
|
|
|
ctx.beginPath()
|
|
|
|
ctx.moveTo(line.pts[0].x, line.pts[0].y)
|
|
|
|
line.pts.forEach(pt => ctx.lineTo(pt.x, pt.y))
|
|
|
|
ctx.stroke()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function Editor(props: EditorProps) {
|
|
|
|
const { file } = props
|
|
|
|
const [brushSize, setBrushSize] = useState(40)
|
|
|
|
const [original, isOriginalLoaded] = useImage(file)
|
|
|
|
const [renders, setRenders] = useState<HTMLImageElement[]>([])
|
|
|
|
const [context, setContext] = useState<CanvasRenderingContext2D>()
|
|
|
|
const [maskCanvas] = useState<HTMLCanvasElement>(() => {
|
|
|
|
return document.createElement('canvas')
|
|
|
|
})
|
|
|
|
const [lines, setLines] = useState<Line[]>([{ pts: [] }])
|
2021-12-09 05:24:03 +01:00
|
|
|
const [lines4Show, setLines4Show] = useState<Line[]>([{ pts: [] }])
|
|
|
|
const [historyLineCount, setHistoryLineCount] = useState<number[]>([])
|
2021-11-15 08:22:34 +01:00
|
|
|
const [{ x, y }, setCoords] = useState({ x: -1, y: -1 })
|
|
|
|
const [showBrush, setShowBrush] = useState(false)
|
2022-02-06 12:52:45 +01:00
|
|
|
const [isPanning, setIsPanning] = useState<boolean>(false)
|
2021-11-15 08:22:34 +01:00
|
|
|
const [showOriginal, setShowOriginal] = useState(false)
|
|
|
|
const [isInpaintingLoading, setIsInpaintingLoading] = useState(false)
|
|
|
|
const [showSeparator, setShowSeparator] = useState(false)
|
2022-02-06 03:37:22 +01:00
|
|
|
const [scale, setScale] = useState<number>(1)
|
|
|
|
const [minScale, setMinScale] = useState<number>()
|
2021-11-27 13:37:37 +01:00
|
|
|
// ['1080', '2000', 'Original']
|
|
|
|
const [sizeLimit, setSizeLimit] = useLocalStorage('sizeLimit', '1080')
|
2021-11-15 08:22:34 +01:00
|
|
|
const windowSize = useWindowSize()
|
2022-02-06 03:37:22 +01:00
|
|
|
const viewportRef = useRef<ReactZoomPanPinchRef | undefined | null>()
|
2021-11-15 08:22:34 +01:00
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const [isDraging, setIsDraging] = useState(false)
|
|
|
|
const [isMultiStrokeKeyPressed, setIsMultiStrokeKeyPressed] = useState(false)
|
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
const draw = useCallback(() => {
|
|
|
|
if (!context) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
context.clearRect(0, 0, context.canvas.width, context.canvas.height)
|
|
|
|
const currRender = renders[renders.length - 1]
|
|
|
|
if (currRender?.src) {
|
|
|
|
context.drawImage(currRender, 0, 0)
|
|
|
|
} else {
|
|
|
|
context.drawImage(original, 0, 0)
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
drawLines(context, lines4Show)
|
|
|
|
}, [context, lines4Show, original, renders])
|
2021-11-15 08:22:34 +01:00
|
|
|
|
|
|
|
const refreshCanvasMask = useCallback(() => {
|
|
|
|
if (!context?.canvas.width || !context?.canvas.height) {
|
|
|
|
throw new Error('canvas has invalid size')
|
|
|
|
}
|
|
|
|
maskCanvas.width = context?.canvas.width
|
|
|
|
maskCanvas.height = context?.canvas.height
|
|
|
|
const ctx = maskCanvas.getContext('2d')
|
|
|
|
if (!ctx) {
|
|
|
|
throw new Error('could not retrieve mask canvas')
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
drawLines(ctx, lines, 'white')
|
|
|
|
}, [context?.canvas.height, context?.canvas.width, lines, maskCanvas])
|
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const runInpainting = useCallback(async () => {
|
|
|
|
setIsInpaintingLoading(true)
|
|
|
|
refreshCanvasMask()
|
|
|
|
try {
|
|
|
|
const res = await inpaint(file, maskCanvas.toDataURL(), sizeLimit)
|
|
|
|
if (!res) {
|
|
|
|
throw new Error('empty response')
|
|
|
|
}
|
|
|
|
// TODO: fix the render if it failed loading
|
|
|
|
const newRender = new Image()
|
|
|
|
await loadImage(newRender, res)
|
|
|
|
renders.push(newRender)
|
|
|
|
lines.push({ pts: [] } as Line)
|
|
|
|
setRenders([...renders])
|
|
|
|
setLines([...lines])
|
|
|
|
|
|
|
|
historyLineCount.push(lines4Show.length)
|
|
|
|
setHistoryLineCount(historyLineCount)
|
|
|
|
lines4Show.length = 0
|
|
|
|
setLines4Show([{ pts: [] } as Line])
|
|
|
|
} catch (e: any) {
|
|
|
|
// eslint-disable-next-line
|
|
|
|
alert(e.message ? e.message : e.toString())
|
|
|
|
}
|
|
|
|
setIsInpaintingLoading(false)
|
|
|
|
draw()
|
|
|
|
}, [
|
|
|
|
draw,
|
|
|
|
file,
|
|
|
|
lines,
|
|
|
|
lines4Show,
|
|
|
|
maskCanvas,
|
|
|
|
refreshCanvasMask,
|
|
|
|
renders,
|
|
|
|
sizeLimit,
|
|
|
|
historyLineCount,
|
|
|
|
])
|
|
|
|
|
2022-02-06 06:50:26 +01:00
|
|
|
const hadDrawSomething = () => {
|
|
|
|
return lines4Show.length !== 0 && lines4Show[0].pts.length !== 0
|
|
|
|
}
|
|
|
|
|
2022-02-06 12:27:49 +01:00
|
|
|
const hadRunInpainting = () => {
|
|
|
|
return renders.length !== 0
|
|
|
|
}
|
|
|
|
|
2022-02-06 06:50:26 +01:00
|
|
|
const clearDrawing = () => {
|
|
|
|
setIsDraging(false)
|
|
|
|
lines4Show.length = 0
|
|
|
|
setLines4Show([{ pts: [] } as Line])
|
|
|
|
}
|
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const handleMultiStrokeKeyDown = () => {
|
|
|
|
if (isInpaintingLoading) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
setIsMultiStrokeKeyPressed(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleMultiStrokeKeyup = () => {
|
|
|
|
if (!isMultiStrokeKeyPressed) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (isInpaintingLoading) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
setIsMultiStrokeKeyPressed(false)
|
2022-02-06 06:50:26 +01:00
|
|
|
if (hadDrawSomething()) {
|
2021-12-09 05:24:03 +01:00
|
|
|
runInpainting()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const predicate = (event: KeyboardEvent) => {
|
|
|
|
return event.key === 'Control' || event.key === 'Meta'
|
|
|
|
}
|
2022-02-06 06:50:26 +01:00
|
|
|
|
|
|
|
useKey(predicate, handleMultiStrokeKeyup, { event: 'keyup' }, [
|
|
|
|
isInpaintingLoading,
|
|
|
|
isMultiStrokeKeyPressed,
|
|
|
|
hadDrawSomething,
|
|
|
|
])
|
|
|
|
|
|
|
|
useKey(
|
|
|
|
predicate,
|
|
|
|
handleMultiStrokeKeyDown,
|
|
|
|
{
|
|
|
|
event: 'keydown',
|
|
|
|
},
|
|
|
|
[isInpaintingLoading]
|
|
|
|
)
|
2021-12-09 05:24:03 +01:00
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
// Draw once the original image is loaded
|
|
|
|
useEffect(() => {
|
2022-02-06 03:37:22 +01:00
|
|
|
if (!original) {
|
2021-11-15 08:22:34 +01:00
|
|
|
return
|
|
|
|
}
|
2022-02-06 03:37:22 +01:00
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
if (isOriginalLoaded) {
|
|
|
|
const rW = windowSize.width / original.naturalWidth
|
|
|
|
const rH = (windowSize.height - TOOLBAR_SIZE) / original.naturalHeight
|
|
|
|
if (rW < 1 || rH < 1) {
|
2022-02-06 03:37:22 +01:00
|
|
|
const s = Math.min(rW, rH)
|
|
|
|
setMinScale(s)
|
|
|
|
setScale(s)
|
2021-11-15 08:22:34 +01:00
|
|
|
} else {
|
2022-02-06 03:37:22 +01:00
|
|
|
setMinScale(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (context?.canvas) {
|
|
|
|
context.canvas.width = original.naturalWidth
|
|
|
|
context.canvas.height = original.naturalHeight
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
|
|
|
draw()
|
|
|
|
}
|
|
|
|
}, [context?.canvas, draw, original, isOriginalLoaded, windowSize])
|
|
|
|
|
2022-02-06 03:37:22 +01:00
|
|
|
// Zoom reset
|
|
|
|
const resetZoom = useCallback(() => {
|
|
|
|
if (!minScale || !original || !windowSize) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const viewport = viewportRef.current
|
|
|
|
if (!viewport) {
|
|
|
|
throw new Error('no viewport')
|
|
|
|
}
|
|
|
|
const offsetX = (windowSize.width - original.width * minScale) / 2
|
|
|
|
const offsetY = (windowSize.height - original.height * minScale) / 2
|
|
|
|
viewport.setTransform(offsetX, offsetY, minScale, 200, 'easeOutQuad')
|
2022-02-06 04:00:53 +01:00
|
|
|
setScale(minScale)
|
|
|
|
}, [minScale, original, windowSize])
|
|
|
|
|
2022-02-06 06:50:26 +01:00
|
|
|
const handleEscPressed = () => {
|
|
|
|
if (isInpaintingLoading) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (isDraging || isMultiStrokeKeyPressed) {
|
|
|
|
clearDrawing()
|
|
|
|
} else {
|
|
|
|
resetZoom()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
useKey(
|
|
|
|
'Escape',
|
|
|
|
handleEscPressed,
|
|
|
|
{
|
|
|
|
event: 'keydown',
|
|
|
|
},
|
2022-02-06 15:00:46 +01:00
|
|
|
[
|
|
|
|
isDraging,
|
|
|
|
isInpaintingLoading,
|
|
|
|
isMultiStrokeKeyPressed,
|
|
|
|
resetZoom,
|
|
|
|
clearDrawing,
|
|
|
|
]
|
2022-02-06 06:50:26 +01:00
|
|
|
)
|
2022-02-06 03:37:22 +01:00
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const onPaint = (px: number, py: number) => {
|
|
|
|
const currShowLine = lines4Show[lines4Show.length - 1]
|
|
|
|
currShowLine.pts.push({ x: px, y: py })
|
|
|
|
|
|
|
|
const currLine = lines[lines.length - 1]
|
|
|
|
currLine.pts.push({ x: px, y: py })
|
|
|
|
|
|
|
|
draw()
|
|
|
|
}
|
|
|
|
|
|
|
|
const onMouseMove = (ev: SyntheticEvent) => {
|
|
|
|
const mouseEvent = ev.nativeEvent as MouseEvent
|
|
|
|
setCoords({ x: mouseEvent.pageX, y: mouseEvent.pageY })
|
|
|
|
}
|
|
|
|
|
|
|
|
const onMouseDrag = (ev: SyntheticEvent) => {
|
2022-02-06 12:52:45 +01:00
|
|
|
if (isPanning) {
|
|
|
|
return
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
if (!isDraging) {
|
2021-11-15 08:22:34 +01:00
|
|
|
return
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
const mouseEvent = ev.nativeEvent as MouseEvent
|
|
|
|
const px = mouseEvent.offsetX
|
|
|
|
const py = mouseEvent.offsetY
|
|
|
|
onPaint(px, py)
|
|
|
|
}
|
2021-11-15 08:22:34 +01:00
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const onPointerUp = () => {
|
2022-02-06 12:52:45 +01:00
|
|
|
if (isPanning) {
|
|
|
|
return
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
if (!original.src) {
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
const canvas = context?.canvas
|
|
|
|
if (!canvas) {
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
if (isInpaintingLoading) {
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2022-02-06 06:50:26 +01:00
|
|
|
if (!isDraging) {
|
|
|
|
return
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
setIsDraging(false)
|
|
|
|
if (isMultiStrokeKeyPressed) {
|
|
|
|
lines.push({ pts: [] } as Line)
|
|
|
|
setLines([...lines])
|
|
|
|
|
|
|
|
lines4Show.push({ pts: [] } as Line)
|
|
|
|
setLines4Show([...lines4Show])
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
if (lines4Show.length !== 0 && lines4Show[0].pts.length !== 0) {
|
|
|
|
runInpainting()
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const onMouseDown = (ev: SyntheticEvent) => {
|
2022-02-06 12:52:45 +01:00
|
|
|
if (isPanning) {
|
|
|
|
return
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
if (!original.src) {
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
const canvas = context?.canvas
|
|
|
|
if (!canvas) {
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
if (isInpaintingLoading) {
|
|
|
|
return
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
setIsDraging(true)
|
|
|
|
const currLine4Show = lines4Show[lines4Show.length - 1]
|
|
|
|
currLine4Show.size = brushSize
|
|
|
|
const currLine = lines[lines.length - 1]
|
|
|
|
currLine.size = brushSize
|
2021-11-15 08:22:34 +01:00
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const mouseEvent = ev.nativeEvent as MouseEvent
|
|
|
|
onPaint(mouseEvent.offsetX, mouseEvent.offsetY)
|
|
|
|
}
|
|
|
|
|
|
|
|
const undo = () => {
|
2022-02-06 06:50:26 +01:00
|
|
|
if (!renders.length) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (!historyLineCount.length) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
const l = lines
|
2021-12-09 05:24:03 +01:00
|
|
|
const count = historyLineCount[historyLineCount.length - 1]
|
|
|
|
for (let i = 0; i <= count; i += 1) {
|
|
|
|
l.pop()
|
|
|
|
}
|
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
setLines([...l, { pts: [] }])
|
2021-12-09 05:24:03 +01:00
|
|
|
historyLineCount.pop()
|
|
|
|
setHistoryLineCount(historyLineCount)
|
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
const r = renders
|
|
|
|
r.pop()
|
|
|
|
setRenders([...r])
|
2021-12-09 05:24:03 +01:00
|
|
|
}
|
2021-11-15 08:22:34 +01:00
|
|
|
|
|
|
|
// Handle Cmd+Z
|
2021-12-09 05:24:03 +01:00
|
|
|
const undoPredicate = (event: KeyboardEvent) => {
|
|
|
|
const isCmdZ = (event.metaKey || event.ctrlKey) && event.key === 'z'
|
2022-02-06 12:27:49 +01:00
|
|
|
// Handle tab switch
|
|
|
|
if (event.key === 'Tab') {
|
|
|
|
event.preventDefault()
|
|
|
|
}
|
2021-12-09 05:24:03 +01:00
|
|
|
if (isCmdZ) {
|
|
|
|
event.preventDefault()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
useKey(undoPredicate, undo)
|
2021-11-15 08:22:34 +01:00
|
|
|
|
2022-02-06 12:27:49 +01:00
|
|
|
useKeyPressEvent(
|
|
|
|
'Tab',
|
|
|
|
ev => {
|
|
|
|
ev?.preventDefault()
|
|
|
|
ev?.stopPropagation()
|
|
|
|
if (hadRunInpainting()) {
|
|
|
|
setShowSeparator(true)
|
|
|
|
setShowOriginal(true)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
ev => {
|
|
|
|
ev?.preventDefault()
|
|
|
|
ev?.stopPropagation()
|
|
|
|
if (hadRunInpainting()) {
|
|
|
|
setShowOriginal(false)
|
|
|
|
setTimeout(() => setShowSeparator(false), 300)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
function download() {
|
|
|
|
const name = file.name.replace(/(\.[\w\d_-]+)$/i, '_cleanup$1')
|
2021-11-27 13:37:37 +01:00
|
|
|
const currRender = renders[renders.length - 1]
|
|
|
|
downloadImage(currRender.currentSrc, name)
|
|
|
|
}
|
|
|
|
|
|
|
|
const onSizeLimitChange = (_sizeLimit: string) => {
|
|
|
|
setSizeLimit(_sizeLimit)
|
2021-11-15 08:22:34 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 05:24:03 +01:00
|
|
|
const toggleShowBrush = (newState: boolean) => {
|
2022-02-06 12:52:45 +01:00
|
|
|
if (newState !== showBrush && !isPanning) {
|
2021-12-09 05:24:03 +01:00
|
|
|
setShowBrush(newState)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-06 12:52:45 +01:00
|
|
|
const getCursor = useCallback(() => {
|
|
|
|
if (isPanning) {
|
|
|
|
return 'grab'
|
|
|
|
}
|
|
|
|
if (showBrush) {
|
|
|
|
return 'none'
|
|
|
|
}
|
|
|
|
return undefined
|
|
|
|
}, [showBrush, isPanning])
|
|
|
|
|
|
|
|
// Toggle clean/zoom tool on spacebar.
|
2022-02-06 15:00:46 +01:00
|
|
|
useKeyPressEvent(
|
2022-02-06 12:52:45 +01:00
|
|
|
' ',
|
|
|
|
ev => {
|
|
|
|
ev?.preventDefault()
|
2022-02-06 14:32:54 +01:00
|
|
|
ev?.stopPropagation()
|
2022-02-06 15:00:46 +01:00
|
|
|
setShowBrush(false)
|
|
|
|
setIsPanning(true)
|
2022-02-06 12:52:45 +01:00
|
|
|
},
|
2022-02-06 15:00:46 +01:00
|
|
|
ev => {
|
|
|
|
ev?.preventDefault()
|
|
|
|
ev?.stopPropagation()
|
|
|
|
setShowBrush(true)
|
|
|
|
setIsPanning(false)
|
|
|
|
}
|
2022-02-06 12:52:45 +01:00
|
|
|
)
|
|
|
|
|
2022-02-06 03:37:22 +01:00
|
|
|
if (!original || !scale || !minScale) {
|
|
|
|
return <></>
|
|
|
|
}
|
|
|
|
|
2021-11-15 08:22:34 +01:00
|
|
|
return (
|
|
|
|
<div
|
2022-02-06 03:37:22 +01:00
|
|
|
className="flex flex-col items-center"
|
2021-11-15 08:22:34 +01:00
|
|
|
style={{
|
2022-02-06 03:37:22 +01:00
|
|
|
height: '100%',
|
|
|
|
width: '100%',
|
2021-11-15 08:22:34 +01:00
|
|
|
}}
|
2021-12-09 05:24:03 +01:00
|
|
|
aria-hidden="true"
|
|
|
|
onMouseMove={onMouseMove}
|
|
|
|
onMouseUp={onPointerUp}
|
2021-11-15 08:22:34 +01:00
|
|
|
>
|
2022-02-06 03:37:22 +01:00
|
|
|
<TransformWrapper
|
|
|
|
ref={r => {
|
|
|
|
if (r) {
|
|
|
|
viewportRef.current = r
|
|
|
|
}
|
|
|
|
}}
|
2022-02-06 12:52:45 +01:00
|
|
|
panning={{ disabled: !isPanning, velocityDisabled: true }}
|
2022-02-06 03:37:22 +01:00
|
|
|
wheel={{ step: 0.05 }}
|
|
|
|
centerZoomedOut
|
|
|
|
alignmentAnimation={{ disabled: true }}
|
|
|
|
centerOnInit
|
|
|
|
limitToBounds={false}
|
2022-02-06 06:50:26 +01:00
|
|
|
doubleClick={{ disabled: true }}
|
2022-02-06 03:37:22 +01:00
|
|
|
initialScale={minScale}
|
|
|
|
minScale={minScale}
|
|
|
|
onZoom={ref => {
|
|
|
|
setScale(ref.state.scale)
|
2021-12-09 05:24:03 +01:00
|
|
|
}}
|
2021-11-15 08:22:34 +01:00
|
|
|
>
|
2022-02-06 03:37:22 +01:00
|
|
|
<TransformComponent
|
|
|
|
wrapperStyle={{
|
|
|
|
width: '100%',
|
|
|
|
height: '100%',
|
2021-11-15 08:22:34 +01:00
|
|
|
}}
|
2022-02-06 03:37:22 +01:00
|
|
|
contentClass={
|
|
|
|
isInpaintingLoading
|
|
|
|
? 'animate-pulse-fast pointer-events-none transition-opacity'
|
|
|
|
: ''
|
|
|
|
}
|
2021-11-15 08:22:34 +01:00
|
|
|
>
|
2022-02-06 03:37:22 +01:00
|
|
|
<>
|
|
|
|
<canvas
|
|
|
|
className="rounded-sm"
|
2022-02-06 12:52:45 +01:00
|
|
|
style={{ cursor: getCursor() }}
|
2022-02-06 03:37:22 +01:00
|
|
|
onContextMenu={e => {
|
|
|
|
e.preventDefault()
|
|
|
|
}}
|
|
|
|
onMouseOver={() => toggleShowBrush(true)}
|
|
|
|
onFocus={() => toggleShowBrush(true)}
|
|
|
|
onMouseLeave={() => toggleShowBrush(false)}
|
|
|
|
onMouseDown={onMouseDown}
|
|
|
|
onMouseMove={onMouseDrag}
|
|
|
|
ref={r => {
|
|
|
|
if (r && !context) {
|
|
|
|
const ctx = r.getContext('2d')
|
|
|
|
if (ctx) {
|
|
|
|
setContext(ctx)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<div
|
|
|
|
className={[
|
|
|
|
'absolute top-0 right-0 pointer-events-none',
|
|
|
|
'overflow-hidden',
|
|
|
|
'border-primary',
|
|
|
|
showSeparator ? 'border-l-4' : '',
|
|
|
|
].join(' ')}
|
|
|
|
style={{
|
|
|
|
width: showOriginal
|
|
|
|
? `${Math.round(original.naturalWidth)}px`
|
|
|
|
: '0px',
|
|
|
|
height: original.naturalHeight,
|
|
|
|
transitionProperty: 'width, height',
|
|
|
|
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
|
|
transitionDuration: '300ms',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<img
|
|
|
|
className="absolute right-0"
|
|
|
|
src={original.src}
|
|
|
|
alt="original"
|
|
|
|
width={`${original.naturalWidth}px`}
|
|
|
|
height={`${original.naturalHeight}px`}
|
|
|
|
style={{
|
|
|
|
width: `${original.naturalWidth}px`,
|
|
|
|
height: `${original.naturalHeight}px`,
|
|
|
|
maxWidth: 'none',
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
</TransformComponent>
|
|
|
|
</TransformWrapper>
|
2021-11-15 08:22:34 +01:00
|
|
|
|
2022-02-06 12:52:45 +01:00
|
|
|
{showBrush && !isInpaintingLoading && !isPanning && (
|
2021-11-15 08:22:34 +01:00
|
|
|
<div
|
|
|
|
className="hidden sm:block absolute rounded-full border border-primary bg-primary bg-opacity-80 pointer-events-none"
|
|
|
|
style={{
|
|
|
|
width: `${brushSize * scale}px`,
|
|
|
|
height: `${brushSize * scale}px`,
|
|
|
|
left: `${x}px`,
|
|
|
|
top: `${y}px`,
|
|
|
|
transform: 'translate(-50%, -50%)',
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<div
|
2022-02-06 13:56:40 +01:00
|
|
|
className="fixed w-full bottom-0 flex items-center justify-center"
|
|
|
|
style={{ height: '90px' }}
|
2021-11-15 08:22:34 +01:00
|
|
|
>
|
2022-02-06 13:56:40 +01:00
|
|
|
<div
|
|
|
|
className={[
|
|
|
|
'flex items-center justify-center space-x-6',
|
|
|
|
'',
|
|
|
|
// 'bg-black backdrop-blur backdrop-filter bg-opacity-10',
|
|
|
|
].join(' ')}
|
|
|
|
>
|
|
|
|
<SizeSelector
|
|
|
|
value={sizeLimit || '1080'}
|
|
|
|
onChange={onSizeLimitChange}
|
|
|
|
originalWidth={original.naturalWidth}
|
|
|
|
originalHeight={original.naturalHeight}
|
|
|
|
/>
|
|
|
|
<Slider
|
|
|
|
label={
|
|
|
|
<span>
|
|
|
|
<span className="hidden md:inline">Brush</span>
|
|
|
|
</span>
|
2022-02-05 14:50:01 +01:00
|
|
|
}
|
2022-02-06 13:56:40 +01:00
|
|
|
min={10}
|
|
|
|
max={150}
|
|
|
|
value={brushSize}
|
|
|
|
onChange={setBrushSize}
|
2022-02-05 14:50:01 +01:00
|
|
|
/>
|
2022-02-06 13:56:40 +01:00
|
|
|
<div>
|
2022-02-06 14:40:49 +01:00
|
|
|
<Button
|
|
|
|
className="mr-2"
|
|
|
|
icon={<ArrowsExpandIcon className="w-6 h-6" />}
|
|
|
|
disabled={scale === minScale}
|
|
|
|
onClick={resetZoom}
|
|
|
|
/>
|
2022-02-06 13:56:40 +01:00
|
|
|
<Button
|
|
|
|
className="mr-2"
|
|
|
|
icon={
|
|
|
|
<svg
|
|
|
|
width="19"
|
|
|
|
height="9"
|
|
|
|
viewBox="0 0 19 9"
|
|
|
|
fill="none"
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
className="w-6 h-6"
|
|
|
|
>
|
|
|
|
<path
|
|
|
|
d="M2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1H2ZM1 8H0V9H1V8ZM8 9C8.55228 9 9 8.55229 9 8C9 7.44771 8.55228 7 8 7V9ZM16.5963 7.42809C16.8327 7.92721 17.429 8.14016 17.9281 7.90374C18.4272 7.66731 18.6402 7.07103 18.4037 6.57191L16.5963 7.42809ZM16.9468 5.83205L17.8505 5.40396L16.9468 5.83205ZM0 1V8H2V1H0ZM1 9H8V7H1V9ZM1.66896 8.74329L6.66896 4.24329L5.33104 2.75671L0.331035 7.25671L1.66896 8.74329ZM16.043 6.26014L16.5963 7.42809L18.4037 6.57191L17.8505 5.40396L16.043 6.26014ZM6.65079 4.25926C9.67554 1.66661 14.3376 2.65979 16.043 6.26014L17.8505 5.40396C15.5805 0.61182 9.37523 -0.710131 5.34921 2.74074L6.65079 4.25926Z"
|
|
|
|
fill="currentColor"
|
|
|
|
/>
|
|
|
|
</svg>
|
|
|
|
}
|
|
|
|
onClick={undo}
|
|
|
|
disabled={renders.length === 0}
|
|
|
|
/>
|
|
|
|
<Button
|
|
|
|
className="mr-2"
|
|
|
|
icon={<EyeIcon className="w-6 h-6" />}
|
|
|
|
onDown={ev => {
|
|
|
|
ev.preventDefault()
|
|
|
|
setShowSeparator(true)
|
|
|
|
setShowOriginal(true)
|
|
|
|
}}
|
|
|
|
onUp={() => {
|
|
|
|
setShowOriginal(false)
|
|
|
|
setTimeout(() => setShowSeparator(false), 300)
|
|
|
|
}}
|
|
|
|
disabled={renders.length === 0}
|
|
|
|
>
|
|
|
|
{undefined}
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
icon={<DownloadIcon className="w-6 h-6" />}
|
|
|
|
disabled={!renders.length}
|
|
|
|
onClick={download}
|
|
|
|
>
|
|
|
|
{undefined}
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div
|
|
|
|
className="absolute bg-black backdrop-blur backdrop-filter bg-opacity-10 rounded-xl"
|
|
|
|
style={{
|
|
|
|
height: '58px',
|
2022-02-06 14:40:49 +01:00
|
|
|
width: '700px',
|
2022-02-06 13:56:40 +01:00
|
|
|
zIndex: -1,
|
|
|
|
marginLeft: '-1px',
|
2022-02-05 14:50:01 +01:00
|
|
|
}}
|
|
|
|
>
|
|
|
|
{undefined}
|
2022-02-06 13:56:40 +01:00
|
|
|
</div>
|
2022-02-05 14:50:01 +01:00
|
|
|
</div>
|
2021-11-15 08:22:34 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|