This commit is contained in:
Qing 2023-12-22 14:00:11 +08:00
parent 141936a937
commit eb9764176c
20 changed files with 738 additions and 255 deletions

View File

@ -17,6 +17,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
@ -1913,6 +1914,38 @@
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
"integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-use-size": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",

View File

@ -19,6 +19,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",

View File

@ -28,7 +28,7 @@ import { useStore } from "@/lib/states"
import Cropper from "./Cropper"
import { InteractiveSegPoints } from "./InteractiveSeg"
import useHotKey from "@/hooks/useHotkey"
import Extender from "./Expender"
import Extender from "./Extender"
const TOOLBAR_HEIGHT = 200
const MIN_BRUSH_SIZE = 10
@ -221,7 +221,9 @@ export default function Editor(props: EditorProps) {
}
const [width, height] = getCurrentWidthHeight()
if (width !== imageWidth || height !== imageHeight) {
setImageSize(width, height)
}
const rW = windowSize.width / width
const rH = (windowSize.height - TOOLBAR_HEIGHT) / height
@ -255,6 +257,8 @@ export default function Editor(props: EditorProps) {
}
}, [
viewportRef,
imageHeight,
imageWidth,
original,
isOriginalLoaded,
windowSize,
@ -619,19 +623,6 @@ export default function Editor(props: EditorProps) {
}
)
useKeyPressEvent(
"Alt",
(ev) => {
ev?.preventDefault()
ev?.stopPropagation()
// TODO: mouse scroll increase/decrease brush size
},
(ev) => {
ev?.preventDefault()
ev?.stopPropagation()
}
)
const getCurScale = (): number => {
let s = minScale
if (viewportRef.current?.instance?.transformState.scale !== undefined) {
@ -782,19 +773,17 @@ export default function Editor(props: EditorProps) {
<Cropper
maxHeight={imageHeight}
maxWidth={imageWidth}
minHeight={Math.min(256, imageHeight)}
minWidth={Math.min(256, imageWidth)}
minHeight={Math.min(512, imageHeight)}
minWidth={Math.min(512, imageWidth)}
scale={getCurScale()}
show={settings.showCropper}
/>
<Extender
maxHeight={imageHeight}
maxWidth={imageWidth}
minHeight={Math.min(256, imageHeight)}
minWidth={Math.min(256, imageWidth)}
minHeight={Math.min(512, imageHeight)}
minWidth={Math.min(512, imageWidth)}
scale={getCurScale()}
show={settings.showExpender}
show={settings.showExtender}
/>
{interactiveSegState.isInteractiveSeg ? (

View File

@ -1,3 +1,4 @@
import { EXTENDER_ALL, EXTENDER_X, EXTENDER_Y } from "@/lib/const"
import { useStore } from "@/lib/states"
import { cn } from "@/lib/utils"
import React, { useEffect, useState } from "react"
@ -18,8 +19,6 @@ interface EVData {
}
interface Props {
maxHeight: number
maxWidth: number
scale: number
minHeight: number
minWidth: number
@ -30,66 +29,44 @@ const clamp = (
newPos: number,
newLength: number,
oldPos: number,
oldLength: number,
minLength: number,
maxLength: number
minLength: number
) => {
return [newPos, newLength]
if (newPos !== oldPos && newLength === oldLength) {
if (newPos < 0) {
return [0, oldLength]
}
if (newPos + newLength > maxLength) {
return [maxLength - oldLength, oldLength]
}
} else {
if (newLength < minLength) {
if (newPos === oldPos) {
return [newPos, minLength]
}
return [newPos + newLength - minLength, minLength]
}
if (newPos < 0) {
return [0, newPos + newLength]
}
if (newPos + newLength > maxLength) {
return [newPos, maxLength - newPos]
}
}
return [newPos, newLength]
}
const Extender = (props: Props) => {
const { minHeight, minWidth, maxHeight, maxWidth, scale, show } = props
const { minHeight, minWidth, scale, show } = props
const [
imageWidth,
imageHeight,
isInpainting,
imageHeight,
imageWdith,
{ x, y, width, height },
setX,
setY,
setWidth,
setHeight,
extenderDirection,
] = useStore((state) => [
state.imageWidth,
state.imageHeight,
state.isInpainting,
state.imageHeight,
state.imageWidth,
state.extenderState,
state.setExtenderX,
state.setExtenderY,
state.setExtenderWidth,
state.setExtenderHeight,
state.settings.extenderDirection,
])
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, imageWidth, imageHeight])
const [evData, setEVData] = useState<EVData>({
initX: 0,
@ -106,11 +83,11 @@ const Extender = (props: Props) => {
}
const clampLeftRight = (newX: number, newWidth: number) => {
return clamp(newX, newWidth, x, width, minWidth, maxWidth)
return clamp(newX, newWidth, x, minWidth)
}
const clampTopBottom = (newY: number, newHeight: number) => {
return clamp(newY, newHeight, y, height, minHeight, maxHeight)
return clamp(newY, newHeight, y, minHeight)
}
const onPointerMove = (e: PointerEvent) => {
@ -126,14 +103,31 @@ const Extender = (props: Props) => {
const moveTop = () => {
const newHeight = evData.initHeight - offsetY
const newY = evData.initY + offsetY
const [clampedY, clampedHeight] = clampTopBottom(newY, newHeight)
let clampedY = newY
let clampedHeight = newHeight
if (extenderDirection === EXTENDER_ALL) {
if (clampedY > 0) {
clampedY = 0
clampedHeight = evData.initHeight - Math.abs(evData.initY)
}
} else {
const clamped = clampTopBottom(newY, newHeight)
clampedY = clamped[0]
clampedHeight = clamped[1]
}
setHeight(clampedHeight)
setY(clampedY)
}
const moveBottom = () => {
const newHeight = evData.initHeight + offsetY
const [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
let [clampedY, clampedHeight] = clampTopBottom(evData.initY, newHeight)
if (extenderDirection === EXTENDER_ALL) {
if (clampedY + clampedHeight < imageHeight) {
clampedHeight = imageHeight
}
}
setHeight(clampedHeight)
setY(clampedY)
}
@ -141,14 +135,30 @@ const Extender = (props: Props) => {
const moveLeft = () => {
const newWidth = evData.initWidth - offsetX
const newX = evData.initX + offsetX
const [clampedX, clampedWidth] = clampLeftRight(newX, newWidth)
let clampedX = newX
let clampedWidth = newWidth
if (extenderDirection === EXTENDER_ALL) {
if (clampedX > 0) {
clampedX = 0
clampedWidth = evData.initWidth - Math.abs(evData.initX)
}
} else {
const clamped = clampLeftRight(newX, newWidth)
clampedX = clamped[0]
clampedWidth = clamped[1]
}
setWidth(clampedWidth)
setX(clampedX)
}
const moveRight = () => {
const newWidth = evData.initWidth + offsetX
const [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
let [clampedX, clampedWidth] = clampLeftRight(evData.initX, newWidth)
if (extenderDirection === EXTENDER_ALL) {
if (clampedX + clampedWidth < imageWdith) {
clampedWidth = imageWdith
}
}
setWidth(clampedWidth)
setX(clampedX)
}
@ -196,31 +206,16 @@ const Extender = (props: Props) => {
break
}
}
if (isMoving) {
const newX = evData.initX + offsetX
const newY = evData.initY + offsetY
const [clampedX, clampedWidth] = clampLeftRight(newX, evData.initWidth)
const [clampedY, clampedHeight] = clampTopBottom(newY, evData.initHeight)
setWidth(clampedWidth)
setHeight(clampedHeight)
setX(clampedX)
setY(clampedY)
}
}
const onPointerDone = (e: PointerEvent) => {
if (isResizing) {
setIsResizing(false)
}
if (isMoving) {
setIsMoving(false)
}
}
useEffect(() => {
if (isResizing || isMoving) {
if (isResizing) {
document.addEventListener("pointermove", onPointerMove, DOC_MOVE_OPTS)
document.addEventListener("pointerup", onPointerDone, DOC_MOVE_OPTS)
document.addEventListener("pointercancel", onPointerDone, DOC_MOVE_OPTS)
@ -238,7 +233,7 @@ const Extender = (props: Props) => {
)
}
}
}, [isResizing, isMoving, width, height, evData])
}, [isResizing, width, height, evData])
const onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
const { ord } = (e.target as HTMLElement).dataset
@ -300,36 +295,55 @@ const Extender = (props: Props) => {
onPointerDown={onCropPointerDown}
className="absolute top-0 h-full w-full"
>
{[EXTENDER_Y, EXTENDER_ALL].includes(extenderDirection) ? (
<>
<div
className="absolute pointer-events-auto top-0 left-0 w-full cursor-ns-resize h-[12px] mt-[-6px]"
data-ord="top"
/>
<div
className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
data-ord="right"
/>
<div
className="absolute pointer-events-auto bottom-0 left-0 w-full cursor-ns-resize h-[12px] mb-[-6px]"
data-ord="bottom"
/>
{createDragHandle("cursor-ns-resize", "top", "")}
{createDragHandle("cursor-ns-resize", "bottom", "")}
</>
) : (
<></>
)}
{[EXTENDER_X, EXTENDER_ALL].includes(extenderDirection) ? (
<>
<div
className="absolute pointer-events-auto top-0 right-0 h-full cursor-ew-resize w-[12px] mr-[-6px]"
data-ord="right"
/>
<div
className="absolute pointer-events-auto top-0 left-0 h-full cursor-ew-resize w-[12px] ml-[-6px]"
data-ord="left"
/>
{createDragHandle("cursor-ew-resize", "left", "")}
{createDragHandle("cursor-ew-resize", "right", "")}
</>
) : (
<></>
)}
{extenderDirection === EXTENDER_ALL ? (
<>
{createDragHandle("cursor-nw-resize", "top", "left")}
{createDragHandle("cursor-ne-resize", "top", "right")}
{createDragHandle("cursor-sw-resize", "bottom", "left")}
{createDragHandle("cursor-se-resize", "bottom", "right")}
{createDragHandle("cursor-ns-resize", "top", "")}
{createDragHandle("cursor-ns-resize", "bottom", "")}
{createDragHandle("cursor-ew-resize", "left", "")}
{createDragHandle("cursor-ew-resize", "right", "")}
</>
) : (
<></>
)}
</div>
)
}
const onInfoBarPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
setIsMoving(true)
setEVData({
initX: x,
initY: y,
@ -345,7 +359,7 @@ const Extender = (props: Props) => {
return (
<div
className={twMerge(
"border absolute pointer-events-auto px-2 py-1 rounded-full hover:cursor-move bg-background",
"border absolute pointer-events-auto px-2 py-1 rounded-full bg-background",
"origin-top-left top-0 left-0"
)}
style={{
@ -362,7 +376,7 @@ const Extender = (props: Props) => {
const createBorder = () => {
return (
<div
className="outline-dashed outline-primary"
className={cn("outline-dashed outline-primary")}
style={{
height,
width,

View File

@ -79,10 +79,6 @@ export default function FileManager(props: Props) {
state.updateFileManagerState,
])
useHotKey("f", () => {
toggleOpen()
})
const { toast } = useToast()
const [scrollTop, setScrollTop] = useState(0)
const [closeScrollTop, setCloseScrollTop] = useState(0)
@ -91,6 +87,37 @@ export default function FileManager(props: Props) {
const debouncedSearchText = useDebounce(fileManagerState.searchText, 300)
const [tab, setTab] = useState(IMAGE_TAB)
const [photos, setPhotos] = useState<Photo[]>([])
const [photoIndex, setPhotoIndex] = useState(0)
useHotKey("f", () => {
toggleOpen()
})
useHotKey(
"left",
() => {
let newIndex = photoIndex
if (photoIndex > 0) {
newIndex = photoIndex - 1
}
setPhotoIndex(newIndex)
onPhotoClick(tab, photos[newIndex].name)
},
[photoIndex, photos]
)
useHotKey(
"right",
() => {
let newIndex = photoIndex
if (photoIndex < photos.length - 1) {
newIndex = photoIndex + 1
}
setPhotoIndex(newIndex)
onPhotoClick(tab, photos[newIndex].name)
},
[photoIndex, photos]
)
useEffect(() => {
if (!open) {
@ -165,6 +192,7 @@ export default function FileManager(props: Props) {
const onClick = ({ index }: { index: number }) => {
toggleOpen()
setPhotoIndex(index)
onPhotoClick(tab, photos[index].name)
}

View File

@ -2,12 +2,6 @@ import { PlayIcon } from "@radix-ui/react-icons"
import { useCallback, useState } from "react"
import { IconButton, ImageUploadButton } from "@/components/ui/button"
import Shortcuts from "@/components/Shortcuts"
import emitter, {
DREAM_BUTTON_MOUSE_ENTER,
DREAM_BUTTON_MOUSE_LEAVE,
EVENT_CUSTOM_MASK,
RERUN_LAST_MASK,
} from "@/lib/event"
import { useImage } from "@/hooks/useImage"
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"
@ -17,9 +11,9 @@ import FileManager from "./FileManager"
import { getMediaFile } from "@/lib/api"
import { useStore } from "@/lib/states"
import SettingsDialog from "./Settings"
import { cn } from "@/lib/utils"
import useHotKey from "@/hooks/useHotkey"
import { cn, fileToImage } from "@/lib/utils"
import Coffee from "./Coffee"
import { useToast } from "./ui/use-toast"
const Header = () => {
const [
@ -27,48 +21,49 @@ const Header = () => {
customMask,
isInpainting,
enableFileManager,
enableManualInpainting,
runMannually,
enableUploadMask,
model,
setFile,
setCustomFile,
runInpainting,
showPrevMask,
hidePrevMask,
imageHeight,
imageWidth,
] = useStore((state) => [
state.file,
state.customMask,
state.isInpainting,
state.serverConfig.enableFileManager,
state.settings.enableManualInpainting,
state.runMannually(),
state.settings.enableUploadMask,
state.settings.model,
state.setFile,
state.setCustomFile,
state.runInpainting,
state.showPrevMask,
state.hidePrevMask,
state.imageHeight,
state.imageWidth,
])
const { toast } = useToast()
const [maskImage, maskImageLoaded] = useImage(customMask)
const [openMaskPopover, setOpenMaskPopover] = useState(false)
const handleRerunLastMask = useCallback(() => {
emitter.emit(RERUN_LAST_MASK)
}, [])
const handleRerunLastMask = () => {
runInpainting()
}
const onRerunMouseEnter = () => {
emitter.emit(DREAM_BUTTON_MOUSE_ENTER)
showPrevMask()
}
const onRerunMouseLeave = () => {
emitter.emit(DREAM_BUTTON_MOUSE_LEAVE)
hidePrevMask()
}
useHotKey(
"r",
() => {
if (!isInpainting) {
handleRerunLastMask()
}
},
{},
[isInpainting, handleRerunLastMask]
)
return (
<header className="h-[60px] px-6 py-4 absolute top-[0] flex justify-between items-center w-full z-20 border-b backdrop-filter backdrop-blur-md bg-background/70">
<div className="flex items-center gap-1">
@ -103,10 +98,31 @@ const Header = () => {
<ImageUploadButton
disabled={isInpainting}
tooltip="Upload custom mask"
onFileUpload={(file) => {
onFileUpload={async (file) => {
let newCustomMask: HTMLImageElement | null = null
try {
newCustomMask = await fileToImage(file)
} catch (e: any) {
toast({
variant: "destructive",
description: e.message ? e.message : e.toString(),
})
return
}
if (
newCustomMask.naturalHeight !== imageHeight ||
newCustomMask.naturalWidth !== imageWidth
) {
toast({
variant: "destructive",
description: `The size of the mask must same as image: ${imageWidth}x${imageHeight}`,
})
return
}
setCustomFile(file)
if (!enableManualInpainting) {
emitter.emit(EVENT_CUSTOM_MASK, { mask: file })
if (!runMannually) {
runInpainting()
}
}}
>
@ -125,7 +141,6 @@ const Header = () => {
}}
onClick={() => {
if (customMask) {
emitter.emit(EVENT_CUSTOM_MASK, { mask: customMask })
}
}}
>
@ -149,7 +164,7 @@ const Header = () => {
{file && !model.need_prompt ? (
<IconButton
disabled={isInpainting}
tooltip="Rerun last mask"
tooltip="Rerun previous mask"
onClick={handleRerunLastMask}
onMouseEnter={onRerunMouseEnter}
onMouseLeave={onRerunMouseLeave}

View File

@ -314,7 +314,7 @@ export function SettingsDialog() {
<Separator />
<FormField
{/* <FormField
control={form.control}
name="enableUploadMask"
render={({ field }) => (
@ -334,7 +334,7 @@ export function SettingsDialog() {
</FormItem>
)}
/>
<Separator />
<Separator /> */}
</div>
)
}

View File

@ -74,7 +74,6 @@ export function Shortcuts() {
/>
<ShortCut content="Cancel Drawing" keys={["Esc"]} />
<ShortCut content="Rerun last mask" keys={["R"]} />
<ShortCut content="Undo" keys={[CmdOrCtrl(), "Z"]} />
<ShortCut content="Redo" keys={[CmdOrCtrl(), "Shift", "Z"]} />
<ShortCut content="Copy Result" keys={[CmdOrCtrl(), "C"]} />

View File

@ -1,4 +1,4 @@
import { FormEvent } from "react"
import { FormEvent, useState } from "react"
import { useToggle } from "react-use"
import { useStore } from "@/lib/states"
import { Switch } from "./ui/switch"
@ -17,17 +17,110 @@ import { SDSampler } from "@/lib/types"
import { Separator } from "./ui/separator"
import { ScrollArea } from "./ui/scroll-area"
import { Sheet, SheetContent, SheetHeader, SheetTrigger } from "./ui/sheet"
import { ChevronLeft, ChevronRight, Upload } from "lucide-react"
import {
ArrowDownFromLine,
ArrowLeftFromLine,
ArrowRightFromLine,
ArrowUpFromLine,
ChevronLeft,
ChevronRight,
HelpCircle,
LucideIcon,
Maximize,
Move,
MoveHorizontal,
MoveVertical,
Upload,
} from "lucide-react"
import { Button, ImageUploadButton } from "./ui/button"
import useHotKey from "@/hooks/useHotkey"
import { Slider } from "./ui/slider"
import { useImage } from "@/hooks/useImage"
import { INSTRUCT_PIX2PIX, PAINT_BY_EXAMPLE } from "@/lib/const"
import {
EXTENDER_ALL,
EXTENDER_BUILTIN_ALL,
EXTENDER_BUILTIN_X_LEFT,
EXTENDER_BUILTIN_X_RIGHT,
EXTENDER_BUILTIN_Y_BOTTOM,
EXTENDER_BUILTIN_Y_TOP,
EXTENDER_X,
EXTENDER_Y,
INSTRUCT_PIX2PIX,
PAINT_BY_EXAMPLE,
} from "@/lib/const"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
const RowContainer = ({ children }: { children: React.ReactNode }) => (
<div className="flex justify-between items-center pr-2">{children}</div>
)
const ExtenderButton = ({
IconCls,
text,
onClick,
}: {
IconCls: LucideIcon
text: string
onClick: () => void
}) => {
const [showExtender] = useStore((state) => [state.settings.showExtender])
return (
<Button
variant="outline"
size="sm"
className="p-1"
disabled={!showExtender}
onClick={onClick}
>
<div className="flex items-center gap-1">
<IconCls size={15} strokeWidth={1} />
{text}
</div>
</Button>
)
}
const LabelTitle = ({
text,
toolTip,
url,
htmlFor,
disabled = false,
}: {
text: string
toolTip?: string
url?: string
htmlFor?: string
disabled?: boolean
}) => {
return (
<Tooltip>
<TooltipTrigger asChild>
<Label
htmlFor={htmlFor ? htmlFor : text.toLowerCase().replace(" ", "-")}
className="font-medium"
disabled={disabled}
>
{text}
</Label>
</TooltipTrigger>
<TooltipContent className="flex flex-col max-w-xs text-sm" side="left">
<p>{toolTip}</p>
{url ? (
<Button variant="link" className="justify-end">
<a href={url} target="_blank">
More info
</a>
</Button>
) : (
<></>
)}
</TooltipContent>
</Tooltip>
)
}
const SidePanel = () => {
const [
settings,
@ -38,6 +131,8 @@ const SidePanel = () => {
showSidePanel,
runInpainting,
updateAppState,
updateExtenderByBuiltIn,
updateExtenderDirection,
] = useStore((state) => [
state.settings,
state.windowSize,
@ -47,6 +142,8 @@ const SidePanel = () => {
state.showSidePanel(),
state.runInpainting,
state.updateAppState,
state.updateExtenderByBuiltIn,
state.updateExtenderDirection,
])
const [exampleImage, isExampleImageLoaded] = useImage(paintByExampleFile)
const [open, toggleOpen] = useToggle(true)
@ -75,7 +172,7 @@ const SidePanel = () => {
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center pr-2">
<Label htmlFor="controlnet">Controlnet</Label>
<LabelTitle text="Controlnet" />
<Switch
id="controlnet"
checked={settings.enableControlnet}
@ -148,7 +245,11 @@ const SidePanel = () => {
return (
<>
<RowContainer>
<Label htmlFor="lcm-lora">LCM Lora</Label>
<LabelTitle
text="LCM Lora"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inference_with_lcm_lora"
toolTip="Enable quality image generation in typically 2-4 steps. Suggest disabling guidance_scale by setting it to 0. You can also try values between 1.0 and 2.0. When LCM Lora is enabled, LCMSampler will be used automatically."
/>
<Switch
id="lcm-lora"
checked={settings.enableLCMLora}
@ -170,7 +271,7 @@ const SidePanel = () => {
return (
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center pr-2">
<Label htmlFor="freeu">Freeu</Label>
<LabelTitle text="Freeu" />
<Switch
id="freeu"
checked={settings.enableFreeu}
@ -182,9 +283,11 @@ const SidePanel = () => {
<div className="flex flex-col gap-4">
<div className="flex justify-center gap-6">
<div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-s1" disabled={!settings.enableFreeu}>
s1
</Label>
<LabelTitle
htmlFor="freeu-s1"
text="s1"
disabled={!settings.enableFreeu}
/>
<NumberInput
id="freeu-s1"
className="w-14"
@ -199,9 +302,11 @@ const SidePanel = () => {
/>
</div>
<div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-s2" disabled={!settings.enableFreeu}>
s2
</Label>
<LabelTitle
htmlFor="freeu-s2"
text="s2"
disabled={!settings.enableFreeu}
/>
<NumberInput
id="freeu-s2"
className="w-14"
@ -219,9 +324,11 @@ const SidePanel = () => {
<div className="flex justify-center gap-6">
<div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-b1" disabled={!settings.enableFreeu}>
b1
</Label>
<LabelTitle
htmlFor="freeu-b1"
text="b1"
disabled={!settings.enableFreeu}
/>
<NumberInput
id="freeu-b1"
className="w-14"
@ -236,9 +343,11 @@ const SidePanel = () => {
/>
</div>
<div className="flex gap-2 items-center justify-center">
<Label htmlFor="freeu-b2" disabled={!settings.enableFreeu}>
b2
</Label>
<LabelTitle
htmlFor="freeu-b2"
text="b2"
disabled={!settings.enableFreeu}
/>
<NumberInput
id="freeu-b2"
className="w-14"
@ -266,7 +375,11 @@ const SidePanel = () => {
return (
<div className="flex flex-col gap-4">
<Label htmlFor="negative-prompt">Negative prompt</Label>
<LabelTitle
text="Negative prompt"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#negative-prompt"
toolTip="Negative prompt guides the model away from generating certain things in an image"
/>
<div className="pl-2 pr-4">
<Textarea
rows={4}
@ -336,7 +449,10 @@ const SidePanel = () => {
}
return (
<div className="flex flex-col gap-1">
<Label htmlFor="image-guidance-scale">Image guidance scale</Label>
<LabelTitle
htmlFor="image-guidance-scale"
text="Image guidance scale"
/>
<RowContainer>
<Slider
className="w-[180px]"
@ -370,7 +486,11 @@ const SidePanel = () => {
return (
<div className="flex flex-col gap-1">
<Label htmlFor="strength">Strength</Label>
<LabelTitle
text="Strength"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#strength"
toolTip="Strength is a measure of how much noise is added to the base image, which influences how similar the output is to the base image. Higher value means more noise and more different from the base image"
/>
<RowContainer>
<Slider
className="w-[180px]"
@ -397,22 +517,145 @@ const SidePanel = () => {
)
}
const renderExpender = () => {
const renderExtender = () => {
return (
<>
<div className="flex flex-col gap-4">
<RowContainer>
<Label htmlFor="Expender">Expender</Label>
<LabelTitle text="Extender" />
<Switch
id="expender"
checked={settings.showExpender}
id="extender"
checked={settings.showExtender}
onCheckedChange={(value) => {
updateSettings({ showExpender: value })
updateSettings({ showExtender: value })
if (value) {
updateSettings({ showCropper: false })
}
}}
/>
</RowContainer>
<Tabs
defaultValue={settings.extenderDirection}
onValueChange={(value) => updateExtenderDirection(value)}
className="flex flex-col justify-center items-center"
>
<TabsList className="w-[140px] mb-2">
<TabsTrigger value={EXTENDER_X} disabled={!settings.showExtender}>
<MoveHorizontal size={20} strokeWidth={1} />
</TabsTrigger>
<TabsTrigger value={EXTENDER_Y} disabled={!settings.showExtender}>
<MoveVertical size={20} strokeWidth={1} />
</TabsTrigger>
<TabsTrigger
value={EXTENDER_ALL}
disabled={!settings.showExtender}
>
<Move size={20} strokeWidth={1} />
</TabsTrigger>
</TabsList>
<TabsContent
value={EXTENDER_X}
className="flex gap-2 justify-center mt-0"
>
<ExtenderButton
IconCls={ArrowLeftFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_LEFT, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowLeftFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_LEFT, 2.0)
}
/>
<ExtenderButton
IconCls={ArrowRightFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_RIGHT, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowRightFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_X_RIGHT, 2.0)
}
/>
</TabsContent>
<TabsContent
value={EXTENDER_Y}
className="flex gap-2 justify-center mt-0"
>
<ExtenderButton
IconCls={ArrowUpFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_TOP, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowUpFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_TOP, 2.0)
}
/>
<ExtenderButton
IconCls={ArrowDownFromLine}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_BOTTOM, 1.5)
}
/>
<ExtenderButton
IconCls={ArrowDownFromLine}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_Y_BOTTOM, 2.0)
}
/>
</TabsContent>
<TabsContent
value={EXTENDER_ALL}
className="flex gap-2 justify-center mt-0"
>
<ExtenderButton
IconCls={Maximize}
text="1.25x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.25)
}
/>
<ExtenderButton
IconCls={Maximize}
text="1.5x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.5)
}
/>
<ExtenderButton
IconCls={Maximize}
text="1.75x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 1.75)
}
/>
<ExtenderButton
IconCls={Maximize}
text="2.0x"
onClick={() =>
updateExtenderByBuiltIn(EXTENDER_BUILTIN_ALL, 2.0)
}
/>
</TabsContent>
</Tabs>
</div>
<Separator />
</>
)
@ -466,23 +709,23 @@ const SidePanel = () => {
>
<div className="flex flex-col gap-4 mt-4">
<RowContainer>
<Label htmlFor="cropper">Cropper</Label>
<LabelTitle text="Cropper" />
<Switch
id="cropper"
checked={settings.showCropper}
onCheckedChange={(value) => {
updateSettings({ showCropper: value })
if (value) {
updateSettings({ showExpender: false })
updateSettings({ showExtender: false })
}
}}
/>
</RowContainer>
{renderExpender()}
{renderExtender()}
<div className="flex flex-col gap-1">
<Label htmlFor="steps">Steps</Label>
<LabelTitle htmlFor="steps" text="Steps" />
<RowContainer>
<Slider
className="w-[180px]"
@ -506,12 +749,16 @@ const SidePanel = () => {
</div>
<div className="flex flex-col gap-1">
<Label htmlFor="guidance-scale">Guidance scale</Label>
<LabelTitle
text="Guidance scale"
url="https://huggingface.co/docs/diffusers/main/en/using-diffusers/inpaint#guidance-scale"
toolTip="Guidance scale affects how aligned the text prompt and generated image are. Higher value means the prompt and generated image are closely aligned, so the output is a stricter interpretation of the prompt"
/>
<RowContainer>
<Slider
className="w-[180px]"
defaultValue={[750]}
min={100}
min={0}
max={1500}
step={1}
value={[Math.floor(settings.sdGuidanceScale * 100)]}
@ -535,7 +782,7 @@ const SidePanel = () => {
{renderStrength()}
<RowContainer>
<Label htmlFor="sampler">Sampler</Label>
<LabelTitle text="Sampler" />
<Select
value={settings.sdSampler as string}
onValueChange={(value) => {
@ -563,7 +810,11 @@ const SidePanel = () => {
<RowContainer>
{/* 每次会从服务器返回更新该值 */}
<Label htmlFor="seed">Seed</Label>
<LabelTitle
text="Seed"
toolTip="Using same parameters and a fixed seed can generate same result image."
/>
{/* <Pin /> */}
<div className="flex gap-2 justify-center items-center">
<Switch
id="seed"
@ -594,7 +845,10 @@ const SidePanel = () => {
{renderLCMLora()}
<div className="flex flex-col gap-1">
<Label htmlFor="mask-blur">Mask blur</Label>
<LabelTitle
text="Mask blur"
toolTip="How much to blur the mask before processing, in pixels."
/>
<RowContainer>
<Slider
className="w-[180px]"
@ -620,7 +874,11 @@ const SidePanel = () => {
</div>
<RowContainer>
<Label htmlFor="match-histograms">Match histograms</Label>
<LabelTitle
text="Match histograms"
toolTip="Match the inpainting result histogram to the source image histogram"
url="https://github.com/Sanster/lama-cleaner/pull/143#issuecomment-1325859307"
/>
<Switch
id="match-histograms"
checked={settings.sdMatchHistograms}

View File

@ -4,12 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Input } from "./input"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip"
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
@ -72,8 +67,6 @@ export interface IconButtonProps extends ButtonProps {
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
({ tooltip, children, ...rest }, ref) => {
return (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
@ -91,8 +84,6 @@ const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)
}
)

View File

@ -20,7 +20,12 @@ const Label = React.forwardRef<
>(({ className, disabled, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className, disabled ? "opacity-50" : "")}
className={cn(
labelVariants(),
className,
disabled ? "opacity-50" : "",
"select-none"
)}
{...props}
/>
))

View File

@ -0,0 +1,42 @@
import * as React from "react"
import { CheckIcon } from "@radix-ui/react-icons"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -15,6 +15,7 @@ const TabsList = React.forwardRef<
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
tabIndex={-1}
{...props}
/>
))
@ -30,6 +31,7 @@ const TabsTrigger = React.forwardRef<
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
tabIndex={-1}
{...props}
/>
))
@ -45,6 +47,7 @@ const TabsContent = React.forwardRef<
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
tabIndex={-1}
{...props}
/>
))

View File

@ -1,6 +1,6 @@
import { ModelInfo, Rect } from "@/lib/types"
import { Settings } from "@/lib/states"
import { dataURItoBlob, srcToFile } from "@/lib/utils"
import { srcToFile } from "@/lib/utils"
import axios from "axios"
export const API_ENDPOINT = import.meta.env.VITE_BACKEND
@ -15,6 +15,7 @@ export default async function inpaint(
imageFile: File,
settings: Settings,
croperRect: Rect,
extenderState: Rect,
mask: File | Blob,
paintByExampleImage: File | null = null
) {
@ -32,11 +33,18 @@ export default async function inpaint(
fd.append("prompt", settings.prompt)
fd.append("negativePrompt", settings.negativePrompt)
fd.append("useCroper", settings.showCropper ? "true" : "false")
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.showCropper ? "true" : "false")
fd.append("useExtender", settings.showExtender ? "true" : "false")
fd.append("extenderX", extenderState.x.toString())
fd.append("extenderY", extenderState.y.toString())
fd.append("extenderHeight", extenderState.height.toString())
fd.append("extenderWidth", extenderState.width.toString())
fd.append("sdMaskBlur", settings.sdMaskBlur.toString())
fd.append("sdStrength", settings.sdStrength.toString())
@ -82,7 +90,7 @@ export default async function inpaint(
})
if (res.ok) {
const blob = await res.blob()
const newSeed = res.headers.get("x-seed")
const newSeed = res.headers.get("X-seed")
return { blob: URL.createObjectURL(blob), seed: newSeed }
}
const errMsg = await res.text()

View File

@ -8,6 +8,15 @@ export const MODEL_TYPE_DIFFUSERS_SDXL_INPAINT = "diffusers_sdxl_inpaint"
export const MODEL_TYPE_OTHER = "diffusers_other"
export const BRUSH_COLOR = "#ffcc00bb"
export const EXTENDER_X = "extender_x"
export const EXTENDER_Y = "extender_y"
export const EXTENDER_ALL = "extender_all"
export const EXTENDER_BUILTIN_X_LEFT = "extender_builtin_x_left"
export const EXTENDER_BUILTIN_X_RIGHT = "extender_builtin_x_right"
export const EXTENDER_BUILTIN_Y_TOP = "extender_builtin_y_top"
export const EXTENDER_BUILTIN_Y_BOTTOM = "extender_builtin_y_bottom"
export const EXTENDER_BUILTIN_ALL = "extender_builtin_all"
export const PAINT_BY_EXAMPLE = "Fantasy-Studio/Paint-by-Example"
export const INSTRUCT_PIX2PIX = "timbrooks/instruct-pix2pix"
export const KANDINSKY_2_2 = "kandinsky-community/kandinsky-2-2-decoder-inpaint"

View File

@ -1,22 +0,0 @@
import mitt from "mitt"
export const EVENT_PROMPT = "prompt"
export const EVENT_CUSTOM_MASK = "custom_mask"
export interface CustomMaskEventData {
mask: File
}
export const EVENT_PAINT_BY_EXAMPLE = "paint_by_example"
export interface PaintByExampleEventData {
image: File
}
export const RERUN_LAST_MASK = "rerun_last_mask"
export const DREAM_BUTTON_MOUSE_ENTER = "dream_button_mouse_enter"
export const DREAM_BUTTON_MOUSE_LEAVE = "dream_btoon_mouse_leave"
const emitter = mitt()
export default emitter

View File

@ -21,6 +21,14 @@ import {
BRUSH_COLOR,
DEFAULT_BRUSH_SIZE,
DEFAULT_NEGATIVE_PROMPT,
EXTENDER_ALL,
EXTENDER_BUILTIN_ALL,
EXTENDER_BUILTIN_X_LEFT,
EXTENDER_BUILTIN_X_RIGHT,
EXTENDER_BUILTIN_Y_BOTTOM,
EXTENDER_BUILTIN_Y_TOP,
EXTENDER_X,
EXTENDER_Y,
MODEL_TYPE_INPAINT,
PAINT_BY_EXAMPLE,
} from "./const"
@ -56,7 +64,8 @@ export type Settings = {
enableManualInpainting: boolean
enableUploadMask: boolean
showCropper: boolean
showExpender: boolean
showExtender: boolean
extenderDirection: string
// For LDM
ldmSteps: number
@ -168,6 +177,9 @@ type AppAction = {
setExtenderY: (newValue: number) => void
setExtenderWidth: (newValue: number) => void
setExtenderHeight: (newValue: number) => void
updateExtenderDirection: (newValue: string) => void
resetExtender: (width: number, height: number) => void
updateExtenderByBuiltIn: (direction: string, scale: number) => void
setServerConfig: (newValue: ServerConfig) => void
setSeed: (newValue: number) => void
@ -281,7 +293,8 @@ const defaultValues: AppState = {
},
enableControlnet: false,
showCropper: false,
showExpender: false,
showExtender: false,
extenderDirection: EXTENDER_ALL,
enableDownloadMask: false,
enableManualInpainting: false,
enableUploadMask: false,
@ -362,7 +375,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
}
return targetFile
},
// todo: 传入 custom mask单独逻辑
runInpainting: async () => {
const {
isInpainting,
@ -372,6 +385,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
imageHeight,
settings,
cropperState,
extenderState,
} = get()
if (isInpainting) {
return
@ -398,7 +412,11 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
// 2. 结果替换当前 render
let maskLineGroup: LineGroup = []
if (useLastLineGroup === true) {
if (lastLineGroup.length === 0 && maskImage === null) {
if (
lastLineGroup.length === 0 &&
maskImage === null &&
!settings.showExtender
) {
toast({
variant: "destructive",
description: "Please draw mask on picture",
@ -407,7 +425,11 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
}
maskLineGroup = lastLineGroup
} else {
if (curLineGroup.length === 0 && maskImage === null) {
if (
curLineGroup.length === 0 &&
maskImage === null &&
!settings.showExtender
) {
toast({
variant: "destructive",
description: "Please draw mask on picture",
@ -455,6 +477,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
targetFile,
settings,
cropperState,
extenderState,
dataURItoBlob(maskCanvas.toDataURL()),
paintByExampleFile
)
@ -465,7 +488,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
const { blob, seed } = res
if (seed) {
set((state) => (state.settings.seed = parseInt(seed, 10)))
get().setSeed(parseInt(seed, 10))
}
const newRender = new Image()
await loadImage(newRender, blob)
@ -794,7 +817,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
state.isPluginRunning = newValue
}),
setFile: (file: File) =>
setFile: (file: File) => {
set((state) => {
state.file = file
state.interactiveSegState = castDraft(
@ -802,7 +825,8 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
)
state.editorState = castDraft(defaultValues.editorState)
state.cropperState = defaultValues.cropperState
}),
})
},
setCustomFile: (file: File) =>
set((state) => {
@ -822,6 +846,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
state.editorState.brushSizeScale =
Math.max(Math.min(width, height), 512) / 512
})
get().resetExtender(width, height)
},
setCropperX: (newValue: number) =>
@ -864,6 +889,68 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
state.extenderState.height = newValue
}),
updateExtenderDirection: (newValue: string) => {
console.log(
`updateExtenderDirection: ${JSON.stringify(get().extenderState)}`
)
set((state) => {
state.settings.extenderDirection = newValue
state.extenderState.x = 0
state.extenderState.y = 0
state.extenderState.width = state.imageWidth
state.extenderState.height = state.imageHeight
})
},
updateExtenderByBuiltIn: (direction: string, scale: number) => {
const newExtenderState = { ...defaultValues.extenderState }
let { x, y, width, height } = newExtenderState
const { imageWidth, imageHeight } = get()
width = imageWidth
height = imageHeight
switch (direction) {
case EXTENDER_BUILTIN_X_LEFT:
x = -Math.ceil(imageWidth * (scale - 1))
width = Math.ceil(imageWidth * scale)
break
case EXTENDER_BUILTIN_X_RIGHT:
width = Math.ceil(imageWidth * scale)
break
case EXTENDER_BUILTIN_Y_TOP:
y = -Math.ceil(imageHeight * (scale - 1))
height = Math.ceil(imageHeight * scale)
break
case EXTENDER_BUILTIN_Y_BOTTOM:
height = Math.ceil(imageHeight * scale)
break
case EXTENDER_BUILTIN_ALL:
x = -Math.ceil((imageWidth * (scale - 1)) / 2)
y = -Math.ceil((imageHeight * (scale - 1)) / 2)
width = Math.ceil(imageWidth * scale)
height = Math.ceil(imageHeight * scale)
break
default:
break
}
set((state) => {
state.extenderState.x = x
state.extenderState.y = y
state.extenderState.width = width
state.extenderState.height = height
})
},
resetExtender: (width: number, height: number) => {
set((state) => {
state.extenderState.x = 0
state.extenderState.y = 0
state.extenderState.width = width
state.extenderState.height = height
})
},
setSeed: (newValue: number) =>
set((state) => {
state.settings.seed = newValue
@ -871,7 +958,7 @@ export const useStore = createWithEqualityFn<AppState & AppAction>()(
})),
{
name: "ZUSTAND_STATE", // name of the item in the storage (must be unique)
version: 1,
version: 0,
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>

View File

@ -67,7 +67,7 @@ export enum SDSampler {
kEulerA = "k_euler_a",
dpmPlusPlus = "dpm++",
uni_pc = "uni_pc",
lcm = "lcm",
// lcm = "lcm",
}
export interface FreeuConfig {

View File

@ -71,6 +71,26 @@ export function canvasToImage(
})
}
export function fileToImage(file: File): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const image = new Image()
image.onload = () => {
resolve(image)
}
image.onerror = () => {
reject("无法加载图像。")
}
image.src = reader.result as string
}
reader.onerror = () => {
reject("无法读取文件。")
}
reader.readAsDataURL(file)
})
}
export function srcToFile(src: string, fileName: string, mimeType: string) {
return fetch(src)
.then(function (res) {

View File

@ -5,6 +5,7 @@ import "inter-ui/inter.css"
import App from "./App.tsx"
import "./globals.css"
import { ThemeProvider } from "next-themes"
import { TooltipProvider } from "./components/ui/tooltip.tsx"
const queryClient = new QueryClient()
@ -12,7 +13,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" disableTransitionOnChange>
<TooltipProvider>
<App />
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>