mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaabaaf7a1 | ||
|
|
410b1251d1 | ||
|
|
c6aebd0585 | ||
|
|
6b23eb4192 | ||
|
|
318f75cb58 | ||
|
|
834782f542 | ||
|
|
2cc587d4e9 | ||
|
|
d242c35a45 | ||
|
|
647703c673 | ||
|
|
81a2ad65c8 | ||
|
|
11e0c7f69d |
@@ -1,7 +1,23 @@
|
||||
import { faArrowPointer, faArrowsCross, faCircleXmark, faCross, faEdit, faHandPointer, faLocationArrow, faLocationArrowUp, faMousePointer, faNavicon, faSparkles, faXmarkCircle, faXmarksLines } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { DialogAddImage } from "~/components/features";
|
||||
import { Button, Slider, TabItem, TabSelector } from "~/components/ui";
|
||||
import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
import { EditEngine } from "~/KonvaApp/EditEngine";
|
||||
import { KonvaCanvasContainer } from "~/KonvaRootComponent/KonvaCanvasContainer";
|
||||
import { EditMode, editModeBrushSize, editModeState, setEditModeBaseImage, setEditModeBrushSize, triggerEditModeClear } from "~/signals/editMode";
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ id: EditMode.SELECT, label: "Select", icon: faLocationArrow, iconClassName: "fa-flip-horizontal mr-1.5" },
|
||||
{ id: EditMode.EDIT, label: "Edit Region", icon: faEdit },
|
||||
];
|
||||
|
||||
const setEditMode = (tabId: string) => {
|
||||
editModeState.value = tabId as EditMode;
|
||||
}
|
||||
|
||||
export const EditModeRootComponent = ({ className }: { className: string }) => {
|
||||
useRenderCounter("EditModeRootComponent");
|
||||
@@ -14,12 +30,86 @@ export const EditModeRootComponent = ({ className }: { className: string }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useSignals();
|
||||
const editMode = editModeState.value;
|
||||
console.log("Edit Root Components rendered, ", editMode)
|
||||
|
||||
const handleGenerate = useCallback(() => {
|
||||
if (!editEngineRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
editEngineRef.current.triggerInpainting();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<KonvaCanvasContainer
|
||||
className={className}
|
||||
ref={konvaContainerCallbackRef}
|
||||
/>
|
||||
{editMode === EditMode.INIT ?
|
||||
(
|
||||
<DialogAddImage
|
||||
isOpen={true}
|
||||
onAddImage={setEditModeBaseImage}
|
||||
cancellable={false}
|
||||
/>
|
||||
)
|
||||
:
|
||||
(
|
||||
<div className="fixed bottom-0 left-0 right-0 z-10 flex max-w-full items-center justify-center">
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute bottom-12 flex min-h-[56px] items-end rounded-xl p-3 transition-all duration-[400ms] ease-in-out",
|
||||
)}
|
||||
>
|
||||
<TabSelector
|
||||
tabs={tabs}
|
||||
activeTab={editMode}
|
||||
onTabChange={(tabId) => { setEditMode(tabId) }}
|
||||
/>
|
||||
<Button
|
||||
icon={faSparkles}
|
||||
variant="primary"
|
||||
className="text-md ml-2 self-end"
|
||||
onClick={handleGenerate}
|
||||
disabled={false}
|
||||
loading={false}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div >
|
||||
)}
|
||||
{editMode === EditMode.EDIT && (
|
||||
<div className="fixed top-20 left-0 right-0 z-10 flex max-w-full items-center justify-center">
|
||||
<div className="glass flex rounded-full items-center justify-center p-2">
|
||||
<EditModeBrushSlider />
|
||||
<Button icon={faCircleXmark} className="ml-2" variant="action"
|
||||
onClick={triggerEditModeClear}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditModeBrushSlider = () => {
|
||||
return (
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={editModeBrushSize.value}
|
||||
onChange={setEditModeBrushSize}
|
||||
step={1}
|
||||
innerLabel={"Brush size"}
|
||||
showDecrement
|
||||
showIncrement
|
||||
className="w-[196px]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@ import {
|
||||
import { UndoStackManager } from "./UndoRedo";
|
||||
|
||||
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
|
||||
import { EditModes, VideoResolutions } from "./constants";
|
||||
import { VideoResolutions } from "./constants";
|
||||
|
||||
import { ToolbarNodeButtonNames } from "~/components/features/ToolbarNode/enums";
|
||||
|
||||
import { RealTimeDrawEngine } from "./RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { EditModeDrawEngine } from "./RenderingPrimitives/EditModeDrawEngine";
|
||||
import { EditMode, editModeState, onEditModeBaseImageChange, onEditModeBrushSizeChange, onEditModeChange, onEditModeInpaintClear } from "~/signals/editMode";
|
||||
import { MediaNode } from "./types";
|
||||
|
||||
export interface RenderingOptions {
|
||||
artstyle: string;
|
||||
@@ -35,7 +37,7 @@ export interface RenderingOptions {
|
||||
}
|
||||
|
||||
export class EditEngine {
|
||||
private editMode: EditModes = EditModes.SELECT;
|
||||
private editMode: EditMode;
|
||||
private boardCanvasRef: HTMLDivElement;
|
||||
private stage: Konva.Stage;
|
||||
private bgLayer: Konva.Layer;
|
||||
@@ -43,7 +45,7 @@ export class EditEngine {
|
||||
private nodeIsolationLayer: Konva.Layer;
|
||||
private uiLayer: Konva.Layer;
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private realTimeDrawEngine: RealTimeDrawEngine;
|
||||
private editDrawEngine: EditModeDrawEngine;
|
||||
private nodesManager: NodesManager;
|
||||
private nodeTransformer: NodeTransformer;
|
||||
private selectionManager: SelectionManager;
|
||||
@@ -59,7 +61,7 @@ export class EditEngine {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Engine Created");
|
||||
}
|
||||
this.editMode = EditModes.SELECT;
|
||||
this.editMode = EditMode.INIT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
this.stage = new Konva.Stage({
|
||||
@@ -94,19 +96,20 @@ export class EditEngine {
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
this.editDrawEngine = new EditModeDrawEngine({
|
||||
width: VideoResolutions.SQUARE_1024.width,
|
||||
height: VideoResolutions.SQUARE_1024.height,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
offScreenCanvas: this.offScreenCanvas,
|
||||
onDraw: async (canvas, lineBounds) => {
|
||||
await this.addPaintNode(canvas, lineBounds);
|
||||
console.log("Drawing Paint Node");
|
||||
this.addPaintNode(canvas, lineBounds);
|
||||
},
|
||||
});
|
||||
|
||||
// Selector Square to select Nodes
|
||||
this.selectorSquare = new SelectorSquare({
|
||||
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
|
||||
captureCanvasRef: this.editDrawEngine.captureCanvas,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
@@ -116,14 +119,15 @@ export class EditEngine {
|
||||
|
||||
//Collection of commands for undo-redo
|
||||
this.undoStackManager = new UndoStackManager(() => {
|
||||
this.realTimeDrawEngine.render();
|
||||
// this.editDrawEngine.render();
|
||||
});
|
||||
|
||||
this.commandManager = new CommandManager({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
renderEngineRef: this.editDrawEngine,
|
||||
undoStackManagerRef: this.undoStackManager,
|
||||
});
|
||||
|
||||
@@ -132,7 +136,7 @@ export class EditEngine {
|
||||
width: this.boardCanvasRef.clientWidth,
|
||||
height: this.boardCanvasRef.clientHeight,
|
||||
},
|
||||
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
|
||||
captureCanvasSize: this.editDrawEngine.captureCanvas.getSize(),
|
||||
uiLayerRef: this.uiLayer,
|
||||
});
|
||||
|
||||
@@ -142,33 +146,36 @@ export class EditEngine {
|
||||
this.setEditMode(this.editMode);
|
||||
}
|
||||
|
||||
private setEditMode(newEditMode: EditModes) {
|
||||
this.editMode = newEditMode;
|
||||
switch (this.editMode) {
|
||||
case EditModes.SELECT:
|
||||
default: {
|
||||
console.log("EDITMODE: SELECT");
|
||||
this.realTimeDrawEngine.disablePaintMode();
|
||||
private setEditMode(newEditMode: EditMode) {
|
||||
if (this.editMode === newEditMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editMode = newEditMode;
|
||||
|
||||
switch (this.editMode) {
|
||||
case EditMode.EDIT:
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
this.inpaintNodeVisibility(true);
|
||||
this.editDrawEngine.enablePaintMode();
|
||||
break;
|
||||
case EditMode.SELECT:
|
||||
default:
|
||||
this.editDrawEngine.disablePaintMode();
|
||||
this.inpaintNodeVisibility(false);
|
||||
this.selectorSquare.enable();
|
||||
this.selectionManager.enable();
|
||||
uiAccess.toolbarMain.enable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: true,
|
||||
});
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
|
||||
active: false,
|
||||
});
|
||||
// uiAccess.toolbarMain.enable();
|
||||
// uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
// active: true,
|
||||
// });
|
||||
// uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
|
||||
// active: false,
|
||||
// });
|
||||
this.matteBox.disable();
|
||||
document.body.style.cursor = "default";
|
||||
return;
|
||||
}
|
||||
case EditModes.EDIT: {
|
||||
console.log("EDITMODE: EDIT");
|
||||
this.realTimeDrawEngine.enablePaintMode();
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
private setupEventSystem() {
|
||||
@@ -195,41 +202,35 @@ export class EditEngine {
|
||||
}) as EventListener,
|
||||
);
|
||||
|
||||
// Listen to Toolbar Main
|
||||
// uiEvents.toolbarMain.UNDO.onClick(() => {
|
||||
// this.undoStackManager.undo();
|
||||
// });
|
||||
// uiEvents.toolbarMain.REDO.onClick(() => {
|
||||
// this.undoStackManager.redo();
|
||||
// this.realTimeDrawEngine.render();
|
||||
// });
|
||||
// Listen to edit mode UI
|
||||
onEditModeChange((mode) => {
|
||||
this.setEditMode(mode);
|
||||
});
|
||||
|
||||
// uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
|
||||
// await this.realTimeDrawEngine.saveOutput();
|
||||
// });
|
||||
onEditModeBaseImageChange((imageFile) => {
|
||||
this.addBackgroundImage(imageFile);
|
||||
})
|
||||
|
||||
// uiEvents.toolbarMain.SELECT.onClick(() => {
|
||||
// console.log("Toolbar Main >> Select");
|
||||
// this.setAppMode(AppModes.SELECT);
|
||||
// });
|
||||
onEditModeBrushSizeChange((size) => {
|
||||
this.editDrawEngine.paintBrushSize = size;
|
||||
})
|
||||
|
||||
// uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
// console.log("Toolbar Main >> Paint");
|
||||
// this.setAppMode(AppModes.PAINT);
|
||||
// });
|
||||
onEditModeInpaintClear(() => {
|
||||
this.clearInpaintNodes();
|
||||
})
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
uiAccess.toolbarNode.changeButtonState(name, { disabled: true });
|
||||
// uiAccess.toolbarNode.changeButtonState(name, { disabled: true });
|
||||
}
|
||||
}
|
||||
|
||||
async enableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
await uiAccess.toolbarNode.changeButtonState(name, { disabled: false });
|
||||
// await uiAccess.toolbarNode.changeButtonState(name, { disabled: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +239,7 @@ export class EditEngine {
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
|
||||
this.editDrawEngine.updateCaptureCanvas(undefined, undefined);
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
@@ -252,7 +253,7 @@ export class EditEngine {
|
||||
}
|
||||
|
||||
// Sandbox is quickly a way to test your idea.
|
||||
public async sandbox() {}
|
||||
public async sandbox() { }
|
||||
|
||||
public onMessage(event: MessageEvent) {
|
||||
console.log("Message From Shared Worker");
|
||||
@@ -261,8 +262,8 @@ export class EditEngine {
|
||||
|
||||
public initializeStage() {
|
||||
// load canvas that was originaly saved TODO Save manager for resharing.
|
||||
uiAccess.toolbarNode.hide();
|
||||
uiAccess.loadingBar.hide();
|
||||
// uiAccess.toolbarNode.hide();
|
||||
// uiAccess.loadingBar.hide();
|
||||
this.setupStage();
|
||||
}
|
||||
|
||||
@@ -295,32 +296,17 @@ export class EditEngine {
|
||||
this.addKeyboardShortcuts();
|
||||
}
|
||||
|
||||
public addImage(imageFile: File) {
|
||||
public addBackgroundImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
canvasPosition: this.editDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.editDrawEngine.captureCanvas.size(),
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
id: "background",
|
||||
name: "background",
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
this.setEditMode(EditModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addImageFromImageBitmap(imageBitmap: ImageBitmap) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageBitmap: imageBitmap,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
this.setEditMode(EditModes.SELECT);
|
||||
this.setEditMode(EditMode.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -341,13 +327,13 @@ export class EditEngine {
|
||||
lineBounds: lineBounds,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
},
|
||||
loaded: async () => { },
|
||||
});
|
||||
this.commandManager.createNode(node);
|
||||
}
|
||||
|
||||
// Events for Undo and Redo
|
||||
// FIXME: Doesn't actually work?
|
||||
private addKeyboardShortcuts() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.ctrlKey && event.key === "z") {
|
||||
@@ -362,4 +348,38 @@ export class EditEngine {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private inpaintNodeVisibility(show: boolean) {
|
||||
console.log("inpaintNodeVisibility:", show);
|
||||
this.nodesManager.getAllNodes().forEach((node) => {
|
||||
console.log("inpaint:", node);
|
||||
if (node instanceof PaintNode) {
|
||||
if (show) {
|
||||
node.kNode.show();
|
||||
} else {
|
||||
node.kNode.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private clearInpaintNodes() {
|
||||
console.log("clearInpaintNodes");
|
||||
const markedForDelete: MediaNode[] = [];
|
||||
this.nodesManager.getAllNodes().forEach((node) => {
|
||||
if (node instanceof PaintNode) {
|
||||
markedForDelete.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (markedForDelete.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.commandManager.deleteSpecificNodes(markedForDelete);
|
||||
}
|
||||
|
||||
public triggerInpainting() {
|
||||
this.editDrawEngine.renderInpainting();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
const ensureBase64Prefix = (base64String: string) => {
|
||||
return base64String.startsWith("data:")
|
||||
? base64String
|
||||
: `data:image/png;base64,${base64String}`;
|
||||
}
|
||||
export const ensureBase64Prefix = (base64String: string) => {
|
||||
return base64String.startsWith("data:")
|
||||
? base64String
|
||||
: `data:image/png;base64,${base64String}`;
|
||||
};
|
||||
|
||||
export { ensureBase64Prefix };
|
||||
export async function imageBitmapToBase64(
|
||||
imageBitmap: ImageBitmap,
|
||||
): Promise<string> {
|
||||
// Create a temporary canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
|
||||
// Draw the ImageBitmap onto the canvas
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Failed to get 2D context");
|
||||
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
|
||||
// Convert to base64
|
||||
const base64String = canvas.toDataURL("image/png");
|
||||
|
||||
// Clean up
|
||||
canvas.remove();
|
||||
|
||||
// Remove the data:image/png;base64, prefix if you want just the base64 string
|
||||
return base64String.split(",")[1];
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ import {
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
|
||||
import { EditModeDrawEngine } from "../RenderingPrimitives/EditModeDrawEngine";
|
||||
interface EngineReferences {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
renderEngineRef: RealTimeDrawEngine;
|
||||
renderEngineRef: RealTimeDrawEngine | EditModeDrawEngine;
|
||||
undoStackManagerRef: UndoStackManager;
|
||||
}
|
||||
|
||||
@@ -62,11 +61,18 @@ export class CommandManager {
|
||||
|
||||
// Set the kNode's manual id to same as kNode's internal _id
|
||||
// This helps identify the kNode in the Konva layer with find
|
||||
node.kNode.id(node.kNode._id.toString());
|
||||
if (node.kNode.id() === "") {
|
||||
node.kNode.id(node.kNode._id.toString());
|
||||
}
|
||||
this.renderEngineRef.render();
|
||||
}
|
||||
deleteNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
|
||||
if (nodes.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = new DeleteCommand({
|
||||
nodes: nodes,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
@@ -80,8 +86,14 @@ export class CommandManager {
|
||||
}
|
||||
|
||||
deleteSpecificNodes(nodes: MediaNode[]) {
|
||||
if (nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeSet = new Set<MediaNode>(nodes);
|
||||
|
||||
const command = new DeleteCommand({
|
||||
nodes: nodes,
|
||||
nodes: nodeSet,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
|
||||
@@ -18,6 +18,8 @@ interface ImageNodeContructor {
|
||||
transform?: TransformationData;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
id?: string;
|
||||
name?: string;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -36,6 +38,8 @@ export class ImageNode extends NetworkedNode {
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
imageBitmap,
|
||||
id,
|
||||
name
|
||||
}: ImageNodeContructor) {
|
||||
// kNodes need to be created first to guaruntee
|
||||
// that it is not undefined in parent's context
|
||||
@@ -49,6 +53,8 @@ export class ImageNode extends NetworkedNode {
|
||||
...transform,
|
||||
draggable: true,
|
||||
strokeScaleEnabled: false,
|
||||
id: id,
|
||||
name: name,
|
||||
});
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
|
||||
@@ -91,7 +91,7 @@ export class SelectorSquare {
|
||||
x2: stagePointerPos.x,
|
||||
y2: stagePointerPos.y,
|
||||
};
|
||||
|
||||
|
||||
this.kSquare.size({
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -141,7 +141,8 @@ export class SelectorSquare {
|
||||
var shapes = this.mediaLayerRef.getChildren();
|
||||
var box = this.kSquare.getClientRect();
|
||||
var foundKNodes = shapes.filter((shape) =>
|
||||
Konva.Util.haveIntersection(box, shape.getClientRect()),
|
||||
Konva.Util.haveIntersection(box, shape.getClientRect())
|
||||
&& shape.isVisible()
|
||||
);
|
||||
if (foundKNodes.length > 0) {
|
||||
const kNodeIds = foundKNodes.map((kNode) => kNode._id);
|
||||
|
||||
@@ -0,0 +1,818 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
import { FileUtilities } from "../FileUtilities/FileUtilities";
|
||||
import { ImageNode, ShapeNode, TextNode } from "../Nodes";
|
||||
import { MediaNode } from "../types";
|
||||
|
||||
import { RenderTask } from "./RenderTask";
|
||||
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
|
||||
// https://www.aiseesoft.com/resource/phone-aspect-ratio-screen-resolution.html#:~:text=16%3A9%20Aspect%20Ratio
|
||||
|
||||
import {
|
||||
isLoadingVisible,
|
||||
loadingProgress,
|
||||
} from "~/signals/uiEvents/loadingIndicator";
|
||||
import { DecodeBase64ToImage } from "~/utilities/DecodeBase64ToImage.ts";
|
||||
import { imageBitmapToBase64 } from "../EngineUtitlities/Base64Helpers";
|
||||
import { EDIT_INIT_BRUSH_SIZE } from "~/signals/editMode";
|
||||
|
||||
interface InpaintImageParams {
|
||||
image: string;
|
||||
mask: string; // White on black
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export class EditModeDrawEngine {
|
||||
private imageNodes: (PaintNode | ImageNode)[];
|
||||
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private outputBitmap: ImageBitmap | undefined;
|
||||
|
||||
// private frames: ImageBitmap[];
|
||||
|
||||
// capturing composite within window
|
||||
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private drawingsLayer: Konva.Layer; // New Layer for Drawings
|
||||
|
||||
private height: number;
|
||||
private width: number;
|
||||
private positionX: number;
|
||||
private positionY: number;
|
||||
|
||||
public captureCanvas: Konva.Rect;
|
||||
public backgroundRasterRect: Konva.Image;
|
||||
|
||||
public fps: number = 24;
|
||||
|
||||
public currentPrompt: string;
|
||||
|
||||
// Paint Color
|
||||
// paint Brush Size
|
||||
// has to exit out of paint mode when shape or image are used.
|
||||
public paintColor: string = "rgba(89, 167, 255, 0.5)";
|
||||
private brushSize: number = EDIT_INIT_BRUSH_SIZE; // Default brush size
|
||||
|
||||
private onDrawCallback?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
private offscreenRenderDiv: HTMLDivElement;
|
||||
|
||||
public backgroundColor: string = "#d2d2d2";
|
||||
|
||||
constructor({
|
||||
width,
|
||||
height,
|
||||
mediaLayerRef,
|
||||
offScreenCanvas,
|
||||
onDraw,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
offScreenCanvas: OffscreenCanvas;
|
||||
onDraw?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
}) {
|
||||
this.imageNodes = [];
|
||||
this.onDrawCallback = onDraw;
|
||||
|
||||
// TODO: Make this dynamic and update this on change of canvas.
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
this.positionX = window.innerWidth / 2 - this.width / 2;
|
||||
this.positionY = window.innerHeight / 2 - this.height / 2;
|
||||
|
||||
this.offScreenCanvas = offScreenCanvas;
|
||||
this.offScreenCanvas.width = this.width;
|
||||
this.offScreenCanvas.height = this.height;
|
||||
|
||||
// this is the whole canvas
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
|
||||
// Create a separate layer for drawings
|
||||
this.drawingsLayer = new Konva.Layer({
|
||||
clearBeforeDraw: true, // Ensures transparent background
|
||||
});
|
||||
|
||||
this.mediaLayerRef.getStage()?.add(this.drawingsLayer); // to od pass in stage
|
||||
|
||||
// Set background layer to red and media layer to green for visibility
|
||||
|
||||
this.fps = 24;
|
||||
|
||||
this.currentPrompt = "";
|
||||
// This is captures a subset of the medialayer ref
|
||||
this.captureCanvas = new Konva.Rect({
|
||||
name: "CaptureCanvas",
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fill: this.backgroundColor,
|
||||
stroke: "black",
|
||||
strokeWidth: 1,
|
||||
draggable: false,
|
||||
});
|
||||
this.offscreenRenderDiv = document.createElement("div");
|
||||
|
||||
this.backgroundRasterRect = new Konva.Image({
|
||||
name: "backgroundRasterRect",
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fill: this.backgroundColor,
|
||||
image: undefined,
|
||||
stroke: "black",
|
||||
strokeWidth: 1,
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
this.mediaLayerRef.add(this.captureCanvas);
|
||||
this.mediaLayerRef.add(this.backgroundRasterRect);
|
||||
// send back
|
||||
this.captureCanvas.setZIndex(0);
|
||||
this.backgroundRasterRect.setZIndex(0);
|
||||
|
||||
// Create a layer clip function that clips the layer to the capture canvas
|
||||
this.mediaLayerRef.clip({
|
||||
...this.captureCanvas.getPosition(),
|
||||
...this.captureCanvas.getSize(),
|
||||
});
|
||||
|
||||
// this.listenToServerEvents();
|
||||
}
|
||||
|
||||
public async listenToServerEvents() {
|
||||
listen("notification", (event) => {
|
||||
console.log(event);
|
||||
// Handle model download events
|
||||
const payload = event.payload as any;
|
||||
|
||||
// Model download started
|
||||
if (payload.model_download_started) {
|
||||
const modelInfo = payload.model_download_started;
|
||||
console.log(
|
||||
`Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`,
|
||||
);
|
||||
|
||||
// Set up loading indicator
|
||||
isLoadingVisible.value = true;
|
||||
loadingProgress.value = 0;
|
||||
|
||||
// Create fake progress updates
|
||||
const downloadTimer = setInterval(() => {
|
||||
loadingProgress.value += 2;
|
||||
// Cap at 95% until we get the completed event
|
||||
if (loadingProgress.value >= 95) {
|
||||
loadingProgress.value = 95;
|
||||
clearInterval(downloadTimer);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Model download completed
|
||||
if (payload.model_download_complete) {
|
||||
const modelInfo = payload.model_download_complete;
|
||||
console.log(
|
||||
`Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`,
|
||||
);
|
||||
|
||||
// Complete the loading progress
|
||||
loadingProgress.value = 100;
|
||||
|
||||
// Hide loading indicator after a short delay
|
||||
setTimeout(() => {
|
||||
isLoadingVisible.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isEnabled: boolean = false;
|
||||
private cleanupFunction: (() => void) | null = null;
|
||||
|
||||
private updateCursor(stage: Konva.Stage) {
|
||||
// Create cursor canvas
|
||||
const cursorCanvas = document.createElement("canvas");
|
||||
const size = this.brushSize * 5; // Match the brush size used in drawing
|
||||
cursorCanvas.width = size * 2; // Double size for padding
|
||||
cursorCanvas.height = size * 2;
|
||||
|
||||
const ctx = cursorCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw the outer circle with a light stroke
|
||||
ctx.beginPath();
|
||||
ctx.arc(size, size, size / 2, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "white";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
// Draw the inner circle with a dark stroke
|
||||
ctx.beginPath();
|
||||
ctx.arc(size, size, size / 2 - 1, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// Convert to data URL
|
||||
const cursorUrl = cursorCanvas.toDataURL();
|
||||
|
||||
// Apply custom cursor
|
||||
stage.container().style.cursor = `url(${cursorUrl}) ${size} ${size}, auto`;
|
||||
}
|
||||
|
||||
public paintMode() {
|
||||
let isDrawing = false;
|
||||
let currentLine: Konva.Line | null = null;
|
||||
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
// Initialize cursor
|
||||
this.updateCursor(stage);
|
||||
|
||||
const startDrawing = (pos: { x: number; y: number }) => {
|
||||
if (!this.isEnabled) return;
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (!stage) return;
|
||||
|
||||
// Convert pointer position to relative position within capture canvas
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
const relativeX = pos.x - captureBox.x;
|
||||
const relativeY = pos.y - captureBox.y;
|
||||
|
||||
currentLine = new Konva.Line({
|
||||
points: [relativeX, relativeY],
|
||||
stroke: this.paintColor,
|
||||
strokeWidth: this.brushSize * 5,
|
||||
lineCap: "round",
|
||||
lineJoin: "round",
|
||||
x: this.captureCanvas.x(),
|
||||
y: this.captureCanvas.y(),
|
||||
draggable: false,
|
||||
});
|
||||
this.drawingsLayer.moveToTop();
|
||||
this.drawingsLayer.add(currentLine); // Add to drawingsLayer
|
||||
isDrawing = true;
|
||||
};
|
||||
|
||||
const draw = (pos: { x: number; y: number }) => {
|
||||
if (!this.isEnabled) return;
|
||||
if (!isDrawing || !currentLine) return;
|
||||
|
||||
// Convert pointer position to relative position
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
const relativeX = pos.x - captureBox.x;
|
||||
const relativeY = pos.y - captureBox.y;
|
||||
|
||||
const newPoints = currentLine.points().concat([relativeX, relativeY]);
|
||||
currentLine.points(newPoints);
|
||||
this.drawingsLayer.batchDraw();
|
||||
};
|
||||
|
||||
const stopDrawing = () => {
|
||||
if (!this.isEnabled) return;
|
||||
if (!isDrawing || !currentLine) return;
|
||||
|
||||
// Store current line before resetting state
|
||||
const lineToConvert = currentLine;
|
||||
|
||||
// Reset drawing state immediately so we can start a new stroke
|
||||
isDrawing = false;
|
||||
currentLine = null;
|
||||
|
||||
// Create a temporary layer to render the line
|
||||
const tempLayer = new Konva.Layer({
|
||||
clearBeforeDraw: true,
|
||||
});
|
||||
this.mediaLayerRef.getStage()?.add(tempLayer);
|
||||
tempLayer.add(lineToConvert);
|
||||
|
||||
// Get the line canvas with transparent background
|
||||
// Get the bounding box of the line
|
||||
const lineBounds = lineToConvert.getClientRect();
|
||||
|
||||
// Create canvas with just enough size to contain the line
|
||||
const lineCanvas = tempLayer.toCanvas({
|
||||
x: lineBounds.x,
|
||||
y: lineBounds.y,
|
||||
width: lineBounds.width,
|
||||
height: lineBounds.height,
|
||||
pixelRatio: 1,
|
||||
});
|
||||
|
||||
if (this.onDrawCallback) {
|
||||
this.onDrawCallback(lineCanvas, lineBounds);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
lineToConvert.destroy();
|
||||
tempLayer.destroy();
|
||||
|
||||
this.drawingsLayer.batchDraw();
|
||||
};
|
||||
|
||||
// Check if point is within capture canvas bounds
|
||||
const isWithinCaptureCanvas = (pos: { x: number; y: number }) => {
|
||||
const captureBox = this.captureCanvas.getClientRect();
|
||||
return (
|
||||
pos.x >= captureBox.x &&
|
||||
pos.x <= captureBox.x + captureBox.width &&
|
||||
pos.y >= captureBox.y &&
|
||||
pos.y <= captureBox.y + captureBox.height
|
||||
);
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
stage.on("mousedown touchstart", (e) => {
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos && isWithinCaptureCanvas(pos)) {
|
||||
console.log("Start Drawing");
|
||||
startDrawing(pos);
|
||||
}
|
||||
});
|
||||
|
||||
stage.on("mousemove touchmove", (e) => {
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos && isWithinCaptureCanvas(pos)) {
|
||||
draw(pos);
|
||||
}
|
||||
});
|
||||
|
||||
stage.on("mouseup touchend", async () => {
|
||||
stopDrawing();
|
||||
});
|
||||
|
||||
// Store cleanup function
|
||||
this.cleanupFunction = () => {
|
||||
console.log("Cleaning up listeners");
|
||||
stage.off("mousedown touchstart");
|
||||
stage.off("mousemove touchmove");
|
||||
stage.off("mouseup touchend");
|
||||
};
|
||||
}
|
||||
|
||||
public enablePaintMode() {
|
||||
this.isEnabled = true;
|
||||
this.disableDragging();
|
||||
if (!this.cleanupFunction) {
|
||||
console.log("Paint Mode Enabled");
|
||||
this.paintMode();
|
||||
}
|
||||
// Update cursor when enabling paint mode
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (stage) {
|
||||
this.updateCursor(stage);
|
||||
}
|
||||
}
|
||||
|
||||
public enableDragging() {
|
||||
// Enable dragging for all nodes in media layer
|
||||
this.imageNodes?.forEach((node) => {
|
||||
console.log("Image Nodes Enable");
|
||||
console.log(node);
|
||||
node.kNode.draggable(true);
|
||||
node.kNode.listening(true);
|
||||
});
|
||||
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
|
||||
public disableDragging() {
|
||||
// Disable dragging for all nodes in media layer
|
||||
this.imageNodes?.forEach((node) => {
|
||||
node.kNode.draggable(false);
|
||||
node.kNode.listening(false);
|
||||
});
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
|
||||
public disablePaintMode() {
|
||||
this.isEnabled = false;
|
||||
this.enableDragging();
|
||||
if (this.cleanupFunction) {
|
||||
this.cleanupFunction();
|
||||
this.cleanupFunction = null;
|
||||
}
|
||||
// Reset cursor when disabling paint mode
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (stage) {
|
||||
stage.container().style.cursor = "default";
|
||||
}
|
||||
}
|
||||
|
||||
public findImageNodeById(
|
||||
id: string,
|
||||
): ImageNode | TextNode | ShapeNode | PaintNode | undefined {
|
||||
return this.imageNodes.find((node) => {
|
||||
if (node.kNode) {
|
||||
return node.kNode.id() === id;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
async updateCaptureCanvas(
|
||||
width: number | undefined,
|
||||
height: number | undefined,
|
||||
) {
|
||||
if (!this.captureCanvas) {
|
||||
return;
|
||||
}
|
||||
if (width) {
|
||||
this.width = width;
|
||||
}
|
||||
if (height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
// Ensures that all the nodes stag in the same place should
|
||||
// there be a window resize.
|
||||
// recompute the position
|
||||
// to ensure that the position of this stays
|
||||
|
||||
const oldPositionX = this.positionX;
|
||||
const oldPositionY = this.positionY;
|
||||
|
||||
this.positionX = window.innerWidth / 2 - this.width / 2;
|
||||
this.positionY = window.innerHeight / 2 - this.height / 2;
|
||||
|
||||
this.captureCanvas.setPosition({
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
});
|
||||
this.captureCanvas.size({ width: this.width, height: this.height });
|
||||
|
||||
// this is the change in positions
|
||||
const deltaX = this.positionX - oldPositionX;
|
||||
const deltaY = this.positionY - oldPositionY;
|
||||
|
||||
var children = this.mediaLayerRef.getChildren();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let node = children[i];
|
||||
|
||||
// skip the capture canvas and preview canvas update.
|
||||
if (node.name() === "CaptureCanvas" || node.name() === "PreviewCanvas") {
|
||||
continue;
|
||||
}
|
||||
const pos = node.getPosition();
|
||||
node.setPosition({
|
||||
x: pos.x + deltaX,
|
||||
y: pos.y + deltaY,
|
||||
});
|
||||
}
|
||||
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
|
||||
debug() {
|
||||
// DEBUG ONLY
|
||||
const rectangle = new Konva.Rect({
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: "green",
|
||||
stroke: "black",
|
||||
strokeWidth: 1,
|
||||
draggable: false,
|
||||
});
|
||||
this.mediaLayerRef.add(rectangle);
|
||||
}
|
||||
|
||||
public placeDebugRect(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
layer: Konva.Layer,
|
||||
): void {
|
||||
const rect = new Konva.Rect({
|
||||
x: x,
|
||||
y: y,
|
||||
width: width, // Default width
|
||||
height: height, // Default height
|
||||
fill: "green",
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
layer.add(rect);
|
||||
layer.draw();
|
||||
}
|
||||
|
||||
// This function uses a portion of the video layer to capture just the capture canvas.
|
||||
// capture everything after seeking each video node.
|
||||
renderPortionOfLayer(
|
||||
layer: Konva.Layer,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): HTMLCanvasElement {
|
||||
const canvas = layer.toCanvas({
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
return canvas;
|
||||
}
|
||||
|
||||
public isProcessing = false;
|
||||
private handleNodeDragEnd = async () => {
|
||||
// Clean up any existing state
|
||||
};
|
||||
|
||||
public async addNodes(node: MediaNode) {
|
||||
if (node instanceof PaintNode || node instanceof ImageNode) {
|
||||
console.debug("Adding node:", node);
|
||||
this.imageNodes.push(node);
|
||||
console.log(this.imageNodes);
|
||||
}
|
||||
|
||||
// ensure the layer doesn't move if added while painting.
|
||||
if (this.isEnabled) {
|
||||
this.disableDragging();
|
||||
}
|
||||
}
|
||||
|
||||
public removeNodes(node: MediaNode) {
|
||||
if (node instanceof PaintNode || node instanceof ImageNode) {
|
||||
const index = this.imageNodes.indexOf(node);
|
||||
if (index > -1) {
|
||||
node.kNode.off("dragend", this.handleNodeDragEnd);
|
||||
this.imageNodes.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private cloneStageForMask(
|
||||
stage: Konva.Stage,
|
||||
layerOfInterest: Konva.Layer,
|
||||
): Konva.Stage {
|
||||
const stageClone = new Konva.Stage({
|
||||
width: stage.width(),
|
||||
height: stage.height(),
|
||||
container: this.offscreenRenderDiv,
|
||||
});
|
||||
|
||||
// Make a white background layer
|
||||
const backgroundLayer = new Konva.Layer();
|
||||
stageClone.add(backgroundLayer);
|
||||
|
||||
// Make a render layer to hold all paint nodes
|
||||
const renderLayer = new Konva.Layer();
|
||||
stageClone.add(renderLayer);
|
||||
|
||||
// Clone all the nodes then reset them to render right
|
||||
layerOfInterest.getChildren().forEach((node) => {
|
||||
if (!(node instanceof Konva.Image) || node.name()) {
|
||||
console.log("Ignoring", node);
|
||||
return;
|
||||
}
|
||||
console.log("Cloning", node);
|
||||
const dupNode = node.clone();
|
||||
dupNode.strokeWidth(0);
|
||||
renderLayer.add(dupNode);
|
||||
});
|
||||
|
||||
// Add a white rectangle to the background layer
|
||||
const backgroundRect = new Konva.Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: backgroundLayer.width(),
|
||||
height: backgroundLayer.height(),
|
||||
fill: "white",
|
||||
});
|
||||
|
||||
backgroundLayer.add(backgroundRect);
|
||||
|
||||
return stageClone;
|
||||
}
|
||||
|
||||
private cloneStageForBackground(
|
||||
stage: Konva.Stage,
|
||||
layerOfInterest: Konva.Layer,
|
||||
): Konva.Stage {
|
||||
const stageClone = new Konva.Stage({
|
||||
width: stage.width(),
|
||||
height: stage.height(),
|
||||
container: this.offscreenRenderDiv,
|
||||
});
|
||||
|
||||
// Make a white background layer
|
||||
const backgroundLayer = new Konva.Layer();
|
||||
stageClone.add(backgroundLayer);
|
||||
|
||||
// Make a render layer to hold all paint nodes
|
||||
const renderLayer = new Konva.Layer();
|
||||
stageClone.add(renderLayer);
|
||||
|
||||
// Clone all the nodes then reset them to render right
|
||||
layerOfInterest.getChildren().forEach((node) => {
|
||||
if (node.id() !== "background") {
|
||||
console.log("Ignoring", node);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Cloning", node);
|
||||
const dupNode = node.clone();
|
||||
dupNode.strokeWidth(0);
|
||||
renderLayer.add(dupNode);
|
||||
});
|
||||
|
||||
// Add a white rectangle to the background layer
|
||||
const backgroundRect = new Konva.Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: backgroundLayer.width(),
|
||||
height: backgroundLayer.height(),
|
||||
fill: "white",
|
||||
});
|
||||
|
||||
backgroundLayer.add(backgroundRect);
|
||||
|
||||
return stageClone;
|
||||
}
|
||||
|
||||
private async renderMask(config: {
|
||||
layerOfInterest: Konva.Layer; // layer where the element that you want to clip lives.
|
||||
// XY and height of a captureCanvas ( region of interest )
|
||||
x?: number; // x position of the region of interest
|
||||
y?: number; // y position of the region of interest
|
||||
width?: number; // size of the region of interest
|
||||
height?: number; // size of the region of interest
|
||||
pixelRatio?: number; // higher means higher quality
|
||||
mimeType?: string; // image/jpeg or image/png
|
||||
quality?: number; // 1.0 is the best.
|
||||
test: boolean; // true == blob else Image Bitmap
|
||||
}): Promise<ImageBitmap> {
|
||||
try {
|
||||
const box = config.layerOfInterest.getClientRect();
|
||||
const stage = config.layerOfInterest.getStage();
|
||||
|
||||
const x = config.x !== undefined ? config.x : Math.floor(box.x);
|
||||
const y = config.y !== undefined ? config.y : Math.floor(box.y);
|
||||
const pixelRatio = config.pixelRatio || 1;
|
||||
|
||||
// Clone the required layer from the stage
|
||||
// Set the right details (like removing highlight stroke)
|
||||
// Then render the cloned stage to a bitmap
|
||||
const stageClone = this.cloneStageForMask(stage, config.layerOfInterest);
|
||||
const maskBlob = (await stageClone.toBlob({
|
||||
x: x,
|
||||
y: y,
|
||||
width: config.width || Math.ceil(box.width),
|
||||
height: config.height || Math.ceil(box.height),
|
||||
pixelRatio: pixelRatio,
|
||||
})) as Blob;
|
||||
|
||||
// if config.test is true, the result is downloaded to the local files
|
||||
// config.test = true;
|
||||
if (config.test) {
|
||||
await FileUtilities.blobToFileJpeg(maskBlob, "1");
|
||||
}
|
||||
|
||||
const result = await createImageBitmap(maskBlob);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async renderBackground(config: {
|
||||
layerOfInterest: Konva.Layer; // layer where the element that you want to clip lives.
|
||||
// XY and height of a captureCanvas ( region of interest )
|
||||
x?: number; // x position of the region of interest
|
||||
y?: number; // y position of the region of interest
|
||||
width?: number; // size of the region of interest
|
||||
height?: number; // size of the region of interest
|
||||
pixelRatio?: number; // higher means higher quality
|
||||
mimeType?: string; // image/jpeg or image/png
|
||||
quality?: number; // 1.0 is the best.
|
||||
test: boolean; // true == blob else Image Bitmap
|
||||
}): Promise<ImageBitmap> {
|
||||
try {
|
||||
const box = config.layerOfInterest.getClientRect();
|
||||
const stage = config.layerOfInterest.getStage();
|
||||
|
||||
const x = config.x !== undefined ? config.x : Math.floor(box.x);
|
||||
const y = config.y !== undefined ? config.y : Math.floor(box.y);
|
||||
const pixelRatio = config.pixelRatio || 1;
|
||||
|
||||
// Clone the required layer from the stage
|
||||
// Set the right details (like removing highlight stroke)
|
||||
// Then render the cloned stage to a bitmap
|
||||
const stageClone = this.cloneStageForBackground(
|
||||
stage,
|
||||
config.layerOfInterest,
|
||||
);
|
||||
const backgroundBlob = (await stageClone.toBlob({
|
||||
x: x,
|
||||
y: y,
|
||||
width: config.width || Math.ceil(box.width),
|
||||
height: config.height || Math.ceil(box.height),
|
||||
pixelRatio: pixelRatio,
|
||||
})) as Blob;
|
||||
|
||||
// if config.test is true, the result is downloaded to the local files
|
||||
if (config.test) {
|
||||
await FileUtilities.blobToFileJpeg(backgroundBlob, "1");
|
||||
}
|
||||
|
||||
const result = await createImageBitmap(backgroundBlob);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async renderInpainting() {
|
||||
if (this.isProcessing) {
|
||||
console.log("isProcessing Returning");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Rendering inpainting");
|
||||
this.isProcessing = true;
|
||||
|
||||
this.mediaLayerRef.draw();
|
||||
|
||||
const frameConfig = {
|
||||
layerOfInterest: this.mediaLayerRef,
|
||||
x: this.captureCanvas.x(),
|
||||
y: this.captureCanvas.y(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
mimeType: "image/jpeg",
|
||||
pixelRatio: 1,
|
||||
quality: 1.0,
|
||||
test: true,
|
||||
};
|
||||
|
||||
const maskBitmap = await this.renderMask(frameConfig);
|
||||
const backgroundBitmap = await this.renderBackground(frameConfig);
|
||||
|
||||
try {
|
||||
const base64Mask = await imageBitmapToBase64(maskBitmap);
|
||||
const base64Bg = await imageBitmapToBase64(backgroundBitmap);
|
||||
|
||||
const base64BitmapResponse = await invoke("inpaint_image", {
|
||||
image: base64Bg,
|
||||
mask: base64Mask,
|
||||
prompt: this.currentPrompt,
|
||||
});
|
||||
|
||||
console.log("inpainting response", base64BitmapResponse);
|
||||
|
||||
const decoded = await DecodeBase64ToImage(base64BitmapResponse as string);
|
||||
|
||||
this.outputBitmap = decoded;
|
||||
} catch (error) {
|
||||
console.error("Error during image processing:", error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: DO NOT REMOVE, we need this for the interface that commands use
|
||||
// It doesn't actually need to do anything
|
||||
public render() {}
|
||||
|
||||
// Add getter/setter for brush size
|
||||
public set paintBrushSize(size: number) {
|
||||
this.brushSize = size * 7.5;
|
||||
const stage = this.mediaLayerRef.getStage();
|
||||
if (stage && this.isEnabled) {
|
||||
this.updateCursor(stage);
|
||||
}
|
||||
}
|
||||
|
||||
public get paintBrushSize(): number {
|
||||
return this.brushSize;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,6 @@ export enum AppModes {
|
||||
RENDERING = "RENDERING",
|
||||
}
|
||||
|
||||
export enum EditModes {
|
||||
SELECT = "SELECT",
|
||||
EDIT = "EDIT",
|
||||
}
|
||||
|
||||
export const VideoResolutions = {
|
||||
VERTICAL_720: { width: 720, height: 1280 },
|
||||
LANDSCAPE_720: { width: 1280, height: 720 },
|
||||
|
||||
@@ -11,21 +11,23 @@ import { BaseDialog } from "~/components/ui/BaseDialog";
|
||||
export const DialogAddImage = ({
|
||||
stagedImage = null,
|
||||
isOpen,
|
||||
closeCallback,
|
||||
onAddImage,
|
||||
closeCallback = () => { },
|
||||
cancellable = true,
|
||||
}: {
|
||||
stagedImage?: File | null;
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
closeCallback?: () => void;
|
||||
onAddImage: (file: File) => void;
|
||||
cancellable?: boolean;
|
||||
}) => {
|
||||
const [assetFile, setAssetFile] = useState<File | null>(null);
|
||||
const previouslyStagedImageRef = useRef<File | null>(null);
|
||||
|
||||
const currFile =
|
||||
stagedImage &&
|
||||
stagedImage !== assetFile &&
|
||||
stagedImage !== previouslyStagedImageRef.current
|
||||
stagedImage !== assetFile &&
|
||||
stagedImage !== previouslyStagedImageRef.current
|
||||
? stagedImage
|
||||
: assetFile;
|
||||
if (previouslyStagedImageRef.current !== stagedImage) {
|
||||
@@ -65,11 +67,13 @@ export const DialogAddImage = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{cancellable && (
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,14 @@ import React, { useRef, useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: IconDefinition;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
interface TabSelectorProps {
|
||||
@@ -84,6 +88,9 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
{tab.icon && (
|
||||
<FontAwesomeIcon icon={tab.icon} className={tab.iconClassName ?? "mr-1.5"} />
|
||||
)}
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { effect, signal } from "@preact/signals-react";
|
||||
import { clamp, normalize } from "~/utilities";
|
||||
|
||||
export enum EditMode {
|
||||
INIT = "INIT",
|
||||
SELECT = "SELECT",
|
||||
EDIT = "EDIT",
|
||||
}
|
||||
|
||||
export const EDIT_MIN_BRUSH_SIZE = 10;
|
||||
export const EDIT_MAX_BRUSH_SIZE = 100;
|
||||
export const EDIT_INIT_BRUSH_SIZE = 50;
|
||||
export const editModePrompt = signal("");
|
||||
export const editModeBrushSize = signal(EDIT_INIT_BRUSH_SIZE);
|
||||
export const editModeState = signal<EditMode>(EditMode.INIT);
|
||||
export const editModeClearSignal = signal(false); // Just a signal to trigger a clear, value doesn't matter
|
||||
|
||||
export const onEditModeChange = (callback: (mode: EditMode) => void) => {
|
||||
effect(() => {
|
||||
callback(editModeState.value);
|
||||
});
|
||||
};
|
||||
|
||||
export const editModeBaseImage = signal<File | null>(null);
|
||||
|
||||
export const onEditModeBaseImageChange = (callback: (file: File) => void) => {
|
||||
effect(() => {
|
||||
if (editModeBaseImage.value) {
|
||||
callback(editModeBaseImage.value)
|
||||
editModeState.value = EditMode.SELECT;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const setEditModeBaseImage = (imageFile: File) => {
|
||||
editModeBaseImage.value = imageFile;
|
||||
}
|
||||
|
||||
export const setEditModeBrushSize = (size: number) => {
|
||||
editModeBrushSize.value = clamp(size, EDIT_MIN_BRUSH_SIZE, EDIT_MAX_BRUSH_SIZE);
|
||||
}
|
||||
|
||||
export const onEditModeBrushSizeChange = (callback: (size: number) => void) => {
|
||||
effect(() => {
|
||||
callback(normalize(editModeBrushSize.value, 0, EDIT_MAX_BRUSH_SIZE));
|
||||
});
|
||||
}
|
||||
|
||||
export const triggerEditModeClear = () => {
|
||||
editModeClearSignal.value = !editModeClearSignal.peek();
|
||||
}
|
||||
|
||||
export const onEditModeInpaintClear = (callback: (_: boolean) => void) => {
|
||||
effect(() => {
|
||||
callback(editModeClearSignal.value);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./EditModeSignals"
|
||||
Reference in New Issue
Block a user