commit
d0351a8603
@ -4,6 +4,10 @@ This project is mainly used for selfhosting LaMa model, some interaction improve
|
||||
|
||||
![example](./assets/lama-cleaner-example.gif)
|
||||
|
||||
- [x] High resolution support
|
||||
- [x] Multi stroke support. Press and hold the `cmd/ctrl` key to enable multi stroke mode.
|
||||
- [ ] Keep image EXIF data
|
||||
|
||||
## Quick Start
|
||||
|
||||
- Install requirements: `pip3 install -r requirements.txt`
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.36546dab.chunk.css",
|
||||
"main.js": "/static/js/main.d5ae39b3.chunk.js",
|
||||
"main.js": "/static/js/main.5c20e42a.chunk.js",
|
||||
"runtime-main.js": "/static/js/runtime-main.5e86ac81.js",
|
||||
"static/js/2.bca222ae.chunk.js": "/static/js/2.bca222ae.chunk.js",
|
||||
"static/js/2.ad4a2500.chunk.js": "/static/js/2.ad4a2500.chunk.js",
|
||||
"index.html": "/index.html",
|
||||
"static/js/2.bca222ae.chunk.js.LICENSE.txt": "/static/js/2.bca222ae.chunk.js.LICENSE.txt"
|
||||
"static/js/2.ad4a2500.chunk.js.LICENSE.txt": "/static/js/2.ad4a2500.chunk.js.LICENSE.txt"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.5e86ac81.js",
|
||||
"static/js/2.bca222ae.chunk.js",
|
||||
"static/js/2.ad4a2500.chunk.js",
|
||||
"static/css/main.36546dab.chunk.css",
|
||||
"static/js/main.d5ae39b3.chunk.js"
|
||||
"static/js/main.5c20e42a.chunk.js"
|
||||
]
|
||||
}
|
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/svg+xml" href="/favicon.svg"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/><meta name="theme-color" content="#ffffff"/><title>lama-cleaner - Image inpainting powered by LaMa</title><link href="/static/css/main.36546dab.chunk.css" rel="stylesheet"></head><body class="h-screen"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="h-full"></div><script>"localhost"===location.hostname&&(self.FIREBASE_APPCHECK_DEBUG_TOKEN=!0)</script><script>!function(e){function r(r){for(var n,l,a=r[0],f=r[1],i=r[2],p=0,s=[];p<a.length;p++)l=a[p],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonplama-cleaner"]=this["webpackJsonplama-cleaner"]||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var i=0;i<a.length;i++)r(a[i]);var c=f;t()}([])</script><script src="/static/js/2.bca222ae.chunk.js"></script><script src="/static/js/main.d5ae39b3.chunk.js"></script></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" type="image/svg+xml" href="/favicon.svg"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/><meta name="theme-color" content="#ffffff"/><title>lama-cleaner - Image inpainting powered by LaMa</title><link href="/static/css/main.36546dab.chunk.css" rel="stylesheet"></head><body class="h-screen"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="h-full"></div><script>"localhost"===location.hostname&&(self.FIREBASE_APPCHECK_DEBUG_TOKEN=!0)</script><script>!function(e){function r(r){for(var n,l,a=r[0],f=r[1],i=r[2],p=0,s=[];p<a.length;p++)l=a[p],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonplama-cleaner"]=this["webpackJsonplama-cleaner"]||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var i=0;i<a.length;i++)r(a[i]);var c=f;t()}([])</script><script src="/static/js/2.ad4a2500.chunk.js"></script><script src="/static/js/main.5c20e42a.chunk.js"></script></body></html>
|
File diff suppressed because one or more lines are too long
1
lama_cleaner/app/build/static/js/main.5c20e42a.chunk.js
Normal file
1
lama_cleaner/app/build/static/js/main.5c20e42a.chunk.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
import { DownloadIcon, EyeIcon } from '@heroicons/react/outline'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useWindowSize, useLocalStorage } from 'react-use'
|
||||
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react'
|
||||
import { useWindowSize, useLocalStorage, useKey } from 'react-use'
|
||||
import inpaint from './adapters/inpainting'
|
||||
import Button from './components/Button'
|
||||
import Slider from './components/Slider'
|
||||
@ -9,6 +9,7 @@ import { downloadImage, loadImage, useImage } from './utils'
|
||||
|
||||
const TOOLBAR_SIZE = 200
|
||||
const BRUSH_COLOR = 'rgba(189, 255, 1, 0.75)'
|
||||
const NO_COLOR = 'rgba(255,255,255,0)'
|
||||
|
||||
interface EditorProps {
|
||||
file: File
|
||||
@ -50,6 +51,8 @@ export default function Editor(props: EditorProps) {
|
||||
return document.createElement('canvas')
|
||||
})
|
||||
const [lines, setLines] = useState<Line[]>([{ pts: [] }])
|
||||
const [lines4Show, setLines4Show] = useState<Line[]>([{ pts: [] }])
|
||||
const [historyLineCount, setHistoryLineCount] = useState<number[]>([])
|
||||
const [{ x, y }, setCoords] = useState({ x: -1, y: -1 })
|
||||
const [showBrush, setShowBrush] = useState(false)
|
||||
const [showOriginal, setShowOriginal] = useState(false)
|
||||
@ -60,6 +63,9 @@ export default function Editor(props: EditorProps) {
|
||||
const [sizeLimit, setSizeLimit] = useLocalStorage('sizeLimit', '1080')
|
||||
const windowSize = useWindowSize()
|
||||
|
||||
const [isDraging, setIsDraging] = useState(false)
|
||||
const [isMultiStrokeKeyPressed, setIsMultiStrokeKeyPressed] = useState(false)
|
||||
|
||||
const draw = useCallback(() => {
|
||||
if (!context) {
|
||||
return
|
||||
@ -71,9 +77,8 @@ export default function Editor(props: EditorProps) {
|
||||
} else {
|
||||
context.drawImage(original, 0, 0)
|
||||
}
|
||||
const currentLine = lines[lines.length - 1]
|
||||
drawLines(context, [currentLine])
|
||||
}, [context, lines, original, renders])
|
||||
drawLines(context, lines4Show)
|
||||
}, [context, lines4Show, original, renders])
|
||||
|
||||
const refreshCanvasMask = useCallback(() => {
|
||||
if (!context?.canvas.width || !context?.canvas.height) {
|
||||
@ -85,9 +90,77 @@ export default function Editor(props: EditorProps) {
|
||||
if (!ctx) {
|
||||
throw new Error('could not retrieve mask canvas')
|
||||
}
|
||||
|
||||
drawLines(ctx, lines, 'white')
|
||||
}, [context?.canvas.height, context?.canvas.width, lines, maskCanvas])
|
||||
|
||||
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,
|
||||
])
|
||||
|
||||
const handleMultiStrokeKeyDown = () => {
|
||||
if (isInpaintingLoading) {
|
||||
return
|
||||
}
|
||||
setIsMultiStrokeKeyPressed(true)
|
||||
}
|
||||
|
||||
const handleMultiStrokeKeyup = () => {
|
||||
if (!isMultiStrokeKeyPressed) {
|
||||
return
|
||||
}
|
||||
if (isInpaintingLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsMultiStrokeKeyPressed(false)
|
||||
if (lines4Show.length !== 0 && lines4Show[0].pts.length !== 0) {
|
||||
runInpainting()
|
||||
}
|
||||
}
|
||||
|
||||
const predicate = (event: KeyboardEvent) => {
|
||||
return event.key === 'Control' || event.key === 'Meta'
|
||||
}
|
||||
useKey(predicate, handleMultiStrokeKeyup, { event: 'keyup' })
|
||||
useKey(predicate, handleMultiStrokeKeyDown, {
|
||||
event: 'keydown',
|
||||
})
|
||||
|
||||
// Draw once the original image is loaded
|
||||
useEffect(() => {
|
||||
if (!context?.canvas) {
|
||||
@ -107,152 +180,111 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
}, [context?.canvas, draw, original, isOriginalLoaded, windowSize])
|
||||
|
||||
// Handle mouse interactions
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
if (!isDraging) {
|
||||
return
|
||||
}
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent
|
||||
const px = mouseEvent.offsetX
|
||||
const py = mouseEvent.offsetY
|
||||
onPaint(px, py)
|
||||
}
|
||||
|
||||
const onPointerUp = () => {
|
||||
if (!original.src) {
|
||||
return
|
||||
}
|
||||
const canvas = context?.canvas
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
if (isInpaintingLoading) {
|
||||
return
|
||||
}
|
||||
setIsDraging(false)
|
||||
if (isMultiStrokeKeyPressed) {
|
||||
lines.push({ pts: [] } as Line)
|
||||
setLines([...lines])
|
||||
|
||||
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)
|
||||
lines4Show.push({ pts: [] } as Line)
|
||||
setLines4Show([...lines4Show])
|
||||
return
|
||||
}
|
||||
|
||||
const onPointerUp = async () => {
|
||||
if (!original.src) {
|
||||
return
|
||||
}
|
||||
setIsInpaintingLoading(true)
|
||||
canvas.removeEventListener('mousemove', onMouseDrag)
|
||||
window.removeEventListener('mouseup', onPointerUp)
|
||||
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])
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line
|
||||
alert(e.message ? e.message : e.toString())
|
||||
}
|
||||
setIsInpaintingLoading(false)
|
||||
draw()
|
||||
if (lines4Show.length !== 0 && lines4Show[0].pts.length !== 0) {
|
||||
runInpainting()
|
||||
}
|
||||
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 onMouseDown = (ev: SyntheticEvent) => {
|
||||
if (!original.src) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
const canvas = context?.canvas
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
canvas.addEventListener('touchstart', onPointerStart)
|
||||
canvas.addEventListener('touchmove', onTouchMove)
|
||||
canvas.addEventListener('touchend', onPointerUp)
|
||||
canvas.onmouseenter = () => setShowBrush(true)
|
||||
canvas.onmouseleave = () => setShowBrush(false)
|
||||
canvas.onmousedown = onMouseDown
|
||||
if (isInpaintingLoading) {
|
||||
return
|
||||
}
|
||||
setIsDraging(true)
|
||||
const currLine4Show = lines4Show[lines4Show.length - 1]
|
||||
currLine4Show.size = brushSize
|
||||
const currLine = lines[lines.length - 1]
|
||||
currLine.size = brushSize
|
||||
|
||||
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,
|
||||
sizeLimit,
|
||||
])
|
||||
const mouseEvent = ev.nativeEvent as MouseEvent
|
||||
onPaint(mouseEvent.offsetX, mouseEvent.offsetY)
|
||||
}
|
||||
|
||||
const undo = useCallback(() => {
|
||||
const undo = () => {
|
||||
const l = lines
|
||||
l.pop()
|
||||
l.pop()
|
||||
const count = historyLineCount[historyLineCount.length - 1]
|
||||
for (let i = 0; i <= count; i += 1) {
|
||||
l.pop()
|
||||
}
|
||||
|
||||
setLines([...l, { pts: [] }])
|
||||
historyLineCount.pop()
|
||||
setHistoryLineCount(historyLineCount)
|
||||
|
||||
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()
|
||||
}
|
||||
const undoPredicate = (event: KeyboardEvent) => {
|
||||
if (!renders.length) {
|
||||
return false
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handler)
|
||||
if (!historyLineCount.length) {
|
||||
return false
|
||||
}
|
||||
}, [renders, undo])
|
||||
const isCmdZ = (event.metaKey || event.ctrlKey) && event.key === 'z'
|
||||
if (isCmdZ) {
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
useKey(undoPredicate, undo)
|
||||
|
||||
function download() {
|
||||
const name = file.name.replace(/(\.[\w\d_-]+)$/i, '_cleanup$1')
|
||||
@ -261,32 +293,49 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
|
||||
const onSizeLimitChange = (_sizeLimit: string) => {
|
||||
// TODO: clean renders
|
||||
// if (renders.length !== 0) {
|
||||
// }
|
||||
setSizeLimit(_sizeLimit)
|
||||
}
|
||||
|
||||
const toggleShowBrush = (newState: boolean) => {
|
||||
if (newState !== showBrush) {
|
||||
setShowBrush(newState)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'flex flex-col items-center',
|
||||
isInpaintingLoading
|
||||
? 'animate-pulse-fast pointer-events-none transition-opacity'
|
||||
: '',
|
||||
isInpaintingLoading ? 'animate-pulse-fast transition-opacity' : '',
|
||||
scale !== 1 ? 'pb-24' : '',
|
||||
].join(' ')}
|
||||
style={{
|
||||
height: scale !== 1 ? original.naturalHeight * scale : undefined,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onPointerUp}
|
||||
>
|
||||
<div
|
||||
className={[scale !== 1 ? '' : 'relative'].join(' ')}
|
||||
style={{ transform: `scale(${scale})`, transformOrigin: 'top center' }}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top center',
|
||||
borderColor: `${isMultiStrokeKeyPressed ? BRUSH_COLOR : NO_COLOR}`,
|
||||
borderWidth: `${8 / scale}px`,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
className="rounded-sm"
|
||||
style={showBrush ? { cursor: 'none' } : {}}
|
||||
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')
|
||||
@ -329,7 +378,7 @@ export default function Editor(props: EditorProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showBrush && (
|
||||
{showBrush && !isInpaintingLoading && (
|
||||
<div
|
||||
className="hidden sm:block absolute rounded-full border border-primary bg-primary bg-opacity-80 pointer-events-none"
|
||||
style={{
|
||||
|
@ -6,6 +6,7 @@ interface ButtonProps {
|
||||
icon?: ReactNode
|
||||
primary?: boolean
|
||||
disabled?: boolean
|
||||
onKeyDown?: () => void
|
||||
onClick?: () => void
|
||||
onDown?: (ev: PointerEvent) => void
|
||||
onUp?: (ev: PointerEvent) => void
|
||||
@ -18,6 +19,7 @@ export default function Button(props: ButtonProps) {
|
||||
disabled,
|
||||
icon,
|
||||
primary,
|
||||
onKeyDown,
|
||||
onClick,
|
||||
onDown,
|
||||
onUp,
|
||||
@ -36,7 +38,7 @@ export default function Button(props: ButtonProps) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
onKeyDown={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onClick={onClick}
|
||||
onPointerDown={(ev: React.PointerEvent<HTMLDivElement>) => {
|
||||
setActive(true)
|
||||
|
Loading…
Reference in New Issue
Block a user