Add shapes to global toolbar and color picker to contextual (#417)

Shapes scaffolding is fully set up, color picker isn't tied up to the
nodes so it doesn't actually change the color yet
This commit is contained in:
Tanish Manku authored and GitHub committed 2025-02-19 21:55:06 -05:00
1 parent 1e38fbad5c
commit 9d1d605f5c
13 files changed
+143 -24

No files matched your search

+10 -6
View File
@@ -366,7 +366,7 @@ export class Engine {
// Listen to Toolbar Main
uiEvents.toolbarMain.UNDO.onClick(() => this.undoStackManager.undo());
uiEvents.toolbarMain.REDO.onClick(() => this.undoStackManager.redo());
uiEvents.toolbarMain.SAVE.onClick(async (/*event*/) => {
await this.realTimeDrawEngine.saveOutput();
@@ -400,8 +400,7 @@ export class Engine {
this.addImage(image);
});
uiEvents.onAddTextToEngine((textdata) => {
this.addText(textdata);
});
@@ -432,7 +431,7 @@ export class Engine {
}
}
});
uiEvents.promptEvents.onPromptStrengthChanged((strength) => {
this.realTimeDrawEngine.currentStrength = strength;
});
@@ -440,6 +439,11 @@ export class Engine {
uiEvents.promptEvents.onPromptTextChanged((prompt) => {
this.realTimeDrawEngine.currentPrompt = prompt;
});
uiEvents.onAddShapeToEngine((shapeData) => {
// TODO: Do something with shapes
console.log("Shape Data", shapeData);
})
}
disableAllButtons() {
@@ -475,7 +479,7 @@ export class Engine {
}
// Sandbox is quickly a way to test your idea.
public async sandbox() {}
public async sandbox() { }
public onMessage(event: MessageEvent) {
console.log("Message From Shared Worker");
@@ -603,7 +607,7 @@ export class Engine {
if (
longestVideoNode === undefined ||
node.videoComponent.duration >
longestVideoNode.videoComponent.duration
longestVideoNode.videoComponent.duration
) {
longestVideoNode = node;
}
@@ -279,8 +279,10 @@ export class SelectionManager {
}
if (node instanceof TextNode) {
showOrUpdate({
// TODO: Add ID to identify/mutate the color?
locked: node.isLocked(),
buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }),
color: node.getNodeData({ x: 0, y: 0 }).textNodeData?.color ?? "#000000",
});
}
if (node instanceof VideoNode) {
@@ -51,7 +51,8 @@ export function getImageNodeButtonStates(
buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.SEGMENTATION ||
buttonName === ButtonNames.DOWNLOAD ||
buttonName === ButtonNames.CHROMA
buttonName === ButtonNames.CHROMA ||
buttonName === ButtonNames.COLOR
) {
buttonStates[buttonName] = {
disabled: true,
@@ -130,7 +131,9 @@ export function getVideoNodeButtonStates(
return buttonStates;
}
// soon to come feature is disabled
if (buttonName === ButtonNames.AI_STYLIZE) {
if (buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.COLOR
) {
buttonStates[buttonName] = {
disabled: true,
hidden: true,
@@ -170,7 +173,8 @@ export function getMultiSelectButtonStates(
buttonName === ButtonNames.AI_STYLIZE ||
buttonName === ButtonNames.SEGMENTATION ||
buttonName === ButtonNames.DOWNLOAD ||
buttonName === ButtonNames.CHROMA
buttonName === ButtonNames.CHROMA ||
buttonName === ButtonNames.COLOR
) {
buttonStates[buttonName] = {
disabled: true,
@@ -71,6 +71,8 @@ export const ContextualToolbarNode = () => {
onLockClicked={(e) => {
dispatchUiEvents.toolbarNode.lock(e);
}}
color={uiAccess.toolbarNode.signal.value.color}
onColorChange={(color) => { uiAccess.toolbarNode.setColor(color) }}
/>
</Transition>
);
@@ -12,6 +12,7 @@ import { LayoutSignalType } from "./contextSignals/layout";
import { AppUiContextInterface } from "./contextSignals/appUi";
import { twMerge } from "tailwind-merge";
import { paperWrapperStyles } from "~/components/styles";
import { dispatchUiEvents } from "~/signals";
export const SignaledToolbarMain = ({
// layoutSignal,
@@ -43,6 +44,12 @@ export const SignaledToolbarMain = ({
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" });
default:
return dispatchers[buttonName];
}
@@ -5,13 +5,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowRotateLeft,
faArrowRotateRight,
faCircle,
faFilePlus,
faFilm,
faFloppyDisk,
faImage,
faLocationArrow,
faMagicWandSparkles,
faShapes,
faSquare,
faText,
faTriangle,
} from "@fortawesome/pro-solid-svg-icons";
import { ToolbarButton } from "../ToolbarButton";
@@ -41,7 +45,7 @@ export const ToolbarMain = ({
"flex w-fit items-center divide-x divide-ui-border",
paperWrapperStyles,
disabled &&
"pointer-events-none cursor-default bg-ui-border shadow-md",
"pointer-events-none cursor-default bg-ui-border shadow-md",
)}
>
<div className="flex items-center gap-2 pr-2">
@@ -56,6 +60,33 @@ export const ToolbarMain = ({
buttonProps={buttonProps.ADD_TEXT}
tooltip="Add Text"
/>
<Popover className="relative">
<PopoverButton as={Fragment}>
<button
data-tooltip="Add Shapes"
className={twMerge(
"size-10 rounded-lg p-2 hover:bg-gray-200/50",
toolTipStyles,
)}
>
<FontAwesomeIcon icon={faShapes} />
</button>
</PopoverButton>
<PopoverPanel
anchor="bottom"
className={twMerge(
// "absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2",
"flex flex-col [--anchor-gap:16px]",
paperWrapperStyles,
)}
>
<ToolbarButton icon={faSquare} buttonProps={buttonProps.ADD_SQUARE} />
<ToolbarButton icon={faCircle} buttonProps={buttonProps.ADD_CIRCLE} />
<ToolbarButton icon={faTriangle} buttonProps={buttonProps.ADD_TRIANGLE} />
</PopoverPanel>
</Popover>
<Popover className="relative">
<PopoverButton as={Fragment}>
<button
@@ -4,6 +4,9 @@ export enum ToolbarMainButtonNames {
ADD_TEXT = "ADD_TEXT",
ADD_IMAGE = "ADD_IMAGE",
ADD_VIDEO = "ADD_VIDEO",
ADD_CIRCLE = "ADD_CIRCLE",
ADD_TRIANGLE = "ADD_TRIANGLE",
ADD_SQUARE = "ADD_SQUARE",
AI_STYLIZE = "AI_STYLIZE",
UNDO = "UNDO",
REDO = "REDO",
@@ -4,14 +4,19 @@ import {
faLockKeyholeOpen,
faLockKeyhole,
faUnlockKeyhole,
faPalette,
} from "@fortawesome/pro-solid-svg-icons";
import {
ToolbarButton,
ToolbarButtonProps,
} from "~/components/features/ToolbarButton";
import { paperWrapperStyles } from "~/components/styles";
import { paperWrapperStyles, toolTipStyles } from "~/components/styles";
import { ToolbarNodeButtonNames } from "./enums";
import { ToolbarNodeButtonData } from "./data";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Fragment } from "react/jsx-runtime";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColorPicker } from "~/components/ui/TextEditor/ColorPicker";
export interface ToolbarNodeProps {
disabled?: boolean;
@@ -24,6 +29,8 @@ export interface ToolbarNodeProps {
buttonsProps?: {
[key in ToolbarNodeButtonNames]: ToolbarButtonProps;
};
color?: string;
onColorChange?: (color: string) => void;
}
export const ToolbarNode = ({
disabled,
@@ -31,6 +38,8 @@ export const ToolbarNode = ({
lockDisabled,
onLockClicked,
buttonsProps,
color = "#000000",
onColorChange = () => { },
}: ToolbarNodeProps) => {
const handleOnLockClicked: React.MouseEventHandler<HTMLButtonElement> = (
e,
@@ -53,7 +62,7 @@ export const ToolbarNode = ({
className: twMerge(
locked && "text-primary hover:bg-primary hover:text-white",
lockDisabledOrUnknown &&
"text-secondary-300 hover:text-secondary-300",
"text-secondary-300 hover:text-secondary-300",
),
disabled: lockDisabledOrUnknown,
}}
@@ -73,14 +82,24 @@ export const ToolbarNode = ({
{ToolbarNodeButtonData.map((buttonDatum, idx) => {
const buttonProps = buttonsProps?.[buttonDatum.name];
return (
<ToolbarButton
icon={buttonDatum.icon}
tooltip={buttonDatum.tooltip}
key={idx}
buttonProps={buttonProps}
/>
);
if (!buttonProps || buttonProps.hidden) {
return;
}
if (buttonDatum.name !== ToolbarNodeButtonNames.COLOR) {
return (
<ToolbarButton
icon={buttonDatum.icon}
tooltip={buttonDatum.tooltip}
key={idx}
buttonProps={buttonProps}
/>
);
} else {
return (
<ColorPicker color={color} onChange={onColorChange} faIcon={faPalette} borderStyle="" />
)
}
})}
</div>
);
@@ -8,6 +8,7 @@ import {
faTrashCan,
faVectorSquare,
faDownload,
faPalette,
} from "@fortawesome/pro-solid-svg-icons";
export const ToolbarNodeButtonData = [
@@ -16,6 +17,11 @@ export const ToolbarNodeButtonData = [
icon: faVectorSquare,
tooltip: "Move",
},
{
name: ButtonNames.COLOR,
icon: faPalette,
tooltip: "Color",
},
{
name: ButtonNames.CHROMA,
icon: faTransporter,
@@ -7,4 +7,5 @@ export enum ToolbarNodeButtonNames {
MOVE_LAYER_UP = "MOVE_LAYER_UP",
MOVE_LAYER_DOWN = "MOVE_LAYER_DOWN",
DOWNLOAD = "DOWNLOAD",
COLOR = "COLOR",
}
@@ -1,6 +1,6 @@
import { ChangeEvent, useState } from "react";
import { twMerge } from "tailwind-merge";
import { faFont, faHashtag } from "@fortawesome/pro-solid-svg-icons";
import { faFont, faHashtag, IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { HexColorPicker } from "react-colorful";
@@ -12,9 +12,15 @@ import { paperWrapperStyles } from "~/components/styles";
export const ColorPicker = ({
color: prevColor,
onChange,
faIcon = faFont,
borderStyle = "border",
showBar = true,
}: {
color: string;
onChange: (newColor: string) => void;
faIcon?: IconDefinition;
borderStyle?: string;
showBar?: boolean;
}) => {
const [{ currColor, textInput }, setStates] = useState<{
currColor: string;
@@ -44,11 +50,15 @@ export const ColorPicker = ({
}));
}
};
return (
<Popover className="relative">
<PopoverButton className="flex size-10 flex-col items-center gap-1 rounded-md border p-2">
<FontAwesomeIcon icon={faFont} />
<span className="h-1 w-full" style={{ backgroundColor: prevColor }} />
<PopoverButton className={twMerge(
"flex size-10 flex-col items-center gap-1 rounded-md p-2",
borderStyle
)}>
<FontAwesomeIcon icon={faIcon} color={prevColor} />
{showBar && <span className="h-1 w-full" style={{ backgroundColor: prevColor }} />}
</PopoverButton>
<PopoverPanel
anchor="bottom"
@@ -16,6 +16,7 @@ export interface ContextualToolbarProps extends ContextualUi {
locked: boolean | "unknown";
lockDisabled: boolean;
buttonStates: ButtonStates;
color: string;
}
interface PartialContextualToolbarProps
extends Partial<Omit<ContextualToolbarProps, "buttonStates">> {
@@ -32,6 +33,7 @@ const toolbarNodeSignal = signal<ContextualToolbarProps>({
locked: false,
lockDisabled: false,
buttonStates: initButtonStates(),
color: "#000000",
});
export const toolbarNode = {
@@ -77,6 +79,11 @@ export const toolbarNode = {
hidden: false,
active: buttonStates[ButtonNames.TRANSFORM].active,
},
[ButtonNames.COLOR]: {
disabled: locked,
hidden: false,
active: buttonStates[ButtonNames.COLOR].active,
},
},
locked: locked,
};
@@ -135,6 +142,12 @@ export const toolbarNode = {
},
};
},
setColor(color: string) {
toolbarNodeSignal.value = {
...toolbarNodeSignal.value,
color,
}
},
batchButtonStates({ buttonStates }: { buttonStates: Partial<ButtonStates> }) {
const prevButtonStates = toolbarNodeSignal.value.buttonStates;
toolbarNodeSignal.value = {
@@ -49,15 +49,32 @@ const onAddTextToEngine = (callback: (data: TextNodeData) => void) => {
});
};
// Add shapes
// TODO: Remove this line and replace with actual shape data that engine needs
type ShapeNodeData = any;
const stagedShape = signal<ShapeNodeData | null>(null);
const addShapeToEngine = (data: ShapeNodeData) => {
stagedShape.value = data;
}
const onAddShapeToEngine = (callback: (data: ShapeNodeData) => void) => {
effect(() => {
if (stagedShape.value) {
callback(stagedShape.value);
}
});
};
//EXPORTS
export const dispatchers = {
addImageToEngine,
addVideoToEngine,
addTextToEngine,
addShapeToEngine,
};
export const events = {
onGetStagedImage,
onGetStagedVideo,
onAddTextToEngine,
onAddShapeToEngine,
};