wip
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { useKeyPressEvent } from 'react-use'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { nanoid } from 'nanoid'
|
||||
import useInputImage from './hooks/useInputImage'
|
||||
@@ -9,6 +8,7 @@ import Workspace from './components/Workspace'
|
||||
import { fileState } from './store/Atoms'
|
||||
import { keepGUIAlive } from './utils'
|
||||
import Header from './components/Header/Header'
|
||||
import useHotKey from './hooks/useHotkey'
|
||||
|
||||
// Keeping GUI Window Open
|
||||
keepGUIAlive()
|
||||
@@ -24,11 +24,15 @@ function App() {
|
||||
}, [userInputImage, setFile])
|
||||
|
||||
// Dark Mode Hotkey
|
||||
useKeyPressEvent('D', ev => {
|
||||
ev?.preventDefault()
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light'
|
||||
setTheme(newTheme)
|
||||
})
|
||||
useHotKey(
|
||||
'shift+d',
|
||||
() => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light'
|
||||
setTheme(newTheme)
|
||||
},
|
||||
{},
|
||||
[theme]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('data-theme', theme)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Settings } from '../store/Atoms'
|
||||
import { Rect, Settings } from '../store/Atoms'
|
||||
import { dataURItoBlob } from '../utils'
|
||||
|
||||
export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}`
|
||||
@@ -7,6 +7,8 @@ export default async function inpaint(
|
||||
imageFile: File,
|
||||
maskBase64: string,
|
||||
settings: Settings,
|
||||
croperRect: Rect,
|
||||
prompt?: string,
|
||||
sizeLimit?: string
|
||||
) {
|
||||
// 1080, 2000, Original
|
||||
@@ -30,6 +32,18 @@ export default async function inpaint(
|
||||
hdSettings.hdStrategyResizeLimit.toString()
|
||||
)
|
||||
|
||||
fd.append('prompt', prompt === undefined ? '' : prompt)
|
||||
fd.append('croperX', croperRect.x.toString())
|
||||
fd.append('croperY', croperRect.y.toString())
|
||||
fd.append('croperHeight', croperRect.height.toString())
|
||||
fd.append('croperWidth', croperRect.width.toString())
|
||||
fd.append('useCroper', settings.showCroper ? 'true' : 'false')
|
||||
fd.append('sdStrength', settings.sdStrength.toString())
|
||||
fd.append('sdSteps', settings.sdSteps.toString())
|
||||
fd.append('sdGuidanceScale', settings.sdGuidanceScale.toString())
|
||||
fd.append('sdSampler', settings.sdSampler.toString())
|
||||
fd.append('sdSeed', settings.sdSeedFixed ? settings.sdSeed.toString() : '-1')
|
||||
|
||||
if (sizeLimit === undefined) {
|
||||
fd.append('sizeLimit', '1080')
|
||||
} else {
|
||||
|
||||
126
lama_cleaner/app/src/components/Croper/Croper.scss
Normal file
126
lama_cleaner/app/src/components/Croper/Croper.scss
Normal file
@@ -0,0 +1,126 @@
|
||||
@use 'sass:math';
|
||||
|
||||
$drag-handle-shortside: 12px;
|
||||
$drag-handle-longside: 40px;
|
||||
|
||||
$half-handle-shortside: math.div($drag-handle-shortside, 2);
|
||||
$half-handle-longside: math.div($drag-handle-longside, 2);
|
||||
|
||||
.crop-border {
|
||||
outline-color: var(--yellow-accent);
|
||||
outline-style: dashed;
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
font-size: 1rem;
|
||||
padding: 0.2rem 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--text-color);
|
||||
background-color: var(--page-bg);
|
||||
border-radius: 9999px;
|
||||
|
||||
border: var(--editor-toolkit-panel-border);
|
||||
box-shadow: 0 0 0 1px #0000001a, 0 3px 16px #00000014, 0 2px 6px 1px #00000017;
|
||||
|
||||
&:hover {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.croper-wrapper {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.croper {
|
||||
position: relative;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
width: $drag-handle-shortside;
|
||||
height: $drag-handle-shortside;
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
border: 2px solid var(--yellow-accent);
|
||||
background-color: var(--yellow-accent-light);
|
||||
pointer-events: auto;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--yellow-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.ord-topleft {
|
||||
cursor: nw-resize;
|
||||
top: (-$half-handle-shortside)-1px;
|
||||
left: (-$half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-topright {
|
||||
cursor: ne-resize;
|
||||
top: -($half-handle-shortside)-1px;
|
||||
right: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-bottomright {
|
||||
cursor: se-resize;
|
||||
bottom: -($half-handle-shortside)-1px;
|
||||
right: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-bottomleft {
|
||||
cursor: sw-resize;
|
||||
bottom: -($half-handle-shortside)-1px;
|
||||
left: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-top,
|
||||
.ord-bottom {
|
||||
left: calc(50% - $half-handle-shortside);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.ord-top {
|
||||
top: (-$half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-bottom {
|
||||
bottom: -($half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-left,
|
||||
.ord-right {
|
||||
top: calc(50% - $half-handle-shortside);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.ord-left {
|
||||
left: (-$half-handle-shortside)-1px;
|
||||
}
|
||||
|
||||
.ord-right {
|
||||
right: -($half-handle-shortside)-1px;
|
||||
}
|
||||
326
lama_cleaner/app/src/components/Croper/Croper.tsx
Normal file
326
lama_cleaner/app/src/components/Croper/Croper.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import {
|
||||
croperHeight,
|
||||
croperWidth,
|
||||
croperX,
|
||||
croperY,
|
||||
isInpaintingState,
|
||||
} from '../../store/Atoms'
|
||||
|
||||
const DOC_MOVE_OPTS = { capture: true, passive: false }
|
||||
|
||||
const DRAG_HANDLE_BORDER = 2
|
||||
const DRAG_HANDLE_SHORT = 12
|
||||
const DRAG_HANDLE_LONG = 40
|
||||
|
||||
interface EVData {
|
||||
initX: number
|
||||
initY: number
|
||||
initHeight: number
|
||||
initWidth: number
|
||||
startResizeX: number
|
||||
startResizeY: number
|
||||
ord: string // top/right/bottom/left
|
||||
}
|
||||
|
||||
interface Props {
|
||||
maxHeight: number
|
||||
maxWidth: number
|
||||
scale: number
|
||||
minHeight: number
|
||||
minWidth: number
|
||||
}
|
||||
|
||||
const Croper = (props: Props) => {
|
||||
const { minHeight, minWidth, maxHeight, maxWidth, scale } = props
|
||||
const [x, setX] = useRecoilState(croperX)
|
||||
const [y, setY] = useRecoilState(croperY)
|
||||
const [height, setHeight] = useRecoilState(croperHeight)
|
||||
const [width, setWidth] = useRecoilState(croperWidth)
|
||||
const isInpainting = useRecoilValue(isInpaintingState)
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [isMoving, setIsMoving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setX(Math.round((maxWidth - 512) / 2))
|
||||
setY(Math.round((maxHeight - 512) / 2))
|
||||
}, [maxHeight, maxWidth, minHeight, minWidth])
|
||||
|
||||
const [evData, setEVData] = useState<EVData>({
|
||||
initX: 0,
|
||||
initY: 0,
|
||||
initHeight: 0,
|
||||
initWidth: 0,
|
||||
startResizeX: 0,
|
||||
startResizeY: 0,
|
||||
ord: 'top',
|
||||
})
|
||||
|
||||
const onDragFocus = () => {
|
||||
console.log('focus')
|
||||
}
|
||||
|
||||
const checkTopBottomLimit = (newY: number, newHeight: number) => {
|
||||
if (newY > 0 && newHeight > minHeight && newY + newHeight <= maxHeight) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkLeftRightLimit = (newX: number, newWidth: number) => {
|
||||
if (newX > 0 && newWidth > minWidth && newX + newWidth <= maxWidth) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
const curX = e.clientX
|
||||
const curY = e.clientY
|
||||
if (isResizing) {
|
||||
switch (evData.ord) {
|
||||
case 'top': {
|
||||
// TODO: 添加四个角以及 drag bar handle
|
||||
const offset = Math.round((curY - evData.startResizeY) / scale)
|
||||
const newHeight = evData.initHeight - offset
|
||||
const newY = evData.initY + offset
|
||||
if (checkTopBottomLimit(newY, newHeight)) {
|
||||
setHeight(newHeight)
|
||||
setY(newY)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'right': {
|
||||
const offset = Math.round((curX - evData.startResizeX) / scale)
|
||||
const newWidth = evData.initWidth + offset
|
||||
if (checkLeftRightLimit(evData.initX, newWidth)) {
|
||||
setWidth(newWidth)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'bottom': {
|
||||
const offset = Math.round((curY - evData.startResizeY) / scale)
|
||||
const newHeight = evData.initHeight + offset
|
||||
if (checkTopBottomLimit(evData.initY, newHeight)) {
|
||||
setHeight(newHeight)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'left': {
|
||||
const offset = Math.round((curX - evData.startResizeX) / scale)
|
||||
const newWidth = evData.initWidth - offset
|
||||
const newX = evData.initX + offset
|
||||
if (checkLeftRightLimit(newX, newWidth)) {
|
||||
setWidth(newWidth)
|
||||
setX(newX)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (isMoving) {
|
||||
const offsetX = Math.round((curX - evData.startResizeX) / scale)
|
||||
const offsetY = Math.round((curY - evData.startResizeY) / scale)
|
||||
const newX = evData.initX + offsetX
|
||||
const newY = evData.initY + offsetY
|
||||
if (
|
||||
checkLeftRightLimit(newX, evData.initWidth) &&
|
||||
checkTopBottomLimit(newY, evData.initHeight)
|
||||
) {
|
||||
setX(newX)
|
||||
setY(newY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerDone = (e: PointerEvent) => {
|
||||
if (isResizing) {
|
||||
setIsResizing(false)
|
||||
}
|
||||
|
||||
if (isMoving) {
|
||||
setIsMoving(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizing || isMoving) {
|
||||
document.addEventListener('pointermove', onPointerMove, DOC_MOVE_OPTS)
|
||||
document.addEventListener('pointerup', onPointerDone, DOC_MOVE_OPTS)
|
||||
document.addEventListener('pointercancel', onPointerDone, DOC_MOVE_OPTS)
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
'pointermove',
|
||||
onPointerMove,
|
||||
DOC_MOVE_OPTS
|
||||
)
|
||||
document.removeEventListener('pointerup', onPointerDone, DOC_MOVE_OPTS)
|
||||
document.removeEventListener(
|
||||
'pointercancel',
|
||||
onPointerDone,
|
||||
DOC_MOVE_OPTS
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [isResizing, isMoving, width, height, evData])
|
||||
|
||||
const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const { ord } = (e.target as HTMLElement).dataset
|
||||
if (ord) {
|
||||
setIsResizing(true)
|
||||
setEVData({
|
||||
initX: x,
|
||||
initY: y,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
startResizeX: e.clientX,
|
||||
startResizeY: e.clientY,
|
||||
ord,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const createCropSelection = () => {
|
||||
return (
|
||||
<div
|
||||
className="drag-elements"
|
||||
onFocus={onDragFocus}
|
||||
onPointerDown={onCropPointerDown}
|
||||
>
|
||||
<div
|
||||
className="drag-handle ord-topleft"
|
||||
data-ord="topleft"
|
||||
aria-label="topleft"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-topright"
|
||||
data-ord="topright"
|
||||
aria-label="topright"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-bottomleft"
|
||||
data-ord="bottomleft"
|
||||
aria-label="bottomleft"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-bottomright"
|
||||
data-ord="bottomright"
|
||||
aria-label="bottomright"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="drag-handle ord-top"
|
||||
data-ord="top"
|
||||
aria-label="top"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-right"
|
||||
data-ord="right"
|
||||
aria-label="right"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-bottom"
|
||||
data-ord="bottom"
|
||||
aria-label="bottom"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
<div
|
||||
className="drag-handle ord-left"
|
||||
data-ord="left"
|
||||
aria-label="left"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={{ transform: `scale(${1 / scale})` }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
setIsMoving(true)
|
||||
setEVData({
|
||||
initX: x,
|
||||
initY: y,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
startResizeX: e.clientX,
|
||||
startResizeY: e.clientY,
|
||||
ord: '',
|
||||
})
|
||||
}
|
||||
|
||||
const createInfoBar = () => {
|
||||
return (
|
||||
<div
|
||||
className="info-bar"
|
||||
onPointerDown={onInfoBarPointerDown}
|
||||
style={{
|
||||
transform: `scale(${1 / scale})`,
|
||||
top: `${-28 / scale - 14}px`,
|
||||
}}
|
||||
>
|
||||
<div className="crop-size">
|
||||
{width} x {height}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createBorder = () => {
|
||||
return (
|
||||
<div
|
||||
className="crop-border"
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
outlineWidth: `${DRAG_HANDLE_BORDER / scale}px`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="croper-wrapper">
|
||||
<div className="croper" style={{ height, width, left: x, top: y }}>
|
||||
{createBorder()}
|
||||
{createInfoBar()}
|
||||
{createCropSelection()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Croper
|
||||
@@ -55,7 +55,7 @@
|
||||
position: fixed;
|
||||
bottom: 0.5rem;
|
||||
border-radius: 3rem;
|
||||
padding: 1rem 3rem;
|
||||
padding: 0.6rem 3rem;
|
||||
display: grid;
|
||||
grid-template-areas: 'toolkit-size-selector toolkit-brush-slider toolkit-btns';
|
||||
column-gap: 2rem;
|
||||
|
||||
@@ -22,7 +22,6 @@ import Button from '../shared/Button'
|
||||
import Slider from './Slider'
|
||||
import SizeSelector from './SizeSelector'
|
||||
import {
|
||||
dataURItoBlob,
|
||||
downloadImage,
|
||||
isMidClick,
|
||||
isRightClick,
|
||||
@@ -30,7 +29,18 @@ import {
|
||||
srcToFile,
|
||||
useImage,
|
||||
} from '../../utils'
|
||||
import { settingState, toastState } from '../../store/Atoms'
|
||||
import {
|
||||
croperState,
|
||||
isInpaintingState,
|
||||
isSDState,
|
||||
propmtState,
|
||||
runManuallyState,
|
||||
settingState,
|
||||
toastState,
|
||||
} from '../../store/Atoms'
|
||||
import useHotKey from '../../hooks/useHotkey'
|
||||
import Croper from '../Croper/Croper'
|
||||
import emitter, { EVENT_PROMPT } from '../../event'
|
||||
|
||||
const TOOLBAR_SIZE = 200
|
||||
const BRUSH_COLOR = '#ffcc00bb'
|
||||
@@ -74,8 +84,14 @@ function mouseXY(ev: SyntheticEvent) {
|
||||
|
||||
export default function Editor(props: EditorProps) {
|
||||
const { file } = props
|
||||
const promptVal = useRecoilValue(propmtState)
|
||||
const settings = useRecoilValue(settingState)
|
||||
const croperRect = useRecoilValue(croperState)
|
||||
const [toastVal, setToastState] = useRecoilState(toastState)
|
||||
const [isInpainting, setIsInpainting] = useRecoilState(isInpaintingState)
|
||||
const runMannually = useRecoilValue(runManuallyState)
|
||||
const isSD = useRecoilValue(isSDState)
|
||||
|
||||
const [brushSize, setBrushSize] = useState(40)
|
||||
const [original, isOriginalLoaded] = useImage(file)
|
||||
const [renders, setRenders] = useState<HTMLImageElement[]>([])
|
||||
@@ -90,7 +106,6 @@ export default function Editor(props: EditorProps) {
|
||||
const [showRefBrush, setShowRefBrush] = useState(false)
|
||||
const [isPanning, setIsPanning] = useState<boolean>(false)
|
||||
const [showOriginal, setShowOriginal] = useState(false)
|
||||
const [isInpaintingLoading, setIsInpaintingLoading] = useState(false)
|
||||
const [scale, setScale] = useState<number>(1)
|
||||
const [panned, setPanned] = useState<boolean>(false)
|
||||
const [minScale, setMinScale] = useState<number>(1.0)
|
||||
@@ -130,83 +145,28 @@ export default function Editor(props: EditorProps) {
|
||||
[context, original]
|
||||
)
|
||||
|
||||
const drawLinesOnMask = (_lineGroups: LineGroup[]) => {
|
||||
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')
|
||||
}
|
||||
|
||||
_lineGroups.forEach(lineGroup => {
|
||||
drawLines(ctx, lineGroup, 'white')
|
||||
})
|
||||
}
|
||||
|
||||
const runInpainting = async () => {
|
||||
if (!hadDrawSomething()) {
|
||||
return
|
||||
}
|
||||
|
||||
const newLineGroups = [...lineGroups, curLineGroup]
|
||||
setCurLineGroup([])
|
||||
setIsDraging(false)
|
||||
setIsInpaintingLoading(true)
|
||||
if (settings.graduallyInpainting) {
|
||||
drawLinesOnMask([curLineGroup])
|
||||
} else {
|
||||
drawLinesOnMask(newLineGroups)
|
||||
}
|
||||
|
||||
let targetFile = file
|
||||
if (settings.graduallyInpainting === true && renders.length > 0) {
|
||||
console.info('gradually inpainting on last result')
|
||||
const lastRender = renders[renders.length - 1]
|
||||
targetFile = await srcToFile(lastRender.currentSrc, file.name, file.type)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await inpaint(
|
||||
targetFile,
|
||||
maskCanvas.toDataURL(),
|
||||
settings,
|
||||
sizeLimit.toString()
|
||||
)
|
||||
if (!res) {
|
||||
throw new Error('empty response')
|
||||
const drawLinesOnMask = useCallback(
|
||||
(_lineGroups: LineGroup[]) => {
|
||||
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')
|
||||
}
|
||||
const newRender = new Image()
|
||||
await loadImage(newRender, res)
|
||||
const newRenders = [...renders, newRender]
|
||||
setRenders(newRenders)
|
||||
draw(newRender, [])
|
||||
// Only append new LineGroup after inpainting success
|
||||
setLineGroups(newLineGroups)
|
||||
|
||||
// clear redo stack
|
||||
resetRedoState()
|
||||
} catch (e: any) {
|
||||
setToastState({
|
||||
open: true,
|
||||
desc: e.message ? e.message : e.toString(),
|
||||
state: 'error',
|
||||
duration: 2000,
|
||||
_lineGroups.forEach(lineGroup => {
|
||||
drawLines(ctx, lineGroup, 'white')
|
||||
})
|
||||
drawOnCurrentRender([])
|
||||
}
|
||||
setIsInpaintingLoading(false)
|
||||
}
|
||||
},
|
||||
[context, maskCanvas]
|
||||
)
|
||||
|
||||
const hadDrawSomething = () => {
|
||||
const hadDrawSomething = useCallback(() => {
|
||||
return curLineGroup.length !== 0
|
||||
}
|
||||
|
||||
const hadRunInpainting = () => {
|
||||
return renders.length !== 0
|
||||
}
|
||||
}, [curLineGroup])
|
||||
|
||||
const drawOnCurrentRender = useCallback(
|
||||
(lineGroup: LineGroup) => {
|
||||
@@ -219,8 +179,107 @@ export default function Editor(props: EditorProps) {
|
||||
[original, renders, draw]
|
||||
)
|
||||
|
||||
const runInpainting = useCallback(
|
||||
async (prompt?: string) => {
|
||||
console.log('runInpainting')
|
||||
if (!hadDrawSomething()) {
|
||||
return
|
||||
}
|
||||
console.log(prompt)
|
||||
|
||||
const newLineGroups = [...lineGroups, curLineGroup]
|
||||
setCurLineGroup([])
|
||||
setIsDraging(false)
|
||||
setIsInpainting(true)
|
||||
if (settings.graduallyInpainting) {
|
||||
drawLinesOnMask([curLineGroup])
|
||||
} else {
|
||||
drawLinesOnMask(newLineGroups)
|
||||
}
|
||||
|
||||
let targetFile = file
|
||||
if (settings.graduallyInpainting === true && renders.length > 0) {
|
||||
console.info('gradually inpainting on last result')
|
||||
const lastRender = renders[renders.length - 1]
|
||||
targetFile = await srcToFile(
|
||||
lastRender.currentSrc,
|
||||
file.name,
|
||||
file.type
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await inpaint(
|
||||
targetFile,
|
||||
maskCanvas.toDataURL(),
|
||||
settings,
|
||||
croperRect,
|
||||
prompt,
|
||||
sizeLimit.toString()
|
||||
)
|
||||
if (!res) {
|
||||
throw new Error('empty response')
|
||||
}
|
||||
const newRender = new Image()
|
||||
await loadImage(newRender, res)
|
||||
const newRenders = [...renders, newRender]
|
||||
setRenders(newRenders)
|
||||
draw(newRender, [])
|
||||
// Only append new LineGroup after inpainting success
|
||||
setLineGroups(newLineGroups)
|
||||
|
||||
// clear redo stack
|
||||
resetRedoState()
|
||||
} catch (e: any) {
|
||||
setToastState({
|
||||
open: true,
|
||||
desc: e.message ? e.message : e.toString(),
|
||||
state: 'error',
|
||||
duration: 4000,
|
||||
})
|
||||
drawOnCurrentRender([])
|
||||
}
|
||||
setIsInpainting(false)
|
||||
},
|
||||
[
|
||||
lineGroups,
|
||||
curLineGroup,
|
||||
maskCanvas,
|
||||
settings.graduallyInpainting,
|
||||
settings,
|
||||
croperRect,
|
||||
sizeLimit,
|
||||
promptVal,
|
||||
drawOnCurrentRender,
|
||||
hadDrawSomething,
|
||||
drawLinesOnMask,
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on(EVENT_PROMPT, () => {
|
||||
if (hadDrawSomething()) {
|
||||
runInpainting(promptVal)
|
||||
} else {
|
||||
setToastState({
|
||||
open: true,
|
||||
desc: 'Please draw mask on picture',
|
||||
state: 'error',
|
||||
duration: 1500,
|
||||
})
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
emitter.off(EVENT_PROMPT)
|
||||
}
|
||||
}, [hadDrawSomething, runInpainting, prompt])
|
||||
|
||||
const hadRunInpainting = () => {
|
||||
return renders.length !== 0
|
||||
}
|
||||
|
||||
const handleMultiStrokeKeyDown = () => {
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
setIsMultiStrokeKeyPressed(true)
|
||||
@@ -230,13 +289,13 @@ export default function Editor(props: EditorProps) {
|
||||
if (!isMultiStrokeKeyPressed) {
|
||||
return
|
||||
}
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsMultiStrokeKeyPressed(false)
|
||||
|
||||
if (!settings.runInpaintingManually) {
|
||||
if (!runMannually) {
|
||||
runInpainting()
|
||||
}
|
||||
}
|
||||
@@ -246,7 +305,7 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
|
||||
useKey(predicate, handleMultiStrokeKeyup, { event: 'keyup' }, [
|
||||
isInpaintingLoading,
|
||||
isInpainting,
|
||||
isMultiStrokeKeyPressed,
|
||||
hadDrawSomething,
|
||||
])
|
||||
@@ -257,7 +316,7 @@ export default function Editor(props: EditorProps) {
|
||||
{
|
||||
event: 'keydown',
|
||||
},
|
||||
[isInpaintingLoading]
|
||||
[isInpainting]
|
||||
)
|
||||
|
||||
// Draw once the original image is loaded
|
||||
@@ -341,7 +400,7 @@ export default function Editor(props: EditorProps) {
|
||||
}, [windowSize, resetZoom])
|
||||
|
||||
const handleEscPressed = () => {
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
if (isDraging || isMultiStrokeKeyPressed) {
|
||||
@@ -361,7 +420,7 @@ export default function Editor(props: EditorProps) {
|
||||
},
|
||||
[
|
||||
isDraging,
|
||||
isInpaintingLoading,
|
||||
isInpainting,
|
||||
isMultiStrokeKeyPressed,
|
||||
resetZoom,
|
||||
drawOnCurrentRender,
|
||||
@@ -404,7 +463,7 @@ export default function Editor(props: EditorProps) {
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
if (!isDraging) {
|
||||
@@ -416,13 +475,29 @@ export default function Editor(props: EditorProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (settings.runInpaintingManually) {
|
||||
if (runMannually) {
|
||||
setIsDraging(false)
|
||||
} else {
|
||||
runInpainting()
|
||||
}
|
||||
}
|
||||
|
||||
const isOutsideCroper = (clickPnt: { x: number; y: number }) => {
|
||||
if (clickPnt.x < croperRect.x) {
|
||||
return true
|
||||
}
|
||||
if (clickPnt.y < croperRect.y) {
|
||||
return true
|
||||
}
|
||||
if (clickPnt.x > croperRect.x + croperRect.width) {
|
||||
return true
|
||||
}
|
||||
if (clickPnt.y > croperRect.y + croperRect.height) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const onMouseDown = (ev: SyntheticEvent) => {
|
||||
if (isPanning) {
|
||||
return
|
||||
@@ -434,7 +509,7 @@ export default function Editor(props: EditorProps) {
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -447,10 +522,14 @@ export default function Editor(props: EditorProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isSD && settings.showCroper && isOutsideCroper(mouseXY(ev))) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsDraging(true)
|
||||
|
||||
let lineGroup: LineGroup = []
|
||||
if (isMultiStrokeKeyPressed || settings.runInpaintingManually) {
|
||||
if (isMultiStrokeKeyPressed || runMannually) {
|
||||
lineGroup = [...curLineGroup]
|
||||
}
|
||||
lineGroup.push({ size: brushSize, pts: [mouseXY(ev)] })
|
||||
@@ -501,7 +580,7 @@ export default function Editor(props: EditorProps) {
|
||||
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
|
||||
|
||||
const undo = () => {
|
||||
if (settings.runInpaintingManually && curLineGroup.length !== 0) {
|
||||
if (runMannually && curLineGroup.length !== 0) {
|
||||
undoStroke()
|
||||
} else {
|
||||
undoRender()
|
||||
@@ -527,14 +606,14 @@ export default function Editor(props: EditorProps) {
|
||||
useKey(undoPredicate, undo, undefined, [undoStroke, undoRender])
|
||||
|
||||
const disableUndo = () => {
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return true
|
||||
}
|
||||
if (renders.length > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (settings.runInpaintingManually) {
|
||||
if (runMannually) {
|
||||
if (curLineGroup.length === 0) {
|
||||
return true
|
||||
}
|
||||
@@ -575,7 +654,7 @@ export default function Editor(props: EditorProps) {
|
||||
}, [draw, renders, redoRenders, redoLineGroups, lineGroups, original])
|
||||
|
||||
const redo = () => {
|
||||
if (settings.runInpaintingManually && redoCurLines.length !== 0) {
|
||||
if (runMannually && redoCurLines.length !== 0) {
|
||||
redoStroke()
|
||||
} else {
|
||||
redoRender()
|
||||
@@ -603,14 +682,14 @@ export default function Editor(props: EditorProps) {
|
||||
useKey(redoPredicate, redo, undefined, [redoStroke, redoRender])
|
||||
|
||||
const disableRedo = () => {
|
||||
if (isInpaintingLoading) {
|
||||
if (isInpainting) {
|
||||
return true
|
||||
}
|
||||
if (redoRenders.length > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (settings.runInpaintingManually) {
|
||||
if (runMannually) {
|
||||
if (redoCurLines.length === 0) {
|
||||
return true
|
||||
}
|
||||
@@ -688,7 +767,7 @@ export default function Editor(props: EditorProps) {
|
||||
}, [showBrush, isPanning])
|
||||
|
||||
// Standard Hotkeys for Brush Size
|
||||
useKeyPressEvent('[', () => {
|
||||
useHotKey('[', () => {
|
||||
setBrushSize(currentBrushSize => {
|
||||
if (currentBrushSize > 10) {
|
||||
return currentBrushSize - 10
|
||||
@@ -700,18 +779,23 @@ export default function Editor(props: EditorProps) {
|
||||
})
|
||||
})
|
||||
|
||||
useKeyPressEvent(']', () => {
|
||||
useHotKey(']', () => {
|
||||
setBrushSize(currentBrushSize => {
|
||||
return currentBrushSize + 10
|
||||
})
|
||||
})
|
||||
|
||||
// Manual Inpainting Hotkey
|
||||
useKeyPressEvent('R', () => {
|
||||
if (settings.runInpaintingManually && hadDrawSomething()) {
|
||||
runInpainting()
|
||||
}
|
||||
})
|
||||
useHotKey(
|
||||
'shift+r',
|
||||
() => {
|
||||
if (runMannually && hadDrawSomething()) {
|
||||
runInpainting()
|
||||
}
|
||||
},
|
||||
{},
|
||||
[runMannually]
|
||||
)
|
||||
|
||||
// Toggle clean/zoom tool on spacebar.
|
||||
useKeyPressEvent(
|
||||
@@ -792,7 +876,7 @@ export default function Editor(props: EditorProps) {
|
||||
}}
|
||||
>
|
||||
<TransformComponent
|
||||
contentClass={isInpaintingLoading ? 'editor-canvas-loading' : ''}
|
||||
contentClass={isInpainting ? 'editor-canvas-loading' : ''}
|
||||
contentStyle={{
|
||||
visibility: initialCentered ? 'visible' : 'hidden',
|
||||
}}
|
||||
@@ -852,10 +936,22 @@ export default function Editor(props: EditorProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settings.showCroper ? (
|
||||
<Croper
|
||||
maxHeight={original.naturalHeight}
|
||||
maxWidth={original.naturalWidth}
|
||||
minHeight={Math.min(256, original.naturalHeight)}
|
||||
minWidth={Math.min(256, original.naturalWidth)}
|
||||
scale={scale}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
|
||||
{showBrush && !isInpaintingLoading && !isPanning && (
|
||||
{showBrush && !isInpainting && !isPanning && (
|
||||
<div className="brush-shape" style={getBrushStyle(x, y)} />
|
||||
)}
|
||||
|
||||
@@ -867,11 +963,15 @@ export default function Editor(props: EditorProps) {
|
||||
)}
|
||||
|
||||
<div className="editor-toolkit-panel">
|
||||
<SizeSelector
|
||||
onChange={onSizeLimitChange}
|
||||
originalWidth={original.naturalWidth}
|
||||
originalHeight={original.naturalHeight}
|
||||
/>
|
||||
{isSD ? (
|
||||
<></>
|
||||
) : (
|
||||
<SizeSelector
|
||||
onChange={onSizeLimitChange}
|
||||
originalWidth={original.naturalWidth}
|
||||
originalHeight={original.naturalHeight}
|
||||
/>
|
||||
)}
|
||||
<Slider
|
||||
label="Brush"
|
||||
min={10}
|
||||
@@ -977,9 +1077,9 @@ export default function Editor(props: EditorProps) {
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
disabled={!hadDrawSomething() || isInpaintingLoading}
|
||||
disabled={!hadDrawSomething() || isInpainting}
|
||||
onClick={() => {
|
||||
if (!isInpaintingLoading && hadDrawSomething()) {
|
||||
if (!isInpainting && hadDrawSomething()) {
|
||||
runInpainting()
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
header {
|
||||
height: 60px;
|
||||
padding: 1rem 2rem;
|
||||
padding: 1rem 1.5rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
@@ -31,4 +31,4 @@ header {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { ArrowLeftIcon, UploadIcon } from '@heroicons/react/outline'
|
||||
import React, { useState } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { fileState } from '../../store/Atoms'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { fileState, isSDState } from '../../store/Atoms'
|
||||
import Button from '../shared/Button'
|
||||
import Shortcuts from '../Shortcuts/Shortcuts'
|
||||
import useResolution from '../../hooks/useResolution'
|
||||
import { ThemeChanger } from './ThemeChanger'
|
||||
import SettingIcon from '../Settings/SettingIcon'
|
||||
import PromptInput from './PromptInput'
|
||||
|
||||
const Header = () => {
|
||||
const [file, setFile] = useRecoilState(fileState)
|
||||
const resolution = useResolution()
|
||||
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`)
|
||||
const isSD = useRecoilValue(isSDState)
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
@@ -37,6 +39,8 @@ const Header = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isSD && file ? <PromptInput /> : <></>}
|
||||
|
||||
<div className="header-icons-wrapper">
|
||||
<ThemeChanger />
|
||||
{file && (
|
||||
|
||||
18
lama_cleaner/app/src/components/Header/PromptInput.scss
Normal file
18
lama_cleaner/app/src/components/Header/PromptInput.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.prompt-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.prompt-wrapper input {
|
||||
all: unset;
|
||||
border-width: 0;
|
||||
border-radius: 0.5rem;
|
||||
min-width: 600px;
|
||||
padding: 0 0.8rem;
|
||||
outline: 1px solid var(--border-color);
|
||||
|
||||
&:focus-visible {
|
||||
border-width: 0;
|
||||
outline: 1px solid var(--yellow-accent);
|
||||
}
|
||||
}
|
||||
44
lama_cleaner/app/src/components/Header/PromptInput.tsx
Normal file
44
lama_cleaner/app/src/components/Header/PromptInput.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import emitter, { EVENT_PROMPT } from '../../event'
|
||||
import { appState, propmtState } from '../../store/Atoms'
|
||||
import Button from '../shared/Button'
|
||||
import TextInput from '../shared/Input'
|
||||
|
||||
// TODO: show progress in input
|
||||
const PromptInput = () => {
|
||||
const [app, setAppState] = useRecoilState(appState)
|
||||
const [prompt, setPrompt] = useRecoilState(propmtState)
|
||||
|
||||
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
const target = evt.target as HTMLInputElement
|
||||
setPrompt(target.value)
|
||||
}
|
||||
|
||||
const handleRepaintClick = () => {
|
||||
if (prompt.length !== 0 && !app.isInpainting) {
|
||||
emitter.emit(EVENT_PROMPT)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="prompt-wrapper">
|
||||
<TextInput
|
||||
value={prompt}
|
||||
onInput={handleOnInput}
|
||||
placeholder="I want to repaint of..."
|
||||
/>
|
||||
<Button
|
||||
border
|
||||
onClick={handleRepaintClick}
|
||||
disabled={prompt.length === 0 || app.isInpainting}
|
||||
>
|
||||
RePaint
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptInput
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { AIModel, settingState } from '../../store/Atoms'
|
||||
import { AIModel, SDSampler, settingState } from '../../store/Atoms'
|
||||
import Selector from '../shared/Selector'
|
||||
import { Switch, SwitchThumb } from '../shared/Switch'
|
||||
import Tooltip from '../shared/Tooltip'
|
||||
@@ -145,6 +145,8 @@ function ModelSettingBlock() {
|
||||
return undefined
|
||||
case AIModel.FCF:
|
||||
return renderFCFModelDesc()
|
||||
case AIModel.SD14:
|
||||
return undefined
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
@@ -182,6 +184,12 @@ function ModelSettingBlock() {
|
||||
'https://arxiv.org/abs/2208.03382',
|
||||
'https://github.com/SHI-Labs/FcF-Inpainting'
|
||||
)
|
||||
case AIModel.SD14:
|
||||
return renderModelDesc(
|
||||
'Stable Diffusion',
|
||||
'https://ommer-lab.com/research/latent-diffusion-models/',
|
||||
'https://github.com/CompVis/stable-diffusion'
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -4,14 +4,28 @@ import SettingBlock from './SettingBlock'
|
||||
|
||||
interface NumberInputSettingProps {
|
||||
title: string
|
||||
allowFloat?: boolean
|
||||
desc?: string
|
||||
value: string
|
||||
suffix?: string
|
||||
width?: number
|
||||
widthUnit?: string
|
||||
disable?: boolean
|
||||
onValue: (val: string) => void
|
||||
}
|
||||
|
||||
function NumberInputSetting(props: NumberInputSettingProps) {
|
||||
const { title, desc, value, suffix, onValue } = props
|
||||
const {
|
||||
title,
|
||||
allowFloat,
|
||||
desc,
|
||||
value,
|
||||
suffix,
|
||||
onValue,
|
||||
width,
|
||||
widthUnit,
|
||||
disable,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<SettingBlock
|
||||
@@ -28,8 +42,10 @@ function NumberInputSetting(props: NumberInputSettingProps) {
|
||||
}}
|
||||
>
|
||||
<NumberInput
|
||||
style={{ width: '80px' }}
|
||||
allowFloat={allowFloat}
|
||||
style={{ width: `${width}${widthUnit}` }}
|
||||
value={`${value}`}
|
||||
disabled={disable}
|
||||
onValue={onValue}
|
||||
/>
|
||||
{suffix && <span>{suffix}</span>}
|
||||
@@ -39,4 +55,11 @@ function NumberInputSetting(props: NumberInputSettingProps) {
|
||||
)
|
||||
}
|
||||
|
||||
NumberInputSetting.defaultProps = {
|
||||
allowFloat: false,
|
||||
width: 80,
|
||||
widthUnit: 'px',
|
||||
disable: false,
|
||||
}
|
||||
|
||||
export default NumberInputSetting
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { settingState } from '../../store/Atoms'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { isSDState, settingState } from '../../store/Atoms'
|
||||
import Modal from '../shared/Modal'
|
||||
import ManualRunInpaintingSettingBlock from './ManualRunInpaintingSettingBlock'
|
||||
import HDSettingBlock from './HDSettingBlock'
|
||||
import ModelSettingBlock from './ModelSettingBlock'
|
||||
import GraduallyInpaintingSettingBlock from './GraduallyInpaintingSettingBlock'
|
||||
import DownloadMaskSettingBlock from './DownloadMaskSettingBlock'
|
||||
import useHotKey from '../../hooks/useHotkey'
|
||||
|
||||
interface SettingModalProps {
|
||||
onClose: () => void
|
||||
@@ -15,6 +16,7 @@ interface SettingModalProps {
|
||||
export default function SettingModal(props: SettingModalProps) {
|
||||
const { onClose } = props
|
||||
const [setting, setSettingState] = useRecoilState(settingState)
|
||||
const isSD = useRecoilValue(isSDState)
|
||||
|
||||
const handleOnClose = () => {
|
||||
setSettingState(old => {
|
||||
@@ -23,6 +25,17 @@ export default function SettingModal(props: SettingModalProps) {
|
||||
onClose()
|
||||
}
|
||||
|
||||
useHotKey(
|
||||
's',
|
||||
() => {
|
||||
setSettingState(old => {
|
||||
return { ...old, show: !old.show }
|
||||
})
|
||||
},
|
||||
{},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleOnClose}
|
||||
@@ -30,11 +43,12 @@ export default function SettingModal(props: SettingModalProps) {
|
||||
className="modal-setting"
|
||||
show={setting.show}
|
||||
>
|
||||
<ManualRunInpaintingSettingBlock />
|
||||
{isSD ? <></> : <ManualRunInpaintingSettingBlock />}
|
||||
|
||||
<GraduallyInpaintingSettingBlock />
|
||||
<DownloadMaskSettingBlock />
|
||||
<ModelSettingBlock />
|
||||
<HDSettingBlock />
|
||||
{isSD ? <></> : <HDSettingBlock />}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { useKeyPressEvent } from 'react-use'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import useHotKey from '../../hooks/useHotkey'
|
||||
import { shortcutsState } from '../../store/Atoms'
|
||||
import Button from '../shared/Button'
|
||||
|
||||
@@ -13,8 +13,7 @@ const Shortcuts = () => {
|
||||
})
|
||||
}
|
||||
|
||||
useKeyPressEvent('h', ev => {
|
||||
ev?.preventDefault()
|
||||
useHotKey('h', () => {
|
||||
shortcutStateHandler()
|
||||
})
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ export default function ShortcutsModal() {
|
||||
<ShortCut content="Decrease Brush Size" keys={['[']} />
|
||||
<ShortCut content="Increase Brush Size" keys={[']']} />
|
||||
<ShortCut content="Toggle Dark Mode" keys={['Shift', 'D']} />
|
||||
<ShortCut content="Toggle Hotkeys Panel" keys={['H']} />
|
||||
<ShortCut content="Toggle Hotkeys Dialog" keys={['H']} />
|
||||
<ShortCut content="Toggle Settings Dialog" keys={['S']} />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
57
lama_cleaner/app/src/components/SidePanel/SidePanel.scss
Normal file
57
lama_cleaner/app/src/components/SidePanel/SidePanel.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.side-panel {
|
||||
position: absolute;
|
||||
top: 68px;
|
||||
right: 1.5rem;
|
||||
padding: 0.3rem 0.3rem;
|
||||
z-index: 4;
|
||||
|
||||
border-radius: 0.8rem;
|
||||
border-style: solid;
|
||||
border-color: var(--border-color);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.side-panel-trigger {
|
||||
font-family: 'WorkSans', sans-serif;
|
||||
font-size: 16px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.side-panel-content {
|
||||
position: relative;
|
||||
font-family: 'WorkSans', sans-serif;
|
||||
font-size: 14px;
|
||||
top: 1rem;
|
||||
right: 1.5rem;
|
||||
padding: 1rem 1rem;
|
||||
z-index: 9;
|
||||
|
||||
// backdrop-filter: blur(12px);
|
||||
color: var(--text-color);
|
||||
background-color: var(--page-bg);
|
||||
|
||||
border-radius: 0.8rem;
|
||||
border-style: solid;
|
||||
border-color: var(--border-color);
|
||||
border-width: 1px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.setting-block-content {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
// input {
|
||||
// height: 24px;
|
||||
// // border-radius: 4px;
|
||||
// }
|
||||
|
||||
// button {
|
||||
// height: 28px;
|
||||
// // border-radius: 4px;
|
||||
// }
|
||||
}
|
||||
188
lama_cleaner/app/src/components/SidePanel/SidePanel.tsx
Normal file
188
lama_cleaner/app/src/components/SidePanel/SidePanel.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||
import { useToggle } from 'react-use'
|
||||
import { SDSampler, settingState } from '../../store/Atoms'
|
||||
import NumberInputSetting from '../Settings/NumberInputSetting'
|
||||
import SettingBlock from '../Settings/SettingBlock'
|
||||
import Selector from '../shared/Selector'
|
||||
import { Switch, SwitchThumb } from '../shared/Switch'
|
||||
import Button from '../shared/Button'
|
||||
import emitter, { EVENT_PROMPT } from '../../event'
|
||||
|
||||
const INPUT_WIDTH = 30
|
||||
|
||||
// TODO: 添加收起来的按钮
|
||||
const SidePanel = () => {
|
||||
const [open, toggleOpen] = useToggle(false)
|
||||
const [setting, setSettingState] = useRecoilState(settingState)
|
||||
|
||||
const onReRunBtnClick = () => {
|
||||
emitter.emit(EVENT_PROMPT)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="side-panel">
|
||||
<PopoverPrimitive.Root open={open}>
|
||||
<PopoverPrimitive.Trigger
|
||||
className="btn-primary side-panel-trigger"
|
||||
onClick={() => toggleOpen()}
|
||||
>
|
||||
Stable Diffusion
|
||||
</PopoverPrimitive.Trigger>
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content className="side-panel-content">
|
||||
<SettingBlock
|
||||
title="Show Croper"
|
||||
input={
|
||||
<Switch
|
||||
checked={setting.showCroper}
|
||||
onCheckedChange={value => {
|
||||
setSettingState(old => {
|
||||
return { ...old, showCroper: value }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SwitchThumb />
|
||||
</Switch>
|
||||
}
|
||||
/>
|
||||
{/*
|
||||
<NumberInputSetting
|
||||
title="Num Samples"
|
||||
width={INPUT_WIDTH}
|
||||
value={`${setting.sdNumSamples}`}
|
||||
desc=""
|
||||
onValue={value => {
|
||||
const val = value.length === 0 ? 0 : parseInt(value, 10)
|
||||
setSettingState(old => {
|
||||
return { ...old, sdNumSamples: val }
|
||||
})
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<NumberInputSetting
|
||||
title="Steps"
|
||||
width={INPUT_WIDTH}
|
||||
value={`${setting.sdSteps}`}
|
||||
desc="Large steps result in better result, but more time-consuming"
|
||||
onValue={value => {
|
||||
const val = value.length === 0 ? 0 : parseInt(value, 10)
|
||||
setSettingState(old => {
|
||||
return { ...old, sdSteps: val }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<NumberInputSetting
|
||||
title="Strength"
|
||||
width={INPUT_WIDTH}
|
||||
allowFloat
|
||||
value={`${setting.sdStrength}`}
|
||||
desc="TODO"
|
||||
onValue={value => {
|
||||
const val = value.length === 0 ? 0 : parseFloat(value)
|
||||
console.log(val)
|
||||
setSettingState(old => {
|
||||
return { ...old, sdStrength: val }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<NumberInputSetting
|
||||
title="Guidance Scale"
|
||||
width={INPUT_WIDTH}
|
||||
allowFloat
|
||||
value={`${setting.sdGuidanceScale}`}
|
||||
desc="TODO"
|
||||
onValue={value => {
|
||||
const val = value.length === 0 ? 0 : parseFloat(value)
|
||||
setSettingState(old => {
|
||||
return { ...old, sdGuidanceScale: val }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingBlock
|
||||
className="sub-setting-block"
|
||||
title="Sampler"
|
||||
input={
|
||||
<Selector
|
||||
width={80}
|
||||
value={setting.sdSampler as string}
|
||||
options={Object.values(SDSampler)}
|
||||
onChange={val => {
|
||||
const sampler = val as SDSampler
|
||||
setSettingState(old => {
|
||||
return { ...old, sdSampler: sampler }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingBlock
|
||||
title="Seed"
|
||||
input={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={onReRunBtnClick}
|
||||
icon={
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20px"
|
||||
height="20px"
|
||||
viewBox="0 0 66.459 66.46"
|
||||
>
|
||||
<path
|
||||
d="M65.542,11.777L33.467,0.037c-0.133-0.049-0.283-0.049-0.42,0L0.916,11.748c-0.242,0.088-0.402,0.32-0.402,0.576 l0.09,40.484c0,0.25,0.152,0.475,0.385,0.566l31.047,12.399v0.072c0,0.203,0.102,0.393,0.27,0.508 c0.168,0.111,0.379,0.135,0.57,0.062l0.385-0.154l0.385,0.154c0.072,0.028,0.15,0.045,0.227,0.045c0.121,0,0.24-0.037,0.344-0.105 c0.168-0.115,0.27-0.305,0.27-0.508v-0.072l31.047-12.399c0.232-0.093,0.385-0.316,0.385-0.568l0.027-40.453 C65.943,12.095,65.784,11.867,65.542,11.777z M32.035,63.134L3.052,51.562V15.013l28.982,11.572L32.035,63.134L32.035,63.134z M33.259,24.439L4.783,13.066l28.48-10.498l28.735,10.394L33.259,24.439z M63.465,51.562L34.484,63.134V26.585l28.981-11.572 V51.562z M14.478,38.021c0-1.692,1.35-2.528,3.016-1.867c1.665,0.663,3.016,2.573,3.016,4.269 c-0.001,1.692-1.351,2.529-3.017,1.867C15.827,41.626,14.477,39.714,14.478,38.021z M5.998,25.375c0-1.693,1.351-2.529,3.017-1.866 c1.666,0.662,3.016,2.572,3.016,4.267c0,1.695-1.351,2.529-3.017,1.867C7.347,28.979,5.998,27.069,5.998,25.375z M22.959,32.124 c0-1.694,1.351-2.53,3.017-1.867c1.666,0.663,3.016,2.573,3.016,4.267c0,1.695-1.352,2.53-3.017,1.867 C24.309,35.728,22.959,33.818,22.959,32.124z M5.995,43.103c0.001-1.692,1.351-2.529,3.017-1.867 c1.666,0.664,3.016,2.573,3.016,4.269c0,1.694-1.351,2.53-3.017,1.867C7.344,46.709,5.995,44.797,5.995,43.103z M22.957,49.853 c0.001-1.695,1.351-2.529,3.017-1.867s3.016,2.572,3.016,4.269c0,1.692-1.351,2.528-3.017,1.866 C24.306,53.458,22.957,51.546,22.957,49.853z M27.81,12.711c-0.766,1.228-3.209,2.087-5.462,1.917 c-2.253-0.169-3.46-1.301-2.695-2.528c0.765-1.227,3.207-2.085,5.461-1.916C27.365,10.352,28.573,11.484,27.81,12.711z M43.928,13.921c-0.764,1.229-3.208,2.086-5.46,1.917c-2.255-0.169-3.46-1.302-2.696-2.528c0.764-1.229,3.209-2.086,5.462-1.918 C43.485,11.563,44.693,12.695,43.928,13.921z M47.04,42.328c-1.041-1.278-0.764-3.705,0.619-5.421 c1.381-1.716,3.344-2.069,4.381-0.792c1.041,1.276,0.764,3.704-0.617,5.42S48.079,43.604,47.04,42.328z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 每次会从服务器返回更新该值 */}
|
||||
<NumberInputSetting
|
||||
title=""
|
||||
width={80}
|
||||
value={`${setting.sdSeed}`}
|
||||
desc=""
|
||||
disable={!setting.sdSeedFixed}
|
||||
onValue={value => {
|
||||
const val = value.length === 0 ? 0 : parseInt(value, 10)
|
||||
setSettingState(old => {
|
||||
return { ...old, sdSeed: val }
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
checked={setting.sdSeedFixed}
|
||||
onCheckedChange={value => {
|
||||
setSettingState(old => {
|
||||
return { ...old, sdSeedFixed: value }
|
||||
})
|
||||
}}
|
||||
style={{ marginLeft: '8px' }}
|
||||
>
|
||||
<SwitchThumb />
|
||||
</Switch>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
</PopoverPrimitive.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidePanel
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
modelDownloaded,
|
||||
switchModel,
|
||||
} from '../adapters/inpainting'
|
||||
import SidePanel from './SidePanel/SidePanel'
|
||||
|
||||
interface WorkspaceProps {
|
||||
file: File
|
||||
@@ -82,6 +83,7 @@ const Workspace = ({ file }: WorkspaceProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanel />
|
||||
<Editor file={file} />
|
||||
<SettingModal onClose={onSettingClose} />
|
||||
<ShortcutsModal />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
column-gap: 1rem;
|
||||
background-color: var(--page-bg);
|
||||
color: var(--btn-text-color);
|
||||
font-family: 'WorkSans', sans-serif;
|
||||
width: max-content;
|
||||
@@ -25,6 +26,13 @@
|
||||
}
|
||||
|
||||
.btn-primary-disabled {
|
||||
background-color: var(--page-bg);
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-border {
|
||||
border-color: var(--btn-border-color);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
interface ButtonProps {
|
||||
border?: boolean
|
||||
disabled?: boolean
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
@@ -17,6 +18,7 @@ interface ButtonProps {
|
||||
const Button: React.FC<ButtonProps> = props => {
|
||||
const {
|
||||
children,
|
||||
border,
|
||||
className,
|
||||
disabled,
|
||||
icon,
|
||||
@@ -55,6 +57,7 @@ const Button: React.FC<ButtonProps> = props => {
|
||||
toolTip ? 'info-tooltip' : '',
|
||||
tooltipPosition ? `info-tooltip-${tooltipPosition}` : '',
|
||||
className,
|
||||
border ? `btn-border` : '',
|
||||
].join(' ')}
|
||||
>
|
||||
{icon}
|
||||
@@ -65,6 +68,7 @@ const Button: React.FC<ButtonProps> = props => {
|
||||
|
||||
Button.defaultProps = {
|
||||
disabled: false,
|
||||
border: false,
|
||||
}
|
||||
|
||||
export default Button
|
||||
|
||||
42
lama_cleaner/app/src/components/shared/Input.tsx
Normal file
42
lama_cleaner/app/src/components/shared/Input.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { FocusEvent, InputHTMLAttributes } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { appState } from '../../store/Atoms'
|
||||
|
||||
const TextInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
>((props: InputHTMLAttributes<HTMLInputElement>, forwardedRef) => {
|
||||
const { onFocus, onBlur, ...itemProps } = props
|
||||
const [_, setAppState] = useRecoilState(appState)
|
||||
|
||||
const handleOnFocus = (evt: FocusEvent<any>) => {
|
||||
setAppState(old => {
|
||||
return { ...old, disableShortCuts: true }
|
||||
})
|
||||
onFocus?.(evt)
|
||||
}
|
||||
|
||||
const handleOnBlur = (evt: FocusEvent<any>) => {
|
||||
setAppState(old => {
|
||||
return { ...old, disableShortCuts: false }
|
||||
})
|
||||
onBlur?.(evt)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
{...itemProps}
|
||||
ref={forwardedRef}
|
||||
type="text"
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default TextInput
|
||||
@@ -1,7 +1,9 @@
|
||||
import { XIcon } from '@heroicons/react/outline'
|
||||
import React, { ReactNode } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import Button from './Button'
|
||||
import { appState } from '../../store/Atoms'
|
||||
|
||||
export interface ModalProps {
|
||||
show: boolean
|
||||
@@ -16,10 +18,14 @@ const Modal = React.forwardRef<
|
||||
ModalProps
|
||||
>((props, forwardedRef) => {
|
||||
const { show, children, onClose, className, title } = props
|
||||
const [_, setAppState] = useRecoilState(appState)
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onClose?.()
|
||||
setAppState(old => {
|
||||
return { ...old, disableShortCuts: false }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,13 @@
|
||||
padding: 0 0.8rem;
|
||||
outline: 1px solid var(--border-color);
|
||||
height: 32px;
|
||||
text-align: right;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--yellow-accent);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import React, { FormEvent, InputHTMLAttributes } from 'react'
|
||||
import React, { FormEvent, InputHTMLAttributes, useState } from 'react'
|
||||
import TextInput from './Input'
|
||||
|
||||
interface NumberInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
value: string
|
||||
allowFloat?: boolean
|
||||
onValue?: (val: string) => void
|
||||
}
|
||||
|
||||
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
(props: NumberInputProps, forwardedRef) => {
|
||||
const { value, onValue, ...itemProps } = props
|
||||
const { value, allowFloat, onValue, ...itemProps } = props
|
||||
const [innerValue, setInnerValue] = useState(value)
|
||||
|
||||
const handleOnInput = (evt: FormEvent<HTMLInputElement>) => {
|
||||
const target = evt.target as HTMLInputElement
|
||||
const val = target.value.replace(/\D/g, '')
|
||||
onValue?.(val)
|
||||
let val = target.value
|
||||
if (allowFloat) {
|
||||
val = val.replace(/[^0-9.]/g, '').replace(/(\..*?)\..*/g, '$1')
|
||||
onValue?.(val)
|
||||
} else {
|
||||
val = val.replace(/\D/g, '')
|
||||
onValue?.(val)
|
||||
}
|
||||
setInnerValue(val)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
<TextInput
|
||||
value={innerValue}
|
||||
onInput={handleOnInput}
|
||||
className="number-input"
|
||||
{...itemProps}
|
||||
ref={forwardedRef}
|
||||
type="text"
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
NumberInput.defaultProps = {
|
||||
allowFloat: false,
|
||||
}
|
||||
|
||||
export default NumberInput
|
||||
|
||||
@@ -51,6 +51,7 @@ const Selector = (props: Props) => {
|
||||
className="select-trigger"
|
||||
style={{ width }}
|
||||
ref={contentRef}
|
||||
onKeyDown={e => e.preventDefault()}
|
||||
>
|
||||
<Select.Value />
|
||||
<Select.Icon>
|
||||
|
||||
@@ -12,6 +12,7 @@ const Switch = React.forwardRef<
|
||||
{...itemProps}
|
||||
ref={forwardedRef}
|
||||
className={`switch-root ${className}`}
|
||||
onKeyDown={e => e.preventDefault()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.toast-viewpoint {
|
||||
position: fixed;
|
||||
top: 48px;
|
||||
right: 0;
|
||||
bottom: 48px;
|
||||
right: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 25px;
|
||||
|
||||
7
lama_cleaner/app/src/event.ts
Normal file
7
lama_cleaner/app/src/event.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import mitt from 'mitt'
|
||||
|
||||
export const EVENT_PROMPT = 'prompt'
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
export default emitter
|
||||
22
lama_cleaner/app/src/hooks/useHotkey.tsx
Normal file
22
lama_cleaner/app/src/hooks/useHotkey.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Options, useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { appState } from '../store/Atoms'
|
||||
|
||||
const useHotKey = (
|
||||
keys: string,
|
||||
callback: any,
|
||||
options?: Options,
|
||||
deps?: any[]
|
||||
) => {
|
||||
const app = useRecoilValue(appState)
|
||||
|
||||
const ref = useHotkeys(
|
||||
keys,
|
||||
callback,
|
||||
{ ...options, enabled: !app.disableShortCuts },
|
||||
deps
|
||||
)
|
||||
return ref
|
||||
}
|
||||
|
||||
export default useHotKey
|
||||
@@ -9,6 +9,7 @@ export enum AIModel {
|
||||
ZITS = 'zits',
|
||||
MAT = 'mat',
|
||||
FCF = 'fcf',
|
||||
SD14 = 'sd1.4',
|
||||
}
|
||||
|
||||
export const fileState = atom<File | undefined>({
|
||||
@@ -16,6 +17,89 @@ export const fileState = atom<File | undefined>({
|
||||
default: undefined,
|
||||
})
|
||||
|
||||
export interface Rect {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
disableShortCuts: boolean
|
||||
isInpainting: boolean
|
||||
}
|
||||
|
||||
export const appState = atom<AppState>({
|
||||
key: 'appState',
|
||||
default: {
|
||||
disableShortCuts: false,
|
||||
isInpainting: false,
|
||||
},
|
||||
})
|
||||
|
||||
export const propmtState = atom<string>({
|
||||
key: 'promptState',
|
||||
default: '',
|
||||
})
|
||||
|
||||
export const isInpaintingState = selector({
|
||||
key: 'isInpainting',
|
||||
get: ({ get }) => {
|
||||
const app = get(appState)
|
||||
return app.isInpainting
|
||||
},
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const app = get(appState)
|
||||
set(appState, { ...app, isInpainting: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperState = atom<Rect>({
|
||||
key: 'croperState',
|
||||
default: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
})
|
||||
|
||||
export const croperX = selector({
|
||||
key: 'croperX',
|
||||
get: ({ get }) => get(croperState).x,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, x: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperY = selector({
|
||||
key: 'croperY',
|
||||
get: ({ get }) => get(croperState).y,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, y: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperHeight = selector({
|
||||
key: 'croperHeight',
|
||||
get: ({ get }) => get(croperState).height,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, height: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
export const croperWidth = selector({
|
||||
key: 'croperWidth',
|
||||
get: ({ get }) => get(croperState).width,
|
||||
set: ({ get, set }, newValue: any) => {
|
||||
const rect = get(croperState)
|
||||
set(croperState, { ...rect, width: newValue })
|
||||
},
|
||||
})
|
||||
|
||||
interface ToastAtomState {
|
||||
open: boolean
|
||||
desc: string
|
||||
@@ -50,6 +134,7 @@ type ModelsHDSettings = { [key in AIModel]: HDSettings }
|
||||
|
||||
export interface Settings {
|
||||
show: boolean
|
||||
showCroper: boolean
|
||||
downloadMask: boolean
|
||||
graduallyInpainting: boolean
|
||||
runInpaintingManually: boolean
|
||||
@@ -62,6 +147,16 @@ export interface Settings {
|
||||
|
||||
// For ZITS
|
||||
zitsWireframe: boolean
|
||||
|
||||
// For SD
|
||||
sdMode: SDMode
|
||||
sdStrength: number
|
||||
sdSteps: number
|
||||
sdGuidanceScale: number
|
||||
sdSampler: SDSampler
|
||||
sdSeed: number
|
||||
sdSeedFixed: boolean // true: use sdSeed, false: random generate seed on backend
|
||||
sdNumSamples: number
|
||||
}
|
||||
|
||||
const defaultHDSettings: ModelsHDSettings = {
|
||||
@@ -100,10 +195,28 @@ const defaultHDSettings: ModelsHDSettings = {
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: false,
|
||||
},
|
||||
[AIModel.SD14]: {
|
||||
hdStrategy: HDStrategy.ORIGINAL,
|
||||
hdStrategyResizeLimit: 768,
|
||||
hdStrategyCropTrigerSize: 512,
|
||||
hdStrategyCropMargin: 128,
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export enum SDSampler {
|
||||
ddim = 'ddim',
|
||||
}
|
||||
|
||||
export enum SDMode {
|
||||
text2img = 'text2img',
|
||||
img2img = 'img2img',
|
||||
inpainting = 'inpainting',
|
||||
}
|
||||
|
||||
export const settingStateDefault: Settings = {
|
||||
show: false,
|
||||
showCroper: false,
|
||||
downloadMask: false,
|
||||
graduallyInpainting: true,
|
||||
runInpaintingManually: false,
|
||||
@@ -114,6 +227,16 @@ export const settingStateDefault: Settings = {
|
||||
ldmSampler: LDMSampler.plms,
|
||||
|
||||
zitsWireframe: true,
|
||||
|
||||
// SD
|
||||
sdMode: SDMode.inpainting,
|
||||
sdStrength: 0.75,
|
||||
sdSteps: 50,
|
||||
sdGuidanceScale: 7.5,
|
||||
sdSampler: SDSampler.ddim,
|
||||
sdSeed: 42,
|
||||
sdSeedFixed: true,
|
||||
sdNumSamples: 1,
|
||||
}
|
||||
|
||||
const localStorageEffect =
|
||||
@@ -164,3 +287,20 @@ export const hdSettingsState = selector({
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const isSDState = selector({
|
||||
key: 'isSD',
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
return settings.model === AIModel.SD14
|
||||
},
|
||||
})
|
||||
|
||||
export const runManuallyState = selector({
|
||||
key: 'runManuallyState',
|
||||
get: ({ get }) => {
|
||||
const settings = get(settingState)
|
||||
const isSD = get(isSDState)
|
||||
return settings.runInpaintingManually || isSD
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
--page-bg-light: rgb(255, 255, 255, 0.5);
|
||||
--page-text-color: #040404;
|
||||
--yellow-accent: #ffcc00;
|
||||
--yellow-accent-light: #ffcc0055;
|
||||
--link-color: rgb(0, 0, 0);
|
||||
--border-color: rgb(100, 100, 120);
|
||||
--border-color-light: rgba(100, 100, 120, 0.5);
|
||||
@@ -57,4 +58,6 @@
|
||||
--box-shadow: inset 0 0.5px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px 5px hsl(210 16.7% 97.6%), 0px 0px 0px 0.5px hsl(205 10.7% 78%),
|
||||
0px 2px 1px -1px hsl(205 10.7% 78%), 0 1px hsl(205 10.7% 78%);
|
||||
|
||||
--croper-bg: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
--page-bg-light: #04040488;
|
||||
--page-text-color: #f9f9f9;
|
||||
--yellow-accent: #ffcc00;
|
||||
--yellow-accent-light: #ffcc0055;
|
||||
--link-color: var(--yellow-accent);
|
||||
--border-color: rgb(100, 100, 120);
|
||||
--border-color-light: rgba(102, 102, 102);
|
||||
@@ -55,4 +56,6 @@
|
||||
--box-shadow: inset 0 0.5px rgba(255, 255, 255, 0.1),
|
||||
inset 0 1px 5px hsl(195 7.1% 11%), 0px 0px 0px 0.5px hsl(207 5.6% 31.6%),
|
||||
0px 2px 1px -1px hsl(207 5.6% 31.6%), 0 1px hsl(207 5.6% 31.6%);
|
||||
|
||||
--croper-bg: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
@use '../components/Editor/Editor';
|
||||
@use '../components/LandingPage/LandingPage';
|
||||
@use '../components/Header/Header';
|
||||
@use '../components/Header/PromptInput';
|
||||
@use '../components/Header/ThemeChanger';
|
||||
@use '../components/Shortcuts/Shortcuts';
|
||||
@use '../components/Settings/Settings.scss';
|
||||
@use '../components/SidePanel/SidePanel.scss';
|
||||
@use '../components/Croper/Croper.scss';
|
||||
|
||||
// Shared
|
||||
@use '../components/FileSelect/FileSelect';
|
||||
|
||||
Reference in New Issue
Block a user