add manual run mode

This commit is contained in:
Sanster 2022-04-23 20:21:34 +08:00
parent d94cae491a
commit f97e17a593
4 changed files with 127 additions and 113 deletions

View File

@ -36,9 +36,11 @@ interface Line {
pts: { x: number; y: number }[] pts: { x: number; y: number }[]
} }
type LineGroup = Array<Line>
function drawLines( function drawLines(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
lines: Line[], lines: LineGroup,
color = BRUSH_COLOR color = BRUSH_COLOR
) { ) {
ctx.strokeStyle = color ctx.strokeStyle = color
@ -57,6 +59,11 @@ function drawLines(
}) })
} }
function mouseXY(ev: SyntheticEvent) {
const mouseEvent = ev.nativeEvent as MouseEvent
return { x: mouseEvent.offsetX, y: mouseEvent.offsetY }
}
export default function Editor(props: EditorProps) { export default function Editor(props: EditorProps) {
const { file } = props const { file } = props
const settings = useRecoilValue(settingState) const settings = useRecoilValue(settingState)
@ -67,9 +74,8 @@ export default function Editor(props: EditorProps) {
const [maskCanvas] = useState<HTMLCanvasElement>(() => { const [maskCanvas] = useState<HTMLCanvasElement>(() => {
return document.createElement('canvas') return document.createElement('canvas')
}) })
const [lines, setLines] = useState<Line[]>([{ pts: [] }]) const [lineGroups, setLineGroups] = useState<LineGroup[]>([])
const [lines4Show, setLines4Show] = useState<Line[]>([{ pts: [] }]) const [curLineGroup, setCurLineGroup] = useState<LineGroup>([])
const [historyLineCount, setHistoryLineCount] = useState<number[]>([])
const [{ x, y }, setCoords] = useState({ x: -1, y: -1 }) const [{ x, y }, setCoords] = useState({ x: -1, y: -1 })
const [showBrush, setShowBrush] = useState(false) const [showBrush, setShowBrush] = useState(false)
const [isPanning, setIsPanning] = useState<boolean>(false) const [isPanning, setIsPanning] = useState<boolean>(false)
@ -88,27 +94,22 @@ export default function Editor(props: EditorProps) {
const [sliderPos, setSliderPos] = useState<number>(0) const [sliderPos, setSliderPos] = useState<number>(0)
const draw = useCallback(() => { const draw = (render: HTMLImageElement, lineGroup: LineGroup) => {
if (!context) { if (!context) {
return return
} }
context.clearRect(0, 0, context.canvas.width, context.canvas.height) context.clearRect(0, 0, context.canvas.width, context.canvas.height)
const currRender = renders[renders.length - 1]
if (currRender?.src) {
context.drawImage( context.drawImage(
currRender, render,
0, 0,
0, 0,
original.naturalWidth, original.naturalWidth,
original.naturalHeight original.naturalHeight
) )
} else { drawLines(context, lineGroup)
context.drawImage(original, 0, 0)
} }
drawLines(context, lines4Show)
}, [context, lines4Show, original, renders])
const refreshCanvasMask = useCallback(() => { const drawAllLinesOnMask = (_lineGroups: LineGroup[]) => {
if (!context?.canvas.width || !context?.canvas.height) { if (!context?.canvas.width || !context?.canvas.height) {
throw new Error('canvas has invalid size') throw new Error('canvas has invalid size')
} }
@ -119,12 +120,23 @@ export default function Editor(props: EditorProps) {
throw new Error('could not retrieve mask canvas') throw new Error('could not retrieve mask canvas')
} }
drawLines(ctx, lines, 'white') _lineGroups.forEach(lineGroup => {
}, [context?.canvas.height, context?.canvas.width, lines, maskCanvas]) drawLines(ctx, lineGroup, 'white')
})
}
const runInpainting = useCallback(async () => { const runInpainting = async () => {
if (!hadDrawSomething()) {
return
}
const newLineGroups = [...lineGroups, curLineGroup]
setLineGroups(newLineGroups)
setCurLineGroup([])
setIsDraging(false)
setIsInpaintingLoading(true) setIsInpaintingLoading(true)
refreshCanvasMask()
drawAllLinesOnMask(newLineGroups)
try { try {
const res = await inpaint( const res = await inpaint(
file, file,
@ -137,46 +149,35 @@ export default function Editor(props: EditorProps) {
} }
const newRender = new Image() const newRender = new Image()
await loadImage(newRender, res) await loadImage(newRender, res)
renders.push(newRender) const newRenders = [...renders, newRender]
lines.push({ pts: [] } as Line) setRenders(newRenders)
setRenders([...renders]) draw(newRender, [])
setLines([...lines])
historyLineCount.push(lines4Show.length)
setHistoryLineCount(historyLineCount)
lines4Show.length = 0
setLines4Show([{ pts: [] } as Line])
} catch (e: any) { } catch (e: any) {
// eslint-disable-next-line // eslint-disable-next-line
alert(e.message ? e.message : e.toString()) alert(e.message ? e.message : e.toString())
} }
setIsInpaintingLoading(false) setIsInpaintingLoading(false)
draw() }
}, [
draw,
file,
lines,
lines4Show,
maskCanvas,
refreshCanvasMask,
renders,
sizeLimit,
historyLineCount,
settings,
])
const hadDrawSomething = () => { const hadDrawSomething = () => {
return lines4Show.length !== 0 && lines4Show[0].pts.length !== 0 return curLineGroup.length !== 0
} }
const hadRunInpainting = () => { const hadRunInpainting = () => {
return renders.length !== 0 return renders.length !== 0
} }
const drawOnCurrentRender = (lineGroup: LineGroup) => {
if (renders.length === 0) {
draw(original, lineGroup)
} else {
draw(renders[renders.length - 1], lineGroup)
}
}
const clearDrawing = () => { const clearDrawing = () => {
setIsDraging(false) setIsDraging(false)
lines4Show.length = 0 setCurLineGroup([])
setLines4Show([{ pts: [] } as Line])
} }
const handleMultiStrokeKeyDown = () => { const handleMultiStrokeKeyDown = () => {
@ -195,7 +196,8 @@ export default function Editor(props: EditorProps) {
} }
setIsMultiStrokeKeyPressed(false) setIsMultiStrokeKeyPressed(false)
if (hadDrawSomething()) {
if (!settings.runInpaintingManually) {
runInpainting() runInpainting()
} }
} }
@ -241,17 +243,22 @@ export default function Editor(props: EditorProps) {
if (context?.canvas) { if (context?.canvas) {
context.canvas.width = original.naturalWidth context.canvas.width = original.naturalWidth
context.canvas.height = original.naturalHeight context.canvas.height = original.naturalHeight
context.drawImage(
original,
0,
0,
original.naturalWidth,
original.naturalHeight
)
} }
if (!initialCentered) { if (!initialCentered) {
viewportRef.current?.centerView(s, 1) viewportRef.current?.centerView(s, 1)
setInitialCentered(true) setInitialCentered(true)
} }
draw()
}, [ }, [
context?.canvas, context?.canvas,
draw,
viewportRef, viewportRef,
original, original,
isOriginalLoaded, isOriginalLoaded,
@ -312,16 +319,6 @@ export default function Editor(props: EditorProps) {
] ]
) )
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 onMouseMove = (ev: SyntheticEvent) => {
const mouseEvent = ev.nativeEvent as MouseEvent const mouseEvent = ev.nativeEvent as MouseEvent
setCoords({ x: mouseEvent.pageX, y: mouseEvent.pageY }) setCoords({ x: mouseEvent.pageX, y: mouseEvent.pageY })
@ -334,10 +331,13 @@ export default function Editor(props: EditorProps) {
if (!isDraging) { if (!isDraging) {
return return
} }
const mouseEvent = ev.nativeEvent as MouseEvent if (curLineGroup.length === 0) {
const px = mouseEvent.offsetX return
const py = mouseEvent.offsetY }
onPaint(px, py) const lineGroup = [...curLineGroup]
lineGroup[lineGroup.length - 1].pts.push(mouseXY(ev))
setCurLineGroup(lineGroup)
drawOnCurrentRender(lineGroup)
} }
const onPointerUp = () => { const onPointerUp = () => {
@ -357,17 +357,15 @@ export default function Editor(props: EditorProps) {
if (!isDraging) { if (!isDraging) {
return return
} }
setIsDraging(false)
if (isMultiStrokeKeyPressed) {
lines.push({ pts: [] } as Line)
setLines([...lines])
lines4Show.push({ pts: [] } as Line) if (isMultiStrokeKeyPressed) {
setLines4Show([...lines4Show]) setIsDraging(false)
return return
} }
if (lines4Show.length !== 0 && lines4Show[0].pts.length !== 0) { if (settings.runInpaintingManually) {
setIsDraging(false)
} else {
runInpainting() runInpainting()
} }
} }
@ -387,36 +385,32 @@ export default function Editor(props: EditorProps) {
return return
} }
setIsDraging(true) setIsDraging(true)
const currLine4Show = lines4Show[lines4Show.length - 1]
currLine4Show.size = brushSize
const currLine = lines[lines.length - 1]
currLine.size = brushSize
const mouseEvent = ev.nativeEvent as MouseEvent let lineGroup: LineGroup = []
onPaint(mouseEvent.offsetX, mouseEvent.offsetY) if (isMultiStrokeKeyPressed || settings.runInpaintingManually) {
lineGroup = [...curLineGroup]
}
lineGroup.push({ size: brushSize, pts: [mouseXY(ev)] })
setCurLineGroup(lineGroup)
drawOnCurrentRender(lineGroup)
} }
const undo = () => { const undo = () => {
if (!renders.length) { if (!renders.length) {
return return
} }
if (!historyLineCount.length) {
return const groups = lineGroups.slice(0, lineGroups.length - 1)
setLineGroups(groups)
setCurLineGroup([])
setIsDraging(false)
const newRenders = renders.slice(0, renders.length - 1)
setRenders(newRenders)
if (newRenders.length === 0) {
draw(original, [])
} else {
draw(newRenders[newRenders.length - 1], [])
} }
const l = lines
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])
} }
// Handle Cmd+Z // Handle Cmd+Z
@ -688,16 +682,37 @@ export default function Editor(props: EditorProps) {
}, 350) }, 350)
}} }}
disabled={renders.length === 0} disabled={renders.length === 0}
> />
{undefined}
</Button>
<Button <Button
icon={<DownloadIcon />} icon={<DownloadIcon />}
disabled={!renders.length} disabled={!renders.length}
onClick={download} onClick={download}
/>
{settings.runInpaintingManually && (
<Button
icon={
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
{undefined} <path
</Button> d="M2 13L1.34921 12.2407C1.16773 12.3963 1.04797 12.6117 1.01163 12.8479L2 13ZM22.5 4L23.49 4.14142C23.5309 3.85444 23.4454 3.5638 23.2555 3.3448C23.0655 3.1258 22.7899 3 22.5 3V4ZM12.5 4V3C12.2613 3 12.0305 3.08539 11.8492 3.24074L12.5 4ZM1 19.5L0.0116283 19.3479C-0.0327373 19.6363 0.051055 19.9297 0.241035 20.1511C0.431014 20.3726 0.708231 20.5 1 20.5V19.5ZM11.5 19.5V20.5C11.7373 20.5 11.9668 20.4156 12.1476 20.2619L11.5 19.5ZM21.5 11L22.1476 11.7619C22.3337 11.6038 22.4554 11.3831 22.49 11.1414L21.5 11ZM2 14H12.5V12H2V14ZM13.169 13.7433L23.169 4.74329L21.831 3.25671L11.831 12.2567L13.169 13.7433ZM22.5 3H12.5V5H22.5V3ZM11.8492 3.24074L1.34921 12.2407L2.65079 13.7593L13.1508 4.75926L11.8492 3.24074ZM1.01163 12.8479L0.0116283 19.3479L1.98837 19.6521L2.98837 13.1521L1.01163 12.8479ZM1 20.5H11.5V18.5H1V20.5ZM12.4884 19.6521L13.4884 13.1521L11.5116 12.8479L10.5116 19.3479L12.4884 19.6521ZM21.51 3.85858L20.51 10.8586L22.49 11.1414L23.49 4.14142L21.51 3.85858ZM20.8524 10.2381L10.8524 18.7381L12.1476 20.2619L22.1476 11.7619L20.8524 10.2381Z"
fill="currentColor"
/>
</svg>
}
disabled={!hadDrawSomething() || isInpaintingLoading}
onClick={() => {
if (!isInpaintingLoading && hadDrawSomething()) {
runInpainting()
}
}}
/>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,24 +1,24 @@
import React, { ReactNode } from 'react' import React from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { settingState } from '../../store/Atoms' import { settingState } from '../../store/Atoms'
import { Switch, SwitchThumb } from '../shared/Switch' import { Switch, SwitchThumb } from '../shared/Switch'
import SettingBlock from './SettingBlock' import SettingBlock from './SettingBlock'
function SavePathSettingBlock() { const ManualRunInpaintingSettingBlock: React.FC = () => {
const [setting, setSettingState] = useRecoilState(settingState) const [setting, setSettingState] = useRecoilState(settingState)
const onCheckChange = (checked: boolean) => { const onCheckChange = (checked: boolean) => {
setSettingState(old => { setSettingState(old => {
return { ...old, saveImageBesideOrigin: checked } return { ...old, runInpaintingManually: checked }
}) })
} }
return ( return (
<SettingBlock <SettingBlock
title="Download image beside origin image" title="Run inpainting manually"
input={ input={
<Switch <Switch
checked={setting.saveImageBesideOrigin} checked={setting.runInpaintingManually}
onCheckedChange={onCheckChange} onCheckedChange={onCheckChange}
> >
<SwitchThumb /> <SwitchThumb />
@ -28,4 +28,4 @@ function SavePathSettingBlock() {
) )
} }
export default SavePathSettingBlock export default ManualRunInpaintingSettingBlock

View File

@ -3,6 +3,7 @@ import React from 'react'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { settingState } from '../../store/Atoms' import { settingState } from '../../store/Atoms'
import Modal from '../shared/Modal' import Modal from '../shared/Modal'
import ManualRunInpaintingSettingBlock from './ManualRunInpaintingSettingBlock'
import HDSettingBlock from './HDSettingBlock' import HDSettingBlock from './HDSettingBlock'
import ModelSettingBlock from './ModelSettingBlock' import ModelSettingBlock from './ModelSettingBlock'
@ -27,9 +28,7 @@ export default function SettingModal(props: SettingModalProps) {
className="modal-setting" className="modal-setting"
show={setting.show} show={setting.show}
> >
{/* It's not possible because this poses a security risk */} <ManualRunInpaintingSettingBlock />
{/* https://stackoverflow.com/questions/34870711/download-a-file-at-different-location-using-html5 */}
{/* <SavePathSettingBlock /> */}
<ModelSettingBlock /> <ModelSettingBlock />
<HDSettingBlock /> <HDSettingBlock />
</Modal> </Modal>

View File

@ -32,7 +32,7 @@ export const shortcutsState = atom<boolean>({
export interface Settings { export interface Settings {
show: boolean show: boolean
saveImageBesideOrigin: boolean runInpaintingManually: boolean
model: AIModel model: AIModel
// For LaMa // For LaMa
@ -47,7 +47,7 @@ export interface Settings {
export const settingStateDefault = { export const settingStateDefault = {
show: false, show: false,
saveImageBesideOrigin: false, runInpaintingManually: false,
model: AIModel.LAMA, model: AIModel.LAMA,
ldmSteps: 50, ldmSteps: 50,
hdStrategy: HDStrategy.RESIZE, hdStrategy: HDStrategy.RESIZE,