mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-30 15:25:57 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9528a7f7 | ||
|
|
9361634d83 | ||
|
|
7cf0363555 | ||
|
|
c2ee3b0ce1 | ||
|
|
ca6fc13171 |
@@ -8,6 +8,7 @@ export interface PopoverItem {
|
||||
selected: boolean;
|
||||
icon?: ReactNode;
|
||||
action?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface PopoverMenuProps {
|
||||
@@ -42,11 +43,13 @@ export const PopoverMenu = ({
|
||||
}: PopoverMenuProps) => {
|
||||
const selectedItem = items.find((item) => item.selected);
|
||||
|
||||
const handleItemClick = (item: PopoverItem) => {
|
||||
const handleItemClick = (item: PopoverItem, close: () => void) => {
|
||||
if (mode === "button" && item.action && onAction) {
|
||||
onAction(item.action);
|
||||
close();
|
||||
} else {
|
||||
onSelect?.(item);
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,67 +63,87 @@ export const PopoverMenu = ({
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button className={className}>
|
||||
{triggerIcon}
|
||||
{mode === "toggle" && selectedItem ? selectedItem.label : null}
|
||||
{mode === "default" && triggerLabel ? triggerLabel : null}
|
||||
</Popover.Button>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<Popover.Button className={className}>
|
||||
{triggerIcon}
|
||||
{mode === "toggle" && selectedItem ? selectedItem.label : null}
|
||||
{mode === "default" && triggerLabel ? triggerLabel : null}
|
||||
</Popover.Button>
|
||||
|
||||
<div className="absolute bottom-full left-0 transform-gpu">
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="translate-y-2 opacity-0"
|
||||
enterTo="translate-y-0 opacity-100"
|
||||
leave="transition duration-100 ease-in"
|
||||
leaveFrom="translate-y-0 opacity-100"
|
||||
leaveTo="translate-y-2 opacity-0"
|
||||
>
|
||||
<Popover.Panel className="origin-bottom">
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-10 mb-2 min-w-48 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
|
||||
)}
|
||||
<div className="absolute bottom-full left-0 transform-gpu">
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="translate-y-2 opacity-0"
|
||||
enterTo="translate-y-0 opacity-100"
|
||||
leave="transition duration-100 ease-in"
|
||||
leaveFrom="translate-y-0 opacity-100"
|
||||
leaveTo="translate-y-2 opacity-0"
|
||||
>
|
||||
{panelTitle && (
|
||||
<div className="mb-2 mt-0.5 px-1.5 text-sm font-normal text-white opacity-70">
|
||||
{panelTitle}
|
||||
</div>
|
||||
)}
|
||||
{mode === "default" && children ? (
|
||||
<div className="text-sm text-white">{children}</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-0 text-sm text-white">
|
||||
{items.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
className={`flex items-center justify-between border-transparent bg-transparent px-1.5 hover:bg-[#63636B]/60 ${mode === "toggle" && item.selected ? "hover:bg-[#63636B]" : "bg-transparent"}`}
|
||||
onClick={() => handleItemClick(item)}
|
||||
variant="secondary"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{showIconsInList && item.icon}
|
||||
{item.label}
|
||||
</div>
|
||||
{mode === "toggle" && (
|
||||
<input type="radio" checked={item.selected} readOnly />
|
||||
<Popover.Panel className="origin-bottom">
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-10 mb-2 min-w-48 rounded-lg bg-[#46464B] p-1.5 shadow-lg",
|
||||
)}
|
||||
>
|
||||
{panelTitle && (
|
||||
<div className="mb-2 mt-0.5 px-1.5 text-sm font-normal text-white opacity-70">
|
||||
{panelTitle}
|
||||
</div>
|
||||
)}
|
||||
{mode === "default" && children ? (
|
||||
<div className="text-sm text-white">{children}</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-0 text-sm text-white">
|
||||
{items.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
className={twMerge(
|
||||
"flex items-center justify-between border-transparent bg-transparent px-1.5",
|
||||
"hover:bg-[#63636B]/60",
|
||||
mode === "toggle" && item.selected
|
||||
? "hover:bg-[#63636B]"
|
||||
: "bg-transparent",
|
||||
item.disabled
|
||||
? "!cursor-not-allowed opacity-50"
|
||||
: "",
|
||||
)}
|
||||
onClick={() =>
|
||||
!item.disabled && handleItemClick(item, close)
|
||||
}
|
||||
variant="secondary"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{showIconsInList && item.icon}
|
||||
{item.label}
|
||||
</div>
|
||||
{mode === "toggle" && (
|
||||
<input
|
||||
type="radio"
|
||||
checked={item.selected}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
{showAddButton && onAdd && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mb-0.5 mt-2 border-none bg-[#7B7B84] py-1 hover:bg-[#8c8c96]"
|
||||
onClick={onAdd}
|
||||
>
|
||||
+ Add
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
{showAddButton && onAdd && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mb-0.5 mt-2 border-none bg-[#7B7B84] py-1 hover:bg-[#8c8c96]"
|
||||
onClick={onAdd}
|
||||
>
|
||||
+ Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ class Editor {
|
||||
camera: THREE.PerspectiveCamera | null = null;
|
||||
render_camera: THREE.PerspectiveCamera | null = null;
|
||||
render_camera_aspect_ratio: CameraAspectRatio =
|
||||
CameraAspectRatio.HORIZONTAL_16_9;
|
||||
CameraAspectRatio.HORIZONTAL_3_2;
|
||||
renderer: THREE.WebGLRenderer | undefined;
|
||||
rawRenderer: THREE.WebGLRenderer | undefined;
|
||||
clock: THREE.Clock | undefined;
|
||||
@@ -253,7 +253,7 @@ class Editor {
|
||||
this.selectedCanvas = false;
|
||||
// Audio Engine Test.
|
||||
|
||||
this.render_camera_aspect_ratio = CameraAspectRatio.HORIZONTAL_16_9;
|
||||
this.render_camera_aspect_ratio = CameraAspectRatio.HORIZONTAL_3_2;
|
||||
this.render_width = this.getRenderDimensions().width;
|
||||
this.render_height = this.getRenderDimensions().height;
|
||||
|
||||
@@ -334,6 +334,20 @@ class Editor {
|
||||
aspectRatio: 16 / 9,
|
||||
};
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_3_2: {
|
||||
return {
|
||||
width: 1200,
|
||||
height: 800,
|
||||
aspectRatio: 3 / 2,
|
||||
};
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_2_3: {
|
||||
return {
|
||||
width: 800,
|
||||
height: 1200,
|
||||
aspectRatio: 2 / 3,
|
||||
};
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return {
|
||||
width: 720,
|
||||
@@ -948,16 +962,28 @@ class Editor {
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 1024
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
? 576
|
||||
: 1000;
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.HORIZONTAL_3_2
|
||||
? 900
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_2_3
|
||||
? 600
|
||||
: 1000;
|
||||
const height =
|
||||
this.render_camera_aspect_ratio === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? 576
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
CameraAspectRatio.VERTICAL_9_16
|
||||
? 1024
|
||||
: 1000;
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.HORIZONTAL_3_2
|
||||
? 600
|
||||
: this.render_camera_aspect_ratio ===
|
||||
CameraAspectRatio.VERTICAL_2_3
|
||||
? 900
|
||||
: 1000;
|
||||
|
||||
this.rawRenderer.setSize(width, height);
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ export const PageEditor = () => {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
<AspectRatioMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export const Wizard = () => {
|
||||
onNext: () => {
|
||||
resetSceneGenerationMetadata();
|
||||
editorEngine?.changeRenderCameraAspectRatio(
|
||||
CameraAspectRatio.HORIZONTAL_16_9,
|
||||
CameraAspectRatio.HORIZONTAL_3_2,
|
||||
);
|
||||
editorEngine?.newScene(textInput.value);
|
||||
showWizard.value = "end";
|
||||
|
||||
@@ -18,13 +18,21 @@ export const AspectRatioMenu = () => {
|
||||
? faRectangle
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16
|
||||
? faRectangleVertical
|
||||
: faSquare;
|
||||
: cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_3_2
|
||||
? faRectangle
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_2_3
|
||||
? faRectangleVertical
|
||||
: faSquare;
|
||||
const buttonText =
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9
|
||||
? "16:9 Horizontal"
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_9_16
|
||||
? "9:16 Vertical"
|
||||
: "1:1 Squared";
|
||||
: cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_3_2
|
||||
? "3:2 Horizontal"
|
||||
: cameraAspectRatio.value === CameraAspectRatio.VERTICAL_2_3
|
||||
? "2:3 Vertical"
|
||||
: "1:1 Squared";
|
||||
|
||||
const handleChangeAspectRatio = (newRatio: CameraAspectRatio) => {
|
||||
Queue.publish({
|
||||
@@ -52,6 +60,26 @@ export const AspectRatioMenu = () => {
|
||||
onClick: () =>
|
||||
handleChangeAspectRatio(CameraAspectRatio.HORIZONTAL_16_9),
|
||||
},
|
||||
{
|
||||
label: "3:2",
|
||||
icon: faRectangle,
|
||||
className: "pl-4",
|
||||
description: "Horizontal",
|
||||
selected:
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_3_2,
|
||||
onClick: () =>
|
||||
handleChangeAspectRatio(CameraAspectRatio.HORIZONTAL_3_2),
|
||||
},
|
||||
{
|
||||
label: "2:3",
|
||||
icon: faRectangleVertical,
|
||||
className: "pl-4",
|
||||
description: "Vertical",
|
||||
selected:
|
||||
cameraAspectRatio.value === CameraAspectRatio.VERTICAL_2_3,
|
||||
onClick: () =>
|
||||
handleChangeAspectRatio(CameraAspectRatio.VERTICAL_2_3),
|
||||
},
|
||||
{
|
||||
label: "9:16",
|
||||
icon: faRectangleVertical,
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export const ControlsTopButtons = () => {
|
||||
const handleResetScene = () => {
|
||||
resetSceneGenerationMetadata();
|
||||
editorEngine?.changeRenderCameraAspectRatio(
|
||||
CameraAspectRatio.HORIZONTAL_16_9,
|
||||
CameraAspectRatio.HORIZONTAL_3_2,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
import {
|
||||
faPlus,
|
||||
@@ -9,19 +10,55 @@ import {
|
||||
faImages,
|
||||
faDownload,
|
||||
faSpinnerThird,
|
||||
faTimes,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faRectangleWide,
|
||||
faRectangleVertical,
|
||||
faSquare,
|
||||
faRectangle,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem, PopoverMenu } from "~/components/reusable/Popover";
|
||||
import { Button } from "~/components";
|
||||
import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
import { cameraAspectRatio } from "~/pages/PageEnigma/signals";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
|
||||
interface ReferenceImage {
|
||||
id: string;
|
||||
url: string;
|
||||
file: File;
|
||||
}
|
||||
|
||||
export const PromptBox = () => {
|
||||
useSignals();
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isEnqueueing, setisEnqueueing] = useState(false);
|
||||
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
|
||||
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
|
||||
{ label: "16:9", selected: true },
|
||||
{ label: "3:2", selected: false },
|
||||
{ label: "2:3", selected: false },
|
||||
{ label: "1:1", selected: false },
|
||||
{
|
||||
label: "3:2",
|
||||
selected: true,
|
||||
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "2:3",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faRectangleVertical} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "1:1",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faSquare} className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
label: "16:9",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faRectangleWide} className="h-4 w-4" />,
|
||||
},
|
||||
]);
|
||||
const [cameraList, setCameraList] = useState<PopoverItem[]>([
|
||||
{
|
||||
@@ -45,6 +82,25 @@ export const PromptBox = () => {
|
||||
}
|
||||
}, [prompt]);
|
||||
|
||||
// Update aspect ratio list based on the current cameraAspectRatio signal
|
||||
useEffect(() => {
|
||||
setAspectRatioList((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
selected:
|
||||
(item.label === "16:9" &&
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9) ||
|
||||
(item.label === "3:2" &&
|
||||
cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_3_2) ||
|
||||
(item.label === "2:3" &&
|
||||
cameraAspectRatio.value === CameraAspectRatio.VERTICAL_2_3) ||
|
||||
(item.label === "1:1" &&
|
||||
cameraAspectRatio.value === CameraAspectRatio.SQUARE_1_1),
|
||||
})),
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cameraAspectRatio.value]);
|
||||
|
||||
const handleCameraSelect = (selectedItem: PopoverItem) => {
|
||||
setCameraList((prev) =>
|
||||
prev.map((item) => ({
|
||||
@@ -73,12 +129,58 @@ export const PromptBox = () => {
|
||||
selected: item.label === selectedItem.label,
|
||||
})),
|
||||
);
|
||||
|
||||
// Map the selected label to the corresponding CameraAspectRatio enum value
|
||||
let newRatio: CameraAspectRatio;
|
||||
switch (selectedItem.label) {
|
||||
case "16:9":
|
||||
newRatio = CameraAspectRatio.HORIZONTAL_16_9;
|
||||
break;
|
||||
case "3:2":
|
||||
newRatio = CameraAspectRatio.HORIZONTAL_3_2;
|
||||
break;
|
||||
case "2:3":
|
||||
newRatio = CameraAspectRatio.VERTICAL_2_3;
|
||||
break;
|
||||
case "1:1":
|
||||
newRatio = CameraAspectRatio.SQUARE_1_1;
|
||||
break;
|
||||
default:
|
||||
newRatio = CameraAspectRatio.HORIZONTAL_16_9;
|
||||
}
|
||||
|
||||
// Publish the change to the engine
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.CHANGE_CAMERA_ASPECT_RATIO,
|
||||
data: newRatio,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
console.log("File uploaded:", file);
|
||||
const files = event.target.files;
|
||||
if (files) {
|
||||
Array.from(files).forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setReferenceImages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: reader.result as string,
|
||||
file,
|
||||
},
|
||||
]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveReference = (id: string) => {
|
||||
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -118,9 +220,14 @@ export const PromptBox = () => {
|
||||
|
||||
setisEnqueueing(true);
|
||||
try {
|
||||
// generate logic here
|
||||
console.log("Enqueuing with prompt:", prompt);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // dummy delay here
|
||||
// Here we would pass both the prompt and reference images to the generation
|
||||
console.log(
|
||||
"Enqueuing with prompt:",
|
||||
prompt,
|
||||
"and reference images:",
|
||||
referenceImages,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
} finally {
|
||||
setisEnqueueing(false);
|
||||
}
|
||||
@@ -133,101 +240,152 @@ export const PromptBox = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass absolute bottom-4 left-1/2 w-[730px] -translate-x-1/2 rounded-xl p-4">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div className="flex justify-center gap-2">
|
||||
<PopoverMenu
|
||||
mode="button"
|
||||
panelTitle="Add Image"
|
||||
items={[
|
||||
{
|
||||
label: "Upload from device",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
|
||||
action: "upload",
|
||||
},
|
||||
{
|
||||
label: "Choose from library",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
|
||||
action: "library",
|
||||
},
|
||||
]}
|
||||
onAction={handleAction}
|
||||
showIconsInList
|
||||
buttonClassName="backdrop-blur-none bg-transparent hover:bg-transparent py-1.5 px-0 pr-0.5 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
|
||||
triggerIcon={<FontAwesomeIcon icon={faPlus} className="text-xl" />}
|
||||
/>
|
||||
// Get the current aspect ratio icon based on the cameraAspectRatio signal
|
||||
const getCurrentAspectRatioIcon = () => {
|
||||
switch (cameraAspectRatio.value) {
|
||||
case CameraAspectRatio.HORIZONTAL_16_9:
|
||||
return faRectangleWide;
|
||||
case CameraAspectRatio.HORIZONTAL_3_2:
|
||||
return faRectangle;
|
||||
case CameraAspectRatio.VERTICAL_2_3:
|
||||
return faRectangleVertical;
|
||||
case CameraAspectRatio.VERTICAL_9_16:
|
||||
return faRectangleVertical;
|
||||
case CameraAspectRatio.SQUARE_1_1:
|
||||
return faSquare;
|
||||
default:
|
||||
return faRectangleWide;
|
||||
}
|
||||
};
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className="text-md mb-2 max-h-[5.5em] flex-1 resize-none overflow-y-auto rounded bg-transparent px-2 pb-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
return (
|
||||
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
|
||||
{referenceImages.length > 0 && (
|
||||
<div className="flex w-full gap-2">
|
||||
{referenceImages.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="glass relative aspect-square w-20 rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Reference"
|
||||
className="h-full w-full rounded-lg object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveReference(image.id)}
|
||||
className="absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-md transition-colors hover:bg-black"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="glass w-[730px] rounded-xl p-4">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleFileUpload}
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex justify-center gap-2">
|
||||
<PopoverMenu
|
||||
items={aspectRatioList}
|
||||
onSelect={handleAspectRatioSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Aspect Ratio"
|
||||
/>
|
||||
<PopoverMenu
|
||||
items={cameraList}
|
||||
onSelect={handleCameraSelect}
|
||||
onAdd={handleAddCamera}
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faCamera} className="h-4 w-4" />
|
||||
}
|
||||
showAddButton
|
||||
mode="button"
|
||||
panelTitle="Add Image"
|
||||
items={[
|
||||
{
|
||||
label: "Upload from device",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
|
||||
action: "upload",
|
||||
},
|
||||
{
|
||||
label: "Choose from library",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
|
||||
action: "library",
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
onAction={handleAction}
|
||||
showIconsInList
|
||||
mode="toggle"
|
||||
panelTitle="Camera"
|
||||
buttonClassName="backdrop-blur-none bg-transparent hover:bg-transparent py-1.5 px-0 pr-1 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
|
||||
triggerIcon={<FontAwesomeIcon icon={faPlus} className="text-xl" />}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Describe your image..."
|
||||
className="text-md mb-2 max-h-[5.5em] flex-1 resize-none overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faDownload}
|
||||
>
|
||||
Download frame
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faSave}
|
||||
>
|
||||
Save frame
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center border-none bg-brand-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
icon={!isEnqueueing ? faSparkles : undefined}
|
||||
onClick={handleEnqueue}
|
||||
disabled={isEnqueueing || !prompt.trim()}
|
||||
>
|
||||
{isEnqueueing ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-lg"
|
||||
/>
|
||||
) : (
|
||||
"Generate"
|
||||
)}
|
||||
</Button>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<PopoverMenu
|
||||
items={aspectRatioList}
|
||||
onSelect={handleAspectRatioSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Aspect Ratio"
|
||||
showIconsInList
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon
|
||||
icon={getCurrentAspectRatioIcon()}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PopoverMenu
|
||||
items={cameraList}
|
||||
onSelect={handleCameraSelect}
|
||||
onAdd={handleAddCamera}
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faCamera} className="h-4 w-4" />
|
||||
}
|
||||
showAddButton
|
||||
showIconsInList
|
||||
mode="toggle"
|
||||
panelTitle="Camera"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faDownload}
|
||||
>
|
||||
Download frame
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faSave}
|
||||
>
|
||||
Save frame
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center border-none bg-brand-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
icon={!isEnqueueing ? faSparkles : undefined}
|
||||
onClick={handleEnqueue}
|
||||
disabled={isEnqueueing || !prompt.trim()}
|
||||
>
|
||||
{isEnqueueing ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-lg"
|
||||
/>
|
||||
) : (
|
||||
"Generate"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,16 @@ export const getMatteOrientation = ({
|
||||
? MatteOrientation.TOP_BOTTOM
|
||||
: MatteOrientation.LEFT_RIGHT;
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_2_3: {
|
||||
return (height / 3) * 2 - width >= 0
|
||||
? MatteOrientation.TOP_BOTTOM
|
||||
: MatteOrientation.LEFT_RIGHT;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_3_2: {
|
||||
return (width / 3) * 2 - height >= 0
|
||||
? MatteOrientation.LEFT_RIGHT
|
||||
: MatteOrientation.TOP_BOTTOM;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_16_9:
|
||||
default: {
|
||||
return (width / 16) * 9 - height >= 0
|
||||
@@ -58,6 +68,12 @@ export const calcMatteWidth = ({
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return (width - (height / 16) * 9) / 2;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_3_2: {
|
||||
return (width - (height / 2) * 3) / 2;
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_2_3: {
|
||||
return (width - (height / 3) * 2) / 2;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
@@ -88,6 +104,12 @@ export const calcMatteHeight = ({
|
||||
case CameraAspectRatio.VERTICAL_9_16: {
|
||||
return (height - (width / 9) * 16) / 2;
|
||||
}
|
||||
case CameraAspectRatio.HORIZONTAL_3_2: {
|
||||
return (height - (width / 3) * 2) / 2;
|
||||
}
|
||||
case CameraAspectRatio.VERTICAL_2_3: {
|
||||
return (height - (width / 2) * 3) / 2;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ export enum CameraAspectRatio {
|
||||
HORIZONTAL_16_9 = "horizontal_16_9",
|
||||
VERTICAL_9_16 = "vertical_9_16",
|
||||
SQUARE_1_1 = "SQUARE_1_1",
|
||||
HORIZONTAL_3_2 = "horizontal_3_2",
|
||||
VERTICAL_2_3 = "vertical_2_3",
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CameraAspectRatio, EditorStates } from "~/pages/PageEnigma/enums";
|
||||
export const editorState = signal<EditorStates>(EditorStates.EDIT);
|
||||
|
||||
export const cameraAspectRatio = signal<CameraAspectRatio>(
|
||||
CameraAspectRatio.HORIZONTAL_16_9,
|
||||
CameraAspectRatio.HORIZONTAL_3_2,
|
||||
);
|
||||
|
||||
export const setCameraAspectRatio = (newAspectRatio: CameraAspectRatio) => {
|
||||
|
||||
Generated
+12
@@ -9,6 +9,7 @@
|
||||
"@awesome.me/kit-fde2be5eb0": "^1.0.7",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
@@ -1423,6 +1424,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/pro-regular-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/pro-regular-svg-icons/-/6.7.2/pro-regular-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-cJolK4K/s56mHAqOyno24SmjviHqSqG9ZfckBKCpWjJlgDexi61EfET5wWbmmEot6/BqasFjw9S6tudFQUL/PQ==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/pro-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/pro-solid-svg-icons/-/6.7.2/pro-solid-svg-icons-6.7.2.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@awesome.me/kit-fde2be5eb0": "^1.0.7",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
|
||||
Reference in New Issue
Block a user