/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import { DownloadIcon, EyeIcon, ViewBoardsIcon } from '@heroicons/react/outline' import { useCallback, useEffect, useState, useRef, useMemo } from 'react' import { useWindowSize } from 'react-use' import inpaint from './adapters/inpainting' import superResolution from './adapters/superResolution' import Button from './components/Button' import Slider from './components/Slider' import { downloadImage, loadImage, useImage } from './utils' import Progress from './components/Progress' import { modelExists, downloadModel } from './adapters/cache' import Modal from './components/Modal' import * as m from './paraglide/messages' interface EditorProps { file: File } interface Line { size?: number pts: { x: number; y: number }[] src: string } function drawLines( ctx: CanvasRenderingContext2D, lines: Line[], color = 'rgba(255, 0, 0, 0.5)' ) { 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() }) } const BRUSH_HIDE_ON_SLIDER_CHANGE_TIMEOUT = 2000 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: [], src: '' }]) const brushRef = useRef(null) const [showBrush, setShowBrush] = useState(false) const [hideBrushTimeout, setHideBrushTimeout] = useState(0) const [showOriginal, setShowOriginal] = useState(false) const [isInpaintingLoading, setIsProcessingLoading] = useState(false) const [generateProgress, setGenerateProgress] = useState(0) const modalRef = useRef(null) const [separator, setSeparator] = useState() const [useSeparator, setUseSeparator] = useState(false) const [originalImg, setOriginalImg] = useState() const [separatorLeft, setSeparatorLeft] = useState(0) const historyListRef = useRef(null) const isBrushSizeChange = useRef(false) const scaledBrushSize = useMemo(() => brushSize, [brushSize]) const canvasDiv = useRef(null) const [downloaded, setDownloaded] = useState(true) const [downloadProgress, setDownloadProgress] = useState(0) const windowSize = useWindowSize() const draw = useCallback( (index = -1) => { if (!context) { return } context.clearRect(0, 0, context.canvas.width, context.canvas.height) const currRender = renders[index === -1 ? renders.length - 1 : index] ?? original const { canvas } = context const divWidth = canvasDiv.current!.offsetWidth const divHeight = canvasDiv.current!.offsetHeight // 计算宽高比 const imgAspectRatio = currRender.width / currRender.height const divAspectRatio = divWidth / divHeight let canvasWidth let canvasHeight // 比较宽高比以决定如何缩放 if (divAspectRatio > imgAspectRatio) { // div 较宽,基于高度缩放 canvasHeight = divHeight canvasWidth = currRender.width * (divHeight / currRender.height) } else { // div 较窄,基于宽度缩放 canvasWidth = divWidth canvasHeight = currRender.height * (divWidth / currRender.width) } canvas.width = canvasWidth canvas.height = canvasHeight if (currRender?.src) { context.drawImage(currRender, 0, 0, canvas.width, canvas.height) } else { context.drawImage(original, 0, 0, canvas.width, canvas.height) } 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') } // Just need the finishing touch const line = lines.slice(-1)[0] if (line) drawLines(ctx, [line], 'white') }, [context?.canvas.height, context?.canvas.width, lines, maskCanvas]) // Draw once the original image is loaded useEffect(() => { if (!context?.canvas) { return } if (isOriginalLoaded) { draw() } }, [context?.canvas, draw, original, isOriginalLoaded, windowSize]) // Handle mouse interactions useEffect(() => { const canvas = context?.canvas if (!canvas) { return } const onMouseMove = (ev: MouseEvent) => { if (brushRef.current) { const x = ev.pageX - scaledBrushSize / 2 const y = ev.pageY - scaledBrushSize / 2 brushRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)` } } 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 - canvas.offsetLeft const py = ev.offsetY - canvas.offsetTop onPaint(px, py) } const onPointerUp = async () => { if (!original.src || showOriginal) { return } if (lines.slice(-1)[0]?.pts.length === 0) { return } const loading = onloading() canvas.removeEventListener('mousemove', onMouseDrag) canvas.removeEventListener('mouseup', onPointerUp) refreshCanvasMask() try { const start = Date.now() console.log('inpaint_start') // each time based on the last result, the first is the original const newFile = renders.slice(-1)[0] ?? file const res = await inpaint(newFile, maskCanvas.toDataURL()) if (!res) { throw new Error('empty response') } // TODO: fix the render if it failed loading const newRender = new Image() newRender.dataset.id = Date.now().toString() await loadImage(newRender, res) renders.push(newRender) lines.push({ pts: [], src: '' } as Line) setRenders([...renders]) setLines([...lines]) console.log('inpaint_processed', { duration: Date.now() - start, }) } catch (e: any) { console.log('inpaint_failed', { error: e, }) // eslint-disable-next-line alert(e.message ? e.message : e.toString()) } if (historyListRef.current) { const { scrollWidth, clientWidth } = historyListRef.current if (scrollWidth > clientWidth) { historyListRef.current.scrollTo(scrollWidth, 0) } } loading.close() draw() } canvas.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, y: ev.touches[0].clientY - coords.y, }) draw() } const onPointerStart = () => { if (!original.src || showOriginal) { return } const currLine = lines[lines.length - 1] currLine.size = brushSize canvas.addEventListener('mousemove', onMouseDrag) canvas.addEventListener('mouseup', onPointerUp) // onPaint(e) } canvas.addEventListener('touchstart', onPointerStart) canvas.addEventListener('touchmove', onTouchMove) canvas.addEventListener('touchend', onPointerUp) canvas.onmouseenter = () => { window.clearTimeout(hideBrushTimeout) setShowBrush(true && !showOriginal) } canvas.onmouseleave = () => setShowBrush(false) canvas.onmousedown = onPointerStart return () => { canvas.removeEventListener('mousemove', onMouseDrag) canvas.removeEventListener('mousemove', onMouseMove) canvas.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, showOriginal, hideBrushTimeout, ]) useEffect(() => { if (!separator || !originalImg) return const separatorMove = (ev: MouseEvent) => { ev.preventDefault() ev.stopPropagation() if (context?.canvas) { const { width } = context?.canvas const canvasRect = context?.canvas.getBoundingClientRect() const separatorOffsetLeft = ev.pageX - canvasRect.left if (separatorOffsetLeft <= width && separatorOffsetLeft >= 0) { setSeparatorLeft(separatorOffsetLeft) } else if (separatorOffsetLeft < 0) { setSeparatorLeft(0) } else if (separatorOffsetLeft > width) { setSeparatorLeft(width) } } } const separatorDown = () => { window.addEventListener('mousemove', separatorMove) setUseSeparator(true) } const separatorUp = () => { window.removeEventListener('mousemove', separatorMove) setUseSeparator(false) } separator.addEventListener('mousedown', separatorDown) window.addEventListener('mouseup', separatorUp) return () => { separator.removeEventListener('mousedown', separatorDown) window.removeEventListener('mouseup', separatorUp) } }, [separator, context]) function download() { const currRender = renders.at(-1) ?? original downloadImage(currRender.currentSrc, 'IMG') } const undo = useCallback(async () => { const l = lines l.pop() l.pop() setLines([...l, { pts: [], src: '' }]) const r = renders r.pop() setRenders([...r]) }, [lines, renders]) 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]) const backTo = useCallback( (index: number) => { lines.splice(index + 1) setLines([...lines, { pts: [], src: '' }]) renders.splice(index + 1) setRenders([...renders]) }, [renders, lines] ) const History = useMemo( () => renders.map((render, index) => { return (
render
) }), [renders, backTo] ) const handleSliderStart = () => { setShowBrush(true) } const handleSliderChange = (sliderValue: number) => { if (!isBrushSizeChange.current) { isBrushSizeChange.current = true } if (brushRef.current) { const x = document.documentElement.clientWidth / 2 - scaledBrushSize / 2 const y = document.documentElement.clientHeight / 2 - scaledBrushSize / 2 brushRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)` } setBrushSize(sliderValue) window.clearTimeout(hideBrushTimeout) setHideBrushTimeout( window.setTimeout(() => { setShowBrush(false) }, BRUSH_HIDE_ON_SLIDER_CHANGE_TIMEOUT) ) } const onloading = useCallback(() => { setIsProcessingLoading(true) setGenerateProgress(0) const progressTimer = window.setInterval(() => { setGenerateProgress(p => { if (p < 90) return p + 10 * Math.random() if (p >= 90 && p < 99) return p + 1 * Math.random() // Do not hide the progress bar after 99%,cause sometimes long time progress // window.setTimeout(() => setIsInpaintingLoading(false), 500) return p }) }, 1000) return { close: () => { clearInterval(progressTimer) setGenerateProgress(100) setIsProcessingLoading(false) }, } }, []) const onSuperResolution = useCallback(async () => { if (!(await modelExists('superResolution'))) { setDownloaded(false) await downloadModel('superResolution', setDownloadProgress) setDownloaded(true) } setIsProcessingLoading(true) try { // 运行 const start = Date.now() console.log('superResolution_start') // each time based on the last result, the first is the original const newFile = renders.at(-1) ?? file const res = await superResolution(newFile, setGenerateProgress) if (!res) { throw new Error('empty response') } // TODO: fix the render if it failed loading const newRender = new Image() newRender.dataset.id = Date.now().toString() await loadImage(newRender, res) renders.push(newRender) lines.push({ pts: [], src: '' } as Line) setRenders([...renders]) setLines([...lines]) console.log('superResolution_processed', { duration: Date.now() - start, }) // 替换当前图片 } catch (error) { console.error('superResolution', error) } finally { setIsProcessingLoading(false) } }, [file, lines, original.naturalHeight, original.naturalWidth, renders]) return (
{/* History */}
{History}
{/* 画图 */}
{ if (r && !context) { const ctx = r.getContext('2d') if (ctx) { setContext(ctx) } } }} />
{ if (r && !originalImg) { setOriginalImg(r) } }} >
original
{ if (r && !separator) { setSeparator(r) } }} >
original
{isInpaintingLoading && (

正在处理中,请耐心等待。。。

It is being processed, please be patient...

)}
{!downloaded && (

{m.upscaleing_model_download_message()}

)} {showBrush && (
)} {/* 工具栏 */}
{renders.length > 0 && ( )} {!showOriginal && ( )}
) }