mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feda9e457f | ||
|
|
5a003c42c8 | ||
|
|
0947fd5930 | ||
|
|
e99cacb040 | ||
|
|
a50c1a8c3d | ||
|
|
6aaafdc742 | ||
|
|
529c7f1a72 | ||
|
|
acdeb6b4cb | ||
|
|
04e47e749b | ||
|
|
6ddcc007b5 | ||
|
|
887b96816b | ||
|
|
e717b0f676 | ||
|
|
742e04bfa8 | ||
|
|
828ac3ed26 | ||
|
|
9939fc5193 | ||
|
|
0bb3500ccb | ||
|
|
267fb64fe2 | ||
|
|
2eb91528e7 | ||
|
|
57169c470a | ||
|
|
6341f8120e | ||
|
|
44c0dfd920 | ||
|
|
d742c61390 |
@@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
faGear,
|
||||
faImages,
|
||||
@@ -23,21 +23,18 @@ import {
|
||||
MenuIconSelector,
|
||||
MenuIconItem,
|
||||
} from "@storyteller/ui-menu-icon-selector";
|
||||
import { Signal, signal } from "@preact/signals-react";
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { setLogoutStates } from "~/signals/authentication/utilities";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useTabStore } from "~/pages/Stores/TabState";
|
||||
import { set3DPageMounted } from "~/pages/PageEnigma/Editor/editor";
|
||||
interface Props {
|
||||
pageName: string;
|
||||
appTabIdSignal: Signal<string>;
|
||||
setAppTabId: (id: string) => void;
|
||||
is3DInitSignal: Signal<boolean>;
|
||||
}
|
||||
|
||||
// This is the tab we start with.
|
||||
export const STARTING_APP_TAB_ID = "2D";
|
||||
|
||||
|
||||
const appMenuTabs: MenuIconItem[] = [
|
||||
{
|
||||
@@ -66,64 +63,18 @@ export const topNavMediaId = signal<string>("");
|
||||
export const topNavMediaUrl = signal<string>("");
|
||||
|
||||
export const TopBar = ({
|
||||
pageName,
|
||||
appTabIdSignal,
|
||||
setAppTabId,
|
||||
pageName
|
||||
}: Props) => {
|
||||
useSignals();
|
||||
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
|
||||
const engine3D = useContext(EngineContext);
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setAppTabId(tabId);
|
||||
};
|
||||
|
||||
const currentAppTabId = appTabIdSignal.value;
|
||||
|
||||
const handleAddToScene = async (
|
||||
url: string,
|
||||
media_id: string | undefined,
|
||||
) => {
|
||||
console.log("Items to add to scene", currentAppTabId);
|
||||
console.log("url", url);
|
||||
console.log("media_id", media_id);
|
||||
|
||||
setUrl(url);
|
||||
setMediaId(media_id ?? "");
|
||||
|
||||
if (currentAppTabId === "2D") {
|
||||
console.log("Adding to 2D scene");
|
||||
// from the uploaded image url.
|
||||
} else if (currentAppTabId === "3D") {
|
||||
console.log("Adding to 3D scene");
|
||||
// media id from the image selected from gallery.
|
||||
if (media_id) {
|
||||
engine3D?.activeScene.loadObject(media_id, "image", true);
|
||||
} else {
|
||||
console.warn("No media id provided");
|
||||
}
|
||||
} else if (currentAppTabId === "VIDEO") {
|
||||
console.log("Adding to Video scene");
|
||||
topNavMediaId.value = media_id ?? "";
|
||||
topNavMediaUrl.value = url;
|
||||
} else if (currentAppTabId === "IMAGE") {
|
||||
console.log("Adding to Image scene");
|
||||
topNavMediaId.value = media_id ?? "";
|
||||
topNavMediaUrl.value = url;
|
||||
} else {
|
||||
console.warn(`Unknown tab type: ${currentAppTabId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenGalleryModal = () => {
|
||||
galleryModalVisibleViewMode.value = true;
|
||||
galleryModalVisibleDuringDrag.value = true;
|
||||
};
|
||||
|
||||
const tabStore = useTabStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -143,21 +94,31 @@ export const TopBar = ({
|
||||
</a>
|
||||
<MenuIconSelector
|
||||
menuItems={appMenuTabs}
|
||||
activeMenu={appTabIdSignal.value}
|
||||
activeMenu={tabStore.activeTabId}
|
||||
disabled={false}
|
||||
onMenuChange={handleTabChange}
|
||||
onMenuChange={(tabId) => {
|
||||
// Disable 3d engine to prevent memory leak.
|
||||
if (tabId === "3D") {
|
||||
set3DPageMounted(true);
|
||||
} else {
|
||||
set3DPageMounted(false);
|
||||
}
|
||||
useTabStore
|
||||
.getState()
|
||||
.setActiveTab(tabId);
|
||||
}}
|
||||
className="w-fit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 font-medium">
|
||||
{currentAppTabId === "3D" ? (
|
||||
{tabStore.activeTabId === "3D" ? (
|
||||
<SceneTitleInput pageName={pageName} />
|
||||
) : (
|
||||
<h1>
|
||||
{currentAppTabId === "2D"
|
||||
{tabStore.activeTabId === "2D"
|
||||
? "Canvas"
|
||||
: currentAppTabId === "VIDEO"
|
||||
: tabStore.activeTabId === "VIDEO"
|
||||
? "Generate Video"
|
||||
: "Generate Image"}
|
||||
</h1>
|
||||
@@ -200,7 +161,6 @@ export const TopBar = ({
|
||||
<GalleryModal
|
||||
mode="view"
|
||||
onDownloadClicked={downloadFileFromUrl}
|
||||
onAddToSceneClicked={handleAddToScene}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { MouseEventHandler, useState, useEffect } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { uiAccess } from "./signals/uiAccess";
|
||||
import { dispatchUiEvents } from "./signals/uiEvents";
|
||||
|
||||
import { ToolbarNode } from "./components/features/ToolbarNode";
|
||||
import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums";
|
||||
import { transitionTimingStyles } from "./components/styles";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const ContextualToolbarNode = () => {
|
||||
const [winSize, setWinSize] = useState<{ width: number; height: number }>({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
const { isShowing, position, ...rest } = uiAccess.toolbarNode.signal.value;
|
||||
const buttonsProps = Object.values(ToolbarNodeButtonNames).reduce(
|
||||
(acc, buttonName) => {
|
||||
acc[buttonName] = {
|
||||
onClick: dispatchUiEvents.toolbarNode[buttonName],
|
||||
disabled:
|
||||
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].disabled,
|
||||
hidden:
|
||||
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].hidden,
|
||||
active:
|
||||
uiAccess.toolbarNode.signal.value.buttonStates[buttonName].active,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as {
|
||||
[key in ToolbarNodeButtonNames]: {
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
disabled: boolean;
|
||||
hidden: boolean;
|
||||
active: boolean;
|
||||
};
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
function resizeHandler() {
|
||||
setWinSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
// Don't do anything if no node is known to be bound
|
||||
if (uiAccess.toolbarNode.signal.value.knodeIds.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the UI component
|
||||
uiAccess.toolbarNode.setColor(color);
|
||||
|
||||
// Signal the engine with the node ID (just grab the first -and expected only- node)
|
||||
const nodeID = uiAccess.toolbarNode.signal.value.knodeIds[0];
|
||||
dispatchUiEvents.toolbarNode.setSelectedColor({ kNodeId: nodeID, color });
|
||||
}
|
||||
|
||||
useSignals();
|
||||
const color = uiAccess.toolbarNode.signal.value.color;
|
||||
|
||||
const boundedTopPosition = Math.min(position.y, winSize.height - 164);
|
||||
return (
|
||||
<Transition
|
||||
as="div"
|
||||
show={isShowing}
|
||||
className={twMerge(
|
||||
transitionTimingStyles,
|
||||
"fixed -translate-x-1/2 translate-y-5 data-[closed]:opacity-0",
|
||||
)}
|
||||
style={{
|
||||
top: boundedTopPosition,
|
||||
left: position.x,
|
||||
}}
|
||||
>
|
||||
<ToolbarNode
|
||||
{...rest}
|
||||
buttonsProps={buttonsProps}
|
||||
locked={uiAccess.toolbarNode.signal.value.locked}
|
||||
onLockClicked={(e) => {
|
||||
dispatchUiEvents.toolbarNode.lock(e);
|
||||
}}
|
||||
color={color}
|
||||
onColorChange={handleColorChange}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,767 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { uiAccess, uiEvents } from "./signals";
|
||||
import {
|
||||
ImageNode,
|
||||
ShapeNode,
|
||||
ShapeType,
|
||||
TextNode,
|
||||
PaintNode,
|
||||
PreviewCopyNode,
|
||||
} from "./KonvaApp/Nodes";
|
||||
import { UndoStackManager } from "./KonvaApp/UndoRedo";
|
||||
import {
|
||||
CommandManager,
|
||||
MatteBox,
|
||||
SceneManager,
|
||||
} from "./KonvaApp/EngineUtitlities";
|
||||
import {
|
||||
NodeIsolator,
|
||||
NodesManager,
|
||||
NodesTranslationEventDetails,
|
||||
NodeTransformationEventDetails,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
SelectionManagerEvents,
|
||||
SelectorSquare,
|
||||
} from "./KonvaApp/NodesManagers";
|
||||
import { EngineOptions, TextNodeData, VideoNodeData } from "./KonvaApp/types";
|
||||
|
||||
import { AppModes, VideoResolutions } from "./KonvaApp/constants";
|
||||
import { ToolbarMainButtonNames } from "./components/features/ToolbarMain/enum";
|
||||
|
||||
import { ToolbarNodeButtonNames } from "./components/features/ToolbarNode/enums";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
|
||||
import { RealTimeDrawEngine } from "./KonvaApp/RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { NodeColor } from "./signals/uiEvents/toolbarNode";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { DecodeBase64ToImage } from "./utilities/DecodeBase64ToImage";
|
||||
import { EncodeImageBitmapToBase64 } from "./utilities/EncodeImageBitmapToBase64";
|
||||
import {
|
||||
FalBackgroundRemoval,
|
||||
FalBackgroundRemovalRequest,
|
||||
} from "@storyteller/tauri-api";
|
||||
|
||||
export interface RenderingOptions {
|
||||
artstyle: string;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
styleStrength: number;
|
||||
}
|
||||
|
||||
export class Engine {
|
||||
private navigateRef: NavigateFunction;
|
||||
private appMode: AppModes = AppModes.INIT;
|
||||
private boardCanvasRef: HTMLDivElement;
|
||||
private stage: Konva.Stage;
|
||||
private bgLayer: Konva.Layer;
|
||||
private mediaLayer: Konva.Layer;
|
||||
private nodeIsolationLayer: Konva.Layer;
|
||||
private uiLayer: Konva.Layer;
|
||||
private previewLayer: Konva.Layer;
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
public realTimeDrawEngine: RealTimeDrawEngine;
|
||||
private nodesManager: NodesManager;
|
||||
private nodeIsolator: NodeIsolator;
|
||||
private nodeTransformer: NodeTransformer;
|
||||
private selectionManager: SelectionManager;
|
||||
private selectorSquare: SelectorSquare;
|
||||
private loadingVideosProvider: LoadingVideosProvider;
|
||||
private matteBox: MatteBox;
|
||||
private backgroundNode: ShapeNode | null = null;
|
||||
|
||||
private sceneManager: SceneManager;
|
||||
public undoStackManager: UndoStackManager;
|
||||
public commandManager: CommandManager;
|
||||
|
||||
public segmentationButtonCanBePressed: boolean = true;
|
||||
// signal reference
|
||||
constructor(boardCanvasRef: HTMLDivElement, options: EngineOptions) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Engine Created");
|
||||
}
|
||||
this.navigateRef = options.navigate;
|
||||
this.appMode = AppModes.SELECT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
this.stage = new Konva.Stage({
|
||||
container: this.boardCanvasRef,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
this.bgLayer = new Konva.Layer();
|
||||
|
||||
this.mediaLayer = new Konva.Layer();
|
||||
this.previewLayer = new Konva.Layer();
|
||||
|
||||
this.nodeIsolationLayer = new Konva.Layer();
|
||||
this.uiLayer = new Konva.Layer();
|
||||
|
||||
this.stage.add(this.bgLayer);
|
||||
this.stage.add(this.previewLayer);
|
||||
this.stage.add(this.mediaLayer);
|
||||
this.stage.add(this.nodeIsolationLayer);
|
||||
this.stage.add(this.uiLayer);
|
||||
|
||||
// Konva Transformer
|
||||
this.nodeTransformer = new NodeTransformer();
|
||||
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
|
||||
// Loading Placeholders
|
||||
|
||||
// Node Isolator
|
||||
this.nodeIsolator = new NodeIsolator({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodeIsolationLayerRef: this.nodeIsolationLayer,
|
||||
});
|
||||
|
||||
// core layer for all the work done.
|
||||
this.offScreenCanvas = new OffscreenCanvas(0, 0);
|
||||
|
||||
// Collection of all Nodes
|
||||
this.nodesManager = new NodesManager();
|
||||
// Partial Collection of selected Nodes
|
||||
this.selectionManager = new SelectionManager({
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
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);
|
||||
},
|
||||
onPreviewCopy: async (image) => {
|
||||
await this.addPreviewCopy(image);
|
||||
},
|
||||
});
|
||||
|
||||
// Selector Square to select Nodes
|
||||
this.selectorSquare = new SelectorSquare({
|
||||
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
stageRef: this.stage,
|
||||
});
|
||||
this.uiLayer.add(this.selectorSquare.getKonvaNode());
|
||||
|
||||
//Collection of commands for undo-redo
|
||||
this.undoStackManager = new UndoStackManager(() => {
|
||||
//this.realTimeDrawEngine.render();
|
||||
});
|
||||
this.commandManager = new CommandManager({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
undoStackManagerRef: this.undoStackManager,
|
||||
});
|
||||
|
||||
// set up secene manager
|
||||
this.sceneManager = new SceneManager({
|
||||
navigateRef: this.navigateRef,
|
||||
loadingVideosProviderRef: this.loadingVideosProvider,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
});
|
||||
|
||||
this.matteBox = new MatteBox({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.clientWidth,
|
||||
height: this.boardCanvasRef.clientHeight,
|
||||
},
|
||||
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
|
||||
uiLayerRef: this.uiLayer,
|
||||
});
|
||||
|
||||
// some of the managers has events
|
||||
// hence, lastly, setup these events
|
||||
this.setupEventSystem();
|
||||
this.setAppMode(this.appMode);
|
||||
}
|
||||
|
||||
private setAppMode(newAppMode: AppModes) {
|
||||
this.appMode = newAppMode;
|
||||
switch (this.appMode) {
|
||||
case AppModes.PAINT: {
|
||||
console.log("APPMODE: PAINT");
|
||||
this.selectorSquare.disable(); // this breaks the paint if you use it in the wrong spot
|
||||
this.selectionManager.disable(); // this prevents selection.
|
||||
this.realTimeDrawEngine.enablePaintMode();
|
||||
|
||||
uiAccess.toolbarMain.enable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.PAINT, {
|
||||
active: true,
|
||||
});
|
||||
this.matteBox.disable();
|
||||
return;
|
||||
}
|
||||
case AppModes.SELECT: {
|
||||
console.log("APPMODE: SELECT");
|
||||
this.realTimeDrawEngine.disablePaintMode();
|
||||
|
||||
this.selectorSquare.enable();
|
||||
this.selectionManager.enable();
|
||||
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 AppModes.PREVIEW: {
|
||||
console.log("APPMODE: RENDER");
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
uiAccess.toolbarMain.disable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.enable();
|
||||
document.body.style.cursor = "not-allowed";
|
||||
return;
|
||||
}
|
||||
case AppModes.RENDERING: {
|
||||
console.log("APPMODE: RENDER");
|
||||
this.selectorSquare.disable();
|
||||
this.selectionManager.disable();
|
||||
uiAccess.toolbarMain.disable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
this.matteBox.enable(true);
|
||||
document.body.style.cursor = "wait";
|
||||
return;
|
||||
}
|
||||
case AppModes.INIT:
|
||||
default: {
|
||||
console.log("APPMODE: INIT");
|
||||
this.selectorSquare.disable();
|
||||
uiAccess.toolbarMain.disable();
|
||||
this.selectionManager.disable();
|
||||
uiAccess.toolbarMain.changeButtonState(ToolbarMainButtonNames.SELECT, {
|
||||
active: false,
|
||||
});
|
||||
document.body.style.cursor = "wait";
|
||||
this.matteBox.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
private setupEventSystem() {
|
||||
// Listen to changes in container size
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.onBoardCanvasResize();
|
||||
});
|
||||
resizeObserver.observe(this.boardCanvasRef);
|
||||
this.onBoardCanvasResize();
|
||||
|
||||
// Listen to Nodes
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSLATIONS,
|
||||
((event: CustomEvent<NodesTranslationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.translateNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSFORMATION,
|
||||
((event: CustomEvent<NodeTransformationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.transformNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
|
||||
// Listen to Tooolbar Node
|
||||
uiEvents.toolbarNode.lock.onClick(() => {
|
||||
this.commandManager.toggleLockNodes();
|
||||
});
|
||||
uiEvents.toolbarNode.DOWNLOAD.onClick(() => {
|
||||
const nodes = this.selectionManager.getSelectedNodes();
|
||||
if (nodes.size > 1) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Error: Download Node Content",
|
||||
message:
|
||||
"Please do not select more than 1 item for the Download Node Content feature, you can only download 1 item at a time",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const node = nodes.values().next().value;
|
||||
try {
|
||||
if (node instanceof VideoNode && node.currentUrl) {
|
||||
downloadURI(node.currentUrl, `Download Video Node-${node.kNode.id}`);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Error: Download Node Content",
|
||||
message: "This item does not have content for download.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
uiEvents.toolbarNode.DELETE.onClick(() =>
|
||||
this.commandManager.deleteNodes(),
|
||||
);
|
||||
uiEvents.toolbarNode.MOVE_LAYER_DOWN.onClick(() =>
|
||||
this.commandManager.moveNodesDown(),
|
||||
);
|
||||
uiEvents.toolbarNode.MOVE_LAYER_UP.onClick(() =>
|
||||
this.commandManager.moveNodesUp(),
|
||||
);
|
||||
uiEvents.toolbarNode.REMOVE_BACKGROUND.onClick(() =>
|
||||
this.removeBackground(),
|
||||
);
|
||||
|
||||
// Listen to Toolbar Main
|
||||
uiEvents.toolbarMain.UNDO.onClick(() => {
|
||||
this.undoStackManager.undo();
|
||||
});
|
||||
uiEvents.toolbarMain.REDO.onClick(() => {
|
||||
this.undoStackManager.redo();
|
||||
//this.realTimeDrawEngine.render();
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
|
||||
//await this.realTimeDrawEngine.saveOutput(); // This works.
|
||||
await invoke("open_login_command");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.GENERATE.onClick(async (/*event*/) => {
|
||||
console.log("generate clicked");
|
||||
await this.realTimeDrawEngine.generateImage();
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.SELECT.onClick(() => {
|
||||
console.log("Toolbar Main >> Select");
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
console.log("Toolbar Main >> Paint");
|
||||
this.setAppMode(AppModes.PAINT);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.ERASER.onClick(() => {
|
||||
console.log("Toolbar Main >> Eraser");
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.PREVIEW.onClick(async () => {
|
||||
console.log("Toolbar Main >> Preview");
|
||||
this.setAppMode(AppModes.PREVIEW);
|
||||
await this.handlePreview();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
});
|
||||
uiEvents.toolbarMain.DOWNLOAD.onClick(async () => {
|
||||
console.log("Toolbar Main >> Render Download");
|
||||
try {
|
||||
this.setAppMode(AppModes.RENDERING);
|
||||
} catch (error) {
|
||||
// throw error to retry
|
||||
uiAccess.dialogError.show({
|
||||
title: "Generation Error",
|
||||
message: error?.toString() || "Unknown Error",
|
||||
});
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
}
|
||||
});
|
||||
// Listen to other toolbars
|
||||
// VideoExtraction Toolbar
|
||||
|
||||
// Listen to other requests coming from the UI
|
||||
uiEvents.onGetStagedImage((image) => {
|
||||
this.addImage(image);
|
||||
});
|
||||
|
||||
uiEvents.onAddTextToEngine((textdata) => {
|
||||
this.addText(textdata);
|
||||
});
|
||||
|
||||
let renderTimeout: NodeJS.Timeout;
|
||||
|
||||
uiEvents.promptEvents.onPromptStrengthChanged(async (strength) => {
|
||||
this.realTimeDrawEngine.currentStrength = strength / 100.0;
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
uiEvents.promptEvents.onPromptTextChanged(async (prompt) => {
|
||||
this.realTimeDrawEngine.currentPrompt = prompt;
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
uiEvents.onAddShapeToEngine((shapeData) => {
|
||||
switch (shapeData.shape) {
|
||||
case "circle":
|
||||
this.addShape(ShapeType.CIRCLE, 100);
|
||||
break;
|
||||
case "square":
|
||||
this.addShape(ShapeType.SQUARE, 100);
|
||||
break;
|
||||
case "triangle":
|
||||
this.addShape(ShapeType.TRIANGLE, 100);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.onBgColorChanged((data) => {
|
||||
this.realTimeDrawEngine.updateBackground(data);
|
||||
});
|
||||
|
||||
uiEvents.toolbarNode.color.onConfirmChanged((nodeColor) => {
|
||||
console.log("Color change event triggered with:", nodeColor);
|
||||
if (!nodeColor) {
|
||||
console.warn("nodeColor is undefined or null");
|
||||
return;
|
||||
}
|
||||
if (!nodeColor.kNodeId) {
|
||||
console.warn("nodeColor.kNodeId is undefined or null");
|
||||
return;
|
||||
}
|
||||
this.changeNodeColor(nodeColor);
|
||||
});
|
||||
uiEvents.toolbarMain.onPaintColorChanged((color) => {
|
||||
this.realTimeDrawEngine.paintColor = color;
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.onEraseBrushSizeChanged((size: number) => {
|
||||
this.realTimeDrawEngine.paintBrushSize = size;
|
||||
});
|
||||
}
|
||||
|
||||
removeBackground() {
|
||||
console.log("Remove background");
|
||||
|
||||
const selectedNodeSet = this.selectionManager.getSelectedNodes();
|
||||
|
||||
if (selectedNodeSet.size !== 1) {
|
||||
return; // Can only remove background from a single image at a time.
|
||||
}
|
||||
|
||||
const [selectedNode] = selectedNodeSet;
|
||||
|
||||
const removeBgInvocation = async (
|
||||
selectedNode: ImageNode,
|
||||
callback: (
|
||||
existingImageNode: ImageNode,
|
||||
createdImage: ImageBitmap,
|
||||
) => void,
|
||||
) => {
|
||||
const image: HTMLImageElement = selectedNode.kNode.image();
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(image);
|
||||
|
||||
const falRemoveBgRequest: FalBackgroundRemovalRequest = {
|
||||
base64_image: base64Bitmap,
|
||||
};
|
||||
|
||||
const response = await FalBackgroundRemoval(falRemoveBgRequest);
|
||||
|
||||
if (response.status !== "success") {
|
||||
console.error("Failed to remove background", response);
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Remove background success", response);
|
||||
|
||||
if ("payload" in response) {
|
||||
const base64BitmapResponse = response.payload?.base64_bytes;
|
||||
|
||||
const createdImage = await DecodeBase64ToImage(
|
||||
base64BitmapResponse as string,
|
||||
);
|
||||
|
||||
callback(selectedNode, createdImage);
|
||||
}
|
||||
};
|
||||
|
||||
const _promise = removeBgInvocation(
|
||||
selectedNode,
|
||||
(selectedNode: ImageNode, createdImage: ImageBitmap) => {
|
||||
if (!this.nodesManager.hasNode(selectedNode)) {
|
||||
return;
|
||||
}
|
||||
this.commandManager.deleteSpecificNodes([selectedNode]);
|
||||
this.addImageFromImageBitmap(createdImage);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
// Print window dimensions for debugging
|
||||
console.log("Window dimensions:", {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
outerWidth: window.outerWidth,
|
||||
outerHeight: window.outerHeight,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(
|
||||
window.innerWidth,
|
||||
window.innerHeight,
|
||||
);
|
||||
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
height: this.boardCanvasRef.offsetHeight,
|
||||
},
|
||||
});
|
||||
this.uiLayer.draw();
|
||||
this.stage.width(this.boardCanvasRef.offsetWidth);
|
||||
this.stage.height(this.boardCanvasRef.offsetHeight);
|
||||
this.stage.draw(); // Redraw the canvas
|
||||
}
|
||||
|
||||
// Sandbox is quickly a way to test your idea.
|
||||
public async sandbox() {}
|
||||
|
||||
public onMessage(event: MessageEvent) {
|
||||
console.log("Message From Shared Worker");
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
public initializeStage(sceneToken?: string) {
|
||||
// load canvas that was originaly saved TODO Save manager for resharing.
|
||||
uiAccess.toolbarNode.hide();
|
||||
uiAccess.loadingBar.hide();
|
||||
// load the scene if there's a scenetoken
|
||||
if (sceneToken) {
|
||||
this.sceneManager.loadScene(sceneToken);
|
||||
}
|
||||
this.setupStage();
|
||||
// this.populateWithDebugItems();
|
||||
}
|
||||
public isInitialized() {
|
||||
return this.stage !== null;
|
||||
}
|
||||
|
||||
public async setupStage() {
|
||||
// Frame rate inicator
|
||||
// const textNode = new Konva.Text({
|
||||
// x: 10,
|
||||
// y: 80,
|
||||
// text: "",
|
||||
// fontSize: 18,
|
||||
// fontFamily: "Source Sans 3",
|
||||
// fill: "white",
|
||||
// });
|
||||
// const anim = new Konva.Animation((frame) => {
|
||||
// if (frame && import.meta.env.DEV) {
|
||||
// const timeDiff = frame.timeDiff;
|
||||
// const frameRate = frame.frameRate;
|
||||
// textNode.setText(
|
||||
// `FrameTime: ${timeDiff.toFixed(0)} ms\nFrameRate: ${frameRate.toFixed(0)} fps`,
|
||||
// );
|
||||
// }
|
||||
// }, this.mediaLayer);
|
||||
|
||||
// anim.start();
|
||||
//this.uiLayer.add(textNode);
|
||||
this.addKeyboardShortcuts();
|
||||
}
|
||||
|
||||
private changeNodeColor(nodeColor: NodeColor) {
|
||||
console.log("Changing node color:", nodeColor);
|
||||
// Find the node that is selected with the id
|
||||
const idSelector = "#" + nodeColor.kNodeId;
|
||||
const selectedNode = this.mediaLayer.findOne(idSelector);
|
||||
console.log("Selected node:", selectedNode);
|
||||
console.log("ID selector:", idSelector);
|
||||
console.log("Media layer:", this.mediaLayer);
|
||||
// TODO: Change the color of the node
|
||||
// Turns out shapes are image nodes... how do we change the image's fill??
|
||||
if (selectedNode) {
|
||||
// Get the shape node from the real time drawing engine's image nodes
|
||||
const shapeNode = this.realTimeDrawEngine.findImageNodeById(
|
||||
nodeColor.kNodeId,
|
||||
);
|
||||
if (shapeNode && shapeNode instanceof ShapeNode) {
|
||||
// Create new shape node with updated color
|
||||
const newShapeNode = new ShapeNode({
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: shapeNode.shapeType,
|
||||
size: shapeNode.kNode.size(),
|
||||
color: nodeColor.color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
newShapeNode.kNode.position(shapeNode.kNode.position());
|
||||
newShapeNode.kNode.zIndex(shapeNode.kNode.zIndex());
|
||||
|
||||
// Remove old node and add new one
|
||||
shapeNode.kNode.destroy();
|
||||
this.commandManager.createNode(newShapeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addText(textNodeData: TextNodeData) {
|
||||
const textNode = new TextNode({
|
||||
textNodeData: textNodeData,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
});
|
||||
this.commandManager.createNode(textNode);
|
||||
}
|
||||
|
||||
public addShape(type: ShapeType, size: number, color?: string) {
|
||||
const shapeNode = new ShapeNode({
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: type,
|
||||
size: { width: size, height: size },
|
||||
color: color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(shapeNode);
|
||||
}
|
||||
public addPreviewCopy(image: Image.Konva) {
|
||||
// Start of Selection
|
||||
const copyNode = new PreviewCopyNode({
|
||||
image: image,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(copyNode);
|
||||
}
|
||||
public addImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
this.setAppMode(AppModes.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.setAppMode(AppModes.SELECT);
|
||||
},
|
||||
});
|
||||
|
||||
this.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addPaintNode(
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) {
|
||||
const node = new PaintNode({
|
||||
canvasElement: canvas,
|
||||
lineBounds: lineBounds,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
//await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(node);
|
||||
}
|
||||
|
||||
// Events for Undo and Redo
|
||||
private addKeyboardShortcuts() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.ctrlKey && event.key === "z") {
|
||||
this.undoStackManager.undo();
|
||||
} else if (
|
||||
(event.ctrlKey && event.key === "y") ||
|
||||
(event.ctrlKey && event.shiftKey && event.key === "Z")
|
||||
) {
|
||||
this.undoStackManager.redo();
|
||||
} else if (event.key === "Delete") {
|
||||
this.commandManager.deleteNodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move to utilities?
|
||||
function downloadURI(uri: string, name: string) {
|
||||
const link = document.createElement("a");
|
||||
link.download = name;
|
||||
link.target = "_blank";
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { uiAccess } from "../signals";
|
||||
import { CommandManager, MatteBox } from "./EngineUtitlities";
|
||||
import { ImageNode } from "./Nodes";
|
||||
import { PaintNode } from "./Nodes/PaintNode";
|
||||
import {
|
||||
NodesManager,
|
||||
NodesTranslationEventDetails,
|
||||
NodeTransformationEventDetails,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
SelectionManagerEvents,
|
||||
SelectorSquare,
|
||||
} from "./NodesManagers";
|
||||
import { UndoStackManager } from "./UndoRedo";
|
||||
|
||||
import { ToolbarMainButtonNames } from "../components/features/ToolbarMain/enum";
|
||||
import { EditModes, VideoResolutions } from "./constants";
|
||||
|
||||
import { ToolbarNodeButtonNames } from "../components/features/ToolbarNode/enums";
|
||||
|
||||
|
||||
export interface RenderingOptions {
|
||||
artstyle: string;
|
||||
positivePrompt: string;
|
||||
negativePrompt: string;
|
||||
cinematic: boolean;
|
||||
enginePreProcessing: boolean;
|
||||
faceDetail: boolean;
|
||||
lipSync: boolean;
|
||||
upscale: boolean;
|
||||
styleStrength: number;
|
||||
}
|
||||
|
||||
export class EditEngine {
|
||||
private editMode: EditModes = EditModes.SELECT;
|
||||
private boardCanvasRef: HTMLDivElement;
|
||||
private stage: Konva.Stage;
|
||||
|
||||
private mediaLayer: Konva.Layer;
|
||||
private nodeIsolationLayer: Konva.Layer;
|
||||
private uiLayer: Konva.Layer;
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private realTimeDrawEngine: RealTimeDrawEngine;
|
||||
private nodesManager: NodesManager;
|
||||
private nodeTransformer: NodeTransformer;
|
||||
private selectionManager: SelectionManager;
|
||||
private selectorSquare: SelectorSquare;
|
||||
private matteBox: MatteBox;
|
||||
|
||||
private undoStackManager: UndoStackManager;
|
||||
private commandManager: CommandManager;
|
||||
|
||||
public segmentationButtonCanBePressed: boolean = true;
|
||||
// signal reference
|
||||
constructor(boardCanvasRef: HTMLDivElement) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Engine Created");
|
||||
}
|
||||
this.editMode = EditModes.SELECT;
|
||||
|
||||
this.boardCanvasRef = boardCanvasRef;
|
||||
|
||||
this.stage = new Konva.Stage({
|
||||
container: this.boardCanvasRef,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
//this.bgLayer = new Konva.Layer();
|
||||
|
||||
this.mediaLayer = new Konva.Layer();
|
||||
|
||||
this.nodeIsolationLayer = new Konva.Layer();
|
||||
this.uiLayer = new Konva.Layer();
|
||||
|
||||
//this.stage.add(this.bgLayer);
|
||||
this.stage.add(this.mediaLayer);
|
||||
this.stage.add(this.nodeIsolationLayer);
|
||||
this.stage.add(this.uiLayer);
|
||||
|
||||
// Konva Transformer
|
||||
this.nodeTransformer = new NodeTransformer();
|
||||
this.uiLayer.add(this.nodeTransformer.getKonvaNode());
|
||||
|
||||
// core layer for all the work done.
|
||||
this.offScreenCanvas = new OffscreenCanvas(0, 0);
|
||||
|
||||
// Collection of all Nodes
|
||||
this.nodesManager = new NodesManager();
|
||||
// Partial Collection of selected Nodes
|
||||
this.selectionManager = new SelectionManager({
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
});
|
||||
|
||||
this.realTimeDrawEngine = new RealTimeDrawEngine({
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
// Selector Square to select Nodes
|
||||
this.selectorSquare = new SelectorSquare({
|
||||
captureCanvasRef: this.realTimeDrawEngine.captureCanvas,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
stageRef: this.stage,
|
||||
});
|
||||
this.uiLayer.add(this.selectorSquare.getKonvaNode());
|
||||
|
||||
//Collection of commands for undo-redo
|
||||
this.undoStackManager = new UndoStackManager(() => {
|
||||
this.realTimeDrawEngine.render();
|
||||
});
|
||||
this.commandManager = new CommandManager({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
nodesManagerRef: this.nodesManager,
|
||||
nodeTransformerRef: this.nodeTransformer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
renderEngineRef: this.realTimeDrawEngine,
|
||||
undoStackManagerRef: this.undoStackManager,
|
||||
});
|
||||
|
||||
this.matteBox = new MatteBox({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.clientWidth,
|
||||
height: this.boardCanvasRef.clientHeight,
|
||||
},
|
||||
captureCanvasSize: this.realTimeDrawEngine.captureCanvas.getSize(),
|
||||
uiLayerRef: this.uiLayer,
|
||||
});
|
||||
|
||||
// some of the managers has events
|
||||
// hence, lastly, setup these events
|
||||
this.setupEventSystem();
|
||||
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();
|
||||
|
||||
this.selectorSquare.enable();
|
||||
this.selectionManager.enable();
|
||||
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() {
|
||||
// Listen to changes in container size
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.onBoardCanvasResize();
|
||||
});
|
||||
resizeObserver.observe(this.boardCanvasRef);
|
||||
this.onBoardCanvasResize();
|
||||
|
||||
// Listen to Nodes
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSLATIONS,
|
||||
((event: CustomEvent<NodesTranslationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.translateNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
this.selectionManager.eventTarget.addEventListener(
|
||||
SelectionManagerEvents.NODES_TRANSFORMATION,
|
||||
((event: CustomEvent<NodeTransformationEventDetails>) => {
|
||||
//console.log("Event: SelectionManager -> Engine", event);
|
||||
this.commandManager.transformNodes(event.detail);
|
||||
}) as EventListener,
|
||||
);
|
||||
|
||||
// Listen to Toolbar Main
|
||||
// uiEvents.toolbarMain.UNDO.onClick(() => {
|
||||
// this.undoStackManager.undo();
|
||||
// });
|
||||
// uiEvents.toolbarMain.REDO.onClick(() => {
|
||||
// this.undoStackManager.redo();
|
||||
// this.realTimeDrawEngine.render();
|
||||
// });
|
||||
|
||||
// uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
|
||||
// await this.realTimeDrawEngine.saveOutput();
|
||||
// });
|
||||
|
||||
// uiEvents.toolbarMain.SELECT.onClick(() => {
|
||||
// console.log("Toolbar Main >> Select");
|
||||
// this.setAppMode(AppModes.SELECT);
|
||||
// });
|
||||
|
||||
// uiEvents.toolbarMain.PAINT.onClick(() => {
|
||||
// console.log("Toolbar Main >> Paint");
|
||||
// this.setAppMode(AppModes.PAINT);
|
||||
// });
|
||||
}
|
||||
|
||||
disableAllButtons() {
|
||||
const buttonNames = Object.values(ToolbarNodeButtonNames);
|
||||
for (const name of buttonNames) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private onBoardCanvasResize() {
|
||||
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
|
||||
console.log("onBoardCanvasResize Event called");
|
||||
console.log("Board Canvas values below");
|
||||
console.log(this.boardCanvasRef.offsetWidth);
|
||||
console.log(this.boardCanvasRef.offsetHeight);
|
||||
|
||||
this.matteBox.updateSize({
|
||||
boardCanvasSize: {
|
||||
width: this.boardCanvasRef.offsetWidth,
|
||||
height: this.boardCanvasRef.offsetHeight,
|
||||
},
|
||||
});
|
||||
|
||||
this.uiLayer.draw();
|
||||
this.stage.width(this.boardCanvasRef.offsetWidth);
|
||||
this.stage.height(this.boardCanvasRef.offsetHeight);
|
||||
this.stage.draw(); // Redraw the canvas
|
||||
}
|
||||
|
||||
// Sandbox is quickly a way to test your idea.
|
||||
public async sandbox() {}
|
||||
|
||||
public onMessage(event: MessageEvent) {
|
||||
console.log("Message From Shared Worker");
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
public initializeStage() {
|
||||
// load canvas that was originaly saved TODO Save manager for resharing.
|
||||
uiAccess.toolbarNode.hide();
|
||||
uiAccess.loadingBar.hide();
|
||||
this.setupStage();
|
||||
}
|
||||
|
||||
public isInitialized() {
|
||||
return this.stage !== null;
|
||||
}
|
||||
|
||||
public async setupStage() {
|
||||
// Frame rate inicator
|
||||
// const textNode = new Konva.Text({
|
||||
// x: 10,
|
||||
// y: 80,
|
||||
// text: "",
|
||||
// fontSize: 18,
|
||||
// fontFamily: "Source Sans 3",
|
||||
// fill: "white",
|
||||
// });
|
||||
// const anim = new Konva.Animation((frame) => {
|
||||
// if (frame && import.meta.env.DEV) {
|
||||
// const timeDiff = frame.timeDiff;
|
||||
// const frameRate = frame.frameRate;
|
||||
// textNode.setText(
|
||||
// `FrameTime: ${timeDiff.toFixed(0)} ms\nFrameRate: ${frameRate.toFixed(0)} fps`,
|
||||
// );
|
||||
// }
|
||||
// }, this.mediaLayer);
|
||||
|
||||
// anim.start();
|
||||
// this.uiLayer.add(textNode);
|
||||
this.addKeyboardShortcuts();
|
||||
}
|
||||
|
||||
public addImage(imageFile: File) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
imageFile: imageFile,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
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.commandManager.createNode(imageNode);
|
||||
}
|
||||
|
||||
public addPaintNode(
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) {
|
||||
var node = new PaintNode({
|
||||
canvasElement: canvas,
|
||||
lineBounds: lineBounds,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
loaded: async () => {
|
||||
await this.realTimeDrawEngine.render();
|
||||
},
|
||||
});
|
||||
this.commandManager.createNode(node);
|
||||
}
|
||||
// Events for Undo and Redo
|
||||
private addKeyboardShortcuts() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.ctrlKey && event.key === "z") {
|
||||
this.undoStackManager.undo();
|
||||
} else if (
|
||||
(event.ctrlKey && event.key === "y") ||
|
||||
(event.ctrlKey && event.shiftKey && event.key === "Z")
|
||||
) {
|
||||
this.undoStackManager.redo();
|
||||
} else if (event.key === "Delete") {
|
||||
this.commandManager.deleteNodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
const ensureBase64Prefix = (base64String: string) => {
|
||||
return base64String.startsWith("data:")
|
||||
? base64String
|
||||
: `data:image/png;base64,${base64String}`;
|
||||
}
|
||||
|
||||
export { ensureBase64Prefix };
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { MediaNode, Position, RGBColor, Transformation } from "../types";
|
||||
import {
|
||||
|
||||
CreateCommand,
|
||||
DeleteCommand,
|
||||
LockNodesCommand,
|
||||
MoveLayerDown,
|
||||
MoveLayerUp,
|
||||
|
||||
TransformCommand,
|
||||
TranslateCommand,
|
||||
UndoStackManager,
|
||||
UnlockNodesCommand,
|
||||
|
||||
} from "../UndoRedo";
|
||||
import {
|
||||
NodesManager,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
|
||||
interface EngineReferences {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
renderEngineRef: RealTimeDrawEngine;
|
||||
undoStackManagerRef: UndoStackManager;
|
||||
}
|
||||
|
||||
export class CommandManager {
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
private renderEngineRef: RealTimeDrawEngine;
|
||||
private undoStackManagerRef: UndoStackManager;
|
||||
|
||||
constructor(engineRefs: EngineReferences) {
|
||||
this.mediaLayerRef = engineRefs.mediaLayerRef;
|
||||
this.nodesManagerRef = engineRefs.nodesManagerRef;
|
||||
this.nodeTransformerRef = engineRefs.nodeTransformerRef;
|
||||
this.selectionManagerRef = engineRefs.selectionManagerRef;
|
||||
this.renderEngineRef = engineRefs.renderEngineRef;
|
||||
this.undoStackManagerRef = engineRefs.undoStackManagerRef;
|
||||
}
|
||||
|
||||
createNode(node: MediaNode) {
|
||||
const command = new CreateCommand({
|
||||
nodes: new Set<MediaNode>([node]),
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
|
||||
// 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());
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
deleteNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
const command = new DeleteCommand({
|
||||
nodes: nodes,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
|
||||
deleteSpecificNodes(nodes: MediaNode[]) {
|
||||
const command = new DeleteCommand({
|
||||
nodes: nodes,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
nodeTransformerRef: this.nodeTransformerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
renderEngineRef: this.renderEngineRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
|
||||
toggleLockNodes() {
|
||||
const nodes = this.selectionManagerRef.getSelectedNodes();
|
||||
const node = nodes.values().next().value;
|
||||
if (!node) {
|
||||
console.log("Node Not Found for Locking");
|
||||
return;
|
||||
}
|
||||
if (node.isLocked()) {
|
||||
const command = new UnlockNodesCommand({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
} else {
|
||||
const command = new LockNodesCommand({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
}
|
||||
}
|
||||
moveNodesUp() {
|
||||
const command = new MoveLayerUp({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
moveNodesDown() {
|
||||
const command = new MoveLayerDown({
|
||||
nodes: this.selectionManagerRef.getSelectedNodes(),
|
||||
nodesManagerRef: this.nodesManagerRef,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.executeCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
translateNodes(props: {
|
||||
nodes: Set<MediaNode>;
|
||||
initialPositions: Map<MediaNode, Position>;
|
||||
finalPositions: Map<MediaNode, Position>;
|
||||
}) {
|
||||
const command = new TranslateCommand({
|
||||
...props,
|
||||
layerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.pushCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
transformNodes(props: {
|
||||
nodes: Set<MediaNode>;
|
||||
initialTransformations: Map<MediaNode, Transformation[]>;
|
||||
finalTransformations: Map<MediaNode, Transformation[]>;
|
||||
}) {
|
||||
const command = new TransformCommand({
|
||||
...props,
|
||||
layerRef: this.mediaLayerRef,
|
||||
});
|
||||
this.undoStackManagerRef.pushCommand(command);
|
||||
//this.renderEngineRef.render();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { Size } from "../types";
|
||||
import { Colors } from "../constants";
|
||||
import { uiAccess } from "../../signals/uiAccess";
|
||||
|
||||
export class MatteBox {
|
||||
private matteBoxPoly: Konva.Line;
|
||||
private lightBoxRect: Konva.Rect;
|
||||
private uiLayerRef: Konva.Layer;
|
||||
private boardCanvasSize: Size;
|
||||
private captureCanvasSize: Size;
|
||||
|
||||
constructor({
|
||||
boardCanvasSize,
|
||||
captureCanvasSize,
|
||||
uiLayerRef,
|
||||
}: {
|
||||
boardCanvasSize: Size;
|
||||
captureCanvasSize: Size;
|
||||
uiLayerRef: Konva.Layer;
|
||||
}) {
|
||||
this.uiLayerRef = uiLayerRef;
|
||||
this.boardCanvasSize = boardCanvasSize;
|
||||
this.captureCanvasSize = captureCanvasSize;
|
||||
this.matteBoxPoly = new Konva.Line({
|
||||
points: getMatteBoxPolygonPoints({
|
||||
boardCanvasSize: this.boardCanvasSize,
|
||||
captureCanvasSize: this.captureCanvasSize,
|
||||
}),
|
||||
fill: Colors.translucentBlack,
|
||||
closed: true,
|
||||
});
|
||||
this.lightBoxRect = new Konva.Rect({
|
||||
size: captureCanvasSize,
|
||||
position: {
|
||||
x: (boardCanvasSize.width - captureCanvasSize.width) / 2,
|
||||
y: (boardCanvasSize.height - captureCanvasSize.height) / 2,
|
||||
},
|
||||
fill: Colors.transparent,
|
||||
// fill: "rgba(100,0,0,0.3)",
|
||||
});
|
||||
}
|
||||
public enable(showMagicBox?: boolean) {
|
||||
this.uiLayerRef.add(this.matteBoxPoly);
|
||||
this.uiLayerRef.add(this.lightBoxRect);
|
||||
if (showMagicBox) {
|
||||
uiAccess.magicBox.show();
|
||||
}
|
||||
}
|
||||
public disable() {
|
||||
this.matteBoxPoly.remove();
|
||||
this.lightBoxRect.remove();
|
||||
if (uiAccess.magicBox.isShowing()) {
|
||||
uiAccess.magicBox.hide();
|
||||
}
|
||||
}
|
||||
public updateSize({
|
||||
boardCanvasSize,
|
||||
captureCanvasSize,
|
||||
}: {
|
||||
boardCanvasSize?: Size;
|
||||
captureCanvasSize?: Size;
|
||||
}) {
|
||||
if (boardCanvasSize) {
|
||||
this.boardCanvasSize = boardCanvasSize;
|
||||
}
|
||||
if (captureCanvasSize) {
|
||||
this.captureCanvasSize = captureCanvasSize;
|
||||
}
|
||||
this.matteBoxPoly.points(
|
||||
getMatteBoxPolygonPoints({
|
||||
boardCanvasSize: this.boardCanvasSize,
|
||||
captureCanvasSize: this.captureCanvasSize,
|
||||
}),
|
||||
);
|
||||
this.lightBoxRect.setAttrs({
|
||||
size: this.captureCanvasSize,
|
||||
position: {
|
||||
x: (this.boardCanvasSize.width - this.captureCanvasSize.width) / 2,
|
||||
y: (this.boardCanvasSize.height - this.captureCanvasSize.height) / 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getMatteBoxPolygonPoints({
|
||||
boardCanvasSize: board,
|
||||
captureCanvasSize: capture,
|
||||
}: {
|
||||
boardCanvasSize: Size;
|
||||
captureCanvasSize: Size;
|
||||
}) {
|
||||
return [
|
||||
//top left
|
||||
0,
|
||||
0,
|
||||
// top right
|
||||
board.width,
|
||||
0,
|
||||
//bottom right
|
||||
board.width,
|
||||
board.height,
|
||||
//bottom middle
|
||||
board.width / 2,
|
||||
board.height,
|
||||
//canvas bottom middle,
|
||||
board.width / 2,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//canvas bottom right,
|
||||
(board.width - capture.width) / 2 + capture.width,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//canvas top right,
|
||||
(board.width - capture.width) / 2 + capture.width,
|
||||
(board.height - capture.height) / 2,
|
||||
//canvas top left,
|
||||
(board.width - capture.width) / 2,
|
||||
(board.height - capture.height) / 2,
|
||||
//canvas bottom left,
|
||||
(board.width - capture.width) / 2,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//canvas bottom middle,
|
||||
board.width / 2,
|
||||
(board.height - capture.height) / 2 + capture.height,
|
||||
//bottom middle
|
||||
board.width / 2,
|
||||
board.height,
|
||||
//bottom left
|
||||
0,
|
||||
board.height,
|
||||
//back to top left
|
||||
0,
|
||||
0,
|
||||
];
|
||||
}
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// TODO: USE API STORTELLE
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
import { NodesManager, SelectionManager } from "../NodesManagers";
|
||||
import {
|
||||
ImageNodeData,
|
||||
NodeData,
|
||||
TextNodeData,
|
||||
TransformationData,
|
||||
VideoNodeData,
|
||||
} from "../types";
|
||||
import { uiAccess } from "../../signals/uiAccess";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { NodeType } from "../Nodes/constants";
|
||||
import { ImageNode, TextNode } from "../Nodes";
|
||||
import { RealTimeDrawEngine as RenderEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
|
||||
|
||||
export class SceneManager {
|
||||
private navigateRef: NavigateFunction;
|
||||
|
||||
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
private renderEngineRef: RenderEngine;
|
||||
private currentSceneToken: string | undefined;
|
||||
|
||||
constructor({
|
||||
navigateRef,
|
||||
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
selectionManagerRef,
|
||||
renderEngineRef,
|
||||
}: {
|
||||
navigateRef: NavigateFunction;
|
||||
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
selectionManagerRef: SelectionManager;
|
||||
renderEngineRef: RenderEngine;
|
||||
}) {
|
||||
this.navigateRef = navigateRef;
|
||||
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
this.selectionManagerRef = selectionManagerRef;
|
||||
this.renderEngineRef = renderEngineRef;
|
||||
}
|
||||
|
||||
public async saveScene() {
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const timestamp = new Date();
|
||||
const sceneTitle =
|
||||
"board_" + timestamp.toISOString().substring(0, 19).replace(/\D/g, "");
|
||||
|
||||
const saveJson = this.extractAllNodesData();
|
||||
console.log("saveJson", saveJson);
|
||||
const file = new File([saveJson], `${sceneTitle}`, {
|
||||
type: "application/json",
|
||||
});
|
||||
if (!this.currentSceneToken) {
|
||||
const uploadResponse = await mediaUploadApi.UploadNewScene({
|
||||
blob: file,
|
||||
fileName: file.name,
|
||||
uuid: uuidv4(),
|
||||
});
|
||||
console.log(uploadResponse);
|
||||
if (!uploadResponse.success || !uploadResponse.data) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Save New Scene Error",
|
||||
message: uploadResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.currentSceneToken = uploadResponse.data;
|
||||
this.navigateRef(`/${this.currentSceneToken}`);
|
||||
} else {
|
||||
const uploadResponse = await mediaUploadApi.UploadSavedScene({
|
||||
blob: file,
|
||||
fileName: file.name,
|
||||
uuid: uuidv4(),
|
||||
mediaToken: this.currentSceneToken,
|
||||
});
|
||||
console.log(uploadResponse);
|
||||
if (!uploadResponse.success || !uploadResponse.data) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Save Scene Error",
|
||||
message: uploadResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async loadScene(mediaFileToken: string) {
|
||||
const mediaFileApi = new MediaFilesApi();
|
||||
const getSceneResponse = await mediaFileApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaFileToken,
|
||||
});
|
||||
if (!getSceneResponse.success || !getSceneResponse.data) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Load Scene Error",
|
||||
message: getSceneResponse.errorMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.currentSceneToken = mediaFileToken;
|
||||
// console.log(getSceneResponse.data);
|
||||
// console.log(getSceneResponse.data.public_bucket_path);
|
||||
const fileResponse = await fetch(getSceneResponse.data.public_bucket_url);
|
||||
if (!fileResponse.ok) {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Load Scene Error",
|
||||
message: "Failed to download Scene File",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const fileBlob = await fileResponse.blob();
|
||||
|
||||
const loadJson: NodeData[] = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
const readerError = () => {
|
||||
uiAccess.dialogError.show({
|
||||
title: "Load Scene Error",
|
||||
message: "Failed to read Scene File",
|
||||
});
|
||||
this.navigateRef("/");
|
||||
console.log("debug file Response", fileResponse);
|
||||
console.log("debug file Blob", fileBlob);
|
||||
reject();
|
||||
};
|
||||
reader.onloadend = () => {
|
||||
try {
|
||||
resolve(JSON.parse(reader.result as string));
|
||||
} catch {
|
||||
readerError();
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
readerError();
|
||||
};
|
||||
reader.readAsText(fileBlob);
|
||||
});
|
||||
|
||||
this.rebuildScene(loadJson);
|
||||
}
|
||||
|
||||
private extractAllNodesData() {
|
||||
const nodesData: NodeData[] = [];
|
||||
this.nodesManagerRef.getAllNodes().forEach((node) => {
|
||||
const nodeData = node.getNodeData(
|
||||
this.renderEngineRef.captureCanvas.position(),
|
||||
);
|
||||
if (nodeData !== null) {
|
||||
nodesData.push(nodeData);
|
||||
}
|
||||
});
|
||||
nodesData.sort((a, b) => a.transform.zIndex - b.transform.zIndex);
|
||||
return JSON.stringify(nodesData);
|
||||
}
|
||||
|
||||
private rebuildScene(nodesData: NodeData[]) {
|
||||
// console.log(nodesData);
|
||||
nodesData.forEach((nodeDatum) => {
|
||||
if (nodeDatum.type === NodeType.IMAGE && nodeDatum.imageNodeData) {
|
||||
this.addImage(nodeDatum.imageNodeData, nodeDatum.transform);
|
||||
} else if (nodeDatum.type === NodeType.VIDEO && nodeDatum.videoNodeData) {
|
||||
this.addVideo(nodeDatum.videoNodeData, nodeDatum.transform);
|
||||
} else if (nodeDatum.type === NodeType.TEXT && nodeDatum.textNodeData) {
|
||||
this.addText({
|
||||
textNodeData: nodeDatum.textNodeData,
|
||||
transform: nodeDatum.transform,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addImage(
|
||||
imageNodeData: ImageNodeData,
|
||||
transform: TransformationData,
|
||||
) {
|
||||
const imageNode = new ImageNode({
|
||||
mediaFileUrl: imageNodeData.mediaFileUrl,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
canvasPosition: this.renderEngineRef.captureCanvas.position(),
|
||||
canvasSize: this.renderEngineRef.captureCanvas.size(),
|
||||
transform: transform,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
});
|
||||
this.nodesManagerRef.saveNode(imageNode);
|
||||
this.renderEngineRef.addNodes(imageNode);
|
||||
}
|
||||
private addVideo(
|
||||
videoNodeData: VideoNodeData,
|
||||
transform: TransformationData,
|
||||
) {
|
||||
const videoNode = new VideoNode({
|
||||
loadingVideosProviderRef: this.loadingVideosProviderRef,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
canvasPosition: this.renderEngineRef.captureCanvas.position(),
|
||||
canvasSize: this.renderEngineRef.captureCanvas.size(),
|
||||
videoNodeData: videoNodeData,
|
||||
transform: transform,
|
||||
});
|
||||
this.nodesManagerRef.saveNode(videoNode);
|
||||
this.renderEngineRef.addNodes(videoNode);
|
||||
}
|
||||
private addText({
|
||||
textNodeData,
|
||||
transform,
|
||||
}: {
|
||||
textNodeData: TextNodeData;
|
||||
transform: TransformationData;
|
||||
}) {
|
||||
const textNode = new TextNode({
|
||||
textNodeData: textNodeData,
|
||||
transform: transform,
|
||||
mediaLayerRef: this.mediaLayerRef,
|
||||
canvasPosition: this.renderEngineRef.captureCanvas.position(),
|
||||
canvasSize: this.renderEngineRef.captureCanvas.size(),
|
||||
selectionManagerRef: this.selectionManagerRef,
|
||||
});
|
||||
this.nodesManagerRef.saveNode(textNode);
|
||||
this.renderEngineRef.addNodes(textNode);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CommandManager } from "./CommandManager";
|
||||
export { MatteBox } from "./MatteBox";
|
||||
export { SceneManager } from "./SceneManager";
|
||||
@@ -1,46 +0,0 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
cancelGeneration,
|
||||
finishGeneration,
|
||||
onStartGeneration,
|
||||
} from "~/signals/generation/generationSignals";
|
||||
|
||||
export class GenerationEngine {
|
||||
private isGenerating = false;
|
||||
|
||||
constructor() {
|
||||
console.log("GenerationEngine constructor");
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
private attachEventListeners() {
|
||||
onStartGeneration((prompt) => {
|
||||
this.invokeGeneration(prompt);
|
||||
});
|
||||
}
|
||||
|
||||
private async invokeGeneration(prompt: string) {
|
||||
if (this.isGenerating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isGenerating = true;
|
||||
console.log("Invoking generation for prompt:", prompt);
|
||||
|
||||
try {
|
||||
const base64BitmapResponse = await invoke("text_to_image", {
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
console.log("Generation finished with response:", base64BitmapResponse);
|
||||
|
||||
const base64string = base64BitmapResponse as string;
|
||||
finishGeneration(base64string);
|
||||
} catch (error) {
|
||||
console.error("Error during image processing:", error);
|
||||
cancelGeneration();
|
||||
} finally {
|
||||
this.isGenerating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
import Konva from "konva";
|
||||
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { highlightStrokeWidth, primaryOrange } from "./constants";
|
||||
import { NodeData, Position } from "../types";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { MouseEventHandler } from "react";
|
||||
import { KonvaEventObject } from "konva/lib/Node";
|
||||
|
||||
export type SupportedNode = Konva.Image | Konva.Group | Konva.Shape;
|
||||
|
||||
export abstract class BaseNode {
|
||||
protected selectionManagerRef: SelectionManager;
|
||||
public kNode: SupportedNode;
|
||||
protected mediaLayerRef: Konva.Layer;
|
||||
|
||||
// Internal State members
|
||||
// do not modify internal
|
||||
protected isSelecting: boolean = false;
|
||||
// This locks interaction when the render engine is rendering
|
||||
protected isProcessing: boolean = false;
|
||||
protected _isLocked: boolean = false;
|
||||
protected _areBaseEventsListening: boolean = false;
|
||||
protected _isKDragEventListening: boolean = false;
|
||||
protected _isKTranformEventListening: boolean = false;
|
||||
|
||||
abstract getNodeData(captureCanvasPosition: Position): NodeData | null;
|
||||
|
||||
constructor({
|
||||
selectionManagerRef,
|
||||
mediaLayerRef,
|
||||
kNode,
|
||||
}: {
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
kNode: SupportedNode;
|
||||
}) {
|
||||
// console.log("Node constructed");
|
||||
// this.uuid = uuidv4();
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.selectionManagerRef = selectionManagerRef;
|
||||
this.kNode = kNode;
|
||||
}
|
||||
|
||||
protected delete() {
|
||||
// Do any other clean up and delete the konva node.
|
||||
this.kNode.destroy();
|
||||
}
|
||||
public async setProcessing(val: boolean) {
|
||||
this.isProcessing = val;
|
||||
}
|
||||
public isKEventRef() {
|
||||
return this._isKDragEventListening && this._isKTranformEventListening;
|
||||
}
|
||||
public setIsKEventRef(flag: boolean) {
|
||||
this._isKDragEventListening = flag;
|
||||
this._isKTranformEventListening = flag;
|
||||
this.removeListenToBaseKNodeTransformations();
|
||||
this.removeListenToBaseKNodeDrags();
|
||||
if (flag) {
|
||||
this.listenToBaseKNodeTransformations();
|
||||
this.listenToBaseKNodeDrags();
|
||||
}
|
||||
}
|
||||
public highlight() {
|
||||
// console.log("Highlight", this.kNode._id);
|
||||
if (this.kNode instanceof Konva.Image || this.kNode instanceof Konva.Shape) {
|
||||
this.kNode.stroke(primaryOrange);
|
||||
this.kNode.strokeWidth(highlightStrokeWidth);
|
||||
return;
|
||||
}
|
||||
if (this.kNode instanceof Konva.Group) {
|
||||
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
|
||||
console.log("Wrapper rect:", wrapperRect);
|
||||
if (wrapperRect != null) {
|
||||
wrapperRect.stroke(primaryOrange);
|
||||
wrapperRect.strokeWidth(highlightStrokeWidth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(
|
||||
"DEV: BaseNode Highlight Error, kNode of BaseNode should be override by MediaNodes that extends kNode",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public unhighlight() {
|
||||
// console.log("unHighlight", this.kNode._id);
|
||||
if (this.kNode instanceof Konva.Image || this.kNode instanceof Konva.Shape) {
|
||||
this.kNode.strokeWidth(0);
|
||||
return;
|
||||
}
|
||||
if (this.kNode instanceof Konva.Group) {
|
||||
const wrapperRect = this.kNode.findOne(".wrapper") as Konva.Rect;
|
||||
console.log("Wrapper rect:", wrapperRect);
|
||||
if (wrapperRect != null) {
|
||||
// Issue investigate
|
||||
wrapperRect.strokeWidth(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(
|
||||
"DEV: BaseNode UnHighlight Error, kNode of BaseNode should be override by MediaNodes that extends kNode",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public moveLayerUp() {
|
||||
this.kNode.moveUp();
|
||||
}
|
||||
public moveLayerDown() {
|
||||
this.kNode.moveDown();
|
||||
}
|
||||
public isLocked() {
|
||||
return this._isLocked;
|
||||
}
|
||||
public lock() {
|
||||
this._isLocked = true;
|
||||
this.kNode.setDraggable(false);
|
||||
this.selectionManagerRef.updateNodeTransformer();
|
||||
if (this.isKEventRef()) {
|
||||
this.selectionManagerRef.updateContextComponents();
|
||||
}
|
||||
}
|
||||
public unlock() {
|
||||
this._isLocked = false;
|
||||
this.kNode.setDraggable(true);
|
||||
this.selectionManagerRef.updateNodeTransformer();
|
||||
if (this.isKEventRef()) {
|
||||
this.selectionManagerRef.updateContextComponents();
|
||||
}
|
||||
}
|
||||
|
||||
public listenToBaseKNode() {
|
||||
if (this._areBaseEventsListening) {
|
||||
return;
|
||||
}
|
||||
this._areBaseEventsListening = true;
|
||||
const handleSelect = (isMultiSelect: boolean) => {
|
||||
// console.log("handle select");
|
||||
if (!isMultiSelect) {
|
||||
// clear selection if not multislect
|
||||
console.log("No Shift >> no multiselect");
|
||||
this.selectionManagerRef.clearSelection();
|
||||
}
|
||||
if (this.selectionManagerRef.isNodeSelected(this)) {
|
||||
this.selectionManagerRef.deselectNode(this);
|
||||
return;
|
||||
}
|
||||
if (this.isSelecting) {
|
||||
this.selectionManagerRef.selectNode(this);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRightClick = (e: KonvaEventObject<MouseEvent>) => {
|
||||
this.selectionManagerRef.clearSelection();
|
||||
this.selectionManagerRef.selectNode(this, undefined, {
|
||||
show: true,
|
||||
at: { x: e.evt.x, y: e.evt.y }
|
||||
});
|
||||
}
|
||||
|
||||
this.kNode.on("contextmenu", (e) => {
|
||||
console.log("CONTEXT MENU", e);
|
||||
e.evt.preventDefault();
|
||||
handleRightClick(e);
|
||||
});
|
||||
|
||||
this.kNode.on("mousedown", (e) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
|
||||
// 3: Right mouse button
|
||||
// If right click, let context event handle it
|
||||
if (e.evt.button === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Selection of Node
|
||||
if (!this.selectionManagerRef.isNodeSelected(this)) {
|
||||
this.isSelecting = true;
|
||||
// send shift key to check for multiselect
|
||||
handleSelect(e.evt.shiftKey);
|
||||
}
|
||||
});
|
||||
|
||||
this.kNode.on("mouseup", (e) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/which
|
||||
// 3: Right mouse button
|
||||
// If right click, let context event handle it
|
||||
if (e.evt.button === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// just coming out of dragging or selecting mode
|
||||
// no need to handle select
|
||||
if (this.selectionManagerRef.isDragging() || this.isSelecting) {
|
||||
this.isSelecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// checking for multiselect, in multiselect deselection is possible
|
||||
handleSelect(e.evt.shiftKey);
|
||||
});
|
||||
}
|
||||
public removeListenToBaseKNode() {
|
||||
this._areBaseEventsListening = false;
|
||||
this.kNode.removeEventListener("mousedown mouseup");
|
||||
}
|
||||
private handleDragStart = () => {
|
||||
console.log("Drag start", this.kNode._id);
|
||||
if (this.isProcessing) {
|
||||
return;
|
||||
}
|
||||
this.selectionManagerRef.dragStart();
|
||||
};
|
||||
|
||||
private handleDragEnd = () => {
|
||||
this.selectionManagerRef.dragEnd();
|
||||
};
|
||||
|
||||
public listenToBaseKNodeDrags() {
|
||||
this.kNode.on("dragstart", this.handleDragStart);
|
||||
this.kNode.on("dragend", this.handleDragEnd);
|
||||
}
|
||||
|
||||
public removeListenToBaseKNodeDrags() {
|
||||
this.kNode.off("dragstart", this.handleDragStart);
|
||||
this.kNode.off("dragend", this.handleDragEnd);
|
||||
}
|
||||
public listenToBaseKNodeTransformations() {
|
||||
this.kNode.on("transformstart", (event) => {
|
||||
console.log("transformstart", event.target._id);
|
||||
this.selectionManagerRef.transformStart();
|
||||
});
|
||||
this.kNode.on("transform", () => {
|
||||
this.kNode.setAttrs({
|
||||
width: this.kNode.width() * this.kNode.scaleX(),
|
||||
height: this.kNode.height() * this.kNode.scaleY(),
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
});
|
||||
this.kNode.on("transformend", (event) => {
|
||||
console.log("transformend", event.target._id);
|
||||
this.selectionManagerRef.transformEnd();
|
||||
});
|
||||
}
|
||||
public removeListenToBaseKNodeTransformations() {
|
||||
this.kNode.removeEventListener("transformend");
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { MediaFilesApi, MediaUploadApi } from "~/Classes/ApiManager";
|
||||
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, Size, NodeData, TransformationData } from "../types";
|
||||
|
||||
import { NetworkedNode, UploadStatus } from "./NetworkedNode";
|
||||
import { NodeType, transparent } from "./constants";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
|
||||
interface ImageNodeContructor {
|
||||
canvasPosition: Position;
|
||||
canvasSize: Size;
|
||||
imageFile?: File;
|
||||
imageBitmap?: ImageBitmap;
|
||||
mediaFileToken?: string;
|
||||
mediaFileUrl?: string;
|
||||
transform?: TransformationData;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class ImageNode extends NetworkedNode {
|
||||
public kNode: Konva.Image;
|
||||
public imageSize?: Size;
|
||||
private loaded!: () => Promise<void>;
|
||||
constructor({
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
imageFile,
|
||||
mediaFileToken,
|
||||
mediaFileUrl,
|
||||
transform: existingTransform,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
imageBitmap,
|
||||
}: ImageNodeContructor) {
|
||||
// kNodes need to be created first to guaruntee
|
||||
// that it is not undefined in parent's context
|
||||
const transform = NodeUtilities.getInitialTransform({
|
||||
existingTransform,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
});
|
||||
const kNode = new Konva.Image({
|
||||
image: undefined, // to do replace with placeholder
|
||||
...transform,
|
||||
draggable: true,
|
||||
strokeScaleEnabled: false,
|
||||
});
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: kNode,
|
||||
localFile: imageFile,
|
||||
});
|
||||
this.kNode = kNode;
|
||||
this.loaded = loaded;
|
||||
// this.imageSize = minNodeSize;
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
|
||||
if (imageFile) {
|
||||
this.loadImageFromFile({
|
||||
imageFile: imageFile,
|
||||
maxSize: canvasSize,
|
||||
refPosition: canvasPosition,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (mediaFileUrl && transform) {
|
||||
this.mediaFileToken = mediaFileToken;
|
||||
this.mediaFileUrl = mediaFileUrl;
|
||||
this.loadImageFromUrl(mediaFileUrl);
|
||||
return;
|
||||
}
|
||||
if (imageBitmap) {
|
||||
this.loadImageFromImageBitmap({
|
||||
imageBitmap: imageBitmap,
|
||||
maxSize: canvasSize,
|
||||
refPosition: canvasPosition,
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("image node creation is fucked");
|
||||
}
|
||||
private loadImageFromFile({
|
||||
imageFile,
|
||||
maxSize,
|
||||
refPosition,
|
||||
}: {
|
||||
imageFile: File;
|
||||
maxSize: Size;
|
||||
refPosition: Position;
|
||||
}) {
|
||||
const imageComponent = new Image();
|
||||
imageComponent.crossOrigin = "anonymous";
|
||||
// this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
|
||||
|
||||
imageComponent.onload = () => {
|
||||
//this.setProgress({ progress: 0, status: UploadStatus.FILE_STAGED });
|
||||
this.imageSize = {
|
||||
width: imageComponent.width,
|
||||
height: imageComponent.height,
|
||||
};
|
||||
const adjustedSize = NodeUtilities.adjustNodeSizeToCanvas({
|
||||
componentSize: this.imageSize,
|
||||
maxSize: maxSize,
|
||||
});
|
||||
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
|
||||
canvasOffset: refPosition,
|
||||
componentSize: adjustedSize,
|
||||
maxSize: maxSize,
|
||||
});
|
||||
this.kNode.image(imageComponent);
|
||||
this.kNode.setSize(adjustedSize);
|
||||
this.kNode.setPosition(centerPosition);
|
||||
this.kNode.moveToTop();
|
||||
this.kNode.fill(transparent);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
//this.uploadImage(imageFile);
|
||||
this.loaded();
|
||||
};
|
||||
imageComponent.onerror = () => {
|
||||
//this.setProgress({ progress: 0, status: UploadStatus.ERROR_ON_FILE });
|
||||
};
|
||||
imageComponent.src = URL.createObjectURL(imageFile);
|
||||
}
|
||||
private async loadImageFromUrl(mediaFileUrl: string) {
|
||||
//this.setProgress({ progress: 75, status: UploadStatus.LOADING });
|
||||
|
||||
const newImage = new Image();
|
||||
newImage.crossOrigin = "anonymous";
|
||||
newImage.onerror = () => {
|
||||
//this.setProgress({ progress: 90, status: UploadStatus.ERROR_ON_LOAD });
|
||||
};
|
||||
newImage.onload = () => {
|
||||
// console.log("network image", newImage);
|
||||
this.kNode.image(newImage);
|
||||
this.kNode.draw();
|
||||
//this.setProgress({ progress: 100, status: UploadStatus.SUCCESS });
|
||||
};
|
||||
newImage.src = mediaFileUrl;
|
||||
this.listenToBaseKNode();
|
||||
}
|
||||
|
||||
private loadImageFromImageBitmap({
|
||||
imageBitmap,
|
||||
maxSize,
|
||||
refPosition,
|
||||
}: {
|
||||
imageBitmap: ImageBitmap;
|
||||
maxSize: Size;
|
||||
refPosition: Position;
|
||||
}) {
|
||||
this.imageSize = {
|
||||
width: imageBitmap.width,
|
||||
height: imageBitmap.height,
|
||||
};
|
||||
const adjustedSize = NodeUtilities.adjustNodeSizeToCanvas({
|
||||
componentSize: this.imageSize,
|
||||
maxSize: maxSize, // TODO
|
||||
});
|
||||
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
|
||||
canvasOffset: refPosition, // TODO
|
||||
componentSize: adjustedSize, // TODO
|
||||
maxSize: maxSize, // TODO
|
||||
});
|
||||
this.kNode.image(imageBitmap);
|
||||
this.kNode.setSize(adjustedSize);
|
||||
this.kNode.setPosition(centerPosition);
|
||||
this.kNode.moveToTop();
|
||||
this.kNode.fill(transparent);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
//this.uploadImage(imageFile);
|
||||
this.loaded();
|
||||
this.listenToBaseKNode();
|
||||
}
|
||||
|
||||
private async uploadImage(imageFile: File) {
|
||||
//this.setProgress({ progress: 10, status: UploadStatus.UPLOADING });
|
||||
|
||||
const mediaUploadApi = new MediaUploadApi();
|
||||
const uploadResponse = await mediaUploadApi.UploadImage({
|
||||
blob: imageFile,
|
||||
uuid: this.uuidGenerate(),
|
||||
fileName: imageFile.name,
|
||||
});
|
||||
// console.log(uploadResponse);
|
||||
if (!uploadResponse.success || !uploadResponse.data) {
|
||||
this.setStatus(UploadStatus.ERROR_ON_UPLOAD, uploadResponse.errorMessage);
|
||||
return;
|
||||
}
|
||||
this.mediaFileToken = uploadResponse.data;
|
||||
this.retreiveImage(this.mediaFileToken);
|
||||
}
|
||||
|
||||
private async retreiveImage(mediaFileToken: string) {
|
||||
//this.setProgress({ progress: 50, status: UploadStatus.RETREIVING });
|
||||
const mediaFileApi = new MediaFilesApi();
|
||||
const mediaFileResponse = await mediaFileApi.GetMediaFileByToken({
|
||||
mediaFileToken: mediaFileToken,
|
||||
});
|
||||
// console.log(mediaFileResponse);
|
||||
if (!mediaFileResponse.success || !mediaFileResponse.data) {
|
||||
this.setStatus(
|
||||
UploadStatus.ERROR_ON_RETREIVE,
|
||||
mediaFileResponse.errorMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.mediaFileUrl = mediaFileResponse.data.media_links.cdn_url;
|
||||
this.loadImageFromUrl(this.mediaFileUrl);
|
||||
}
|
||||
|
||||
public async retry() {
|
||||
if (this.mediaFileUrl) {
|
||||
this.loadImageFromUrl(this.mediaFileUrl);
|
||||
return;
|
||||
}
|
||||
if (this.mediaFileToken) {
|
||||
this.retreiveImage(this.mediaFileToken);
|
||||
return;
|
||||
}
|
||||
if (this.localFile) {
|
||||
this.uploadImage(this.localFile);
|
||||
return;
|
||||
}
|
||||
console.warn("Image Node has no data to recontruct itself!");
|
||||
}
|
||||
public getNodeData(canvasPostion: Position) {
|
||||
if (!this.mediaFileUrl) {
|
||||
console.error("Image Node can not be saved");
|
||||
return null;
|
||||
}
|
||||
const data: NodeData = {
|
||||
type: NodeType.IMAGE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPostion.x,
|
||||
y: this.kNode.position().y - canvasPostion.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
imageNodeData: {
|
||||
mediaFileUrl: this.mediaFileUrl,
|
||||
mediaFileToken: this.mediaFileToken,
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { BaseNode, SupportedNode } from "./BaseNode";
|
||||
import { Size } from "../types";
|
||||
import { LoadingVideosProvider } from "../EngineUtitlities/LoadingVideosProvider";
|
||||
import { NodeProgressEventDetail } from "../types/events";
|
||||
|
||||
export enum UploadStatus {
|
||||
INIT = "init",
|
||||
FILE_STAGED = "file_staged",
|
||||
ERROR_ON_FILE = "error_on_file",
|
||||
UPLOADING = "uploading",
|
||||
ERROR_ON_UPLOAD = "error_on_upload",
|
||||
RETREIVING = "retreiving",
|
||||
ERROR_ON_RETREIVE = "error_on_retreive",
|
||||
LOADING = "loading",
|
||||
ERROR_ON_LOAD = "error_on_load",
|
||||
SUCCESS = "success",
|
||||
}
|
||||
|
||||
export abstract class NetworkedNode extends BaseNode {
|
||||
public kNode: SupportedNode;
|
||||
|
||||
// members to deal with loading progress
|
||||
public didFinishLoading: boolean = false;
|
||||
protected _progress: number = 0;
|
||||
protected _progressMessage?: string;
|
||||
protected progressStatus: string = UploadStatus.INIT;
|
||||
public progressEvent: EventTarget;
|
||||
|
||||
// members to deal with the file carried
|
||||
protected localFile?: File;
|
||||
protected mediaFileToken?: string;
|
||||
protected mediaFileUrl?: string;
|
||||
public mediaFileSize?: Size;
|
||||
|
||||
// loading screen
|
||||
protected loadingVideosProviderRef?: LoadingVideosProvider;
|
||||
|
||||
// error and retry handling
|
||||
public errorMessage?: string;
|
||||
abstract retry(): void;
|
||||
|
||||
constructor({
|
||||
kNode,
|
||||
selectionManagerRef,
|
||||
mediaLayerRef,
|
||||
localFile,
|
||||
loadingVideosProviderRef,
|
||||
}: {
|
||||
kNode: SupportedNode;
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
loadingVideosProviderRef?: LoadingVideosProvider;
|
||||
localFile?: File;
|
||||
}) {
|
||||
super({
|
||||
kNode,
|
||||
selectionManagerRef,
|
||||
mediaLayerRef,
|
||||
});
|
||||
this.kNode = kNode;
|
||||
this.loadingVideosProviderRef = loadingVideosProviderRef;
|
||||
this.didFinishLoading = false;
|
||||
this.progressEvent = new EventTarget();
|
||||
this.localFile = localFile;
|
||||
}
|
||||
public progress() {
|
||||
return this._progress;
|
||||
}
|
||||
public progressMessage() {
|
||||
return this._progressMessage;
|
||||
}
|
||||
public status() {
|
||||
return this.progressStatus;
|
||||
}
|
||||
public isError() {
|
||||
const errorStatues: string[] = [
|
||||
UploadStatus.ERROR_ON_FILE,
|
||||
UploadStatus.ERROR_ON_UPLOAD,
|
||||
UploadStatus.ERROR_ON_RETREIVE,
|
||||
UploadStatus.ERROR_ON_LOAD,
|
||||
];
|
||||
return errorStatues.includes(this.progressStatus);
|
||||
}
|
||||
|
||||
protected uuidGenerate() {
|
||||
return uuidv4();
|
||||
}
|
||||
protected setStatus(newStatus: string, message?: string) {
|
||||
this.progressStatus = newStatus;
|
||||
this.errorMessage = message;
|
||||
this.selectionManagerRef.updateContextComponents();
|
||||
}
|
||||
protected setProgress<S>({
|
||||
progress,
|
||||
name,
|
||||
status,
|
||||
message,
|
||||
}: {
|
||||
progress: number;
|
||||
name?: string;
|
||||
status?: S;
|
||||
message?: string;
|
||||
}) {
|
||||
this._progress = progress;
|
||||
if (message) {
|
||||
this._progressMessage = message;
|
||||
}
|
||||
if (status && typeof status === "string") {
|
||||
this.progressStatus = status;
|
||||
}
|
||||
|
||||
if (this._progress === 100) {
|
||||
this.didFinishLoading = true;
|
||||
}
|
||||
// TODO: change to use
|
||||
this.progressEvent.dispatchEvent(
|
||||
new CustomEvent<NodeProgressEventDetail<S>>(
|
||||
name ? name : "GenericNodeProgress",
|
||||
{
|
||||
detail: {
|
||||
node: this,
|
||||
progress: this._progress,
|
||||
message: message,
|
||||
status: status,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
this.selectionManagerRef.updateContextComponents();
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { Position, Size, TransformationData } from "../types";
|
||||
import { minNodeSize, transparent } from "./constants";
|
||||
|
||||
export const NodeUtilities = {
|
||||
adjustNodeSizeToCanvas,
|
||||
downloadOffscreenCanvas,
|
||||
getInitialTransform,
|
||||
isAssetUrlAvailable,
|
||||
positionNodeOnCanvasCenter,
|
||||
printKNodeAttrs,
|
||||
urlToBlob,
|
||||
};
|
||||
function downloadOffscreenCanvas(canvas: OffscreenCanvas) {
|
||||
canvas.convertToBlob().then((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "offscreen-canvas-image.png"; // Specify the file name
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url); // Clean up
|
||||
}
|
||||
});
|
||||
}
|
||||
function getInitialTransform({
|
||||
existingTransform,
|
||||
mediaFileSize,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
}: {
|
||||
existingTransform?: TransformationData;
|
||||
mediaFileSize?: Size;
|
||||
canvasPosition: Position;
|
||||
canvasSize: Size;
|
||||
}) {
|
||||
return existingTransform
|
||||
? {
|
||||
...existingTransform,
|
||||
position: {
|
||||
x: existingTransform.position.x + canvasPosition.x,
|
||||
y: existingTransform.position.y + canvasPosition.y,
|
||||
},
|
||||
fill: transparent,
|
||||
}
|
||||
: {
|
||||
position: positionNodeOnCanvasCenter({
|
||||
canvasOffset: canvasPosition,
|
||||
componentSize: mediaFileSize ?? minNodeSize,
|
||||
maxSize: canvasSize,
|
||||
}),
|
||||
size: mediaFileSize ?? minNodeSize,
|
||||
fill: "gray",
|
||||
};
|
||||
}
|
||||
|
||||
function adjustNodeSizeToCanvas({
|
||||
componentSize,
|
||||
maxSize,
|
||||
}: {
|
||||
componentSize: Size;
|
||||
maxSize: Size;
|
||||
}) {
|
||||
const adjustedSize = {
|
||||
width: componentSize.width,
|
||||
height: componentSize.height,
|
||||
};
|
||||
if (adjustedSize.width > maxSize.width) {
|
||||
const scaleDown = maxSize.width / adjustedSize.width;
|
||||
adjustedSize.width = adjustedSize.width * scaleDown;
|
||||
adjustedSize.height = adjustedSize.height * scaleDown;
|
||||
}
|
||||
if (adjustedSize.height > maxSize.height) {
|
||||
const scaleDownAgain = maxSize.height / adjustedSize.height;
|
||||
adjustedSize.width = adjustedSize.width * scaleDownAgain;
|
||||
adjustedSize.height = adjustedSize.height * scaleDownAgain;
|
||||
}
|
||||
return adjustedSize;
|
||||
}
|
||||
|
||||
function positionNodeOnCanvasCenter({
|
||||
canvasOffset,
|
||||
componentSize,
|
||||
maxSize,
|
||||
}: {
|
||||
canvasOffset: Position;
|
||||
componentSize: Size;
|
||||
maxSize: Size;
|
||||
}) {
|
||||
return {
|
||||
x: canvasOffset.x + maxSize.width / 2 - componentSize.width / 2,
|
||||
y: canvasOffset.y + maxSize.height / 2 - componentSize.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function printKNodeAttrs(kNode: Konva.Node) {
|
||||
console.log({
|
||||
position: kNode.position(),
|
||||
size: kNode.size(),
|
||||
scale: kNode.scale(),
|
||||
rotation: kNode.rotation(),
|
||||
});
|
||||
}
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Function to check if the URL returns a 200 status
|
||||
async function checkUrl(url: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
console.error("Error fetching URL:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// wait using a while loop to display the result.
|
||||
async function isAssetUrlAvailable({
|
||||
url,
|
||||
sleepDurationMs = 500,
|
||||
totalRetries,
|
||||
}: {
|
||||
url: string;
|
||||
sleepDurationMs?: number;
|
||||
totalRetries?: number;
|
||||
}): Promise<boolean> {
|
||||
let retryCount = 0;
|
||||
let isAvailable = false;
|
||||
const totalRetriesReached = () => {
|
||||
if (!totalRetries) {
|
||||
return false;
|
||||
}
|
||||
return retryCount >= totalRetries;
|
||||
};
|
||||
while (!isAvailable && !totalRetriesReached()) {
|
||||
isAvailable = await checkUrl(url);
|
||||
if (!isAvailable) {
|
||||
console.log("Asset at Url not available yet, retrying...");
|
||||
retryCount = retryCount + 1;
|
||||
await sleep(sleepDurationMs);
|
||||
} else if (import.meta.env.DEV) {
|
||||
console.log("Preview Image is available:", url);
|
||||
}
|
||||
}
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
async function urlToBlob(url: string): Promise<Blob> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch resource: ${response.statusText}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
return blob;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, NodeData } from "../types";
|
||||
import { NodeType } from "./constants";
|
||||
|
||||
import { NetworkedNode } from "./NetworkedNode";
|
||||
|
||||
interface PaintNodeConstructor {
|
||||
canvasElement: HTMLCanvasElement;
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class PaintNode extends NetworkedNode {
|
||||
retry(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
constructor({
|
||||
canvasElement,
|
||||
lineBounds,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: PaintNodeConstructor) {
|
||||
const cl = canvasElement;
|
||||
|
||||
// Convert circle to image
|
||||
const dataURL = cl.toDataURL();
|
||||
const imageObj = new Image();
|
||||
imageObj.src = dataURL;
|
||||
|
||||
const paintShape = new Konva.Image({
|
||||
x: lineBounds.x,
|
||||
y: lineBounds.y,
|
||||
width: lineBounds.width,
|
||||
height: lineBounds.height,
|
||||
image: imageObj,
|
||||
listening: false, // Disable event listening to allow captureCanvas to receive events
|
||||
draggable: true,
|
||||
// stroke: '#000000',
|
||||
// strokeWidth: 2,
|
||||
// dash: [5, 5], // Creates dotted outline THIS IS FOR DEBUG
|
||||
globalCompositeOperation: "source-over",
|
||||
fill: "transparent",
|
||||
});
|
||||
|
||||
imageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded();
|
||||
};
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: paintShape,
|
||||
});
|
||||
this.kNode.moveToTop();
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
public getNodeData(canvasPosition: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.SHAPE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPosition.x,
|
||||
y: this.kNode.position().y - canvasPosition.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, Size, NodeData, TransformationData } from "../types";
|
||||
import { NodeType } from "./constants";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
import { NetworkedNode } from "./NetworkedNode";
|
||||
|
||||
interface PreviewCopyConstructor {
|
||||
image: Konva.Image;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class PreviewCopyNode extends NetworkedNode {
|
||||
retry(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
constructor({
|
||||
image,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: PreviewCopyConstructor) {
|
||||
// Create the actual shape inside the group
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: image,
|
||||
});
|
||||
this.kNode.moveToTop();
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
public getNodeData(canvasPosition: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.SHAPE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPosition.x,
|
||||
y: this.kNode.position().y - canvasPosition.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { BaseNode } from "./BaseNode";
|
||||
import {
|
||||
NodeData,
|
||||
Position,
|
||||
Size,
|
||||
TextNodeChildrenTransformData,
|
||||
TextNodeData,
|
||||
TransformationData,
|
||||
} from "../types";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
import { NodeType } from "./constants";
|
||||
|
||||
export class PureTextNode extends BaseNode {
|
||||
public kNode: Konva.Group;
|
||||
public rectNode: Konva.Rect;
|
||||
public textNode: Konva.Text;
|
||||
private originalTextSize: Size;
|
||||
private textNodeData: TextNodeData;
|
||||
|
||||
public didFinishLoading: boolean = true;
|
||||
|
||||
constructor({
|
||||
textNodeData,
|
||||
selectionManagerRef,
|
||||
mediaLayerRef,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
transform: existingTransform,
|
||||
textChildrenTransforms,
|
||||
}: {
|
||||
textNodeData: TextNodeData;
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
canvasSize: Size;
|
||||
canvasPosition: Position;
|
||||
transform?: TransformationData;
|
||||
textChildrenTransforms?: TextNodeChildrenTransformData;
|
||||
}) {
|
||||
const textNode = new Konva.Text({
|
||||
...textNodeData,
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 500,
|
||||
});
|
||||
const kNode = new Konva.Group({
|
||||
position: NodeUtilities.positionNodeOnCanvasCenter({
|
||||
canvasOffset: canvasPosition,
|
||||
componentSize: {
|
||||
width: textNode.width() + 20,
|
||||
height: textNode.height() + 20,
|
||||
},
|
||||
maxSize: canvasSize,
|
||||
}),
|
||||
width: textNode.width() + 20,
|
||||
height: textNode.height() + 20,
|
||||
draggable: true,
|
||||
});
|
||||
const rectNode = new Konva.Rect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: textNode.width() + 20,
|
||||
height: textNode.height() + 20,
|
||||
strokeScaleEnabled: false,
|
||||
name: "wrapper",
|
||||
});
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: kNode,
|
||||
});
|
||||
this.textNodeData = textNodeData;
|
||||
this.kNode = kNode;
|
||||
this.textNode = textNode;
|
||||
this.rectNode = rectNode;
|
||||
this.kNode.add(this.rectNode);
|
||||
this.kNode.add(this.textNode);
|
||||
if (existingTransform && textChildrenTransforms) {
|
||||
this.applyTransform({
|
||||
existingTransform,
|
||||
textChildrenTransforms,
|
||||
canvasPosition,
|
||||
});
|
||||
}
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.originalTextSize = this.textNode.size();
|
||||
this.listenToBaseKNode();
|
||||
}
|
||||
public listenToBaseKNodeTransformations() {
|
||||
this.kNode.on("transformstart", () => {
|
||||
// console.log("transformstart", event.target._id);
|
||||
this.selectionManagerRef.transformStart();
|
||||
});
|
||||
|
||||
this.kNode.on("transform", () => {
|
||||
const newBoxSize = {
|
||||
width: this.kNode.width() * this.kNode.scaleX(),
|
||||
height: this.kNode.height() * this.kNode.scaleY(),
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
};
|
||||
this.rectNode.setAttrs(newBoxSize);
|
||||
this.kNode.setAttrs(newBoxSize);
|
||||
const scaleX = (newBoxSize.width - 20) / this.originalTextSize.width;
|
||||
const scaleY = (newBoxSize.height - 20) / this.originalTextSize.height;
|
||||
this.textNode.setAttrs({
|
||||
// make sure these stay the same
|
||||
x: 10,
|
||||
y: 10,
|
||||
size: this.originalTextSize,
|
||||
//while these changes in size
|
||||
scaleX: scaleX,
|
||||
scaleY: scaleY,
|
||||
});
|
||||
});
|
||||
this.kNode.on("transformend", () => {
|
||||
// console.log("transformend", event.target._id);
|
||||
NodeUtilities.printKNodeAttrs(this.kNode);
|
||||
NodeUtilities.printKNodeAttrs(this.textNode);
|
||||
this.selectionManagerRef.transformEnd(this);
|
||||
});
|
||||
}
|
||||
public getNodeData(canvasPostion: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.TEXT,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPostion.x,
|
||||
y: this.kNode.position().y - canvasPostion.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
textNodeData: this.textNodeData,
|
||||
textChildrenTransforms: {
|
||||
wrapperRectTransform: {
|
||||
position: this.rectNode.position(),
|
||||
size: this.rectNode.size(),
|
||||
rotation: this.rectNode.rotation(),
|
||||
scale: {
|
||||
x: this.rectNode.scaleX(),
|
||||
y: this.rectNode.scaleY(),
|
||||
},
|
||||
zIndex: this.rectNode.getZIndex(),
|
||||
},
|
||||
textNodeTransform: {
|
||||
position: this.textNode.position(),
|
||||
size: this.textNode.size(),
|
||||
rotation: this.textNode.rotation(),
|
||||
scale: {
|
||||
x: this.textNode.scaleX(),
|
||||
y: this.textNode.scaleY(),
|
||||
},
|
||||
zIndex: this.textNode.getZIndex(),
|
||||
},
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
private applyTransform({
|
||||
canvasPosition,
|
||||
existingTransform,
|
||||
textChildrenTransforms,
|
||||
}: {
|
||||
canvasPosition: Position;
|
||||
existingTransform: TransformationData;
|
||||
textChildrenTransforms: TextNodeChildrenTransformData;
|
||||
}) {
|
||||
this.kNode.setAttrs({
|
||||
...existingTransform,
|
||||
position: {
|
||||
x: existingTransform.position.x + canvasPosition.x,
|
||||
y: existingTransform.position.y + canvasPosition.y,
|
||||
},
|
||||
});
|
||||
this.rectNode.setAttrs(textChildrenTransforms.wrapperRectTransform);
|
||||
this.textNode.setAttrs(textChildrenTransforms.textNodeTransform);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { Position, Size, NodeData, TransformationData } from "../types";
|
||||
import { NodeType } from "./constants";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
import { NetworkedNode } from "./NetworkedNode";
|
||||
|
||||
export enum ShapeType {
|
||||
CIRCLE = "circle",
|
||||
SQUARE = "square",
|
||||
TRIANGLE = "triangle",
|
||||
}
|
||||
|
||||
interface ShapeNodeConstructor {
|
||||
canvasPosition: Position;
|
||||
canvasSize: Size;
|
||||
shapeType: ShapeType;
|
||||
size: Size;
|
||||
color?: string; // Hex color string
|
||||
transform?: TransformationData;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
selectionManagerRef: SelectionManager;
|
||||
loaded: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class ShapeNode extends NetworkedNode {
|
||||
retry(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
public shapeType: ShapeType;
|
||||
public size: Size;
|
||||
public color: string;
|
||||
|
||||
constructor({
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
shapeType,
|
||||
size,
|
||||
color = "#2D81FF", // Default red if no color provided
|
||||
transform: existingTransform,
|
||||
mediaLayerRef,
|
||||
selectionManagerRef,
|
||||
loaded,
|
||||
}: ShapeNodeConstructor) {
|
||||
// Create the actual shape inside the group
|
||||
const transform = NodeUtilities.getInitialTransform({
|
||||
existingTransform,
|
||||
mediaFileSize: size,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
});
|
||||
|
||||
let shape = null;
|
||||
|
||||
switch (shapeType) {
|
||||
case ShapeType.CIRCLE:
|
||||
shape = new Konva.Ellipse({
|
||||
radiusX: size.width / 2,
|
||||
radiusY: size.height / 2,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
break;
|
||||
|
||||
case ShapeType.SQUARE:
|
||||
shape = new Konva.Rect({
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
break;
|
||||
|
||||
case ShapeType.TRIANGLE:
|
||||
shape = new Konva.Shape({
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
sceneFunc: (context, shape) => {
|
||||
context.beginPath();
|
||||
context.moveTo(shape.width() / 2, 0);
|
||||
context.lineTo(shape.width(), shape.height());
|
||||
context.lineTo(0, shape.height());
|
||||
context.closePath();
|
||||
context.fillStrokeShape(shape);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Invalid shape type");
|
||||
}
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: shape,
|
||||
});
|
||||
|
||||
this.shapeType = shapeType;
|
||||
this.size = size;
|
||||
this.color = color;
|
||||
|
||||
// Center the shape
|
||||
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
|
||||
canvasOffset: canvasPosition,
|
||||
componentSize: this.size,
|
||||
maxSize: canvasSize,
|
||||
});
|
||||
|
||||
// If the shape is an ellipse, the center is the origin so we need to offset it
|
||||
if (shapeType === ShapeType.CIRCLE) {
|
||||
centerPosition.x += size.width / 2;
|
||||
centerPosition.y += size.height / 2;
|
||||
}
|
||||
|
||||
// Add shape to group
|
||||
// bring to the front
|
||||
this.kNode.moveToTop();
|
||||
this.kNode.setPosition(centerPosition);
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
public getNodeData(canvasPosition: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.SHAPE,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPosition.x,
|
||||
y: this.kNode.position().y - canvasPosition.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { BaseNode } from "./BaseNode";
|
||||
import {
|
||||
NodeData,
|
||||
Position,
|
||||
Size,
|
||||
TextAlign,
|
||||
TextNodeData,
|
||||
TransformationData,
|
||||
} from "../types";
|
||||
import { NodeUtilities } from "./NodeUtilities";
|
||||
import { NodeType, transparent } from "./constants";
|
||||
|
||||
export class TextNode extends BaseNode {
|
||||
public kNode: Konva.Image;
|
||||
private textNodeData: TextNodeData;
|
||||
private originalTextSize?: Size;
|
||||
public didFinishLoading: boolean = true;
|
||||
|
||||
constructor({
|
||||
textNodeData,
|
||||
selectionManagerRef,
|
||||
mediaLayerRef,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
transform: existingTransform,
|
||||
}: {
|
||||
textNodeData: TextNodeData;
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
canvasSize: Size;
|
||||
canvasPosition: Position;
|
||||
transform?: TransformationData;
|
||||
}) {
|
||||
const transform = NodeUtilities.getInitialTransform({
|
||||
existingTransform,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
});
|
||||
// Create a new ImageNode to house the canvas text
|
||||
const kNode = new Konva.Image({
|
||||
image: undefined, // will use the canvas as the image source
|
||||
...transform,
|
||||
draggable: true,
|
||||
strokeScaleEnabled: false,
|
||||
});
|
||||
|
||||
super({
|
||||
selectionManagerRef: selectionManagerRef,
|
||||
mediaLayerRef: mediaLayerRef,
|
||||
kNode: kNode,
|
||||
});
|
||||
this.textNodeData = textNodeData;
|
||||
this.kNode = kNode;
|
||||
this.mediaLayerRef.add(this.kNode);
|
||||
this.listenToBaseKNode();
|
||||
|
||||
this.loadTextImageUsingCanvas({
|
||||
maxSize: canvasSize,
|
||||
refPosition: canvasPosition,
|
||||
existingTransform: existingTransform,
|
||||
});
|
||||
}
|
||||
|
||||
private loadTextImageUsingCanvas({
|
||||
maxSize,
|
||||
refPosition,
|
||||
existingTransform,
|
||||
}: {
|
||||
maxSize: Size;
|
||||
refPosition: Position;
|
||||
existingTransform?: TransformationData;
|
||||
}) {
|
||||
// Create a temporary canvas to draw the text
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// calvulate and set canvas
|
||||
const lines: string[] = this.textNodeData.text.split("\n");
|
||||
const { fontSize, fontFamily, fontStyle, fontWeight, maxWidth, textAlign } =
|
||||
this.textNodeData;
|
||||
this.originalTextSize = {
|
||||
width: maxWidth,
|
||||
height: fontSize * lines.length * 1.5 + fontSize * 0.5,
|
||||
};
|
||||
canvas.width = this.originalTextSize.width;
|
||||
canvas.height = this.originalTextSize.height;
|
||||
|
||||
// Set the font styles to match the text node
|
||||
context.font = `${fontWeight} ${fontStyle} ${fontSize}px ${fontFamily}`;
|
||||
|
||||
context.fillStyle = this.textNodeData.color;
|
||||
context.textAlign = textAlign;
|
||||
context.textBaseline = "top";
|
||||
|
||||
// Draw the text on the canvas
|
||||
lines.forEach((line, idx) => {
|
||||
const yOffset = fontSize * (1.5 * idx + 0.5);
|
||||
const xOffset =
|
||||
textAlign === TextAlign.LEFT
|
||||
? 0
|
||||
: textAlign === TextAlign.CENTER
|
||||
? maxWidth / 2
|
||||
: maxWidth; // textAlign === TextAlign.Right
|
||||
|
||||
context.fillText(line, xOffset, yOffset);
|
||||
});
|
||||
|
||||
// Convert canvas to data URL
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
|
||||
// Create a new Image object
|
||||
const imageComponent = new Image();
|
||||
imageComponent.crossOrigin = "anonymous";
|
||||
imageComponent.onload = () => {
|
||||
if (existingTransform) {
|
||||
this.kNode.setAttrs({
|
||||
image: imageComponent,
|
||||
fill: transparent,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const imageSize = {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
};
|
||||
|
||||
const centerPosition = NodeUtilities.positionNodeOnCanvasCenter({
|
||||
canvasOffset: refPosition,
|
||||
componentSize: imageSize,
|
||||
maxSize: maxSize,
|
||||
});
|
||||
|
||||
this.kNode.setAttrs({
|
||||
image: imageComponent,
|
||||
size: imageSize,
|
||||
position: centerPosition,
|
||||
fill: transparent,
|
||||
});
|
||||
};
|
||||
imageComponent.src = dataURL;
|
||||
}
|
||||
|
||||
public getNodeData(canvasPostion: Position) {
|
||||
const data: NodeData = {
|
||||
type: NodeType.TEXT,
|
||||
transform: {
|
||||
position: {
|
||||
x: this.kNode.position().x - canvasPostion.x,
|
||||
y: this.kNode.position().y - canvasPostion.y,
|
||||
},
|
||||
size: this.kNode.size(),
|
||||
rotation: this.kNode.rotation(),
|
||||
scale: {
|
||||
x: this.kNode.scaleX(),
|
||||
y: this.kNode.scaleY(),
|
||||
},
|
||||
zIndex: this.kNode.getZIndex(),
|
||||
},
|
||||
textNodeData: this.textNodeData,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Colors } from "../constants";
|
||||
|
||||
export enum NodeType {
|
||||
IMAGE = "image",
|
||||
TEXT = "text",
|
||||
VIDEO = "video",
|
||||
SHAPE = "shape",
|
||||
}
|
||||
export const transparent = Colors.transparent;
|
||||
export const primaryOrange = Colors.primaryOrange;
|
||||
|
||||
export const highlightStrokeWidth = 2;
|
||||
export const minNodeSize = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export { NodeType } from "./constants";
|
||||
export { BaseNode } from "./BaseNode";
|
||||
export { ImageNode } from "./ImageNode";
|
||||
export { PreviewCopyNode } from "./PreviewCopy";
|
||||
export { TextNode } from "./TextNode";
|
||||
export { ShapeNode , ShapeType} from "./ShapeNode";
|
||||
export { PaintNode } from "./PaintNode";
|
||||
@@ -1,122 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { MediaNode, TransformationData } from "../types";
|
||||
import { uiAccess } from "../../signals/uiAccess";
|
||||
|
||||
export class NodeIsolator {
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private backgroundRect: Konva.Rect;
|
||||
private referenceRect: Konva.Rect;
|
||||
private nodeIsolationLayerRef: Konva.Layer;
|
||||
private currentNode: MediaNode | undefined;
|
||||
private originalKNodeTransformation: TransformationData | undefined;
|
||||
private adjustSizeFnRef: (() => void) | undefined;
|
||||
|
||||
constructor({
|
||||
mediaLayerRef,
|
||||
nodeIsolationLayerRef,
|
||||
}: {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodeIsolationLayerRef: Konva.Layer;
|
||||
}) {
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodeIsolationLayerRef = nodeIsolationLayerRef;
|
||||
this.backgroundRect = new Konva.Rect({
|
||||
fill: "rgba(0,0,0,0.5)",
|
||||
});
|
||||
this.referenceRect = new Konva.Rect({
|
||||
fill: "gray",
|
||||
stroke: "salmon",
|
||||
strokeWidth: 10,
|
||||
dash: [20, 10],
|
||||
strokeScaleEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
private adjustSizes() {
|
||||
this.backgroundRect.setAttrs({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
if (this.currentNode && this.originalKNodeTransformation) {
|
||||
this.referenceRect.setAttrs(this.originalKNodeTransformation);
|
||||
|
||||
const originalSize = {
|
||||
width:
|
||||
this.originalKNodeTransformation.size.width *
|
||||
this.originalKNodeTransformation.scale.x,
|
||||
height:
|
||||
this.originalKNodeTransformation.size.height *
|
||||
this.originalKNodeTransformation.scale.y,
|
||||
};
|
||||
const maxWidth = window.innerWidth * 0.8;
|
||||
const isolationResize = {
|
||||
width: maxWidth,
|
||||
height: (maxWidth / originalSize.width) * originalSize.height,
|
||||
};
|
||||
const maxHeight = window.innerHeight - 350;
|
||||
if (isolationResize.height > maxHeight) {
|
||||
isolationResize.height = maxHeight;
|
||||
isolationResize.width =
|
||||
(maxHeight / originalSize.height) * originalSize.width;
|
||||
}
|
||||
const isolationReposition = {
|
||||
x: (window.innerWidth - isolationResize.width) / 2,
|
||||
y: (maxHeight - isolationResize.height) / 2 + 100,
|
||||
};
|
||||
this.currentNode.kNode.setAttrs({
|
||||
scale: { x: 1, y: 1 },
|
||||
rotation: 0,
|
||||
size: isolationResize,
|
||||
position: isolationReposition,
|
||||
});
|
||||
}
|
||||
}
|
||||
public enterIsolation(node: MediaNode) {
|
||||
// console.log("NodeIsolator > enterIsolation");
|
||||
uiAccess.toolbarMain.disable();
|
||||
this.currentNode = node;
|
||||
this.adjustSizeFnRef = () => this.adjustSizes();
|
||||
this.preserveKNodeTransformation(node.kNode);
|
||||
|
||||
this.currentNode.kNode.remove();
|
||||
this.nodeIsolationLayerRef.add(this.backgroundRect);
|
||||
this.nodeIsolationLayerRef.add(this.currentNode.kNode);
|
||||
this.mediaLayerRef.add(this.referenceRect);
|
||||
this.adjustSizeFnRef();
|
||||
|
||||
window.addEventListener("resize", this.adjustSizeFnRef);
|
||||
}
|
||||
|
||||
public exitIsolation() {
|
||||
// console.log("NodeIsolator > exitIsolation");
|
||||
if (this.adjustSizeFnRef) {
|
||||
window.removeEventListener("resize", this.adjustSizeFnRef);
|
||||
this.adjustSizeFnRef = undefined;
|
||||
}
|
||||
if (!this.currentNode) {
|
||||
console.error("NodeIsolator lost crrent node before isolation!!");
|
||||
return;
|
||||
}
|
||||
this.currentNode.kNode.remove();
|
||||
this.backgroundRect.remove();
|
||||
this.referenceRect.remove();
|
||||
this.mediaLayerRef.add(this.currentNode.kNode);
|
||||
this.currentNode.kNode.setAttrs(this.originalKNodeTransformation);
|
||||
this.currentNode = undefined;
|
||||
this.originalKNodeTransformation = undefined;
|
||||
uiAccess.toolbarMain.enable();
|
||||
}
|
||||
private preserveKNodeTransformation(kNode: Konva.Node) {
|
||||
this.originalKNodeTransformation = {
|
||||
position: kNode.position(),
|
||||
size: kNode.size(),
|
||||
rotation: kNode.rotation(),
|
||||
scale: {
|
||||
x: kNode.scaleX(),
|
||||
y: kNode.scaleY(),
|
||||
},
|
||||
zIndex: kNode.zIndex(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { MediaNode } from "../types";
|
||||
|
||||
export class NodeTransformer {
|
||||
private kTransformer: Konva.Transformer;
|
||||
|
||||
constructor() {
|
||||
this.kTransformer = new Konva.Transformer({
|
||||
padding: 0,
|
||||
anchorStyleFunc: (anchor) => {
|
||||
// anchor is Konva.Rect instance
|
||||
// you manually change its styling
|
||||
anchor.cornerRadius(10);
|
||||
if (anchor.hasName("top-center") || anchor.hasName("bottom-center")) {
|
||||
anchor.height(6);
|
||||
anchor.offsetY(3);
|
||||
anchor.width(30);
|
||||
anchor.offsetX(15);
|
||||
}
|
||||
if (anchor.hasName("middle-left") || anchor.hasName("middle-right")) {
|
||||
anchor.height(30);
|
||||
anchor.offsetY(15);
|
||||
anchor.width(6);
|
||||
anchor.offsetX(3);
|
||||
}
|
||||
// if (anchor.hasName("rotater")) {
|
||||
// anchor.offsetY(-25);
|
||||
// }
|
||||
// you also can set other properties
|
||||
// e.g. you can set fillPatternImage to set icon to the anchor
|
||||
},
|
||||
});
|
||||
}
|
||||
public getKonvaNode() {
|
||||
return this.kTransformer;
|
||||
}
|
||||
public addNodes({ selectedNodes }: { selectedNodes: Set<MediaNode> }) {
|
||||
const kNodesArray = Array.from(selectedNodes).reduce((acc, node) => {
|
||||
acc.push(node.kNode);
|
||||
return acc;
|
||||
}, [] as Konva.Node[]);
|
||||
|
||||
//if there are more things in the ui layer, turn this on
|
||||
// this.kTransformer.moveToTop();
|
||||
|
||||
this.kTransformer.nodes(kNodesArray);
|
||||
}
|
||||
public clear() {
|
||||
this.kTransformer.nodes([]);
|
||||
}
|
||||
public enable() {
|
||||
this.kTransformer.rotateEnabled(true);
|
||||
this.kTransformer.resizeEnabled(true);
|
||||
this.kTransformer.draggable(true);
|
||||
}
|
||||
public disable() {
|
||||
this.kTransformer.rotateEnabled(false);
|
||||
this.kTransformer.resizeEnabled(false);
|
||||
this.kTransformer.draggable(false);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { MediaNode } from "../types";
|
||||
|
||||
export class NodesManager {
|
||||
private allNodes: Set<MediaNode>;
|
||||
private allZIndices: Map<MediaNode, number>;
|
||||
|
||||
constructor() {
|
||||
this.allNodes = new Set();
|
||||
this.allZIndices = new Map();
|
||||
}
|
||||
public getAllNodes() {
|
||||
return this.allNodes;
|
||||
}
|
||||
|
||||
public getAllZIndices() {
|
||||
return this.allZIndices;
|
||||
}
|
||||
public getSortedZIndices() {
|
||||
return Array.from(this.allZIndices).sort(
|
||||
(nodeA, nodeB) => nodeA[1] - nodeB[1],
|
||||
);
|
||||
}
|
||||
public updateAllZIndices() {
|
||||
this.allNodes.forEach((node) => {
|
||||
this.allZIndices.set(node, node.kNode.getZIndex());
|
||||
});
|
||||
}
|
||||
|
||||
public saveNode(node: MediaNode): void {
|
||||
if (this.allNodes.has(node)) {
|
||||
return;
|
||||
}
|
||||
this.allNodes.add(node);
|
||||
this.updateAllZIndices();
|
||||
// this.printNodesAndZIndices();
|
||||
}
|
||||
|
||||
public removeNode(node: MediaNode): void {
|
||||
if (this.allNodes.has(node)) {
|
||||
this.allNodes.delete(node);
|
||||
this.allZIndices.delete(node);
|
||||
this.updateAllZIndices();
|
||||
}
|
||||
// this.printNodesAndZIndices();
|
||||
}
|
||||
|
||||
public hasNode(node: MediaNode): boolean {
|
||||
return this.allNodes.has(node);
|
||||
}
|
||||
|
||||
/*************************
|
||||
* For Debugging use
|
||||
*/
|
||||
public printNodesAndZIndices() {
|
||||
console.log(this.allNodes);
|
||||
console.log(this.allZIndices);
|
||||
const trueZIndices = Array.from(this.allNodes).map((node) => ({
|
||||
id: node.kNode._id,
|
||||
z: node.kNode.getZIndex(),
|
||||
}));
|
||||
console.log("True ZIndces", trueZIndices);
|
||||
}
|
||||
}
|
||||
-384
@@ -1,384 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { NodeTransformer } from "./NodeTransformer";
|
||||
import { MediaNode, Position, Transformation } from "../types";
|
||||
import { uiAccess, uiEvents } from "../../signals";
|
||||
import { NetworkedNode } from "../Nodes/NetworkedNode";
|
||||
import { LoadingBarStatus } from "../../components/ui";
|
||||
import { ImageNode, ShapeNode, TextNode, PreviewCopyNode } from "../Nodes";
|
||||
import {
|
||||
calculateContextualsPosition,
|
||||
getImageNodeButtonStates,
|
||||
getMultiSelectButtonStates, getPreviewCopyNodeButtonStates,
|
||||
getShapeNodeButtonStates,
|
||||
getTextNodeButtonStates,
|
||||
getVideoNodeButtonStates,
|
||||
} from "./ToolbarNodeUtilities";
|
||||
|
||||
export enum SelectionManagerEvents {
|
||||
NODES_TRANSLATIONS = "nodestranslation",
|
||||
NODES_TRANSFORMATION = "nodestransformation",
|
||||
}
|
||||
export interface NodesTranslationEventDetails {
|
||||
nodes: Set<MediaNode>;
|
||||
initialPositions: Map<MediaNode, Position>;
|
||||
finalPositions: Map<MediaNode, Position>;
|
||||
}
|
||||
export interface NodeTransformationEventDetails {
|
||||
nodes: Set<MediaNode>;
|
||||
initialTransformations: Map<MediaNode, Transformation[]>;
|
||||
finalTransformations: Map<MediaNode, Transformation[]>;
|
||||
}
|
||||
|
||||
export class SelectionManager {
|
||||
private selectedNodes: Set<MediaNode>;
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private initialPositions: Map<MediaNode, Position>;
|
||||
private initialTransformations: Map<MediaNode, Transformation[]>;
|
||||
public eventTarget: EventTarget;
|
||||
private _isDragging: boolean = false;
|
||||
private disabled: boolean = false;
|
||||
|
||||
public firstSelectedNode: MediaNode | undefined;
|
||||
|
||||
constructor({
|
||||
mediaLayerRef,
|
||||
nodeTransformerRef,
|
||||
}: {
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
}) {
|
||||
this.firstSelectedNode = undefined;
|
||||
this.selectedNodes = new Set();
|
||||
|
||||
this.initialPositions = new Map<MediaNode, Position>();
|
||||
this.initialTransformations = new Map<MediaNode, Transformation[]>();
|
||||
this.eventTarget = new EventTarget();
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodeTransformerRef = nodeTransformerRef;
|
||||
}
|
||||
// This lets us perfom operations on the selected node.
|
||||
public getSelectedNodes(): Set<MediaNode> {
|
||||
return this.selectedNodes;
|
||||
}
|
||||
public selectNodes(nodes: MediaNode[]) {
|
||||
const unlockedNodes = nodes.filter((node) => {
|
||||
return !node.isLocked();
|
||||
});
|
||||
unlockedNodes.forEach((node) => {
|
||||
this.selectNode(node, true);
|
||||
});
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
public disable() {
|
||||
this.clearSelection();
|
||||
this.disabled = true;
|
||||
}
|
||||
public enable() {
|
||||
this.disabled = false;
|
||||
}
|
||||
public isDisabled() {
|
||||
return this.disable;
|
||||
}
|
||||
public selectNode(node: MediaNode, doNotDraw?: boolean, contextMenu: {
|
||||
show?: boolean,
|
||||
at?: { x: number, y: number }
|
||||
} = {
|
||||
show: false,
|
||||
}): boolean {
|
||||
if (
|
||||
this.disabled || // no-op when disabled
|
||||
this.selectedNodes.has(node) || // if the node is already selected
|
||||
(this.selectedNodes.size > 0 && node.isLocked()) ||
|
||||
// if in multiselect but picked a locked item
|
||||
(this.selectedNodes.size > 0 &&
|
||||
this.selectedNodes.values().next().value?.isLocked())
|
||||
// if the first node picked is a locked item
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node.highlight();
|
||||
if (this.selectedNodes.size === 0) {
|
||||
node.setIsKEventRef(true);
|
||||
}
|
||||
this.selectedNodes.add(node);
|
||||
this.updateNodeTransformer();
|
||||
this.updateContextComponents();
|
||||
|
||||
console.log("SelectionManager > selectNode > showContextMenu", contextMenu.show);
|
||||
if (contextMenu.show) {
|
||||
this.showContextComponents(contextMenu.at);
|
||||
}
|
||||
|
||||
if (!doNotDraw) {
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public deselectNode(node: MediaNode, doNotDraw?: boolean): void {
|
||||
if (this.disabled) {
|
||||
// no-op when disabled
|
||||
return;
|
||||
}
|
||||
node.unhighlight();
|
||||
if (node.isKEventRef()) {
|
||||
node.setIsKEventRef(false);
|
||||
}
|
||||
this.selectedNodes.delete(node);
|
||||
this.updateNodeTransformer();
|
||||
this.updateContextComponents();
|
||||
this.showContextComponents();
|
||||
if (!doNotDraw) {
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
if (this.disabled) {
|
||||
//no-op when disabled
|
||||
return;
|
||||
}
|
||||
this.selectedNodes.forEach((node) => {
|
||||
node.unhighlight();
|
||||
if (node.isKEventRef()) {
|
||||
node.setIsKEventRef(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedNodes.clear();
|
||||
this.nodeTransformerRef.clear();
|
||||
this.hideContextComponents();
|
||||
this.mediaLayerRef.batchDraw();
|
||||
}
|
||||
|
||||
public onToggleLock() {
|
||||
this.updateContextComponents();
|
||||
}
|
||||
public isDragging() {
|
||||
return this._isDragging;
|
||||
}
|
||||
public isNodeSelected(node: MediaNode): boolean {
|
||||
return this.selectedNodes.has(node);
|
||||
}
|
||||
|
||||
public dragStart() {
|
||||
// track the dragging state for mouseevents in nodes
|
||||
this._isDragging = true;
|
||||
this.hideContextComponents();
|
||||
|
||||
// track all nodes initial position when dragstart
|
||||
this.selectedNodes.forEach((selectedNode) => {
|
||||
this.initialPositions.set(selectedNode, selectedNode.kNode.position());
|
||||
});
|
||||
}
|
||||
|
||||
public dragEnd() {
|
||||
// release the dragging state for mouseevents in nodes
|
||||
this._isDragging = false;
|
||||
// track and map the final positions
|
||||
const finalPositions = new Map<MediaNode, Position>();
|
||||
this.selectedNodes.forEach((currNode) => {
|
||||
finalPositions.set(currNode, currNode.kNode.position());
|
||||
});
|
||||
// dispatch the info for engine to manage un-redo stack
|
||||
this.eventTarget.dispatchEvent(
|
||||
new CustomEvent<NodesTranslationEventDetails>(
|
||||
SelectionManagerEvents.NODES_TRANSLATIONS,
|
||||
{
|
||||
detail: {
|
||||
nodes: this.selectedNodes,
|
||||
initialPositions: new Map(this.initialPositions),
|
||||
finalPositions: finalPositions,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
// done, clear data and show menu
|
||||
this.initialPositions.clear();
|
||||
this.updateContextComponents();
|
||||
}
|
||||
|
||||
private getKNodeTransformation(kNode: Konva.Node) {
|
||||
return {
|
||||
kNodeId: kNode._id,
|
||||
position: kNode.position(),
|
||||
size: kNode.size(),
|
||||
rotation: kNode.rotation(),
|
||||
scale: {
|
||||
x: kNode.scaleX(),
|
||||
y: kNode.scaleY(),
|
||||
},
|
||||
};
|
||||
}
|
||||
public transformStart() {
|
||||
// track all nodes initial transformation when transformStart
|
||||
this.selectedNodes.forEach((selectedNode) => {
|
||||
const transformations = [this.getKNodeTransformation(selectedNode.kNode)];
|
||||
if (selectedNode.kNode instanceof Konva.Group) {
|
||||
const childKNodes = selectedNode.kNode.getChildren();
|
||||
childKNodes.forEach((childKNode: Konva.Node) => {
|
||||
transformations.push(this.getKNodeTransformation(childKNode));
|
||||
});
|
||||
}
|
||||
this.initialTransformations.set(selectedNode, transformations);
|
||||
});
|
||||
this.hideContextComponents();
|
||||
}
|
||||
public transformEnd() {
|
||||
// track and map the final positions
|
||||
const finalTransformations = new Map<MediaNode, Transformation[]>();
|
||||
this.selectedNodes.forEach((selectedNode) => {
|
||||
const transformations = [this.getKNodeTransformation(selectedNode.kNode)];
|
||||
if (selectedNode.kNode instanceof Konva.Group) {
|
||||
const childKNodes = selectedNode.kNode.getChildren();
|
||||
childKNodes.forEach((childKNode: Konva.Node) => {
|
||||
transformations.push(this.getKNodeTransformation(childKNode));
|
||||
});
|
||||
}
|
||||
finalTransformations.set(selectedNode, transformations);
|
||||
});
|
||||
// dispatch the info for engine to manage un-redo stack
|
||||
this.eventTarget.dispatchEvent(
|
||||
new CustomEvent<NodeTransformationEventDetails>(
|
||||
SelectionManagerEvents.NODES_TRANSFORMATION,
|
||||
{
|
||||
detail: {
|
||||
nodes: this.selectedNodes,
|
||||
initialTransformations: new Map(this.initialTransformations),
|
||||
finalTransformations: finalTransformations,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public updateNodeTransformer() {
|
||||
const lockFlag = this.selectedNodes.values().next().value?.isLocked();
|
||||
if (lockFlag) {
|
||||
this.nodeTransformerRef.disable();
|
||||
} else {
|
||||
this.nodeTransformerRef.enable();
|
||||
}
|
||||
this.nodeTransformerRef.addNodes({ selectedNodes: this.selectedNodes });
|
||||
}
|
||||
|
||||
public hideContextComponents() {
|
||||
uiAccess.toolbarNode.hide();
|
||||
uiAccess.loadingBar.hide();
|
||||
}
|
||||
|
||||
public showContextComponents(at?: { x: number, y: number }) {
|
||||
const showOrUpdate = uiAccess.toolbarNode.isShowing()
|
||||
? uiAccess.toolbarNode.update
|
||||
: uiAccess.toolbarNode.show;
|
||||
|
||||
// If a position is received, set it
|
||||
// Otherwise use the previous position
|
||||
const position = at ? at : uiAccess.toolbarNode.signal.value.position;
|
||||
|
||||
const node = this.selectedNodes.values().next().value;
|
||||
if (node === undefined) {
|
||||
this.hideContextComponents();
|
||||
return; // no node
|
||||
}
|
||||
if (this.selectedNodes.size === 1) {
|
||||
if (node instanceof ImageNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getImageNodeButtonStates({ locked: node.isLocked() }),
|
||||
position: position
|
||||
});
|
||||
}
|
||||
if (node instanceof PreviewCopyNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getPreviewCopyNodeButtonStates({ locked: node.isLocked() }),
|
||||
position: position
|
||||
});
|
||||
}
|
||||
else if (node instanceof ShapeNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getShapeNodeButtonStates({ locked: node.isLocked() }),
|
||||
color: node.color,
|
||||
position: position
|
||||
});
|
||||
}
|
||||
else if (node instanceof TextNode) {
|
||||
showOrUpdate({
|
||||
knodeIds: [node.kNode.id()],
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }),
|
||||
color: node.getNodeData({ x: 0, y: 0 })?.textNodeData?.color ?? "#000000",
|
||||
position: position
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// show loading bar is the node is noding
|
||||
if (node instanceof NetworkedNode) {
|
||||
if (!node.didFinishLoading && !uiAccess.loadingBar.isShowing()) {
|
||||
// console.log("show loading bar");
|
||||
uiAccess.loadingBar.show();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (node) {
|
||||
showOrUpdate({
|
||||
locked: node.isLocked(),
|
||||
buttonStates: getMultiSelectButtonStates({ locked: node.isLocked() }),
|
||||
position: position
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateContextComponents() {
|
||||
// use first node as reference
|
||||
const node = this.selectedNodes.values().next().value;
|
||||
if (node === undefined) {
|
||||
this.hideContextComponents();
|
||||
return; // no nodes
|
||||
}
|
||||
|
||||
// console.log("SelectionManager > updateContextComponents for node:", node);
|
||||
const coord = calculateContextualsPosition(
|
||||
this.nodeTransformerRef.getKonvaNode(),
|
||||
);
|
||||
|
||||
if (node.isLocked() !== uiAccess.toolbarNode.isLocked()) {
|
||||
// console.log("setting lock");
|
||||
uiAccess.toolbarNode.setLocked(node.isLocked());
|
||||
}
|
||||
console.debug("Setting context position to:", coord)
|
||||
uiAccess.toolbarNode.setPosition({
|
||||
x: coord.x,
|
||||
y: coord.y,
|
||||
});
|
||||
if (node instanceof NetworkedNode) {
|
||||
uiAccess.loadingBar.update({
|
||||
position: coord,
|
||||
progress: node.progress(),
|
||||
status: node.isError()
|
||||
? LoadingBarStatus.ERROR
|
||||
: LoadingBarStatus.LOADING,
|
||||
message: node.progressMessage(),
|
||||
});
|
||||
uiEvents.toolbarNode.retry.onClick((e) => {
|
||||
if (e) {
|
||||
console.log("CONTEXTUAL LOADING BAR RETRY ONCLICK", e, node);
|
||||
node.retry();
|
||||
}
|
||||
});
|
||||
if (node.didFinishLoading) {
|
||||
// console.log("node finished loading", node);
|
||||
uiAccess.loadingBar.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { NodesManager } from "./NodesManager";
|
||||
import { SelectionManager } from "./SelectionManager";
|
||||
import { MediaNode } from "../types";
|
||||
|
||||
interface SqaureCoordinates {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
}
|
||||
export class SelectorSquare {
|
||||
private kSquare: Konva.Rect;
|
||||
private selecting: boolean = false;
|
||||
private sqaureCoordinates: SqaureCoordinates = {
|
||||
x1: 0,
|
||||
x2: 0,
|
||||
y1: 0,
|
||||
y2: 0,
|
||||
};
|
||||
|
||||
// engine class references
|
||||
private captureCanvasRef: Konva.Rect;
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
private stageRef: Konva.Stage;
|
||||
|
||||
constructor({
|
||||
captureCanvasRef,
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
selectionManagerRef,
|
||||
stageRef,
|
||||
}: {
|
||||
captureCanvasRef: Konva.Rect;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
selectionManagerRef: SelectionManager;
|
||||
stageRef: Konva.Stage;
|
||||
}) {
|
||||
this.kSquare = new Konva.Rect({
|
||||
fill: "rgba(45, 129, 255, 0.3)",
|
||||
stroke: "#00a1ff",
|
||||
strokeWidth: 1,
|
||||
visible: false,
|
||||
// disable events to not interrupt with events
|
||||
listening: false,
|
||||
});
|
||||
this.captureCanvasRef = captureCanvasRef;
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
this.selectionManagerRef = selectionManagerRef;
|
||||
this.stageRef = stageRef;
|
||||
}
|
||||
public getKonvaNode() {
|
||||
return this.kSquare;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.stageRef.off("mousedown touchstart");
|
||||
this.stageRef.off("mousemove touchmove");
|
||||
this.stageRef.off("mouseup touchend");
|
||||
this.stageRef.off("mouseleave");
|
||||
}
|
||||
public enable() {
|
||||
// Remove any existing event handlers first to prevent duplicates
|
||||
this.disable();
|
||||
|
||||
this.stageRef.on("mousedown touchstart", (e) => {
|
||||
const stagePointerPos = this.stageRef.getPointerPosition();
|
||||
if (
|
||||
(e.target !== this.stageRef && e.target !== this.captureCanvasRef) || // do nothing if we mousedown on any shape
|
||||
stagePointerPos === null || // do nothing if pointers not available
|
||||
e.evt.shiftKey // do nothing so then multiselect is more forgiving in misclicks
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// start handle mousedown
|
||||
e.evt.preventDefault();
|
||||
if (e.target === this.stageRef || e.target === this.captureCanvasRef) {
|
||||
//moused down on empty space, clear previous selection first
|
||||
this.selectionManagerRef.clearSelection();
|
||||
}
|
||||
|
||||
// this starts drawing the square
|
||||
this.sqaureCoordinates = {
|
||||
x1: stagePointerPos.x,
|
||||
y1: stagePointerPos.y,
|
||||
x2: stagePointerPos.x,
|
||||
y2: stagePointerPos.y,
|
||||
};
|
||||
|
||||
this.kSquare.size({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
this.selecting = true;
|
||||
});
|
||||
|
||||
this.stageRef.on("mousemove touchmove", (e) => {
|
||||
// do nothing if we didn't start selection
|
||||
const stagePointerPos = this.stageRef.getPointerPosition();
|
||||
if (!this.selecting || stagePointerPos === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle drawing square according to mouse move
|
||||
e.evt.preventDefault();
|
||||
this.sqaureCoordinates = {
|
||||
...this.sqaureCoordinates,
|
||||
x2: stagePointerPos.x,
|
||||
y2: stagePointerPos.y,
|
||||
};
|
||||
const { x1, y1, x2, y2 } = this.sqaureCoordinates;
|
||||
this.kSquare.moveToTop();
|
||||
this.kSquare.setAttrs({
|
||||
visible: true,
|
||||
x: Math.min(x1, x2),
|
||||
y: Math.min(y1, y2),
|
||||
width: Math.abs(x2 - x1),
|
||||
height: Math.abs(y2 - y1),
|
||||
});
|
||||
});
|
||||
|
||||
this.stageRef.on("mouseup touchend", (e) => {
|
||||
// do nothing if we didn't start selection
|
||||
if (!this.selecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selecting = false;
|
||||
if (!this.kSquare.visible()) {
|
||||
return;
|
||||
}
|
||||
e.evt.preventDefault();
|
||||
// update visibility
|
||||
this.kSquare.visible(false);
|
||||
// Find all the Nodes and feed them into Selectmanager
|
||||
var shapes = this.mediaLayerRef.getChildren();
|
||||
var box = this.kSquare.getClientRect();
|
||||
var foundKNodes = shapes.filter((shape) =>
|
||||
Konva.Util.haveIntersection(box, shape.getClientRect()),
|
||||
);
|
||||
if (foundKNodes.length > 0) {
|
||||
const kNodeIds = foundKNodes.map((kNode) => kNode._id);
|
||||
const foundNodes = Array.from(
|
||||
this.nodesManagerRef.getAllNodes(),
|
||||
).reduce((accNodes, currNode) => {
|
||||
if (kNodeIds.includes(currNode.kNode._id) && !currNode.isLocked()) {
|
||||
accNodes.push(currNode);
|
||||
}
|
||||
return accNodes;
|
||||
}, [] as MediaNode[]);
|
||||
console.log("selector square found", foundNodes);
|
||||
this.selectionManagerRef.selectNodes(foundNodes);
|
||||
}
|
||||
});
|
||||
|
||||
this.stageRef.on("mouseleave", (e) => {
|
||||
if (!this.selecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selecting = false;
|
||||
if (!this.kSquare.visible()) {
|
||||
return;
|
||||
}
|
||||
e.evt.preventDefault();
|
||||
// update visibility
|
||||
this.kSquare.visible(false);
|
||||
// and then do nothing, this is considered a cancel
|
||||
});
|
||||
}
|
||||
}
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ToolbarNodeButtonNames } from "../../components/features/ToolbarNode";
|
||||
import { ContextualToolbarProps } from "../../signals/uiAccess/toolbarNode";
|
||||
export function calculateContextualsPosition(kNode: Konva.Transformer) {
|
||||
const w = kNode.getSize().width * kNode.scaleX();
|
||||
const h = kNode.getSize().height * kNode.scaleY();
|
||||
const x0 = kNode.getPosition().x;
|
||||
const y0 = kNode.getPosition().y;
|
||||
|
||||
const d = kNode.getAbsoluteRotation();
|
||||
const r = d >= 0 ? (d * Math.PI) / 180 : ((360 + d) * Math.PI) / 180;
|
||||
|
||||
let px: number, py: number;
|
||||
if (r < Math.PI / 2) {
|
||||
//top right quadrants
|
||||
px = x0 + (h * Math.sin(r) + w * Math.cos(r)) / 2 - h * Math.sin(r);
|
||||
py = y0 + h * Math.cos(r) + w * Math.cos(Math.PI / 2 - r);
|
||||
} else if (r < Math.PI) {
|
||||
//lower right quadrants
|
||||
px = x0 - (h * Math.sin(r) - w * Math.cos(r)) / 2;
|
||||
py = y0 + w * Math.cos(Math.PI / 2 - r);
|
||||
} else if (r < (Math.PI * 3) / 2) {
|
||||
//lower left quadrants
|
||||
px = x0 + h * Math.cos(r) - (h * Math.cos(r) + w * Math.sin(r)) / 2;
|
||||
py = y0;
|
||||
} else {
|
||||
//top left quadrants
|
||||
px = x0 + (-h * Math.sin(r) + w * Math.cos(r)) / 2;
|
||||
py = y0 + h * Math.cos(r);
|
||||
}
|
||||
return { x: px, y: py };
|
||||
}
|
||||
|
||||
export function getImageNodeButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
switch (buttonName) {
|
||||
case ButtonNames.AI_STYLIZE:
|
||||
case ButtonNames.SEGMENTATION:
|
||||
case ButtonNames.DOWNLOAD:
|
||||
case ButtonNames.CHROMA:
|
||||
case ButtonNames.COLOR:
|
||||
case ButtonNames.TRANSFORM:
|
||||
// hidden buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
|
||||
default:
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
|
||||
export function getPreviewCopyNodeButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
switch (buttonName) {
|
||||
case ButtonNames.AI_STYLIZE:
|
||||
case ButtonNames.SEGMENTATION:
|
||||
case ButtonNames.DOWNLOAD:
|
||||
case ButtonNames.CHROMA:
|
||||
case ButtonNames.COLOR:
|
||||
case ButtonNames.TRANSFORM:
|
||||
// hidden buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
|
||||
default:
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
export function getTextNodeButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
switch (buttonName) {
|
||||
case ButtonNames.AI_STYLIZE:
|
||||
case ButtonNames.SEGMENTATION:
|
||||
case ButtonNames.DOWNLOAD:
|
||||
case ButtonNames.CHROMA:
|
||||
case ButtonNames.REMOVE_BACKGROUND:
|
||||
case ButtonNames.TRANSFORM:
|
||||
// hidden buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
|
||||
default:
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
export function getVideoNodeButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
switch (buttonName) {
|
||||
case ButtonNames.AI_STYLIZE:
|
||||
case ButtonNames.COLOR:
|
||||
case ButtonNames.REMOVE_BACKGROUND:
|
||||
case ButtonNames.TRANSFORM:
|
||||
// soon to come feature is disabled
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
default:
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
|
||||
export function getShapeNodeButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
switch (buttonName) {
|
||||
case ButtonNames.AI_STYLIZE:
|
||||
case ButtonNames.SEGMENTATION:
|
||||
case ButtonNames.DOWNLOAD:
|
||||
case ButtonNames.CHROMA:
|
||||
case ButtonNames.REMOVE_BACKGROUND:
|
||||
case ButtonNames.TRANSFORM:
|
||||
// hidden buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
default:
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
|
||||
export function getMultiSelectButtonStates(
|
||||
props: { locked?: boolean | "unknown" } = {},
|
||||
) {
|
||||
const ButtonNames = ToolbarNodeButtonNames;
|
||||
return Object.values(ButtonNames).reduce(
|
||||
(buttonStates, buttonName) => {
|
||||
switch (buttonName) {
|
||||
case ButtonNames.AI_STYLIZE:
|
||||
case ButtonNames.SEGMENTATION:
|
||||
case ButtonNames.DOWNLOAD:
|
||||
case ButtonNames.CHROMA:
|
||||
case ButtonNames.COLOR:
|
||||
case ButtonNames.REMOVE_BACKGROUND:
|
||||
case ButtonNames.TRANSFORM:
|
||||
// hidden buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: true,
|
||||
hidden: true,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
default:
|
||||
// all other buttons
|
||||
buttonStates[buttonName] = {
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
active: false,
|
||||
};
|
||||
return buttonStates;
|
||||
}
|
||||
},
|
||||
{} as ContextualToolbarProps["buttonStates"],
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export { NodesManager } from "./NodesManager";
|
||||
export { NodeIsolator } from "./NodeIsolator";
|
||||
export { NodeTransformer } from "./NodeTransformer";
|
||||
export { SelectionManager, SelectionManagerEvents } from "./SelectionManager";
|
||||
export { SelectorSquare } from "./SelectorSquare";
|
||||
|
||||
export type {
|
||||
NodesTranslationEventDetails,
|
||||
NodeTransformationEventDetails,
|
||||
} from "./SelectionManager";
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
//@ts-ignore
|
||||
// TODO: have to export and modify the code
|
||||
import { ICanvasConfig } from "konva/lib/Canvas";
|
||||
|
||||
import { Canvas } from "konva/lib/Canvas";
|
||||
import { OffScreenSceneContext } from "./OffScreenSceneContext";
|
||||
|
||||
export class OffScreenSceneCanvas extends Canvas {
|
||||
constructor(
|
||||
config: ICanvasConfig = { width: 0, height: 0, willReadFrequently: false },
|
||||
) {
|
||||
super(config); // creates canvas then you have to replace it
|
||||
//@ts-ignore
|
||||
// replace here also kills the css but thats ok
|
||||
this._canvas = new OffscreenCanvas(config.width, config.height);
|
||||
|
||||
this.context = new OffScreenSceneContext(this, {
|
||||
willReadFrequently: config.willReadFrequently,
|
||||
});
|
||||
console.log("Created Offscreen Scene Canvas");
|
||||
this.setSize(config.width, config.height);
|
||||
}
|
||||
|
||||
setPixelRatio(pixelRatio: number) {
|
||||
console.log("overriden setPixelRatio");
|
||||
var previousRatio = this.pixelRatio;
|
||||
this.pixelRatio = pixelRatio;
|
||||
this.setSize(
|
||||
this.getWidth() / previousRatio,
|
||||
this.getHeight() / previousRatio,
|
||||
);
|
||||
}
|
||||
public override setWidth(width: number) {
|
||||
// take into account pixel ratio
|
||||
// console.log("overriden setWidth");
|
||||
this.width = this._canvas.width = width * this.pixelRatio;
|
||||
var pixelRatio = this.pixelRatio,
|
||||
_context = this.getContext()._context;
|
||||
_context.scale(pixelRatio, pixelRatio);
|
||||
}
|
||||
public override setHeight(height: number) {
|
||||
// take into account pixel ratio
|
||||
// console.log("overriden setHeight");
|
||||
this.height = this._canvas.height = height * this.pixelRatio;
|
||||
var pixelRatio = this.pixelRatio,
|
||||
_context = this.getContext()._context;
|
||||
_context.scale(pixelRatio, pixelRatio);
|
||||
}
|
||||
getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
setSize(width: number | undefined, height: number | undefined) {
|
||||
this.setWidth(width || 0);
|
||||
this.setHeight(height || 0);
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { Context } from "konva/lib/Context";
|
||||
import { Canvas } from "konva/lib/Canvas";
|
||||
|
||||
export class OffScreenSceneContext extends Context {
|
||||
constructor(canvas: Canvas, { willReadFrequently = false } = {}) {
|
||||
super(canvas);
|
||||
this._context = canvas._canvas.getContext("2d", {
|
||||
willReadFrequently,
|
||||
}) as CanvasRenderingContext2D;
|
||||
}
|
||||
}
|
||||
-879
@@ -1,879 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
import { FileUtilities } from "../../utilities/FileUtilities";
|
||||
import {
|
||||
ImageNode,
|
||||
TextNode,
|
||||
ShapeNode,
|
||||
PreviewCopyNode,
|
||||
PaintNode,
|
||||
} from "../Nodes";
|
||||
import { MediaNode } from "../types";
|
||||
import { RenderTask } from "./RenderTask";
|
||||
|
||||
import {
|
||||
isLoadingVisible,
|
||||
loadingProgress,
|
||||
} from "../../signals/uiEvents/loadingIndicator";
|
||||
|
||||
import { EncodeImageBitmapToBase64 } from "../../utilities/EncodeImageBitmapToBase64";
|
||||
import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap";
|
||||
|
||||
interface ServerSettings {
|
||||
model_path: string;
|
||||
lora_path?: string;
|
||||
}
|
||||
|
||||
export class RealTimeDrawEngine {
|
||||
private imageNodes: (ImageNode | TextNode | ShapeNode | PaintNode)[];
|
||||
|
||||
public offScreenCanvas: OffscreenCanvas;
|
||||
private outputBitmap: ImageBitmap | undefined;
|
||||
|
||||
// private frames: ImageBitmap[];
|
||||
// capturing composite within window
|
||||
|
||||
public mediaLayerRef: Konva.Layer;
|
||||
private drawingsLayer: Konva.Layer; // New Layer for Drawings
|
||||
|
||||
private height: number;
|
||||
private width: number;
|
||||
private positionX: number;
|
||||
private positionY: number;
|
||||
private positionPreviewX: number;
|
||||
private positionPreviewY: number;
|
||||
|
||||
private port: MessagePort | undefined;
|
||||
public captureCanvas: Konva.Rect;
|
||||
public backgroundRasterRect: Konva.Image;
|
||||
|
||||
public fps: number = 24;
|
||||
|
||||
public currentPrompt: string;
|
||||
public currentStrength: number;
|
||||
|
||||
public lastRenderedBitmap: ImageBitmap | undefined;
|
||||
|
||||
// Paint Color
|
||||
// paint Brush Size
|
||||
// has to exit out of paint mode when shape or image are used.
|
||||
public paintColor: string = "#000000";
|
||||
private brushSize: number = 5; // Default brush size
|
||||
|
||||
public onDrawCallback?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
private offscreenRenderDiv: HTMLDivElement;
|
||||
|
||||
private client: WebSocketClient | null = null;
|
||||
private isConnected: boolean = false;
|
||||
|
||||
public backgroundColor: string = "#d2d2d2"; // off white
|
||||
constructor({
|
||||
width,
|
||||
height,
|
||||
mediaLayerRef,
|
||||
offScreenCanvas,
|
||||
onDraw,
|
||||
onPreviewCopy, // New Parameter
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
offScreenCanvas: OffscreenCanvas;
|
||||
onDraw?: (
|
||||
canvas: HTMLCanvasElement,
|
||||
lineBounds: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
onPreviewCopyCallback?: (previewCopy: Konva.Image) => void; // New Parameter
|
||||
}) {
|
||||
this.imageNodes = [];
|
||||
this.onDrawCallback = onDraw;
|
||||
|
||||
// TODO: Make this dynamic and update this on change of canvas.
|
||||
this.width = width * 1.5;
|
||||
this.height = height * 1;
|
||||
|
||||
this.positionX = window.innerWidth - this.width;
|
||||
this.positionY = window.innerHeight - this.height;
|
||||
|
||||
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.port = undefined;
|
||||
|
||||
this.fps = 24;
|
||||
|
||||
this.currentPrompt = "";
|
||||
this.currentStrength = 1;
|
||||
|
||||
// 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,
|
||||
stroke: "blue",
|
||||
strokeWidth: 1,
|
||||
cornerRadius: 10,
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
this.offscreenRenderDiv = document.createElement("div");
|
||||
|
||||
this.mediaLayerRef.add(this.captureCanvas);
|
||||
|
||||
this.captureCanvas.setZIndex(0);
|
||||
|
||||
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;
|
||||
|
||||
// this starts the python server
|
||||
public async startServer() {
|
||||
try {
|
||||
this.client = new WebSocketClient("ws://localhost:8765");
|
||||
|
||||
// Set up message handlers
|
||||
this.client.onProgress((progress) => {
|
||||
console.log(
|
||||
`Progress: ${progress.message} (${progress.progress * 100}%)`,
|
||||
);
|
||||
if (progress.error) {
|
||||
console.error("Error:", progress.error);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for connection to be established
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!this.client) return reject("Client not initialized");
|
||||
|
||||
this.client.ws.onopen = () => {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
};
|
||||
this.client.ws.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
// Load initial model
|
||||
await this.loadModel({
|
||||
model_path: "C:/Users/Tensor/Downloads/animagineXL40_v4Opt.safetensors",
|
||||
lora_path: "C:/Users/Tensor/Downloads/anyu_all.safetensors",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to start server:", error);
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadModel(settings: ServerSettings) {
|
||||
if (!this.client || !this.isConnected) {
|
||||
throw new Error("Server not connected");
|
||||
}
|
||||
await this.client.loadModel(settings);
|
||||
}
|
||||
|
||||
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)) {
|
||||
startDrawing(pos);
|
||||
}
|
||||
});
|
||||
|
||||
stage.on("mousemove touchmove", (e) => {
|
||||
const pos = stage.getPointerPosition();
|
||||
if (pos && isWithinCaptureCanvas(pos)) {
|
||||
draw(pos);
|
||||
}
|
||||
});
|
||||
|
||||
stage.on("mouseup touchend", async () => {
|
||||
stopDrawing();
|
||||
//await this.render();
|
||||
});
|
||||
|
||||
// Store cleanup function
|
||||
this.cleanupFunction = () => {
|
||||
stage.off("mousedown touchstart");
|
||||
stage.off("mousemove touchmove");
|
||||
stage.off("mouseup touchend");
|
||||
};
|
||||
}
|
||||
|
||||
public enablePaintMode() {
|
||||
this.isEnabled = true;
|
||||
this.disableDragging();
|
||||
if (!this.cleanupFunction) {
|
||||
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,
|
||||
) {
|
||||
console.log("updateCaptureCanvas");
|
||||
if (!this.captureCanvas) {
|
||||
return;
|
||||
}
|
||||
const scaleFactor = 0.9;
|
||||
// Store old values for calculating delta
|
||||
const oldPositionX = this.positionX;
|
||||
const oldPositionY = this.positionY;
|
||||
|
||||
const oldWidth = this.width;
|
||||
const oldHeight = this.height;
|
||||
|
||||
// Original aspect ratio
|
||||
const originalAspectRatio = oldWidth / oldHeight;
|
||||
|
||||
// Calculate new dimensions based on window size
|
||||
let newWidth: number;
|
||||
let newHeight: number;
|
||||
|
||||
if (width === undefined) {
|
||||
newWidth = window.innerWidth * scaleFactor;
|
||||
} else {
|
||||
newWidth = width * scaleFactor;
|
||||
}
|
||||
|
||||
if (height === undefined) {
|
||||
newHeight = window.innerHeight * scaleFactor;
|
||||
} else {
|
||||
newHeight = height * scaleFactor;
|
||||
}
|
||||
|
||||
// Preserve aspect ratio
|
||||
const newAspectRatio = newWidth / newHeight;
|
||||
|
||||
if (newAspectRatio > originalAspectRatio) {
|
||||
// Width is constraining factor
|
||||
newWidth = newHeight * originalAspectRatio;
|
||||
} else {
|
||||
// Height is constraining factor
|
||||
newHeight = newWidth / originalAspectRatio;
|
||||
}
|
||||
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
|
||||
console.log("New dimensions:", this.width, this.height);
|
||||
|
||||
// Calculate new position to center the canvas in the window
|
||||
this.positionX = (window.innerWidth - this.width) / 2;
|
||||
const headerPadding = 56;
|
||||
this.positionY = (window.innerHeight - headerPadding - this.height) / 2;
|
||||
|
||||
console.log("New centered position:", this.positionX, this.positionY);
|
||||
|
||||
// Update the canvas position and size
|
||||
this.captureCanvas.setPosition({
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
});
|
||||
this.captureCanvas.size({ width: this.width, height: this.height });
|
||||
|
||||
// Calculate scale factors to maintain proportions
|
||||
const scaleX = this.width / oldWidth;
|
||||
const scaleY = this.height / oldHeight;
|
||||
|
||||
// Use a uniform scale factor to preserve aspect ratio
|
||||
const uniformScale = Math.min(scaleX, scaleY);
|
||||
|
||||
// Update all children except the capture canvas
|
||||
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") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pos = node.getPosition();
|
||||
const oldScale = node.scale();
|
||||
|
||||
// Apply both position shift and scaling to maintain relative positioning
|
||||
node.setPosition({
|
||||
x: (pos.x - oldPositionX) * uniformScale + this.positionX,
|
||||
y: (pos.y - oldPositionY) * uniformScale + this.positionY,
|
||||
});
|
||||
|
||||
// Update scale proportionally if the node has a scale property
|
||||
if (
|
||||
oldScale &&
|
||||
typeof oldScale.x === "number" &&
|
||||
typeof oldScale.y === "number"
|
||||
) {
|
||||
node.scale({
|
||||
x: oldScale.x * uniformScale,
|
||||
y: oldScale.y * uniformScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async sendCanvasPayload(renderTask: RenderTask) {
|
||||
if (!this.port) {
|
||||
return console.log("Undefined Worker");
|
||||
}
|
||||
this.port.postMessage(renderTask);
|
||||
}
|
||||
|
||||
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
|
||||
//await this.render();
|
||||
};
|
||||
|
||||
public async addNodes(node: MediaNode) {
|
||||
if (
|
||||
node instanceof ImageNode ||
|
||||
node instanceof TextNode ||
|
||||
node instanceof ShapeNode ||
|
||||
node instanceof PaintNode ||
|
||||
node instanceof PreviewCopyNode
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
//await this.render();
|
||||
}
|
||||
|
||||
public removeNodes(node: MediaNode) {
|
||||
if (
|
||||
node instanceof ImageNode ||
|
||||
node instanceof TextNode ||
|
||||
node instanceof ShapeNode ||
|
||||
node instanceof PaintNode ||
|
||||
node instanceof PreviewCopyNode
|
||||
) {
|
||||
const index = this.imageNodes.indexOf(node);
|
||||
if (index > -1) {
|
||||
node.kNode.off("dragend", this.handleNodeDragEnd);
|
||||
this.imageNodes.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private cloneStageForRender(
|
||||
stage: Konva.Stage,
|
||||
layerOfInterest: Konva.Layer,
|
||||
): Konva.Stage {
|
||||
const stageClone = new Konva.Stage({
|
||||
width: stage.width(),
|
||||
height: stage.height(),
|
||||
container: this.offscreenRenderDiv,
|
||||
});
|
||||
|
||||
const renderLayer = new Konva.Layer();
|
||||
|
||||
// Clone all the nodes then reset them to render right
|
||||
layerOfInterest.getChildren().forEach((node) => {
|
||||
const dupNode = node.clone();
|
||||
dupNode.strokeWidth(0);
|
||||
renderLayer.add(dupNode);
|
||||
});
|
||||
|
||||
stageClone.add(renderLayer);
|
||||
|
||||
return stageClone;
|
||||
}
|
||||
|
||||
private async 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];
|
||||
}
|
||||
|
||||
public async saveOutput() {
|
||||
if (!this.outputBitmap) {
|
||||
console.error("No output bitmap available to save");
|
||||
return;
|
||||
}
|
||||
|
||||
const base64Bitmap = await EncodeImageBitmapToBase64(this.outputBitmap);
|
||||
|
||||
const saveResponse = await invoke("save_image", {
|
||||
image: base64Bitmap,
|
||||
});
|
||||
}
|
||||
|
||||
public async generateImage() {
|
||||
if (!this.lastRenderedBitmap) {
|
||||
console.error("No rendered bitmap available to generate from");
|
||||
return;
|
||||
}
|
||||
|
||||
const base64Bitmap = await this.imageBitmapToBase64(
|
||||
this.lastRenderedBitmap,
|
||||
);
|
||||
|
||||
const generateResponse = await invoke("image_generation_command", {
|
||||
image: base64Bitmap,
|
||||
prompt: this.currentPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
public async render() {
|
||||
// only pick nodes that intersect wi th the canvas on screen bounds to freeze.
|
||||
if (this.isProcessing) {
|
||||
console.log("isProcessing Returning");
|
||||
return;
|
||||
}
|
||||
console.log("Calling Render");
|
||||
this.isProcessing = true;
|
||||
|
||||
this.mediaLayerRef.draw();
|
||||
// Output all nodes in mediaLayerRef
|
||||
const nodes = this.mediaLayerRef.getChildren();
|
||||
console.log("All nodes in mediaLayer:", nodes);
|
||||
console.log(
|
||||
`context: x:${this.positionX} y:${this.positionY} ${this.width} x ${this.height}`,
|
||||
);
|
||||
|
||||
const bitmap = (await this.renderFrame({
|
||||
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: false,
|
||||
})) as ImageBitmap;
|
||||
|
||||
this.lastRenderedBitmap = bitmap;
|
||||
|
||||
setCanvasRenderBitmap(bitmap);
|
||||
|
||||
try {
|
||||
} catch (error) {
|
||||
console.error("Error during image processing:", error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Add method to check connection status
|
||||
public isServerConnected(): boolean {
|
||||
return this.isConnected && this.client !== null;
|
||||
}
|
||||
|
||||
// Add method to reconnect if needed
|
||||
public async reconnect() {
|
||||
if (this.client) {
|
||||
this.client.ws.close();
|
||||
}
|
||||
await this.startServer();
|
||||
}
|
||||
|
||||
private async renderFrame(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 | Blob> {
|
||||
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.cloneStageForRender(stage, config.layerOfInterest);
|
||||
const stageBlob = (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(stageBlob, "1");
|
||||
}
|
||||
|
||||
const result = await createImageBitmap(stageBlob);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add method to create or update background
|
||||
public updateBackground(color: string) {
|
||||
this.captureCanvas.fill(color);
|
||||
|
||||
const captureCanvasImage = this.captureCanvas.toDataURL();
|
||||
const imageSource = new Image();
|
||||
imageSource.src = captureCanvasImage;
|
||||
|
||||
imageSource.onload = () => {
|
||||
this.backgroundRasterRect.fill(color);
|
||||
this.backgroundRasterRect.image(imageSource);
|
||||
|
||||
this.mediaLayerRef.batchDraw();
|
||||
//this.render();
|
||||
};
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { bool } from "@techstark/opencv-js";
|
||||
|
||||
export enum WorkerEvent {
|
||||
START = "start",
|
||||
PROGRESS = "progress",
|
||||
COMPLETE = "complete",
|
||||
DATA = "data",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
export interface RenderTaskPayload {
|
||||
offscreenCanvas: OffscreenCanvas;
|
||||
id: number;
|
||||
}
|
||||
|
||||
// RenderSharedWorker.ts
|
||||
export interface RenderTask {
|
||||
jobID: number;
|
||||
isDone: bool;
|
||||
event: WorkerEvent;
|
||||
data: RenderTaskPayload | undefined;
|
||||
}
|
||||
|
||||
export interface RenderTaskProgress {
|
||||
isProcessing: boolean;
|
||||
progress: number;
|
||||
jobID: number;
|
||||
event: WorkerEvent;
|
||||
}
|
||||
|
||||
export interface RenderTaskResult {
|
||||
event: Worker;
|
||||
jobID: number;
|
||||
data: {
|
||||
mediaUrl: string;
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
const dataURL = this.videoLayer.toDataURL({
|
||||
x: this.captureCanvas.x(),
|
||||
y: this.captureCanvas.y(),
|
||||
width: this.captureCanvas.width(),
|
||||
height: this.captureCanvas.height(),
|
||||
});
|
||||
|
||||
|
||||
// Create an image element to display the captured area
|
||||
const img = new Image();
|
||||
img.src = dataURL;
|
||||
document.body.appendChild(img); // Append the image to the body or any other container
|
||||
|
||||
// Optionally, you can download the captured image
|
||||
const link = document.createElement("a");
|
||||
link.href = dataURL;
|
||||
link.download = "snapshot.png";
|
||||
link.click();
|
||||
@@ -1,77 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ICommand } from "./ICommand";
|
||||
import { MediaNode } from "../types";
|
||||
import {
|
||||
NodesManager,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { toolbarNode } from "../../signals/uiAccess/toolbarNode";
|
||||
|
||||
export class CreateCommand implements ICommand {
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodes: Set<MediaNode>;
|
||||
private zIndices: Map<MediaNode, number>;
|
||||
|
||||
private nodesManagerRef: NodesManager;
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
|
||||
private renderEngineRef: RealTimeDrawEngine;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
nodeTransformerRef,
|
||||
renderEngineRef,
|
||||
selectionManagerRef,
|
||||
}: {
|
||||
nodes: Set<MediaNode>;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
renderEngineRef: RealTimeDrawEngine;
|
||||
selectionManagerRef: SelectionManager;
|
||||
}) {
|
||||
this.nodes = nodes;
|
||||
this.zIndices = new Map(nodesManagerRef.getAllZIndices());
|
||||
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
this.nodeTransformerRef = nodeTransformerRef;
|
||||
this.renderEngineRef = renderEngineRef;
|
||||
this.selectionManagerRef = selectionManagerRef;
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.mediaLayerRef.add(node.kNode);
|
||||
this.nodesManagerRef.saveNode(node);
|
||||
this.renderEngineRef.addNodes(node);
|
||||
});
|
||||
this.mediaLayerRef.draw();
|
||||
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.nodesManagerRef.removeNode(node);
|
||||
this.renderEngineRef.removeNodes(node);
|
||||
this.selectionManagerRef.deselectNode(node);
|
||||
node.kNode.remove();
|
||||
if (
|
||||
this.zIndices.get(node) &&
|
||||
node.kNode.getZIndex() !== this.zIndices.get(node)
|
||||
) {
|
||||
node.kNode.setZIndex(this.zIndices.get(node));
|
||||
}
|
||||
});
|
||||
this.nodeTransformerRef.clear();
|
||||
toolbarNode.hide();
|
||||
this.mediaLayerRef.draw();
|
||||
this.renderEngineRef.render();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ICommand } from "./ICommand";
|
||||
import { MediaNode } from "../types";
|
||||
import {
|
||||
NodesManager,
|
||||
NodeTransformer,
|
||||
SelectionManager,
|
||||
} from "../NodesManagers";
|
||||
import { RealTimeDrawEngine as RenderEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
|
||||
import { toolbarNode } from "../../signals/uiAccess/toolbarNode";
|
||||
|
||||
export class DeleteCommand implements ICommand {
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodes: Set<MediaNode>;
|
||||
private zIndices: Map<MediaNode, number> = new Map();
|
||||
|
||||
private nodesManagerRef: NodesManager;
|
||||
private nodeTransformerRef: NodeTransformer;
|
||||
private selectionManagerRef: SelectionManager;
|
||||
|
||||
private renderEngineRef: RenderEngine;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
nodeTransformerRef,
|
||||
renderEngineRef,
|
||||
selectionManagerRef,
|
||||
}: {
|
||||
nodes: Set<MediaNode>;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
nodeTransformerRef: NodeTransformer;
|
||||
renderEngineRef: RenderEngine;
|
||||
selectionManagerRef: SelectionManager;
|
||||
}) {
|
||||
this.nodes = new Set<MediaNode>(nodes);
|
||||
this.zIndices = new Map(nodesManagerRef.getAllZIndices());
|
||||
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
this.nodeTransformerRef = nodeTransformerRef;
|
||||
this.renderEngineRef = renderEngineRef;
|
||||
this.selectionManagerRef = selectionManagerRef;
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.nodesManagerRef.removeNode(node);
|
||||
this.renderEngineRef.removeNodes(node);
|
||||
this.selectionManagerRef.deselectNode(node);
|
||||
node.kNode.remove();
|
||||
});
|
||||
this.nodeTransformerRef.clear();
|
||||
toolbarNode.hide();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.nodes.forEach((node) => {
|
||||
this.mediaLayerRef.add(node.kNode);
|
||||
this.nodesManagerRef.saveNode(node);
|
||||
this.renderEngineRef.addNodes(node);
|
||||
});
|
||||
this.nodesManagerRef.getAllNodes().forEach((node) => {
|
||||
if (
|
||||
this.zIndices.get(node) &&
|
||||
node.kNode.getZIndex() !== this.zIndices.get(node)
|
||||
) {
|
||||
node.kNode.setZIndex(this.zIndices.get(node));
|
||||
}
|
||||
});
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface ICommand {
|
||||
execute(): void | boolean | Promise<boolean>;
|
||||
undo(): void;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { MediaNode } from "../types";
|
||||
import { ICommand } from "./ICommand";
|
||||
|
||||
export class LockNodesCommand implements ICommand {
|
||||
private nodes: Set<MediaNode>;
|
||||
constructor({ nodes }: { nodes: Set<MediaNode> }) {
|
||||
this.nodes = new Set(nodes);
|
||||
}
|
||||
execute() {
|
||||
this.nodes.forEach((node) => {
|
||||
node.lock();
|
||||
});
|
||||
}
|
||||
undo() {
|
||||
this.nodes.forEach((node) => {
|
||||
node.unlock();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ICommand } from "./ICommand";
|
||||
import { MediaNode } from "../types";
|
||||
import { NodesManager } from "../NodesManagers";
|
||||
|
||||
export class MoveLayerDown implements ICommand {
|
||||
private sortedMovingNodes: [MediaNode, number][];
|
||||
private sortedAllNodesByZ: [MediaNode, number][];
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
}: {
|
||||
nodes: Set<MediaNode>;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
}) {
|
||||
this.sortedMovingNodes = Array.from(nodes)
|
||||
.reduce(
|
||||
(accNodes, currNode) => {
|
||||
accNodes.push([currNode, currNode.kNode.getZIndex()]);
|
||||
return accNodes;
|
||||
},
|
||||
[] as [MediaNode, number][],
|
||||
)
|
||||
// this sort the remapped array in ascending order
|
||||
.sort(([, zA], [, zB]) => zA - zB);
|
||||
|
||||
// set up references
|
||||
this.sortedAllNodesByZ = nodesManagerRef.getSortedZIndices();
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
}
|
||||
|
||||
execute() {
|
||||
let hasMovedNode = false;
|
||||
let currMinIndex = 0;
|
||||
this.sortedMovingNodes.forEach(([movingNode, movingNodeZindex]) => {
|
||||
if (movingNodeZindex === currMinIndex) {
|
||||
currMinIndex = currMinIndex + 1;
|
||||
} else {
|
||||
movingNode.moveLayerDown();
|
||||
hasMovedNode = true;
|
||||
}
|
||||
});
|
||||
this.nodesManagerRef.updateAllZIndices();
|
||||
this.mediaLayerRef.draw();
|
||||
// if (import.meta.env.DEV && !hasMovedNode) {
|
||||
// console.log("No Node Moved-DOWN");
|
||||
// }
|
||||
return hasMovedNode;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.sortedAllNodesByZ.forEach(([node, zIndex]) => {
|
||||
node.kNode.setZIndex(zIndex);
|
||||
});
|
||||
this.nodesManagerRef.updateAllZIndices();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ICommand } from "./ICommand";
|
||||
import { MediaNode } from "../types";
|
||||
import { NodesManager } from "../NodesManagers";
|
||||
|
||||
export class MoveLayerUp implements ICommand {
|
||||
private sortedMovingNodes: [MediaNode, number][];
|
||||
private sortedAllNodesByZ: [MediaNode, number][];
|
||||
private mediaLayerRef: Konva.Layer;
|
||||
private nodesManagerRef: NodesManager;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
mediaLayerRef,
|
||||
nodesManagerRef,
|
||||
}: {
|
||||
nodes: Set<MediaNode>;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
nodesManagerRef: NodesManager;
|
||||
}) {
|
||||
this.sortedMovingNodes = Array.from(nodes)
|
||||
.reduce(
|
||||
(accNodes, currNode) => {
|
||||
accNodes.push([currNode, currNode.kNode.getZIndex()]);
|
||||
return accNodes;
|
||||
},
|
||||
[] as [MediaNode, number][],
|
||||
)
|
||||
// this sort the remapped array in descending order
|
||||
.sort(([, zA], [, zB]) => zB - zA);
|
||||
|
||||
// set up references
|
||||
this.sortedAllNodesByZ = nodesManagerRef.getSortedZIndices();
|
||||
this.mediaLayerRef = mediaLayerRef;
|
||||
this.nodesManagerRef = nodesManagerRef;
|
||||
}
|
||||
|
||||
execute() {
|
||||
let hasMovedNode = false;
|
||||
let currMaxIndex = this.sortedAllNodesByZ.length;
|
||||
this.sortedMovingNodes.forEach(([movingNode, movingNodeZindex]) => {
|
||||
if (movingNodeZindex === currMaxIndex) {
|
||||
currMaxIndex = currMaxIndex - 1;
|
||||
} else {
|
||||
movingNode.moveLayerUp();
|
||||
hasMovedNode = true;
|
||||
}
|
||||
});
|
||||
this.nodesManagerRef.updateAllZIndices();
|
||||
this.mediaLayerRef.draw();
|
||||
// if (import.meta.env.DEV && !hasMovedNode) {
|
||||
// console.log("No Node Moved-UP");
|
||||
// }
|
||||
return hasMovedNode;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.sortedAllNodesByZ.forEach(([node, zIndex]) => {
|
||||
node.kNode.setZIndex(zIndex);
|
||||
});
|
||||
this.nodesManagerRef.updateAllZIndices();
|
||||
this.mediaLayerRef.draw();
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ICommand } from "./ICommand";
|
||||
import { MediaNode, Transformation } from "../types";
|
||||
|
||||
export class TransformCommand implements ICommand {
|
||||
private nodes: Set<MediaNode>;
|
||||
private initialTransformations: Map<MediaNode, Transformation[]>;
|
||||
private finalTransformations: Map<MediaNode, Transformation[]>;
|
||||
private layerRef: Konva.Layer;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
initialTransformations,
|
||||
finalTransformations,
|
||||
layerRef,
|
||||
}: {
|
||||
nodes: Set<MediaNode>;
|
||||
initialTransformations: Map<MediaNode, Transformation[]>;
|
||||
finalTransformations: Map<MediaNode, Transformation[]>;
|
||||
layerRef: Konva.Layer;
|
||||
}) {
|
||||
this.nodes = new Set<MediaNode>(nodes);
|
||||
this.initialTransformations = initialTransformations;
|
||||
this.finalTransformations = finalTransformations;
|
||||
this.layerRef = layerRef;
|
||||
console.log("transform command", this);
|
||||
}
|
||||
private transformKNode(kNode: Konva.Node, transform: Transformation) {
|
||||
kNode.setAttrs({
|
||||
position: transform.position,
|
||||
size: transform.size,
|
||||
rotation: transform.rotation,
|
||||
scale: transform.scale,
|
||||
});
|
||||
}
|
||||
private findKNodeOfTransform = (
|
||||
node: MediaNode,
|
||||
transform: Transformation,
|
||||
) => {
|
||||
const parentKNode = node.kNode;
|
||||
if (parentKNode._id === transform.kNodeId) {
|
||||
this.transformKNode(parentKNode, transform);
|
||||
return;
|
||||
}
|
||||
if (parentKNode instanceof Konva.Group) {
|
||||
const childKNode = parentKNode
|
||||
.getChildren()
|
||||
.find((currChild) => currChild._id === transform.kNodeId);
|
||||
if (childKNode) {
|
||||
this.transformKNode(childKNode, transform);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn("Error in Transform", node, transform);
|
||||
}
|
||||
};
|
||||
|
||||
execute() {
|
||||
this.nodes.forEach((node) => {
|
||||
const finalTransformations = this.finalTransformations.get(node);
|
||||
if (finalTransformations) {
|
||||
finalTransformations.forEach((transform) =>
|
||||
this.findKNodeOfTransform(node, transform),
|
||||
);
|
||||
}
|
||||
});
|
||||
this.layerRef.draw();
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.nodes.forEach((node) => {
|
||||
const initialTransformations = this.initialTransformations.get(node);
|
||||
if (initialTransformations) {
|
||||
initialTransformations.forEach((transform) =>
|
||||
this.findKNodeOfTransform(node, transform),
|
||||
);
|
||||
}
|
||||
});
|
||||
this.layerRef.draw();
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import { ICommand } from "./ICommand";
|
||||
import { MediaNode, Position } from "../types";
|
||||
|
||||
export class TranslateCommand implements ICommand {
|
||||
private nodes: Set<MediaNode>;
|
||||
private initialPositions: Map<MediaNode, Position>;
|
||||
private finalPositions: Map<MediaNode, Position>;
|
||||
private layerRef: Konva.Layer;
|
||||
|
||||
constructor({
|
||||
nodes,
|
||||
initialPositions,
|
||||
finalPositions,
|
||||
layerRef,
|
||||
}: {
|
||||
nodes: Set<MediaNode>;
|
||||
initialPositions: Map<MediaNode, Position>;
|
||||
finalPositions: Map<MediaNode, Position>;
|
||||
layerRef: Konva.Layer;
|
||||
}) {
|
||||
this.nodes = new Set<MediaNode>(nodes);
|
||||
this.initialPositions = initialPositions;
|
||||
this.finalPositions = finalPositions;
|
||||
this.layerRef = layerRef;
|
||||
}
|
||||
execute() {
|
||||
this.nodes.forEach((node) => {
|
||||
const finalPosition = this.finalPositions.get(node);
|
||||
if (finalPosition) {
|
||||
node.kNode.position(finalPosition);
|
||||
}
|
||||
});
|
||||
this.layerRef.draw();
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.nodes.forEach((node) => {
|
||||
const initialPosition = this.initialPositions.get(node);
|
||||
if (initialPosition) {
|
||||
node.kNode.position(initialPosition);
|
||||
}
|
||||
});
|
||||
this.layerRef.draw();
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ICommand } from "./ICommand";
|
||||
|
||||
export class UndoStackManager {
|
||||
private disabled: boolean = false;
|
||||
private undoStack: ICommand[] = [];
|
||||
private redoStack: ICommand[] = [];
|
||||
private readonly STACK_LIMIT = 20;
|
||||
private onCommandExecuted: () => void;
|
||||
|
||||
constructor(onCommandExecuted: () => void) {
|
||||
this.onCommandExecuted = onCommandExecuted;
|
||||
}
|
||||
|
||||
setDisabled(newState: boolean) {
|
||||
this.disabled = newState;
|
||||
}
|
||||
isDisabled() {
|
||||
return this.disabled;
|
||||
}
|
||||
async executeCommand(command: ICommand) {
|
||||
const result = await command.execute();
|
||||
if (result !== undefined && result === false) {
|
||||
//command is not executed if it deliberatly returned false;
|
||||
//in that case, no need to do anything
|
||||
return;
|
||||
}
|
||||
this.pushCommand(command);
|
||||
}
|
||||
pushCommand(command: ICommand) {
|
||||
this.undoStack.push(command);
|
||||
this.redoStack = []; // Clear the redo stack
|
||||
|
||||
// Ensure the undo stack does not exceed the limit
|
||||
if (this.undoStack.length > this.STACK_LIMIT) {
|
||||
this.undoStack.shift(); // Remove the oldest command
|
||||
}
|
||||
console.log("command stack:", this.undoStack);
|
||||
}
|
||||
clear() {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
undo() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const command = this.undoStack.pop();
|
||||
if (command) {
|
||||
command.undo();
|
||||
this.redoStack.push(command);
|
||||
|
||||
// Ensure the redo stack does not exceed the limit
|
||||
if (this.redoStack.length > this.STACK_LIMIT) {
|
||||
this.redoStack.shift(); // Remove the oldest command
|
||||
}
|
||||
console.log("undo:", this.undoStack);
|
||||
}
|
||||
this.onCommandExecuted();
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const command = this.redoStack.pop();
|
||||
if (command) {
|
||||
command.execute();
|
||||
this.undoStack.push(command);
|
||||
|
||||
// Ensure the undo stack does not exceed the limit
|
||||
if (this.undoStack.length > this.STACK_LIMIT) {
|
||||
this.undoStack.shift(); // Remove the oldest command
|
||||
}
|
||||
|
||||
console.log("Redo:", this.redoStack);
|
||||
}
|
||||
this.onCommandExecuted();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { MediaNode } from "../types";
|
||||
import { ICommand } from "./ICommand";
|
||||
|
||||
export class UnlockNodesCommand implements ICommand {
|
||||
private nodes: Set<MediaNode>;
|
||||
constructor({ nodes }: { nodes: Set<MediaNode> }) {
|
||||
this.nodes = new Set(nodes);
|
||||
}
|
||||
execute() {
|
||||
this.nodes.forEach((node) => {
|
||||
node.unlock();
|
||||
});
|
||||
}
|
||||
undo() {
|
||||
this.nodes.forEach((node) => {
|
||||
node.lock();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export { UndoStackManager } from "./UndoRedoManager";
|
||||
export { CreateCommand } from "./CreateCommand";
|
||||
export { DeleteCommand } from "./DeleteCommand";
|
||||
export { LockNodesCommand } from "./LockNodesCommand";
|
||||
export { MoveLayerUp } from "./MoveLayerUp";
|
||||
export { MoveLayerDown } from "./MoveLayerDown";
|
||||
export { TransformCommand } from "./TransformCommand";
|
||||
export { TranslateCommand } from "./TranslateCommand";
|
||||
export { UnlockNodesCommand } from "./UnlockNodesCommand";
|
||||
@@ -1,25 +0,0 @@
|
||||
export enum AppModes {
|
||||
INIT = "INIT",
|
||||
PREVIEW = "PREVIEW",
|
||||
SELECT = "SELECT",
|
||||
PAINT = "PAINT",
|
||||
ERASER = "ERASER",
|
||||
RENDERING = "RENDERING",
|
||||
}
|
||||
|
||||
export enum EditModes {
|
||||
SELECT = "SELECT",
|
||||
EDIT = "EDIT",
|
||||
}
|
||||
|
||||
export const VideoResolutions = {
|
||||
VERTICAL_720: { width: 720, height: 1280 },
|
||||
LANDSCAPE_720: { width: 1280, height: 720 },
|
||||
SQUARE_1024: { width: 1024, height: 1024 },
|
||||
};
|
||||
|
||||
export const Colors = {
|
||||
transparent: "rgba(0,0,0,0)",
|
||||
translucentBlack: "rgba(0,0,0,0.3)",
|
||||
primaryOrange: "rgba(45, 129, 255, 1)",
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// to do fix https://codesandbox.io/p/sandbox/react-konva-infinite-grid-kkndq?file=%2Fsrc%2Findex.js the dotted background doesn't move when draggable.
|
||||
import { Engine } from "../Engine";
|
||||
import { EngineOptions } from "./types";
|
||||
export { Engine as EngineType };
|
||||
export const KonvaApp = (element: HTMLDivElement, options: EngineOptions) => {
|
||||
const engine = new Engine(element, options);
|
||||
engine.initializeStage(options.sceneToken);
|
||||
return engine;
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { NodeType, BaseNode, ImageNode, TextNode, ShapeNode } from "../Nodes";
|
||||
|
||||
import { TransformationData } from "./Transformation";
|
||||
|
||||
import { TextNodeData } from "./Text";
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
|
||||
export type MediaNode =
|
||||
// | NetworkedNode
|
||||
BaseNode | ImageNode | TextNode | ShapeNode | PaintNode;
|
||||
|
||||
export type ImageNodeData = {
|
||||
mediaFileUrl: string;
|
||||
mediaFileToken?: string;
|
||||
};
|
||||
export type VideoNodeData = {
|
||||
mediaFileUrl: string;
|
||||
mediaFileToken?: string;
|
||||
videoWidth?: number;
|
||||
videoHeight?: number;
|
||||
isChroma: boolean;
|
||||
chromaColor: {
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
};
|
||||
extractionUrl?: string;
|
||||
};
|
||||
|
||||
export type ShapeNodeData = {
|
||||
shape: "triangle" | "square" | "circle";
|
||||
}
|
||||
|
||||
export type NodeData = {
|
||||
type: NodeType;
|
||||
transform: TransformationData;
|
||||
|
||||
// Text Node Data
|
||||
imageNodeData?: ImageNodeData;
|
||||
textNodeData?: TextNodeData;
|
||||
videoNodeData?: VideoNodeData;
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
export enum FontWeight {
|
||||
NORMAL = "normal",
|
||||
BOLD = "bold",
|
||||
}
|
||||
export enum FontStyle {
|
||||
NORMAL = "normal",
|
||||
ITALIC = "italic",
|
||||
}
|
||||
export enum TextAlign {
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
CENTER = "center",
|
||||
// JUSTIFY = "justify",
|
||||
}
|
||||
export enum TextDecoration {
|
||||
NONE = "none",
|
||||
STRIKETHROUGH = "line-through",
|
||||
UNDERLINE = "underline",
|
||||
}
|
||||
export type TextFormatData = {
|
||||
color: string;
|
||||
maxWidth: number;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
fontStyle: FontStyle;
|
||||
fontWeight: FontWeight;
|
||||
textAlign: TextAlign;
|
||||
textDecoration: TextDecoration;
|
||||
};
|
||||
export type TextNodeData = {
|
||||
text: string;
|
||||
} & TextFormatData;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Position, Size, Scale } from "./basicValueTypes";
|
||||
|
||||
// This is meant for the transformation command
|
||||
export interface Transformation {
|
||||
kNodeId: number | string;
|
||||
position: Position;
|
||||
size: Size;
|
||||
scale: Scale;
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
// This is meant for saving transformation data
|
||||
// for scene saving and loading
|
||||
export interface TransformationData extends Omit<Transformation, "kNodeId"> {
|
||||
zIndex: number;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Vector2d } from "konva/lib/types";
|
||||
|
||||
export interface Position extends Vector2d {}
|
||||
export interface Scale extends Vector2d {}
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface RGBColor {
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { NetworkedNode } from "../Nodes/NetworkedNode";
|
||||
|
||||
export interface ProgressEventDetails<S> {
|
||||
progress: number;
|
||||
message?: string;
|
||||
status?: S;
|
||||
}
|
||||
export interface NodeProgressEventDetail<S> extends ProgressEventDetails<S> {
|
||||
node: NetworkedNode;
|
||||
}
|
||||
|
||||
export enum VideoExtractionEvents {
|
||||
"SESSION_CREATING" = "SESSION_CREATING",
|
||||
"SESSION_IDLE" = "SESSION_IDLE",
|
||||
"EXTRACTION_POINT_REQUEST" = "EXTRACTION_POINT_REQUEST",
|
||||
"SESSION_CLOSING" = "SESSION_CLOSING",
|
||||
"SESSION_CLOSED" = "SESSION_CLOSED",
|
||||
}
|
||||
export interface VideoExtractionEventDetails
|
||||
extends ProgressEventDetails<VideoExtractionEvents> {
|
||||
status: VideoExtractionEvents;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
export interface EngineOptions {
|
||||
navigate: NavigateFunction;
|
||||
sceneToken?: string;
|
||||
}
|
||||
|
||||
export * from "./basicValueTypes";
|
||||
export * from "./NodeData";
|
||||
export * from "./Text";
|
||||
export * from "./Transformation";
|
||||
@@ -1,16 +0,0 @@
|
||||
import { forwardRef, HTMLAttributes } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
// import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
|
||||
export const KonvaCanvasContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
// useRenderCounter("KonvaCanvasContainer");
|
||||
|
||||
const classes = twMerge("pegboard -z-10", "h-[calc(100vh-56px)]", className);
|
||||
return <div ref={ref} className={classes} {...props} />;
|
||||
});
|
||||
|
||||
KonvaCanvasContainer.displayName = "KonvaCanvasContainer";
|
||||
@@ -1,174 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
// Components
|
||||
import { KonvaCanvasContainer } from "./KonvaCanvasContainer";
|
||||
import { ContextualToolbarNode } from "./ContextualToolbarNode";
|
||||
// import { SignaledCanvasDragDropFiles } from "./SignaledCanvasDragDropFiles";
|
||||
// import { SignaledDialogs } from "./SignaledDialogs";
|
||||
import { SignaledToolbarMain } from "./SignaledToolbarMain";
|
||||
|
||||
// The KonvaApp is the root of the Konva stage
|
||||
// and only entry point for anything in Konva JS
|
||||
import { EngineType } from "./KonvaApp";
|
||||
import { KonvaApp } from "./KonvaApp";
|
||||
|
||||
// all the signal-contexts are wrapped in hooks
|
||||
import { useAppUiContext } from "./contextSignals/appUi";
|
||||
import { useLayoutContext } from "./contextSignals/layout";
|
||||
|
||||
// common hooks
|
||||
import { useRenderCounter } from "./useRenderCounter";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./global.css";
|
||||
// TODO REFACTOR THIS INTO THE COMPONENT LIBRARY its used in 3d and 2d
|
||||
|
||||
// These imports are required for the 2D prompt box component
|
||||
// Job context is currently missing
|
||||
import { PromptBox2D } from "@storyteller/ui-promptbox";
|
||||
import { UndoRedo } from "./components/reusable/UndoRedo/UndoRedo";
|
||||
import { getCanvasRenderBitmap } from "./signals/canvasRenderBitmap";
|
||||
import { EncodeImageBitmapToBase64 } from "./utilities/EncodeImageBitmapToBase64";
|
||||
import { uploadImage } from "../../components/reusable/UploadModalMedia/uploadImage";
|
||||
// import { getCanvasRenderBitmap } from "./signals/canvasRenderBitmap";
|
||||
// import { EncodeImageBitmapToBase64 } from "./utilities/EncodeImageBitmapToBase64";
|
||||
import { JobProvider, useJobContext } from "./JobContext";
|
||||
import { RealTimeDrawEngine } from "./KonvaApp/RenderingPrimitives/RealTimeDrawEngine";
|
||||
// import { VideoResolutions } from "./KonvaApp/constants";
|
||||
|
||||
import {
|
||||
DialogAddImage,
|
||||
DialogEditText,
|
||||
DialogError,
|
||||
} from "./components/features";
|
||||
import { dispatchUiEvents } from "./signals";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { dialogError } from "./signals/uiAccess/dialogError";
|
||||
|
||||
export const KonvaRootComponent = ({
|
||||
className,
|
||||
sceneToken,
|
||||
}: {
|
||||
className: string;
|
||||
sceneToken?: string;
|
||||
}) => {
|
||||
useSignals();
|
||||
// This is a hook that will log the number of times the component has rerendered
|
||||
// Let's make sure we only log once
|
||||
useRenderCounter("KonvaRootComponent");
|
||||
const navigate = useNavigate();
|
||||
const appUiContext = useAppUiContext();
|
||||
const layoutContext = useLayoutContext();
|
||||
|
||||
// Add state to track engine initialization
|
||||
const [isEngineReady, setIsEngineReady] = useState(false);
|
||||
const engineRef = useRef<EngineType | null>(null);
|
||||
const renderEngineRef = useRef<RealTimeDrawEngine | null>(null);
|
||||
|
||||
const konvaContainerCallbackRef = useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
// Only initialize if we have a node and haven't initialized yet
|
||||
if (node !== null && !isEngineReady && engineRef.current === null) {
|
||||
try {
|
||||
const options = {
|
||||
navigate: navigate,
|
||||
sceneToken: sceneToken,
|
||||
};
|
||||
|
||||
engineRef.current = KonvaApp(node, options);
|
||||
renderEngineRef.current = engineRef.current.realTimeDrawEngine;
|
||||
// Only set ready if initialization succeeded
|
||||
if (engineRef.current) {
|
||||
setIsEngineReady(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize KonvaApp:", error);
|
||||
engineRef.current = null;
|
||||
setIsEngineReady(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[navigate, sceneToken, isEngineReady],
|
||||
);
|
||||
|
||||
// Modal stuff
|
||||
const props = dialogError.signal.value;
|
||||
const { isShowing, title, message } = props;
|
||||
const onClose = useCallback(() => {
|
||||
dialogError.hide();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Always render the canvas container */}
|
||||
<KonvaCanvasContainer
|
||||
ref={konvaContainerCallbackRef}
|
||||
className={className}
|
||||
/>
|
||||
|
||||
{/* Conditionally render the loading overlay */}
|
||||
{!isEngineReady && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-gray-500">Initializing canvas...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Conditionally render the UI components that need the engine */}
|
||||
{isEngineReady && engineRef.current && (
|
||||
<UndoRedo engine={engineRef.current}>
|
||||
{/* <SignaledCanvasDragDropFiles
|
||||
openAddImage={appUiContext.openAddImage}
|
||||
openAddVideo={appUiContext.openAddVideo}
|
||||
/> */}
|
||||
<JobProvider>
|
||||
<PromptBox2D
|
||||
uploadImage={uploadImage}
|
||||
getCanvasRenderBitmap={getCanvasRenderBitmap}
|
||||
EncodeImageBitmapToBase64={EncodeImageBitmapToBase64}
|
||||
useJobContext={useJobContext}
|
||||
onEnqueuePressed={async () => {
|
||||
// will set the snapshot of the canvas internally.
|
||||
await renderEngineRef.current?.render();
|
||||
}}
|
||||
/>{" "}
|
||||
</JobProvider>
|
||||
|
||||
<SignaledToolbarMain
|
||||
layoutSignal={layoutContext.signal}
|
||||
appUiContext={appUiContext}
|
||||
/>
|
||||
{/* <SignaledDialogs
|
||||
appUiSignal={appUiContext.signal}
|
||||
resetAll={appUiContext.resetAll}
|
||||
/> */}
|
||||
<ContextualToolbarNode />
|
||||
</UndoRedo>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
|
||||
<DialogEditText
|
||||
isOpen={appUiContext.signal.value.isEditTextOpen}
|
||||
onDoneEditText={(data) => {
|
||||
dispatchUiEvents.addTextToEngine(data);
|
||||
}}
|
||||
closeCallback={appUiContext.resetAll}
|
||||
/>
|
||||
|
||||
<DialogAddImage
|
||||
isOpen={appUiContext.signal.value.isAddImageOpen}
|
||||
stagedImage={appUiContext.signal.value.stagedImage}
|
||||
closeCallback={appUiContext.resetAll}
|
||||
onAddImage={(file) => {
|
||||
dispatchUiEvents.addImageToEngine(file);
|
||||
}}
|
||||
/>
|
||||
|
||||
<DialogError
|
||||
isShowing={isShowing}
|
||||
title={title}
|
||||
message={message}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { Signal } from "@preact/signals-react";
|
||||
import { dispatchUiEvents } from "./signals/uiEvents";
|
||||
import {
|
||||
DialogAddImage,
|
||||
DialogEditText,
|
||||
DialogError,
|
||||
} from "./components/features";
|
||||
import { AppUiSignalType } from "./contextSignals/appUi";
|
||||
import { dialogError } from "./signals/uiAccess/dialogError";
|
||||
|
||||
export const SignaledDialogs = ({
|
||||
appUiSignal,
|
||||
resetAll,
|
||||
}: {
|
||||
appUiSignal: Signal<AppUiSignalType>;
|
||||
resetAll: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* Dialogs that are opened from within Reactland*/}
|
||||
<DialogAddImage
|
||||
isOpen={appUiSignal.value.isAddImageOpen ?? false}
|
||||
stagedImage={appUiSignal.value.stagedImage}
|
||||
closeCallback={resetAll}
|
||||
onAddImage={(file) => {
|
||||
dispatchUiEvents.addImageToEngine(file);
|
||||
}}
|
||||
/>
|
||||
<DialogEditText
|
||||
isOpen={appUiSignal.value.isEditTextOpen ?? false}
|
||||
onDoneEditText={(data) => {
|
||||
dispatchUiEvents.addTextToEngine(data);
|
||||
}}
|
||||
closeCallback={resetAll}
|
||||
/>
|
||||
{/* Dialogs that are opened from the Engine*/}
|
||||
<SignaledDialogError />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SignaledDialogError = () => {
|
||||
const props = dialogError.signal.value;
|
||||
const { isShowing, title, message } = props;
|
||||
const onClose = useCallback(() => {
|
||||
dialogError.hide();
|
||||
}, []);
|
||||
return (
|
||||
<DialogError
|
||||
isShowing={isShowing}
|
||||
title={title}
|
||||
message={message}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import { MouseEventHandler } from "react";
|
||||
// import { useSignalEffect } from "@preact/signals-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { toolbarMain } from "./signals/uiAccess/toolbarMain";
|
||||
import { dispatchers } from "./signals/uiEvents/toolbarMain";
|
||||
|
||||
import { ToolbarMain } from "./components/features/ToolbarMain";
|
||||
import { LoadingBar } from "./components/ui/LoadingBar";
|
||||
|
||||
import { ToolbarMainButtonNames } from "./components/features/ToolbarMain/enum";
|
||||
import { LayoutSignalType } from "./contextSignals/layout";
|
||||
import { AppUiContextInterface } from "./contextSignals/appUi";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { paperWrapperStyles } from "./components/styles";
|
||||
import { dispatchUiEvents } from "./signals/uiEvents";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const SignaledToolbarMain = ({
|
||||
// layoutSignal,
|
||||
appUiContext,
|
||||
}: {
|
||||
layoutSignal: LayoutSignalType;
|
||||
appUiContext: AppUiContextInterface;
|
||||
}) => {
|
||||
useSignals();
|
||||
const loadingBar = toolbarMain.loadingBar.signal.value;
|
||||
const getButtonDispatcher = (buttonName: ToolbarMainButtonNames) => {
|
||||
switch (buttonName) {
|
||||
case ToolbarMainButtonNames.ADD_TEXT:
|
||||
return () => appUiContext.openEditText();
|
||||
case ToolbarMainButtonNames.ADD_IMAGE:
|
||||
return () => appUiContext.openAddImage();
|
||||
case ToolbarMainButtonNames.ADD_VIDEO:
|
||||
return () => appUiContext.openAddVideo();
|
||||
case ToolbarMainButtonNames.ADD_CIRCLE:
|
||||
return () => dispatchUiEvents.addShapeToEngine({ shape: "circle" });
|
||||
case ToolbarMainButtonNames.ADD_TRIANGLE:
|
||||
return () => dispatchUiEvents.addShapeToEngine({ shape: "triangle" });
|
||||
case ToolbarMainButtonNames.ADD_SQUARE:
|
||||
return () => dispatchUiEvents.addShapeToEngine({ shape: "square" });
|
||||
case ToolbarMainButtonNames.SELECT:
|
||||
case ToolbarMainButtonNames.ERASER:
|
||||
case ToolbarMainButtonNames.PAINT:
|
||||
return (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
toolbarMain.setMode(buttonName);
|
||||
dispatchers[buttonName](e);
|
||||
};
|
||||
default:
|
||||
return dispatchers[buttonName];
|
||||
}
|
||||
};
|
||||
const buttonProps = Object.values(ToolbarMainButtonNames).reduce(
|
||||
(acc, buttonName) => {
|
||||
acc[buttonName] = {
|
||||
disabled: toolbarMain.signal.value.buttonStates[buttonName].disabled,
|
||||
active: toolbarMain.signal.value.buttonStates[buttonName].active,
|
||||
onClick: getButtonDispatcher(buttonName),
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as {
|
||||
[key in ToolbarMainButtonNames]: {
|
||||
disabled: boolean;
|
||||
active: boolean;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const handleOnClickRetry = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
dispatchers.loadingBarRetry(e);
|
||||
};
|
||||
return (
|
||||
<div className="fixed left-4 top-1/2 -translate-y-1/2">
|
||||
<Transition
|
||||
as="div"
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"absolute left-0 right-0 -m-8 mx-auto w-96 -translate-y-14 items-end p-4",
|
||||
)}
|
||||
show={loadingBar.isShowing}
|
||||
>
|
||||
<LoadingBar
|
||||
progress={loadingBar.progress}
|
||||
message={loadingBar.message}
|
||||
status={loadingBar.status}
|
||||
onRetry={handleOnClickRetry}
|
||||
colReverse
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<ToolbarMain
|
||||
disabled={toolbarMain.signal.value.disabled}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
export enum FontWeight {
|
||||
NORMAL = "normal",
|
||||
BOLD = "bold",
|
||||
}
|
||||
export enum FontStyle {
|
||||
NORMAL = "normal",
|
||||
ITALIC = "italic",
|
||||
}
|
||||
export enum TextAlign {
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
CENTER = "center",
|
||||
// JUSTIFY = "justify",
|
||||
}
|
||||
export enum TextDecoration {
|
||||
NONE = "none",
|
||||
STRIKETHROUGH = "line-through",
|
||||
UNDERLINE = "underline",
|
||||
}
|
||||
export type TextFormatData = {
|
||||
color: string;
|
||||
maxWidth: number;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
fontStyle: FontStyle;
|
||||
fontWeight: FontWeight;
|
||||
textAlign: TextAlign;
|
||||
textDecoration: TextDecoration;
|
||||
};
|
||||
export type TextNodeData = {
|
||||
text: string;
|
||||
} & TextFormatData;
|
||||
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { DialogTitle } from "@headlessui/react";
|
||||
|
||||
// TODO Port the file uploader to the new ui library
|
||||
import { FileUploader } from "../../../components/reusable/FileUploader";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
|
||||
import { IMAGE_FILE_TYPE } from "../../../constants/fileTypeEnums";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
|
||||
export const DialogAddImage = ({
|
||||
stagedImage = null,
|
||||
isOpen,
|
||||
closeCallback,
|
||||
onAddImage,
|
||||
isProcessing = false,
|
||||
}: {
|
||||
stagedImage?: File | null;
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
onAddImage: (file: File) => void;
|
||||
isProcessing?: boolean;
|
||||
}) => {
|
||||
const [assetFile, setAssetFile] = useState<File | null>(null);
|
||||
const previouslyStagedImageRef = useRef<File | null>(null);
|
||||
|
||||
const currFile =
|
||||
stagedImage &&
|
||||
stagedImage !== assetFile &&
|
||||
stagedImage !== previouslyStagedImageRef.current
|
||||
? stagedImage
|
||||
: assetFile;
|
||||
if (previouslyStagedImageRef.current !== stagedImage) {
|
||||
previouslyStagedImageRef.current = stagedImage;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setAssetFile(null);
|
||||
closeCallback();
|
||||
}
|
||||
|
||||
// Show dialog if no image is set (currFile is null) and not processing
|
||||
const shouldShowDialog = isOpen && (!currFile || !isProcessing);
|
||||
|
||||
return (
|
||||
<Modal isOpen={shouldShowDialog} onClose={closeCallback}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h1 className="text-2xl font-bold">Upload Image</h1>
|
||||
<div className="flex flex-col rounded-lg border-2 border-dashed border-white/20">
|
||||
<FileUploader
|
||||
fileTypes={Object.values(IMAGE_FILE_TYPE)}
|
||||
file={currFile ?? undefined}
|
||||
handleChange={(file: File) => {
|
||||
setAssetFile(file);
|
||||
if (file) {
|
||||
onAddImage(file);
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{currFile && (
|
||||
<div
|
||||
className="bg-ui-border relative flex items-center justify-center"
|
||||
style={{ height: "calc(100vh - 500px)" }}
|
||||
>
|
||||
<img
|
||||
src={URL.createObjectURL(currFile)}
|
||||
className="max-h-full object-contain"
|
||||
alt="upload img"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from "./DialogAddImage";
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
export enum FontWeight {
|
||||
NORMAL = "normal",
|
||||
BOLD = "bold",
|
||||
}
|
||||
export enum FontStyle {
|
||||
NORMAL = "normal",
|
||||
ITALIC = "italic",
|
||||
}
|
||||
export enum TextAlign {
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
CENTER = "center",
|
||||
// JUSTIFY = "justify",
|
||||
}
|
||||
export enum TextDecoration {
|
||||
NONE = "none",
|
||||
STRIKETHROUGH = "line-through",
|
||||
UNDERLINE = "underline",
|
||||
}
|
||||
export type TextFormatData = {
|
||||
color: string;
|
||||
maxWidth: number;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
fontStyle: FontStyle;
|
||||
fontWeight: FontWeight;
|
||||
textAlign: TextAlign;
|
||||
textDecoration: TextDecoration;
|
||||
};
|
||||
export type TextNodeData = {
|
||||
text: string;
|
||||
} & TextFormatData;
|
||||
import { useState } from "react";
|
||||
import { TextEditor } from "../../../components/ui";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
|
||||
export const DialogEditText = ({
|
||||
isOpen,
|
||||
closeCallback,
|
||||
onDoneEditText,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
onDoneEditText: (doneData: TextNodeData) => void;
|
||||
}) => {
|
||||
const [text, setText] = useState<string>("");
|
||||
const [textFormatData, setTextFormatData] = useState<TextFormatData>({
|
||||
color: "#000000",
|
||||
maxWidth: 500,
|
||||
fontFamily: "Arial",
|
||||
fontSize: 20,
|
||||
fontStyle: FontStyle.NORMAL,
|
||||
fontWeight: FontWeight.NORMAL,
|
||||
textAlign: TextAlign.CENTER,
|
||||
textDecoration: TextDecoration.NONE,
|
||||
});
|
||||
|
||||
const handleOnChangeText = (newText: string) => {
|
||||
setText(newText);
|
||||
};
|
||||
const handleOnChangeFormatting = (newFormatData: Partial<TextFormatData>) => {
|
||||
setTextFormatData((curr) => ({
|
||||
...curr,
|
||||
...newFormatData,
|
||||
}));
|
||||
};
|
||||
const handleOnDoneEditText = () => {
|
||||
const textNodeData = {
|
||||
text: text,
|
||||
...textFormatData,
|
||||
// fill: textFormatData.color,
|
||||
// fontFamily: textFormatData.fontFamily,
|
||||
// fontSize: textFormatData.fontSize,
|
||||
// align: textFormatData.textAlign,
|
||||
// fontStyle:
|
||||
// textFormatData.fontStyle === FontStyle.NORMAL &&
|
||||
// textFormatData.fontWeight === FontWeight.NORMAL
|
||||
// ? "normal"
|
||||
// : `${textFormatData.fontWeight !== FontWeight.NORMAL ? textFormatData.fontWeight : ""} ${textFormatData.fontStyle !== FontStyle.NORMAL ? textFormatData.fontStyle : ""}`,
|
||||
// textDecoration:
|
||||
// textFormatData.textDecoration === TextDecoration.NONE
|
||||
// ? ""
|
||||
// : textFormatData.textDecoration,
|
||||
};
|
||||
onDoneEditText(textNodeData);
|
||||
closeCallback();
|
||||
};
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={closeCallback} className="w-fit max-w-2xl">
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-2xl font-bold">Edit Text</h2>
|
||||
<TextEditor
|
||||
text={text}
|
||||
formatData={textFormatData}
|
||||
onChangeText={handleOnChangeText}
|
||||
onChangeFormatting={handleOnChangeFormatting}
|
||||
/>
|
||||
<div className="flex w-full justify-end gap-2">
|
||||
<Button onClick={closeCallback} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="hover:animate-pulse"
|
||||
onClick={handleOnDoneEditText}
|
||||
disabled={text === ""}
|
||||
>
|
||||
Enter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { DialogEditText } from "./DialogEditText";
|
||||
@@ -1,39 +0,0 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
dialogPanelStyles,
|
||||
paperWrapperStyles,
|
||||
} from "../../../components/styles";
|
||||
import {
|
||||
Description,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
} from "@headlessui/react";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
|
||||
export const DialogError = ({
|
||||
isShowing,
|
||||
title,
|
||||
message,
|
||||
onClose,
|
||||
}: {
|
||||
isShowing: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={isShowing} onClose={onClose} className="relative z-50">
|
||||
<div>
|
||||
<DialogPanel className={twMerge(paperWrapperStyles, dialogPanelStyles)}>
|
||||
<DialogTitle className="font-bold">{title}</DialogTitle>
|
||||
<Description>{message}</Description>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import { ButtonHTMLAttributes, MouseEventHandler } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconProps,
|
||||
} from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { Tooltip } from "../../components/ui/Tooltip";
|
||||
|
||||
export interface ToolbarButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
active?: boolean;
|
||||
hidden?: boolean;
|
||||
prominent?: boolean;
|
||||
}
|
||||
|
||||
export const ToolbarButton = ({
|
||||
children,
|
||||
icon,
|
||||
tooltip,
|
||||
onClick,
|
||||
buttonProps = {},
|
||||
iconProps,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
icon: IconDefinition;
|
||||
tooltip?: string;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
buttonProps?: ToolbarButtonProps;
|
||||
iconProps?: Omit<FontAwesomeIconProps, "icon">;
|
||||
}) => {
|
||||
const {
|
||||
className: customButtonClassNames,
|
||||
disabled,
|
||||
active,
|
||||
hidden,
|
||||
onClick: customOnClick,
|
||||
...restButtonProps
|
||||
} = buttonProps;
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mergedButtonClasses = twMerge(
|
||||
"rounded-lg hover:bg-white/15 transition-all duration-100 border-2 border-transparent text-white/80 px-2",
|
||||
children ? "w-fit flex items-center gap-2.5 text-nowrap" : "size-10",
|
||||
active && "bg-primary/30 border-2 border-primary hover:bg-primary/30",
|
||||
disabled && "pointer-events-none text-secondary-300",
|
||||
buttonProps.prominent &&
|
||||
"border border-primary-400/30 text-primary-500 hover:bg-primary-100/40 hover:border-primary-400/60",
|
||||
customButtonClassNames,
|
||||
);
|
||||
|
||||
const Button = (
|
||||
<button
|
||||
className={mergedButtonClasses}
|
||||
disabled={disabled}
|
||||
autoFocus={false}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
outline: "none",
|
||||
boxShadow: "none",
|
||||
WebkitTapHighlightColor: "transparent",
|
||||
}}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
} else if (customOnClick) {
|
||||
customOnClick(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} {...iconProps} />
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip position="right" tip={tooltip}>
|
||||
{Button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return Button;
|
||||
};
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
import { faCameraMovie, faPlayCircle } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { ToolbarButtonProps } from "../ToolbarButton";
|
||||
|
||||
export const ButtonPreviewAndRender = ({
|
||||
buttonPreviewProps,
|
||||
buttonRenderProps,
|
||||
}: {
|
||||
buttonPreviewProps: ToolbarButtonProps;
|
||||
buttonRenderProps: ToolbarButtonProps;
|
||||
}) => {
|
||||
return <div className="flex"></div>;
|
||||
};
|
||||
const ButtonPreview = (buttonProps: ToolbarButtonProps) => {
|
||||
const {
|
||||
className: customButtonClassNames,
|
||||
disabled,
|
||||
active,
|
||||
hidden,
|
||||
onClick,
|
||||
...restButtonProps
|
||||
} = buttonProps;
|
||||
return (
|
||||
<Tooltip tip="Preview">
|
||||
<Button
|
||||
className="text-nowrap rounded-r-none border-r border-white"
|
||||
icon={faPlayCircle}
|
||||
disabled={disabled}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
{...restButtonProps}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
const ButtonRender = (buttonProps: ToolbarButtonProps) => {
|
||||
const {
|
||||
className: customButtonClassNames,
|
||||
disabled,
|
||||
active,
|
||||
hidden,
|
||||
onClick,
|
||||
...restButtonProps
|
||||
} = buttonProps;
|
||||
return (
|
||||
<Tooltip tip="Click to Finish Movie">
|
||||
<Button
|
||||
className="text-nowrap rounded-l-none"
|
||||
icon={faCameraMovie}
|
||||
disabled={disabled}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Render Movie
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
import {
|
||||
faArrowRotateLeft,
|
||||
faArrowRotateRight,
|
||||
faCircle,
|
||||
faFilePlus,
|
||||
faImage,
|
||||
faLocationArrow,
|
||||
faPaintbrush,
|
||||
faShapes,
|
||||
faSquare,
|
||||
faText,
|
||||
faTriangle,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { Fragment, MouseEventHandler } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ToolbarButton } from "../../../components/features/ToolbarButton";
|
||||
// import { Tooltip } from "~/components/ui/Tooltip";
|
||||
|
||||
// style and constants
|
||||
import { paperWrapperStyles, toolTipStyles } from "../../../components/styles";
|
||||
import { ToolbarMainButtonNames } from "./enum";
|
||||
import { dispatchUiEvents } from "../../../signals/uiEvents";
|
||||
import { paintColor } from "../../../signals/uiEvents/toolbarMain/paintMode";
|
||||
// import { ColorPicker } from "../../../components/ui/TextEditor/ColorPicker";
|
||||
import { PaintModeMenu } from "../../../components/ui/ToolbarMain/PaintModeMenu";
|
||||
// import { EraseModeMenu } from "../../../components/ui/ToolbarMain/EraseModeMenu";
|
||||
// import { BackgroundMenu } from "../../../components/ui/ToolbarMain/BackgroundMenu";
|
||||
// import { bgColor } from "../../../signals/uiEvents/toolbarMain/backgroundMenu";
|
||||
import { useAppUiContext } from "~/pages/Page2d/contextSignals/appUi";
|
||||
|
||||
export const ToolbarMain = ({
|
||||
disabled = false,
|
||||
buttonProps,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
buttonProps: {
|
||||
[key in ToolbarMainButtonNames]: {
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-fit flex-col items-center gap-y-2",
|
||||
paperWrapperStyles,
|
||||
"glass",
|
||||
disabled &&
|
||||
"bg-ui-border pointer-events-none cursor-default shadow-md",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ToolbarButton
|
||||
icon={faLocationArrow}
|
||||
iconProps={{ className: "fa-flip-horizontal" }}
|
||||
buttonProps={buttonProps.SELECT}
|
||||
tooltip="Select"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faText}
|
||||
buttonProps={buttonProps.ADD_TEXT}
|
||||
tooltip="Add Text"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<PopoverButton as={Fragment}>
|
||||
<button
|
||||
data-tooltip="Add Shapes"
|
||||
className={twMerge(
|
||||
"size-10 rounded-lg border-2 border-transparent text-white/80 transition-all duration-100 hover:bg-white/15",
|
||||
toolTipStyles.base,
|
||||
toolTipStyles.right,
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShapes} />
|
||||
</button>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="right"
|
||||
className={twMerge(
|
||||
"flex flex-row [--anchor-gap:12px]",
|
||||
paperWrapperStyles,
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
icon={faSquare}
|
||||
buttonProps={{
|
||||
...buttonProps.ADD_SQUARE,
|
||||
onClick: (e) => {
|
||||
buttonProps.ADD_SQUARE.onClick?.(e);
|
||||
close();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faCircle}
|
||||
buttonProps={{
|
||||
...buttonProps.ADD_CIRCLE,
|
||||
onClick: (e) => {
|
||||
buttonProps.ADD_CIRCLE.onClick?.(e);
|
||||
close();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faTriangle}
|
||||
buttonProps={{
|
||||
...buttonProps.ADD_TRIANGLE,
|
||||
onClick: (e) => {
|
||||
buttonProps.ADD_TRIANGLE.onClick?.(e);
|
||||
close();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</PopoverPanel>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
<Popover className="relative">
|
||||
<PopoverButton as={Fragment}>
|
||||
<button
|
||||
data-tooltip="Add Media"
|
||||
className={twMerge(
|
||||
"size-10 rounded-lg border-2 border-transparent text-white/80 transition-all duration-100 hover:bg-white/15",
|
||||
toolTipStyles.base,
|
||||
toolTipStyles.right,
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilePlus} />
|
||||
</button>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="right"
|
||||
className={twMerge(
|
||||
// "absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2",
|
||||
"flex [--anchor-gap:12px]",
|
||||
paperWrapperStyles,
|
||||
)}
|
||||
>
|
||||
<ToolbarButton icon={faImage} buttonProps={buttonProps.ADD_IMAGE}>
|
||||
<span className="text-[16px]">Add Image</span>
|
||||
</ToolbarButton>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{/* Conditionally show paint menu on mode selection */}
|
||||
{buttonProps.PAINT.active ? (
|
||||
<div className="relative">
|
||||
<PaintModeMenu
|
||||
color={paintColor.value}
|
||||
onChange={dispatchUiEvents.toolbarMain.setPaintColor}
|
||||
faIcon={faPaintbrush}
|
||||
borderStyle="border-2 bg-primary/30 border-2 border-primary hover:bg-primary/30 text-white"
|
||||
showBar={false}
|
||||
staticIconColor="white"
|
||||
streamChanges
|
||||
defaultOpen={true}
|
||||
anchor="right"
|
||||
anchorGap={12}
|
||||
closeOnMouseLeave={true}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute -bottom-1.5 -right-1.5 h-[18px] w-[18px] rounded-full border border-gray-400"
|
||||
style={{
|
||||
background: `
|
||||
linear-gradient(${paintColor.value}, ${paintColor.value}),
|
||||
conic-gradient(rgb(204, 204, 204) 25%, rgb(255, 255, 255) 0deg, rgb(255, 255, 255) 50%, rgb(204, 204, 204) 0deg, rgb(204, 204, 204) 75%, rgb(255, 255, 255) 0deg) 0% 0% / 8px 8px
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ToolbarButton
|
||||
icon={faPaintbrush}
|
||||
buttonProps={buttonProps.PAINT}
|
||||
tooltip="Paint Brush"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ToolbarButton
|
||||
icon={faArrowRotateLeft}
|
||||
buttonProps={buttonProps.UNDO}
|
||||
tooltip="Undo"
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={faArrowRotateRight}
|
||||
buttonProps={buttonProps.REDO}
|
||||
tooltip="Redo"
|
||||
/>
|
||||
{/* <ToolbarButton
|
||||
icon={faFloppyDisk}
|
||||
buttonProps={buttonProps.SAVE}
|
||||
tooltip="Save"
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
{/* <hr className="w-full border-t border-white/15" />
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="relative">
|
||||
<Tooltip tip="Background Color" position="right">
|
||||
<div>
|
||||
<BackgroundMenu
|
||||
color={bgColor.value}
|
||||
onChange={dispatchUiEvents.toolbarMain.setBgColor}
|
||||
faIcon={faImage}
|
||||
borderStyle="bg-transparent hover:bg-white/15 text-white"
|
||||
showBar={false}
|
||||
staticIconColor="white"
|
||||
streamChanges
|
||||
defaultOpen={false}
|
||||
anchor="right"
|
||||
anchorGap={12}
|
||||
closeOnMouseLeave={true}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute -bottom-1.5 -right-1.5 h-[18px] w-[18px] rounded-full border border-gray-400"
|
||||
style={{ backgroundColor: bgColor.value }}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
export enum ToolbarMainButtonNames {
|
||||
MENU = "MENU",
|
||||
SELECT = "SELECT",
|
||||
ADD_TEXT = "ADD_TEXT",
|
||||
ADD_IMAGE = "ADD_IMAGE",
|
||||
ADD_VIDEO = "ADD_VIDEO",
|
||||
ADD_CIRCLE = "ADD_CIRCLE",
|
||||
ADD_TRIANGLE = "ADD_TRIANGLE",
|
||||
ADD_SQUARE = "ADD_SQUARE",
|
||||
PAINT = "PAINT",
|
||||
ERASER = "ERASER",
|
||||
AI_STYLIZE = "AI_STYLIZE",
|
||||
UNDO = "UNDO",
|
||||
REDO = "REDO",
|
||||
SAVE = "SAVE",
|
||||
PREVIEW = "PREVIEW",
|
||||
DOWNLOAD = "DOWNLOAD",
|
||||
BACKGROUND = "BACKGROUND",
|
||||
|
||||
// 4o generation
|
||||
GENERATE = "GENERATE",
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { ToolbarMain } from "./ToolbarMain";
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { faPalette } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
ToolbarButton,
|
||||
ToolbarButtonProps,
|
||||
} from "../../../components/features/ToolbarButton";
|
||||
import { paperWrapperStyles } from "../../../components/styles";
|
||||
import { ToolbarNodeButtonNames } from "./enums";
|
||||
import { ToolbarNodeButtonData } from "./data";
|
||||
import { ColorPicker } from "../../../components/ui/TextEditor/ColorPicker";
|
||||
import { DEFAULT_PAINT_COLOR } from "../../../signals/uiEvents/toolbarMain/paintMode";
|
||||
|
||||
export interface ToolbarNodeProps {
|
||||
disabled?: boolean;
|
||||
locked: boolean | "unknown";
|
||||
onLockClicked: (
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
currLock: boolean,
|
||||
) => void;
|
||||
lockDisabled?: boolean;
|
||||
buttonsProps?: {
|
||||
[key in ToolbarNodeButtonNames]: ToolbarButtonProps;
|
||||
};
|
||||
color?: string;
|
||||
onColorChange?: (color: string) => void;
|
||||
}
|
||||
export const ToolbarNode = ({
|
||||
disabled,
|
||||
locked,
|
||||
lockDisabled,
|
||||
onLockClicked,
|
||||
buttonsProps,
|
||||
color = DEFAULT_PAINT_COLOR,
|
||||
onColorChange = () => {},
|
||||
}: ToolbarNodeProps) => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
disabled && "pointer-events-none cursor-default bg-ui-border shadow-md",
|
||||
"flex flex-col gap-1 transition",
|
||||
)}
|
||||
>
|
||||
{ToolbarNodeButtonData.map((buttonDatum, idx) => {
|
||||
const buttonProps = buttonsProps?.[buttonDatum.name];
|
||||
console.log(buttonDatum, idx);
|
||||
if (!buttonProps || buttonProps.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonDatum.name !== ToolbarNodeButtonNames.COLOR) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
icon={buttonDatum.icon}
|
||||
key={idx}
|
||||
buttonProps={{ ...buttonProps, className: "w-full p-2" }}
|
||||
>
|
||||
<span className="text-[16px]">{buttonDatum.tooltip}</span>
|
||||
</ToolbarButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={onColorChange}
|
||||
faIcon={faPalette}
|
||||
borderStyle="w-full justify-start h-auto gap-2.5 text-white/80"
|
||||
showBar={false}
|
||||
key={idx}
|
||||
>
|
||||
<span className="text-[16px]">{buttonDatum.tooltip}</span>
|
||||
</ColorPicker>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ToolbarNodeButtonNames as ButtonNames } from "./enums";
|
||||
import {
|
||||
faArrowUpFromLine,
|
||||
faArrowDownFromLine,
|
||||
faHatWitch,
|
||||
faScalpelLineDashed,
|
||||
faTransporter,
|
||||
faTrashCan,
|
||||
faVectorSquare,
|
||||
faDownload,
|
||||
faPalette,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export const ToolbarNodeButtonData = [
|
||||
{
|
||||
name: ButtonNames.TRANSFORM,
|
||||
icon: faVectorSquare,
|
||||
tooltip: "Move",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.COLOR,
|
||||
icon: faPalette,
|
||||
tooltip: "Color",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.REMOVE_BACKGROUND,
|
||||
icon: faScalpelLineDashed,
|
||||
tooltip: "Remove Background",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.CHROMA,
|
||||
icon: faTransporter,
|
||||
tooltip: "Green Screen Removal",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.AI_STYLIZE,
|
||||
icon: faHatWitch,
|
||||
tooltip: "AI Stylize",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.SEGMENTATION,
|
||||
icon: faScalpelLineDashed,
|
||||
tooltip: "Select an Extraction",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.MOVE_LAYER_UP,
|
||||
icon: faArrowUpFromLine,
|
||||
tooltip: "Move Layer Up",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.MOVE_LAYER_DOWN,
|
||||
icon: faArrowDownFromLine,
|
||||
tooltip: "Move Layer Down",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.DELETE,
|
||||
icon: faTrashCan,
|
||||
tooltip: "Delete",
|
||||
},
|
||||
{
|
||||
name: ButtonNames.DOWNLOAD,
|
||||
icon: faDownload,
|
||||
tooltip: "Download",
|
||||
},
|
||||
];
|
||||
@@ -1,12 +0,0 @@
|
||||
export enum ToolbarNodeButtonNames {
|
||||
TRANSFORM = "TRANSFORM",
|
||||
CHROMA = "CRHOMA",
|
||||
AI_STYLIZE = "AI_STYLIZE",
|
||||
SEGMENTATION = "SEGMENTATION",
|
||||
DELETE = "DELETE",
|
||||
MOVE_LAYER_UP = "MOVE_LAYER_UP",
|
||||
MOVE_LAYER_DOWN = "MOVE_LAYER_DOWN",
|
||||
DOWNLOAD = "DOWNLOAD",
|
||||
COLOR = "COLOR",
|
||||
REMOVE_BACKGROUND = "REMOVE_BACKGROUND",
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./ToolbarNode";
|
||||
export * from "./enums";
|
||||
@@ -1,5 +0,0 @@
|
||||
export { DialogAddImage } from "./DialogAddImage";
|
||||
export { DialogError } from "./DialogError";
|
||||
export { ToolbarMain } from "./ToolbarMain";
|
||||
export { ToolbarNode } from "./ToolbarNode";
|
||||
export { DialogEditText } from "./DialogEditText";
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { faFileArrowUp, faFileAudio } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const DragAndDropZone = ({
|
||||
file,
|
||||
fileTypes,
|
||||
}: {
|
||||
file?: File;
|
||||
fileTypes: string[];
|
||||
}) => {
|
||||
if (!file) {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="text-4xl" />
|
||||
<div className="flex flex-col gap-0">
|
||||
<p className="font-medium">
|
||||
<u>Upload a file</u> or drop it here
|
||||
</p>
|
||||
|
||||
<p className="flex items-center gap-2 text-sm font-normal opacity-50">
|
||||
{fileTypes.join(", ").toString()} supported
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fileName = file.name.split(".")[0].toUpperCase();
|
||||
const fileSize =
|
||||
file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
: `${Math.floor(file.size / 1024)} KB`;
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<FontAwesomeIcon icon={faFileAudio} className="text-4xl" />
|
||||
<div className="flex grow flex-col gap-0">
|
||||
<p className="font-medium">
|
||||
{file.name.slice(0, file.name.lastIndexOf("."))}
|
||||
</p>
|
||||
<p className="flex items-center gap-2 text-sm font-normal">
|
||||
<span className="opacity-50">
|
||||
{`${fileName} file size: ${fileSize} `}
|
||||
</span>
|
||||
<u className="transition-all hover:text-white/80">Change File</u>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { FileUploader as DragDropFileUploader } from "react-drag-drop-files";
|
||||
// Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files
|
||||
|
||||
import { DragAndDropZone } from "./DragAndDropZone";
|
||||
|
||||
export const FileUploader = ({
|
||||
file,
|
||||
fileTypes,
|
||||
handleChange,
|
||||
}: {
|
||||
file: File | undefined;
|
||||
handleChange: (file: File) => void;
|
||||
fileTypes: string[];
|
||||
}) => (
|
||||
<DragDropFileUploader
|
||||
handleChange={handleChange}
|
||||
name="file"
|
||||
maxSize={50}
|
||||
types={fileTypes}
|
||||
>
|
||||
<DragAndDropZone file={file} fileTypes={fileTypes} />
|
||||
</DragDropFileUploader>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./FileUploader";
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useRef, useCallback, useContext, ReactNode, useEffect } from "react";
|
||||
import { EngineType } from "~/KonvaApp";
|
||||
interface UndoRedoProps {
|
||||
children: ReactNode;
|
||||
engine: EngineType | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A transparent component that provides undo/redo functionality
|
||||
* with keyboard shortcuts for macOS (Command+Z, Command+Shift+Z)
|
||||
*/
|
||||
export const UndoRedo = ({
|
||||
engine,
|
||||
children,
|
||||
}: UndoRedoProps) => {
|
||||
console.log("UndoRedo");
|
||||
// We don't need a separate ref since we want to track the prop changes
|
||||
// Create the event handler with useCallback
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (!engine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't capture events when user is editing text
|
||||
const target = e.target as HTMLElement;
|
||||
const isEditingText = target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable;
|
||||
|
||||
// Command+Z for Undo
|
||||
// Ctrl+Shift+Z for Redo (Windows/Linux alternative)
|
||||
// Windows/Linux: Ctrl+Shift+Z for Redo
|
||||
if (!navigator.userAgent.includes('Mac') && e.ctrlKey && e.shiftKey && e.key === 'z') {
|
||||
console.log("Ctrl+Shift+Z pressed: Redo (Windows/Linux)");
|
||||
e.preventDefault();
|
||||
engine.undoStackManager.redo();
|
||||
}
|
||||
|
||||
// Delete key handling - only if not editing text
|
||||
if (!isEditingText && (e.key === 'Delete' || e.key === 'Backspace')) {
|
||||
console.log("Delete/Backspace pressed: Delete selected nodes");
|
||||
e.preventDefault();
|
||||
engine.commandManager.deleteNodes();
|
||||
}
|
||||
|
||||
// macOS: Command+Z for Undo
|
||||
if (navigator.userAgent.includes('Mac') && e.metaKey && e.key === 'z' && !e.shiftKey) {
|
||||
console.log("Command+Z pressed: Undo");
|
||||
e.preventDefault();
|
||||
engine.undoStackManager.undo();
|
||||
}
|
||||
|
||||
// macOS: Command+Shift+Z for Redo
|
||||
if (navigator.userAgent.includes('Mac') && e.metaKey && e.key === 'z' && e.shiftKey) {
|
||||
console.log("Command+Shift+Z pressed: Redo");
|
||||
e.preventDefault();
|
||||
engine.undoStackManager.redo();
|
||||
engine.realTimeDrawEngine.render();
|
||||
}
|
||||
}, [engine]);
|
||||
|
||||
// Actually set up the event listeners using useEffect
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// Cleanup function to remove the event listener
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]); // Depend on the memoized handler
|
||||
|
||||
return children;
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const paperWrapperStyles =
|
||||
"rounded-xl border border-ui-panel-border p-1.5 shadow-lg bg-ui-background";
|
||||
|
||||
export const transitionTimingStyles = "transition duration-150 ease-in-out";
|
||||
|
||||
export const toolTipStyles = {
|
||||
base: twMerge(
|
||||
"relative",
|
||||
"after:absolute after:z-40",
|
||||
"after:content-[attr(data-tooltip)] after:text-black after:text-nowrap after:text-sm after:font-medium after:text-white/80 after:glass",
|
||||
"after:rounded-lg after:border after:border-ui-border after:bg-ui-panel after:px-2.5 after:py-1 after:shadow-xl",
|
||||
"after:hidden hover:after:block",
|
||||
),
|
||||
top: twMerge(
|
||||
"after:left-1/2 after:bottom-full after:-translate-x-1/2 after:mb-3.5",
|
||||
),
|
||||
bottom: twMerge(
|
||||
"after:left-1/2 after:top-full after:-translate-x-1/2 after:mt-3.5",
|
||||
),
|
||||
left: twMerge(
|
||||
"after:top-1/2 after:right-full after:-translate-y-1/2 after:mr-3.5",
|
||||
),
|
||||
right: twMerge(
|
||||
"after:top-1/2 after:left-full after:-translate-y-1/2 after:ml-3.5",
|
||||
),
|
||||
};
|
||||
|
||||
export const dialogPanelStyles = twMerge(
|
||||
"flex max-w-5xl flex-col justify-between gap-4 p-6 bg-[#1C1C20]",
|
||||
);
|
||||
@@ -1,179 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faBell, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverMenu } from "@storyteller/ui-popover";
|
||||
import { CompletedCard } from "./CompletedCard";
|
||||
import { InProgressCard } from "./InProgressCard";
|
||||
import { useJobContext } from "~/components/JobContext";
|
||||
import { Api } from "~/KonvaApp/Api";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
|
||||
// Dummy data types
|
||||
interface CompletedItem {
|
||||
token: string;
|
||||
maybe_title: string;
|
||||
public_bucket_path: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ActiveJob {
|
||||
job_token: string;
|
||||
request: {
|
||||
maybe_model_title: string;
|
||||
};
|
||||
status: {
|
||||
status: string;
|
||||
progress_percentage: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function Activity() {
|
||||
const { jobTokens, removeJobToken } = useJobContext();
|
||||
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
|
||||
const [jobs, setJobs] = useState<ActiveJob[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const pollData = async () => {
|
||||
if (jobTokens.length > 0) {
|
||||
console.log("Current Job Tokens:", jobTokens);
|
||||
const api = new Api();
|
||||
|
||||
// Create a map to store the latest job data
|
||||
const updatedJobs = new Map<string, ActiveJob>();
|
||||
|
||||
for (const jobToken of jobTokens) {
|
||||
const job = await api.pollJobSession(jobToken);
|
||||
|
||||
if (job.success && job.data) {
|
||||
const { status, progress_percentage } = job.data.status;
|
||||
const { job_token } = job.data;
|
||||
|
||||
if (
|
||||
status.toLowerCase() === "complete_success" ||
|
||||
status.toLowerCase() === "failed"
|
||||
) {
|
||||
// If job is complete_success or failed, we'll remove it later
|
||||
removeJobToken(job_token);
|
||||
|
||||
// Add to complete_success items if it was successful
|
||||
if (status.toLowerCase() === "complete_success") {
|
||||
const newCompletedItem: CompletedItem = {
|
||||
token: job_token,
|
||||
maybe_title:
|
||||
job.data.request.maybe_model_title || "Image Generation",
|
||||
public_bucket_path: job.data.result.image_url || "",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
toast.success("Done Processing Image");
|
||||
setCompletedItems((prev) => [newCompletedItem, ...prev]);
|
||||
}
|
||||
} else {
|
||||
// Store the job in our map
|
||||
const updatedJob: ActiveJob = {
|
||||
job_token,
|
||||
request: {
|
||||
maybe_model_title:
|
||||
job.data.request.maybe_model_title || "Image Generation",
|
||||
},
|
||||
status: {
|
||||
status: status.toUpperCase(),
|
||||
progress_percentage: progress_percentage || 0,
|
||||
},
|
||||
};
|
||||
updatedJobs.set(job_token, updatedJob);
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch job status:", job.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Update jobs state with the deduplicated jobs
|
||||
setJobs(Array.from(updatedJobs.values()));
|
||||
}
|
||||
if (jobTokens.length === 0) {
|
||||
setJobs([]);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial load
|
||||
// Initial call to pollData
|
||||
pollData();
|
||||
|
||||
// Set up polling interval (every 5 seconds)
|
||||
const interval = setInterval(async () => {
|
||||
await pollData();
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [jobTokens, removeJobToken]);
|
||||
|
||||
return (
|
||||
<Tooltip content="Activity" position="bottom">
|
||||
<PopoverMenu
|
||||
mode="default"
|
||||
buttonClassName="h-[42px] w-[42px] px-4"
|
||||
panelClassName="w-[360px] p-2"
|
||||
position="bottom"
|
||||
align="end"
|
||||
triggerIcon={
|
||||
<div>
|
||||
<span className="text-md flex items-center gap-2">
|
||||
<>
|
||||
{jobs.length > 0 ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
)}
|
||||
</>
|
||||
</span>
|
||||
{jobs.length > 0 && (
|
||||
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{/* In progress */}
|
||||
{jobs.length > 0 && (
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} job={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
spin
|
||||
size="2x"
|
||||
className="text-gray-400"
|
||||
/>
|
||||
<h3 className="text-gray-300">Retrieving Activities</h3>
|
||||
</div>
|
||||
) : completedItems.length === 0 && jobs.length === 0 ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<h3 className="text-gray-300">No activities yet</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Completed */}
|
||||
<div className="flex flex-col">
|
||||
{completedItems.map((item) => (
|
||||
<CompletedCard key={item.token} job={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverMenu>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { BaseDialog } from "../BaseDialog";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
job: {
|
||||
token: string;
|
||||
maybe_title: string;
|
||||
public_bucket_path: string;
|
||||
updated_at: string;
|
||||
maybe_style_name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function CompletedCard({ job }: Props) {
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const imageUrl = job.public_bucket_path;
|
||||
const fullImageUrl = job.public_bucket_path;
|
||||
console.log("I am hjere");
|
||||
console.log(job);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
|
||||
<img
|
||||
src={
|
||||
loadError ? "/resources/images/movie-placeholder.png" : imageUrl
|
||||
}
|
||||
className="h-full w-full object-cover"
|
||||
alt={job.maybe_title ?? "unknown"}
|
||||
crossOrigin="anonymous"
|
||||
onError={() => setLoadError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<div className="font-medium">{job.maybe_title || "Untitled"}</div>
|
||||
<div className="text-sm text-white/60">
|
||||
{dayjs(job.updated_at).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseDialog
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
className="h-[80vh] w-[75vw] max-w-[75vw] p-0"
|
||||
>
|
||||
<div className="relative grid h-full grid-cols-3 gap-6">
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="absolute right-4 top-4 z-10 flex h-7 w-7 items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-lg" />
|
||||
</button>
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/40">
|
||||
<img
|
||||
src={fullImageUrl}
|
||||
className="h-full w-full object-contain"
|
||||
alt={job.maybe_title ?? "unknown"}
|
||||
crossOrigin="anonymous"
|
||||
onError={() => setLoadError(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 py-5">
|
||||
<div className="text-xl font-medium">
|
||||
{job.maybe_title || "Untitled"}
|
||||
</div>
|
||||
<div className="text-sm text-white/60">
|
||||
Created: {dayjs(job.updated_at).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
{job.maybe_style_name && (
|
||||
<div className="text-sm text-white/60">
|
||||
Style: {job.maybe_style_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faClose,
|
||||
faCircleExclamation,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
job: {
|
||||
job_token: string;
|
||||
request: {
|
||||
maybe_model_title: string;
|
||||
};
|
||||
status: {
|
||||
status: string;
|
||||
progress_percentage: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getPercent(status: string, percentage: number) {
|
||||
if (["COMPLETE_FAILURE", "ATTEMPT_FAILED", "DEAD"].includes(status)) {
|
||||
return 0;
|
||||
} else {
|
||||
return percentage;
|
||||
}
|
||||
}
|
||||
|
||||
export function InProgressCard({ job }: Props) {
|
||||
const completePercent = getPercent(
|
||||
job.status.status,
|
||||
job.status.progress_percentage,
|
||||
);
|
||||
|
||||
const deleteJob = useCallback(async (jobToDelete: Props["job"]) => {
|
||||
// Simulate API call
|
||||
console.log("Deleting job:", jobToDelete.job_token);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:bg-white/10">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex aspect-square h-14 w-14 items-center justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
|
||||
{(job.status.status === "STARTED" ||
|
||||
job.status.status === "PENDING") && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
|
||||
)}
|
||||
{(job.status.status === "COMPLETE_FAILURE" ||
|
||||
job.status.status === "ATTEMPT_FAILED") && (
|
||||
<FontAwesomeIcon icon={faCircleExclamation} size={"lg"} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<div className="font-medium">
|
||||
{job.request.maybe_model_title || "Untitled"}
|
||||
</div>
|
||||
|
||||
<div className="text-sm capitalize text-white/60">
|
||||
{job.status.status.split("_").join(" ")}... {completePercent}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{job.status.status !== "STARTED" && (
|
||||
<div className="pr-5">
|
||||
<button
|
||||
onClick={() => deleteJob(job)}
|
||||
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
|
||||
title="Cancel"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { Activity } from "./Activity";
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
Transition,
|
||||
TransitionChild,
|
||||
} from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { dialogPanelStyles, paperWrapperStyles } from "../../components/styles";
|
||||
|
||||
interface BaseDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const BaseDialog = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
className = "max-w-2xl",
|
||||
}: BaseDialogProps) => {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/60" aria-hidden="true" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-150"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-150"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
dialogPanelStyles,
|
||||
"w-full",
|
||||
"max-w-2xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?: IconDefinition;
|
||||
iconClassName?: string;
|
||||
iconFlip?: boolean;
|
||||
htmlFor?: string;
|
||||
variant?: "primary" | "secondary" | "tertiary" | "action";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
icon,
|
||||
iconClassName,
|
||||
children,
|
||||
className: propsClassName,
|
||||
htmlFor,
|
||||
variant: propsVariant = "primary",
|
||||
disabled,
|
||||
iconFlip = false,
|
||||
loading,
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "tertiary": {
|
||||
return "bg-tertiary hover:bg-tertiary-400 text-white";
|
||||
}
|
||||
case "secondary": {
|
||||
return "bg-ui-controls hover:bg-ui-controls/80 text-white";
|
||||
}
|
||||
case "action": {
|
||||
return "bg-action hover:bg-action-500 text-white";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-primary hover:bg-primary-600 text-white";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disabledClass = twMerge(
|
||||
disabled || loading ? "opacity-50 pointer-events-none" : "",
|
||||
);
|
||||
|
||||
const className = twMerge(
|
||||
"text-[15px] font-semibold rounded-lg px-3 py-2 shadow-sm",
|
||||
"flex gap-2 items-center justify-center",
|
||||
"transition-all duration-150",
|
||||
getVariantClassNames(propsVariant),
|
||||
propsClassName,
|
||||
disabledClass,
|
||||
);
|
||||
|
||||
// const ButtonType = htmlFor ? "label" : "button";
|
||||
if (htmlFor) {
|
||||
return (
|
||||
<label className={className} htmlFor={htmlFor} style={rest.style}>
|
||||
{loading && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
{loading && iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
disabled={disabled || loading}
|
||||
{...{ ...rest, htmlFor }}
|
||||
>
|
||||
{loading && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && !iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
{loading && iconFlip ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
{icon && iconFlip ? (
|
||||
<FontAwesomeIcon icon={icon} className={iconClassName} />
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
Combobox as HeadlessComboBox,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
} from "@headlessui/react";
|
||||
|
||||
interface ComboBoxInterface {
|
||||
options: string[];
|
||||
value: string;
|
||||
onChange: (newVal: string) => void;
|
||||
}
|
||||
export const Combobox = ({ options, value, onChange }: ComboBoxInterface) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options.filter((option) => {
|
||||
return option.toLowerCase().includes(option.toLowerCase());
|
||||
});
|
||||
|
||||
return (
|
||||
<HeadlessComboBox
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onClose={() => setQuery("")}
|
||||
>
|
||||
<div className="relative flex h-10 rounded-lg border border-ui-panel-border">
|
||||
<ComboboxInput
|
||||
className="rounded-lg px-3"
|
||||
displayValue={(font: string) => font}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<ComboboxButton className="absolute right-0 top-0 flex h-10 items-center justify-center px-3 transition-colors duration-300 hover:text-primary">
|
||||
<FontAwesomeIcon icon={faChevronDown} className="size-4" />
|
||||
</ComboboxButton>
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
anchor="bottom"
|
||||
className="mt-1 w-[var(--input-width)] rounded-lg border border-ui-panel-border empty:invisible"
|
||||
>
|
||||
{filteredOptions.map((option, idx) => (
|
||||
<ComboboxOption
|
||||
key={idx}
|
||||
value={option}
|
||||
className="bg-ui-controls cursor-pointer px-3 py-2 data-[focus]:bg-gray-600 data-[selected]:font-medium"
|
||||
>
|
||||
{option}
|
||||
</ComboboxOption>
|
||||
))}
|
||||
</ComboboxOptions>
|
||||
</HeadlessComboBox>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
interface Props {
|
||||
size: number;
|
||||
email_hash?: string;
|
||||
username?: string;
|
||||
avatarIndex?: number;
|
||||
backgroundIndex?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Gravatar(props: Props) {
|
||||
//url to the local fallback image
|
||||
const localDefaultUrl =
|
||||
props.avatarIndex === undefined
|
||||
? "/resources/avatars/0.webp"
|
||||
: `/resources/avatars/${props.avatarIndex}.webp`;
|
||||
|
||||
// submitted fallback need to be a url served from our server for gravatar to grab it
|
||||
const defaultImageUrl =
|
||||
props.avatarIndex === undefined
|
||||
? "https://storyteller.ai/images/avatars/2000x2000/0.webp"
|
||||
: `https://storyteller.ai/images/avatars/2000x2000/${props.avatarIndex}.webp`;
|
||||
|
||||
// NB: Gravatar suggests URI encoding these:
|
||||
// https://en.gravatar.com/site/implement/images/
|
||||
const encondedDefaultImage = encodeURIComponent(defaultImageUrl);
|
||||
|
||||
const gravatarUrl = `https://www.gravatar.com/avatar/${props.email_hash}?s=${props.size}&d=${encondedDefaultImage}`;
|
||||
|
||||
const [{ imgUrl, showLoader }, setState] = useState<{
|
||||
imgUrl: string;
|
||||
showLoader: boolean;
|
||||
}>({
|
||||
imgUrl: gravatarUrl,
|
||||
showLoader: true,
|
||||
});
|
||||
|
||||
const altText = props.username ? `${props.username}'s gravatar` : "gravatar";
|
||||
|
||||
function getBackgroundColor(backgroundIndex?: number): string {
|
||||
switch (backgroundIndex) {
|
||||
case 0:
|
||||
return "#E66462";
|
||||
case 1:
|
||||
return "#FD881B";
|
||||
case 2:
|
||||
return "#E7C13C";
|
||||
case 3:
|
||||
return "#4BA905";
|
||||
case 4:
|
||||
return "#25B8A0";
|
||||
case 5:
|
||||
return "#0078D1";
|
||||
case 6:
|
||||
return "#7F52C1";
|
||||
case 7:
|
||||
return "#FF66AC";
|
||||
case 8:
|
||||
return "#259FEC";
|
||||
default:
|
||||
return `#1a1a27`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative aspect-square overflow-hidden rounded-full border-2 border-ui-border",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{showLoader && (
|
||||
<div className="absolute flex h-full w-full items-center justify-center bg-secondary">
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
crossOrigin="anonymous"
|
||||
alt={altText}
|
||||
src={imgUrl}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
style={{ backgroundColor: getBackgroundColor(props.backgroundIndex) }}
|
||||
onLoad={() => {
|
||||
setState((curr) => ({
|
||||
imgUrl: curr.imgUrl,
|
||||
showLoader: false,
|
||||
}));
|
||||
}}
|
||||
onError={() => {
|
||||
setState((curr) => ({
|
||||
imgUrl:
|
||||
curr.imgUrl !== localDefaultUrl ? localDefaultUrl : curr.imgUrl,
|
||||
showLoader: false,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Gravatar };
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Button } from "./Button";
|
||||
import { faEyedropper } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type ColorPickerHTMLAttributes = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
"color" | "onChange" | "onChangeCapture"
|
||||
>;
|
||||
interface ColorPickerBaseProps<T extends string>
|
||||
extends ColorPickerHTMLAttributes {
|
||||
color: T;
|
||||
onChange: (newColor: T) => void;
|
||||
}
|
||||
|
||||
export const HexEyedropPicker = ({
|
||||
color,
|
||||
onDropperChange,
|
||||
dropperClassName,
|
||||
pickerClassName,
|
||||
onPickerChange,
|
||||
Picker,
|
||||
}: {
|
||||
color: string;
|
||||
onDropperChange?: (newColor: string) => void;
|
||||
dropperClassName?: string;
|
||||
pickerClassName?: string;
|
||||
onPickerChange: (newColor: string) => void;
|
||||
Picker: (props: Partial<ColorPickerBaseProps<string>>) => JSX.Element;
|
||||
}) => {
|
||||
const handleDropperClick = () => {
|
||||
if (!("EyeDropper" in window)) {
|
||||
// NOTE: This shouldn't happen cuz we're using Chromium
|
||||
console.error("EyeDropper not found in window");
|
||||
return;
|
||||
}
|
||||
|
||||
const startEyeDrop = async (): Promise<string | null> => {
|
||||
// @ts-ignore-next-line
|
||||
const dropper = new window.EyeDropper();
|
||||
|
||||
try {
|
||||
const result = await dropper.open();
|
||||
console.log(result);
|
||||
return result.sRGBHex;
|
||||
} catch (err) {
|
||||
// User cancelled the picker
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
startEyeDrop().then((newColor) => {
|
||||
if (newColor) {
|
||||
onDropperChange?.(newColor);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// const [isDragging, setIsDragging] = useState(false);
|
||||
// const handleOnMouseDown = () => {
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
// const handleOnMouseUp = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className={"relative"}>
|
||||
<Button
|
||||
className={twMerge([
|
||||
"z-20 size-8 rounded-xl bg-ui-background/30 text-white shadow-none duration-200 hover:bg-white/10 hover:text-white hover:shadow-xl",
|
||||
dropperClassName,
|
||||
])}
|
||||
icon={faEyedropper}
|
||||
onClick={handleDropperClick}
|
||||
/>
|
||||
<Picker
|
||||
className={pickerClassName}
|
||||
color={color}
|
||||
onChange={onPickerChange}
|
||||
// onMouseDown={handleOnMouseDown}
|
||||
// onMouseUp={handleOnMouseUp}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
|
||||
interface NumberInputProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export const InputNumber = ({ value, onChange }: NumberInputProps) => {
|
||||
const [inputValue, setInputValue] = useState<string>(value.toString());
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
if (
|
||||
newValue === "" ||
|
||||
(!isNaN(Number(newValue)) && Number(newValue) <= 100)
|
||||
) {
|
||||
setInputValue(newValue);
|
||||
const numericValue = parseFloat(newValue);
|
||||
if (newValue !== "") {
|
||||
onChange(Math.round(numericValue * 100) / 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (inputValue === "") {
|
||||
setInputValue(Math.round(value * 100) / 100 + "");
|
||||
}
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
const newValue = Math.min(Number(inputValue) + 1, 100);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
const newValue = Math.max(Number(inputValue) - 1, 0);
|
||||
setInputValue(Math.round(newValue * 100) / 100 + "");
|
||||
onChange(Math.round(newValue * 100) / 100);
|
||||
};
|
||||
|
||||
const incrementButtonStyle =
|
||||
"flex h-4 w-6 items-center justify-center text-xs text-white bg-ui-border hover:bg-secondary-500 outline-none focus:outline-none transition-all active:outline-none";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
inputClassName="w-16 h-7 rounded-md text-sm"
|
||||
/>
|
||||
|
||||
<div className="absolute right-0 top-0 flex h-7 flex-col rounded-r-md border-l">
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-tr-md")}
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronUp} />
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-br-md")}
|
||||
onClick={handleDecrement}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
export interface LabeledSelectProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options?: string[];
|
||||
}
|
||||
|
||||
// TODO: finish up this hover drop down component (WIP) - BFlat
|
||||
|
||||
export const LabeledSelect = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options = ["1", "2", "3", "4"],
|
||||
}: LabeledSelectProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const timeoutRef = useRef<number>();
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<PopoverButton as={Button} variant="secondary" className="px-3 py-1.5">
|
||||
{label} — {value}
|
||||
</PopoverButton>
|
||||
{isOpen && (
|
||||
<PopoverPanel
|
||||
static
|
||||
className="absolute z-10 mt-1 w-fit origin-top-left rounded-md border bg-white p-1 shadow-lg transition-all duration-200 ease-out"
|
||||
style={{
|
||||
opacity: isOpen ? 1 : 0,
|
||||
transform: `scale(${isOpen ? 1 : 0.95}) translateY(${isOpen ? 0 : -4}px)`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option}
|
||||
className="justify-start px-3 py-1.5"
|
||||
onClick={() => {
|
||||
onChange(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { MouseEventHandler } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faArrowRightRotate } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export enum LoadingBarStatus {
|
||||
IDLE = "idle",
|
||||
LOADING = "loading",
|
||||
SUCCESS = "success",
|
||||
ERROR = "error",
|
||||
}
|
||||
export interface LoadingBarProps {
|
||||
colReverse?: boolean;
|
||||
progress: number;
|
||||
status: LoadingBarStatus;
|
||||
message?: string;
|
||||
onRetry?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export const LoadingBar = ({
|
||||
colReverse,
|
||||
progress = 0,
|
||||
status,
|
||||
onRetry,
|
||||
message,
|
||||
}: LoadingBarProps) => {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
// default styles
|
||||
"flex w-full flex-col gap-2",
|
||||
// position styles
|
||||
colReverse && "flex-col-reverse",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"h-1.5 w-full rounded-full",
|
||||
status && status === LoadingBarStatus.IDLE
|
||||
? "bg-gray-50"
|
||||
: "pointer-events-none border-0 bg-gray-600",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge("h-full rounded-full bg-primary/80")}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex grow items-center justify-center gap-2">
|
||||
{message && <label>{message}</label>}
|
||||
|
||||
{status === LoadingBarStatus.ERROR && (
|
||||
<Button
|
||||
icon={faArrowRightRotate}
|
||||
onClick={onRetry}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
export const LoadingSpinner = ({
|
||||
isShowing,
|
||||
message,
|
||||
}: {
|
||||
isShowing: boolean;
|
||||
message?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Transition show={isShowing}>
|
||||
<div
|
||||
className={twMerge(
|
||||
// default styles
|
||||
"flex flex-col items-center gap-2",
|
||||
// base transition properties
|
||||
"transition-opacity ease-in-out",
|
||||
// Shared closed styles
|
||||
"data-[closed]:opacity-0",
|
||||
// Entering styles
|
||||
"data-[enter]:duration-100",
|
||||
// Leaving styles
|
||||
"data-[leave]:duration-300",
|
||||
)}
|
||||
>
|
||||
<Spinner className="size-10" />
|
||||
{message && <label>{message}</label>}
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { HTMLAttributes, useState } from "react";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Transition } from "@headlessui/react";
|
||||
|
||||
export const PanelBottom = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const gridClasses = twMerge(
|
||||
"row-span-3 row-start-10",
|
||||
"col-span-12 col-start-1",
|
||||
);
|
||||
const buttonClasses = twMerge(
|
||||
"w-12 h-6 bg-ui-panel",
|
||||
"border-ui-border border-l border-t border-r rounded-t-md",
|
||||
"px-4 flex items-center justify-center",
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOpen && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
className={twMerge(buttonClasses, "fixed bottom-0 left-1")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronUp} />
|
||||
</button>
|
||||
)}
|
||||
<Transition show={isOpen}>
|
||||
<div
|
||||
className={twMerge(
|
||||
gridClasses,
|
||||
//base styles
|
||||
"bg-ui-panel border-ui-border relative border p-2 transition ease-in-out",
|
||||
|
||||
// Shared closed styles
|
||||
"data-[closed]:opacity-0",
|
||||
// Entering styles
|
||||
"data-[enter]:data-[closed]:translate-y-full data-[enter]:duration-100",
|
||||
// Leaving styles
|
||||
"data-[leave]:data-[closed]:translate-y-full data-[leave]:duration-300",
|
||||
|
||||
//extra overriderclassnames
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={twMerge(buttonClasses, "absolute -top-6 left-1")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import { HTMLAttributes, useState } from "react";
|
||||
import {
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Transition } from "@headlessui/react";
|
||||
|
||||
export const PanelRight = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const gridClasses = twMerge(
|
||||
"col-span-3 col-start-10",
|
||||
"row-span-12 row-start-1",
|
||||
);
|
||||
const buttonClasses =
|
||||
"w-6 bg-ui-panel border-ui-border border-l border-t border-b rounded-l-md py-4";
|
||||
return (
|
||||
<>
|
||||
{!isOpen && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
className={twMerge(buttonClasses, "fixed right-0 top-1")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
</button>
|
||||
)}
|
||||
<Transition show={isOpen}>
|
||||
<div
|
||||
className={twMerge(
|
||||
gridClasses,
|
||||
// base styles
|
||||
"relative border border-ui-border bg-ui-panel p-2 transition ease-in-out",
|
||||
// Shared closed styles
|
||||
"data-[closed]:opacity-0",
|
||||
// Entering styles
|
||||
"data-[enter]:data-[closed]:translate-x-full data-[enter]:duration-100",
|
||||
// Leaving styles
|
||||
"data-[leave]:data-[closed]:translate-x-full data-[leave]:duration-300",
|
||||
|
||||
//extra overriderclassnames
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={twMerge(buttonClasses, "absolute -left-6 top-1")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import React, { useState, useRef, useEffect, ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "./Button";
|
||||
import { faMinus, faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface SliderProps {
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
step: number;
|
||||
className?: string;
|
||||
showTooltip?: boolean;
|
||||
tooltipContent?: ReactNode;
|
||||
suffix?: string;
|
||||
innerLabel?: string;
|
||||
showDecrement?: boolean;
|
||||
showIncrement?: boolean;
|
||||
}
|
||||
|
||||
export const Slider = ({
|
||||
min,
|
||||
max,
|
||||
value,
|
||||
onChange,
|
||||
step,
|
||||
className,
|
||||
showTooltip,
|
||||
tooltipContent,
|
||||
suffix,
|
||||
innerLabel,
|
||||
showDecrement,
|
||||
showIncrement,
|
||||
}: SliderProps) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [showHandleTooltip, setShowHandleTooltip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const setClampedValue = (value: number) => {
|
||||
const roundedValue = Math.round(value / step) * step;
|
||||
const clampedValue = Math.min(Math.max(roundedValue, min), max);
|
||||
setLocalValue(clampedValue);
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
const updatePosition = (x: number) => {
|
||||
if (!sliderRef.current) return;
|
||||
const rect = sliderRef.current.getBoundingClientRect();
|
||||
const fraction = (x - rect.left) / rect.width;
|
||||
const newValue = fraction * (max - min) + min;
|
||||
setClampedValue(newValue);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
setShowHandleTooltip(true);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
updatePosition(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
updatePosition(e.clientX);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
setShowHandleTooltip(false);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isDragging) {
|
||||
setShowHandleTooltip(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isDragging) {
|
||||
setShowHandleTooltip(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecrement = () => {
|
||||
setClampedValue(localValue - step);
|
||||
};
|
||||
|
||||
const handleIncrement = () => {
|
||||
setClampedValue(localValue + step);
|
||||
};
|
||||
|
||||
const percentage = ((localValue - min) / (max - min)) * 100;
|
||||
const displayValue =
|
||||
localValue.toFixed(step.toString().split(".")[1]?.length || 0) +
|
||||
(suffix || "");
|
||||
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
{showDecrement && (
|
||||
<Button
|
||||
icon={faMinus}
|
||||
className="my-auto mr-1 size-6 rounded-full bg-transparent text-white/80 hover:bg-white/10 focus-visible:outline-primary active:bg-primary/30"
|
||||
onClick={handleDecrement}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={twMerge(
|
||||
"glass group relative h-7 w-full cursor-pointer overflow-hidden rounded-lg border border-ui-border",
|
||||
isDragging && "!bg-ui-controls/90",
|
||||
className,
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute h-full bg-primary/30 transition-colors duration-300 group-hover:bg-primary/50",
|
||||
isDragging && "!bg-primary/50",
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
>
|
||||
{innerLabel && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"absolute top-1/2 ml-2.5 -translate-y-1/2 text-nowrap text-sm font-medium text-white/60 transition-colors duration-300 group-hover:text-white",
|
||||
isDragging && "!text-white",
|
||||
)}
|
||||
>
|
||||
{innerLabel}
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute right-0 top-1/2 mr-1 h-4 w-0.5 -translate-y-1/2 rounded-full",
|
||||
isDragging ? "bg-white" : "bg-white/50",
|
||||
)}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showIncrement && (
|
||||
<Button
|
||||
icon={faPlus}
|
||||
className="my-auto ml-1 size-6 rounded-full bg-transparent text-white/80 hover:bg-white/10 focus-visible:outline-primary active:bg-primary/30"
|
||||
onClick={handleIncrement}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showTooltip && (
|
||||
<Transition
|
||||
as={"div"}
|
||||
show={showHandleTooltip}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="absolute -top-8 z-10 rounded-md bg-ui-panel px-2 py-1 text-xs text-black shadow-lg"
|
||||
style={{ left: `${percentage}%`, transform: "translateX(-50%)" }}
|
||||
>
|
||||
{tooltipContent ? tooltipContent : displayValue}
|
||||
</div>
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const Spinner = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<div className={twMerge("aspect-square size-10", className)}>
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="size-full animate-spin"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Field, Label, Switch as HeadlessUiSwitch } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const Switch = ({
|
||||
checked,
|
||||
disabled,
|
||||
label,
|
||||
onChange,
|
||||
}: {
|
||||
checked: boolean;
|
||||
disabled?: boolean | "semi";
|
||||
label?: string;
|
||||
onChange: (newState: boolean) => void;
|
||||
}) => {
|
||||
const labelDisabledStyles = disabled ? "opacity-50" : "";
|
||||
const switchContainerBaseStyle =
|
||||
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-0 focus:ring-indigo-500 focus:ring-offset-0";
|
||||
const switchContainerStateStyle = checked
|
||||
? "bg-primary hover:bg-primary-400"
|
||||
: "bg-gray-500 hover:bg-gray-400";
|
||||
const switchButtonBaseStyle =
|
||||
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform";
|
||||
const switchButtonStateStyle = checked ? "translate-x-6" : "translate-x-1";
|
||||
|
||||
return (
|
||||
<Field className="flex items-center">
|
||||
{label && (
|
||||
<Label
|
||||
className={twMerge(
|
||||
"mr-3 grow text-sm font-medium transition-opacity",
|
||||
labelDisabledStyles,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<HeadlessUiSwitch
|
||||
disabled={disabled === "semi" ? false : disabled}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className={twMerge(switchContainerBaseStyle, switchContainerStateStyle)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(switchButtonBaseStyle, switchButtonStateStyle)}
|
||||
/>
|
||||
</HeadlessUiSwitch>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TabSelectorProps {
|
||||
tabs: TabItem[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
disabledMessage?: string;
|
||||
}
|
||||
|
||||
export const TabSelector: React.FC<TabSelectorProps> = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
className,
|
||||
disabled,
|
||||
disabledMessage,
|
||||
}) => {
|
||||
// Find the index of the active tab
|
||||
const selectedIndex = tabs.findIndex((tab) => tab.id === activeTab);
|
||||
const tabsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({
|
||||
left: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
// Handle tab change
|
||||
const handleTabChange = (index: number) => {
|
||||
onTabChange(tabs[index].id);
|
||||
};
|
||||
|
||||
// Update the indicator position when the selected tab changes
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && tabsRef.current[selectedIndex]) {
|
||||
const currentTab = tabsRef.current[selectedIndex];
|
||||
if (currentTab) {
|
||||
setIndicatorStyle({
|
||||
left: currentTab.offsetLeft,
|
||||
width: currentTab.offsetWidth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedIndex, tabs]);
|
||||
|
||||
const TabGroupElement = (
|
||||
<TabGroup
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={handleTabChange}
|
||||
className={twMerge(
|
||||
"w-full",
|
||||
className,
|
||||
disabled ? "cursor-not-allowed opacity-60" : "",
|
||||
)}
|
||||
>
|
||||
<TabList className="glass relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1 shadow-lg">
|
||||
{/* Animated indicator */}
|
||||
<div
|
||||
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md border-2 border-primary bg-primary/30 transition-all duration-200 ease-in-out"
|
||||
style={{
|
||||
left: indicatorStyle.left,
|
||||
width: indicatorStyle.width,
|
||||
}}
|
||||
/>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
ref={(el) => (tabsRef.current[index] = el)}
|
||||
disabled={disabled}
|
||||
className={({ selected }) =>
|
||||
twMerge(
|
||||
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center font-semibold transition-all duration-200 ease-in-out",
|
||||
selected ? "text-white" : "text-gray-300 hover:text-white",
|
||||
disabled ? "cursor-not-allowed opacity-60" : "",
|
||||
)
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
</TabGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{disabled && (
|
||||
<Tooltip
|
||||
tip={disabledMessage ?? "Cannot change tab. Generation in progress."}
|
||||
position="bottom"
|
||||
>
|
||||
{TabGroupElement}
|
||||
</Tooltip>
|
||||
)}
|
||||
{!disabled && TabGroupElement}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import { TextareaHTMLAttributes, forwardRef, useEffect, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { kebabCase } from "~/utilities";
|
||||
|
||||
export type ResizeType =
|
||||
| "none"
|
||||
| "both"
|
||||
| "horizontal"
|
||||
| "vertical"
|
||||
| undefined;
|
||||
|
||||
const resizeStyles = {
|
||||
none: "resize-none",
|
||||
both: "resize",
|
||||
horizontal: "resize-x",
|
||||
vertical: "resize-y",
|
||||
undefined: "",
|
||||
};
|
||||
|
||||
export interface TextareaInterface
|
||||
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
resize?: ResizeType;
|
||||
forceBlurOnOutsideClick?: boolean;
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaInterface>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
label,
|
||||
resize = "vertical",
|
||||
id,
|
||||
forceBlurOnOutsideClick,
|
||||
...rest
|
||||
}: TextareaInterface,
|
||||
ref,
|
||||
) => {
|
||||
const resizeStyle = resizeStyles[resize];
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Function to blur the textarea when clicking outside of it
|
||||
useEffect(() => {
|
||||
if (!forceBlurOnOutsideClick) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
textareaRef.current &&
|
||||
!textareaRef.current.contains(event.target as Node)
|
||||
) {
|
||||
textareaRef.current.blur();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [forceBlurOnOutsideClick]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{label && <label htmlFor={id ? id : kebabCase(label)}>{label}</label>}
|
||||
|
||||
<textarea
|
||||
ref={(node) => {
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
(ref as any).current = node;
|
||||
}
|
||||
if (textareaRef) {
|
||||
(textareaRef as any).current = node;
|
||||
}
|
||||
}}
|
||||
id={id ? id : label ? kebabCase(label) : undefined}
|
||||
className={twMerge(
|
||||
"rounded-lg border border-ui-border px-4 py-2.5 text-white outline-none focus:outline-none",
|
||||
className,
|
||||
resizeStyle,
|
||||
)}
|
||||
style={{
|
||||
outline: "2px solid transparent",
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
// disableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "#2d81ff";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// enableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "transparent";
|
||||
}}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import {
|
||||
faBold,
|
||||
faItalic,
|
||||
faStrikethrough,
|
||||
faUnderline,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { buttonGroupButton, buttonGroupGrouper } from "./styles";
|
||||
import { FontStyle, FontWeight, TextDecoration } from "../../../TextTypes";
|
||||
export const ButtonsTextStyles = ({
|
||||
fontStyle: currStyle,
|
||||
fontWeight: currWeight,
|
||||
textDecoration: currDecoration,
|
||||
onChangeFontStyle,
|
||||
onChangeFontWeight,
|
||||
onChangeTextDecoration,
|
||||
}: {
|
||||
fontStyle: FontStyle;
|
||||
fontWeight: FontWeight;
|
||||
textDecoration: TextDecoration;
|
||||
onChangeFontStyle: (newVal: FontStyle) => void;
|
||||
onChangeFontWeight: (newVal: FontWeight) => void;
|
||||
onChangeTextDecoration: (newVal: TextDecoration) => void;
|
||||
}) => {
|
||||
const isBold = currWeight === FontWeight.BOLD ? true : undefined;
|
||||
const isItalic = currStyle === FontStyle.ITALIC ? true : undefined;
|
||||
const isSelectedDecoration = (buttonVal: TextDecoration) =>
|
||||
currDecoration === buttonVal ? true : undefined;
|
||||
|
||||
const toggleBold = () => {
|
||||
if (currWeight === FontWeight.BOLD) {
|
||||
onChangeFontWeight(FontWeight.NORMAL);
|
||||
} else {
|
||||
onChangeFontWeight(FontWeight.BOLD);
|
||||
}
|
||||
};
|
||||
const handleOnChangeFontStyle = (selected: FontStyle) => {
|
||||
if (selected === currStyle) {
|
||||
onChangeFontStyle(FontStyle.NORMAL);
|
||||
} else {
|
||||
onChangeFontStyle(selected);
|
||||
}
|
||||
};
|
||||
const handleOnChangeTextDecoration = (selected: TextDecoration) => {
|
||||
if (selected === currDecoration) {
|
||||
onChangeTextDecoration(TextDecoration.NONE);
|
||||
} else {
|
||||
onChangeTextDecoration(selected);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={buttonGroupGrouper}>
|
||||
<button
|
||||
data-selected={isBold}
|
||||
className={buttonGroupButton}
|
||||
onClick={toggleBold}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBold} />
|
||||
</button>
|
||||
<button
|
||||
data-selected={isItalic}
|
||||
className={buttonGroupButton}
|
||||
onClick={() => handleOnChangeFontStyle(FontStyle.ITALIC)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faItalic} />
|
||||
</button>
|
||||
<button
|
||||
data-selected={isSelectedDecoration(TextDecoration.STRIKETHROUGH)}
|
||||
className={buttonGroupButton}
|
||||
disabled
|
||||
onClick={() =>
|
||||
handleOnChangeTextDecoration(TextDecoration.STRIKETHROUGH)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faStrikethrough} />
|
||||
</button>
|
||||
<button
|
||||
data-selected={isSelectedDecoration(TextDecoration.UNDERLINE)}
|
||||
className={buttonGroupButton}
|
||||
disabled
|
||||
onClick={() => handleOnChangeTextDecoration(TextDecoration.UNDERLINE)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUnderline} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import {
|
||||
faAlignCenter,
|
||||
// faAlignJustify,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { TextAlign } from "../../../TextTypes";
|
||||
import { buttonGroupButton, buttonGroupGrouper } from "./styles";
|
||||
export const ButtonsAlignments = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: TextAlign;
|
||||
onChange: (newVal: TextAlign) => void;
|
||||
}) => {
|
||||
const isSelected = (alignment: TextAlign) =>
|
||||
value === alignment ? true : undefined;
|
||||
return (
|
||||
<div className={buttonGroupGrouper}>
|
||||
<button
|
||||
data-selected={isSelected(TextAlign.LEFT)}
|
||||
className={buttonGroupButton}
|
||||
onClick={() => onChange(TextAlign.LEFT)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAlignLeft} />
|
||||
</button>
|
||||
<button
|
||||
data-selected={isSelected(TextAlign.CENTER)}
|
||||
className={buttonGroupButton}
|
||||
onClick={() => onChange(TextAlign.CENTER)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAlignCenter} />
|
||||
</button>
|
||||
<button
|
||||
data-selected={isSelected(TextAlign.RIGHT)}
|
||||
className={buttonGroupButton}
|
||||
onClick={() => onChange(TextAlign.RIGHT)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAlignRight} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,183 +0,0 @@
|
||||
import {
|
||||
faFont,
|
||||
faHashtag,
|
||||
IconDefinition,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { HexAlphaColorPicker } from "react-colorful";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { paperWrapperStyles } from "../../../components/styles";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { HexEyedropPicker } from "../HexEyedropPicker";
|
||||
|
||||
export const ColorPicker = ({
|
||||
color: prevColor,
|
||||
onChange,
|
||||
faIcon = faFont,
|
||||
borderStyle,
|
||||
showBar = true,
|
||||
anchor = "bottom",
|
||||
anchorGap = 0,
|
||||
fillBg = false,
|
||||
staticIconColor,
|
||||
streamChanges = false,
|
||||
defaultOpen = false,
|
||||
closeOnMouseLeave = false,
|
||||
children,
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (newColor: string) => void;
|
||||
faIcon?: IconDefinition;
|
||||
borderStyle?: string;
|
||||
showBar?: boolean;
|
||||
anchor?: "top" | "right" | "bottom" | "left";
|
||||
anchorGap?: number;
|
||||
fillBg?: boolean;
|
||||
staticIconColor?: string;
|
||||
streamChanges?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
closeOnMouseLeave?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [{ currColor, textInput }, setStates] = useState<{
|
||||
currColor: string;
|
||||
textInput: string;
|
||||
}>({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(defaultOpen);
|
||||
}, [defaultOpen]);
|
||||
|
||||
const handleMouseLeave = (close: () => void) => {
|
||||
if (closeOnMouseLeave) {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (isHexColorCode(e.target.value)) {
|
||||
setStates({
|
||||
currColor: `#${e.target.value}`,
|
||||
textInput: e.target.value,
|
||||
});
|
||||
} else {
|
||||
setStates((curr) => ({
|
||||
...curr,
|
||||
textInput: e.target.value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const isHexColorCode = (color: string) => {
|
||||
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setStates({
|
||||
currColor: color,
|
||||
textInput: color.substring(1),
|
||||
});
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<PopoverButton
|
||||
className={twMerge(
|
||||
"flex size-10 items-center gap-1 rounded-lg bg-ui-controls p-2",
|
||||
showBar ? "flex-col" : "justify-center",
|
||||
borderStyle,
|
||||
)}
|
||||
style={fillBg ? { backgroundColor: prevColor } : {}}
|
||||
onMouseEnter={() => defaultOpen && setIsOpen(true)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faIcon}
|
||||
color={staticIconColor ?? (fillBg ? undefined : prevColor)}
|
||||
/>
|
||||
{children}
|
||||
{showBar && (
|
||||
<span
|
||||
className="h-1 w-full"
|
||||
style={{ backgroundColor: prevColor }}
|
||||
/>
|
||||
)}
|
||||
</PopoverButton>
|
||||
{(open || isOpen) && (
|
||||
<div onMouseLeave={() => handleMouseLeave(close)}>
|
||||
<PopoverPanel
|
||||
anchor={anchor}
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2 overflow-hidden p-4",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--anchor-gap": `${anchorGap}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
static
|
||||
>
|
||||
<HexEyedropPicker
|
||||
color={currColor}
|
||||
onPickerChange={handleColorChange}
|
||||
onDropperChange={handleColorChange}
|
||||
Picker={HexAlphaColorPicker}
|
||||
/>
|
||||
<Input
|
||||
className="mt-2"
|
||||
inputClassName="bg-ui-background/30"
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
onChange={handleTextInput}
|
||||
/>
|
||||
{!closeOnMouseLeave && (
|
||||
<div className="flex w-full justify-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStates({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user