import { DownloadIcon, EyeIcon } from '@heroicons/react/outline' import React, { useCallback, useEffect, useState } from 'react' import { useWindowSize } from 'react-use' import inpaint from './adapters/inpainting' import Button from './components/Button' import Slider from './components/Slider' import { downloadImage, loadImage, shareImage, useImage } from './utils' const TOOLBAR_SIZE = 200 const BRUSH_COLOR = 'rgba(189, 255, 1, 0.75)' 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([]) const [context, setContext] = useState() const [maskCanvas] = useState(() => { return document.createElement('canvas') }) const [lines, setLines] = useState([{ pts: [] }]) const [{ x, y }, setCoords] = useState({ x: -1, y: -1 }) const [showBrush, setShowBrush] = useState(false) const [showOriginal, setShowOriginal] = useState(false) const [isInpaintingLoading, setIsInpaintingLoading] = useState(false) const [showSeparator, setShowSeparator] = useState(false) const [scale, setScale] = useState(1) const windowSize = useWindowSize() 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) } const currentLine = lines[lines.length - 1] drawLines(context, [currentLine]) }, [context, lines, original, renders]) 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') } drawLines(ctx, lines, 'white') }, [context?.canvas.height, context?.canvas.width, lines, maskCanvas]) // Draw once the original image is loaded useEffect(() => { if (!context?.canvas) { return } if (isOriginalLoaded) { context.canvas.width = original.naturalWidth context.canvas.height = original.naturalHeight const rW = windowSize.width / original.naturalWidth const rH = (windowSize.height - TOOLBAR_SIZE) / original.naturalHeight if (rW < 1 || rH < 1) { setScale(Math.min(rW, rH)) } else { setScale(1) } draw() } }, [context?.canvas, draw, original, isOriginalLoaded, windowSize]) // Handle mouse interactions useEffect(() => { const canvas = context?.canvas if (!canvas) { return } const onMouseDown = (ev: MouseEvent) => { if (!original.src) { return } const currLine = lines[lines.length - 1] currLine.size = brushSize canvas.addEventListener('mousemove', onMouseDrag) window.addEventListener('mouseup', onPointerUp) onPaint(ev.offsetX, ev.offsetY) } const onMouseMove = (ev: MouseEvent) => { setCoords({ x: ev.pageX, y: ev.pageY }) } const onPaint = (px: number, py: number) => { const currLine = lines[lines.length - 1] currLine.pts.push({ x: px, y: py }) draw() } const onMouseDrag = (ev: MouseEvent) => { const px = ev.offsetX const py = ev.offsetY onPaint(px, py) } const onPointerUp = async () => { if (!original.src) { return } setIsInpaintingLoading(true) canvas.removeEventListener('mousemove', onMouseDrag) window.removeEventListener('mouseup', onPointerUp) refreshCanvasMask() try { const start = Date.now() const res = await inpaint(file, maskCanvas.toDataURL()) 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]) } catch (e: any) { // eslint-disable-next-line alert(e.message ? e.message : e.toString()) } setIsInpaintingLoading(false) draw() } window.addEventListener('mousemove', onMouseMove) const onTouchMove = (ev: TouchEvent) => { ev.preventDefault() ev.stopPropagation() const currLine = lines[lines.length - 1] const coords = canvas.getBoundingClientRect() currLine.pts.push({ x: (ev.touches[0].clientX - coords.x) / scale, y: (ev.touches[0].clientY - coords.y) / scale, }) draw() } const onPointerStart = (ev: TouchEvent) => { if (!original.src) { return } const currLine = lines[lines.length - 1] currLine.size = brushSize canvas.addEventListener('mousemove', onMouseDrag) window.addEventListener('mouseup', onPointerUp) const coords = canvas.getBoundingClientRect() const px = (ev.touches[0].clientX - coords.x) / scale const py = (ev.touches[0].clientY - coords.y) / scale onPaint(px, py) } canvas.addEventListener('touchstart', onPointerStart) canvas.addEventListener('touchmove', onTouchMove) canvas.addEventListener('touchend', onPointerUp) canvas.onmouseenter = () => setShowBrush(true) canvas.onmouseleave = () => setShowBrush(false) canvas.onmousedown = onMouseDown return () => { canvas.removeEventListener('mousemove', onMouseDrag) window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mouseup', onPointerUp) canvas.removeEventListener('touchstart', onPointerStart) canvas.removeEventListener('touchmove', onTouchMove) canvas.removeEventListener('touchend', onPointerUp) canvas.onmouseenter = null canvas.onmouseleave = null canvas.onmousedown = null } }, [ brushSize, context, file, draw, lines, refreshCanvasMask, maskCanvas, original.src, renders, original.naturalHeight, original.naturalWidth, scale, ]) const undo = useCallback(() => { const l = lines l.pop() l.pop() setLines([...l, { pts: [] }]) const r = renders r.pop() setRenders([...r]) }, [lines, renders]) // Handle Cmd+Z useEffect(() => { const handler = (event: KeyboardEvent) => { if (!renders.length) { return } const isCmdZ = (event.metaKey || event.ctrlKey) && event.key === 'z' if (isCmdZ) { event.preventDefault() undo() } } window.addEventListener('keydown', handler) return () => { window.removeEventListener('keydown', handler) } }, [renders, undo]) function download() { const base64 = context?.canvas.toDataURL(file.type) if (!base64) { throw new Error('could not get canvas data') } const name = file.name.replace(/(\.[\w\d_-]+)$/i, '_cleanup$1') downloadImage(base64, name) } return (
{ if (r && !context) { const ctx = r.getContext('2d') if (ctx) { setContext(ctx) } } }} />
original
{showBrush && (
)}
Brush Size } min={10} max={150} value={brushSize} onChange={setBrushSize} /> {renders.length ? ( <> ) : ( <> )}
) }