Compare commits

...
Author SHA1 Message Date
Michael Chung 1ce9c8bed8 Added reference images to the generation
enqueueImageGeneration now accepts additionalImages
enqueueImageGeneration needs testing.
Polling for jobs doesn't work,
most api calls don't work and need integration testing.
2025-04-03 17:26:52 -04:00
Michael Chung 25c15bffd8 Merge branch 'main' into feature/gen 2025-04-03 12:37:31 -04:00
bflatastic 8ff91690c1 bring back objects, skybox, creatures tabs 2025-04-03 23:24:14 +07:00
BFlat 97bbe0fe07 add image ui (#471) 2025-04-03 23:01:02 +07:00
Michael Chung 2d361c6636 Merge branch 'main' into feature/gen 2025-04-03 11:55:38 -04:00
BFlat 9361634d83 final fix ratio default to 3:2 (#469) 2025-04-03 12:41:40 +07:00
bflatastic 7cf0363555 fixed ratio in render 3:2 2025-04-03 12:32:45 +07:00
bflatastic c2ee3b0ce1 render default to 3:2 ratio 2025-04-03 12:30:57 +07:00
BFlat ca6fc13171 aspect ratio update (#468)
- added 3:2, 2:3 ratios (defaulted to 3:2 [1200 x 800 px])
- added letterbox accordingly
- aspect ratio buttons to show icons for each ratio
- popover to close when item selected
2025-04-03 12:23:54 +07:00
Michael Chung 534afbbbf9 Wire screenshot upload and generate to the engine. 2025-04-02 21:36:42 -04:00
Brandon Thomas cb1ea1f828 fix CDN in local development 2025-04-02 18:48:44 -04:00
bflatastic 010e3be5dc make letterbox always visible 2025-04-02 22:59:59 +07:00
bflatastic d8dab1a66e fix trimming 2 2025-04-02 22:42:28 +07:00
bflatastic 97b236d04d paste trim fix 2025-04-02 22:40:56 +07:00
bflatastic 15fe59cc19 added enqueueing state to generate button, limited textarea height and updates 2025-04-02 22:36:53 +07:00
BFlat 89bec31540 removed video controls and add prompt box (#467) 2025-04-02 22:01:19 +07:00
22 changed files with 959 additions and 48 deletions
@@ -36,4 +36,168 @@ export class EngineApi extends ApiManager {
};
});
}
public async uploadSceneSnapshot({
screenshot,
sceneMediaToken,
}: {
screenshot: string; // base64 encoded PNG
sceneMediaToken?: string;
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/scene_snapshot`;
const formData = new FormData();
formData.append("snapshot", screenshot); // Changed from "screenshot" to "snapshot" to match API spec
if (sceneMediaToken) {
formData.append("scene_media_token", sceneMediaToken);
}
const response = await fetch(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
},
credentials: "include",
body: formData,
});
const postResponse = await response.json();
console.log(postResponse);
let result: { success: boolean; data?: string; errorMessage?: string };
if (postResponse.success) {
result = {
success: true,
data: postResponse.snapshot_media_token,
errorMessage: undefined,
};
} else {
result = {
success: false,
errorMessage: postResponse.BadInput,
};
}
return result;
}
public async enqueueImageGeneration({
prompt,
snapshotMediaToken,
additionalImages,
}: {
prompt: string;
snapshotMediaToken: string;
additionalImages?: string[];
}): Promise<ApiResponse<string>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/prompt`;
const body = {
prompt,
snapshot_media_token: snapshotMediaToken, // Changed from scene_media_token to snapshot_media_token
additional_images: additionalImages,
};
const postResponse = await this.post<
{
prompt: string;
snapshot_media_token: string;
additional_images?: string[];
},
{ success?: boolean; job_token?: string; BadInput?: string }
>({
endpoint,
body,
});
// Check if the response is successful
const isSuccess = postResponse.success ?? false;
// Prepare the result object
const result = {
success: isSuccess,
data: isSuccess ? postResponse.job_token : undefined,
errorMessage: isSuccess ? undefined : postResponse.BadInput,
};
return result;
}
public async pollJobSession(jobToken: string): Promise<
ApiResponse<{
status: string;
result: {
generated_images?: string[];
error?: string;
};
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session/${jobToken}`;
const response = await this.get<{
success?: boolean;
status?: string;
result?: {
generated_images?: string[];
error?: string;
};
BadInput?: string;
}>({
endpoint,
});
const success = response.success ?? false;
const status = response.status ?? "";
const result = response.result ?? {
generated_images: [],
error: undefined,
};
const errorMessage = response.BadInput;
return {
success,
data: { status, result },
errorMessage,
};
}
public async pollStudioSessionJobs(jobToken: string): Promise<
ApiResponse<{
status: string;
result: {
generated_images?: string[];
error?: string;
};
}>
> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/image_studio/session_jobs/${jobToken}`;
const response = await this.get<{
success?: boolean;
status?: string;
result?: {
generated_images?: string[];
error?: string;
};
BadInput?: string;
}>({
endpoint,
});
const success = response.success ?? false;
const status = response.status ?? "";
const result = response.result ?? {
generated_images: [],
error: undefined,
};
const errorMessage = response.BadInput;
return {
success,
data: { status, result },
errorMessage,
};
}
}
+2 -1
View File
@@ -19,7 +19,8 @@ class CdnHostHelper {
let origin;
if (isLocalDev) {
origin = 'https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev';
//origin = 'https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev';
origin = 'https://cdn-2.fakeyou.com';
} else {
origin = 'https://cdn-2.fakeyou.com';
}
@@ -0,0 +1,149 @@
import { ReactNode } from "react";
import { Popover, Transition } from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { Button } from "../Button";
export interface PopoverItem {
label: string;
selected: boolean;
icon?: ReactNode;
action?: string;
disabled?: boolean;
}
interface PopoverMenuProps {
items?: PopoverItem[];
onSelect?: (item: PopoverItem) => void;
onAdd?: () => void;
triggerIcon?: ReactNode;
showAddButton?: boolean;
showIconsInList?: boolean;
mode?: "default" | "toggle" | "button";
triggerLabel?: string;
children?: ReactNode;
buttonClassName?: string;
panelClassName?: string;
onAction?: (action: string) => void;
panelTitle?: string;
}
export const PopoverMenu = ({
items = [],
onSelect,
onAdd,
triggerIcon,
showAddButton = false,
showIconsInList = false,
mode = "default",
triggerLabel,
children,
buttonClassName,
onAction,
panelTitle,
}: PopoverMenuProps) => {
const selectedItem = items.find((item) => item.selected);
const handleItemClick = (item: PopoverItem, close: () => void) => {
if (mode === "button" && item.action && onAction) {
onAction(item.action);
close();
} else {
onSelect?.(item);
close();
}
};
const className = twMerge(
"text-sm font-medium rounded-lg px-3 py-2 shadow-sm",
"flex gap-2 items-center justify-center outline-none",
"transition-all duration-150",
"bg-[#5F5F68]/60 px-3 text-white backdrop-blur-lg hover:bg-[#5F5F68]/90",
buttonClassName,
);
return (
<Popover className="relative">
{({ 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",
)}
>
{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>
)}
</div>
)}
</div>
</Popover.Panel>
</Transition>
</div>
</>
)}
</Popover>
);
};
@@ -0,0 +1 @@
export * from "./Popover";
@@ -15,6 +15,8 @@ export const uploadImage = async ({
}) => {
const mediaUploadApi = new MediaUploadApi();
console.log("Uploading image:", assetFile);
progressCallback({ status: UploaderStates.uploadingImage });
const imageResponse = await mediaUploadApi.UploadImage({
@@ -24,7 +26,10 @@ export const uploadImage = async ({
maybe_title: "char_frame_" + title,
});
console.log("Image response:", imageResponse);
if (imageResponse == undefined) {
console.log("Error: Could not upload image!");
progressCallback({
status: UploaderStates.imageCreateError,
errorMessage: "Could not upload image!",
@@ -33,6 +38,7 @@ export const uploadImage = async ({
}
if (!imageResponse.success || !imageResponse.data) {
console.log("Error:", imageResponse.errorMessage);
progressCallback({
status: UploaderStates.imageCreateError,
errorMessage: imageResponse.errorMessage,
@@ -40,5 +46,9 @@ export const uploadImage = async ({
return;
}
progressCallback({ status: UploaderStates.success, data: imageResponse.data });
console.log("Upload successful:", imageResponse.data);
progressCallback({
status: UploaderStates.success,
data: imageResponse.data,
});
};
@@ -18,6 +18,7 @@ export * from "./LoadingSpinner";
export * from "./NumberInput";
export * from "./Pagination";
export * from "./Pill";
export * from "./Popover";
export * from "./SearchFilter";
export * from "./Select";
export * from "./Slider";
@@ -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,
@@ -641,6 +655,25 @@ class Editor {
document.body.appendChild(this.stats.dom);
}
public snapShotOfCurrentFrame(shouldDownload: boolean = true) {
const snapshot = this.renderer?.domElement.toDataURL("image/png");
if (!snapshot) {
console.error("Error: Snapshot could not be created.");
return null;
}
const base64Snapshot = snapshot.split(",")[1];
if (shouldDownload) {
const link = document.createElement("a");
link.download = "scene-snapshot.png";
link.href = snapshot;
link.click();
}
return base64Snapshot;
}
public async newScene(sceneTitleInput: string) {
this.activeScene.clear();
this.audio_engine = new AudioEngine();
@@ -948,16 +981,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);
@@ -27,6 +27,7 @@ import { SceneContainer } from "./comps/SceneContainer";
import { AspectRatioMenu } from "./comps/AspectRatioMenu";
import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
export const PageEditor = () => {
useSignals();
@@ -112,7 +113,6 @@ export const PageEditor = () => {
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D />
<AspectRatioMenu />
</div>
</div>
@@ -134,7 +134,7 @@ export const PageEditor = () => {
<Outliner />
<PreviewEngineCamera />
</div>
<ControlsVideo />
<PromptBox />
<ControlPanelSceneObject />
</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,
@@ -67,7 +67,7 @@ export const ControlsTopButtons = () => {
const handleResetScene = () => {
resetSceneGenerationMetadata();
editorEngine?.changeRenderCameraAspectRatio(
CameraAspectRatio.HORIZONTAL_16_9,
CameraAspectRatio.HORIZONTAL_3_2,
);
};
@@ -0,0 +1,450 @@
import { useState, useRef, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
import { useContext } from "react";
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
import {
faPlus,
faCamera,
faSave,
faSparkles,
faUpload,
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;
mediaToken: string;
}
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
import { scene } from "~/signals/scene";
import { UploaderStates } from "~/enums/UploaderStates";
export const PromptBox = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const [prompt, setPrompt] = useState("");
const [isEnqueueing, setisEnqueueing] = useState(false);
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
{
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[]>([
{
label: "Main View",
selected: true,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
},
{
label: "Camera 2",
selected: false,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
},
]);
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}, [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) => ({
...item,
selected: item.label === selectedItem.label,
})),
);
};
const handleAddCamera = () => {
const newIndex = cameraList.length + 1;
setCameraList((prev) => [
...prev,
{
label: `Camera ${newIndex}`,
selected: false,
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
},
]);
};
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
setAspectRatioList((prev) =>
prev.map((item) => ({
...item,
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 files = event.target.files;
if (files) {
Array.from(files).forEach((file) => {
const reader = new FileReader();
reader.onloadend = () => {
uploadImage({
title: `reference-image-${Math.random().toString(36).substring(2, 15)}`,
assetFile: file,
progressCallback: (newState) => {
console.debug("Upload progress:", newState.data);
if (newState.status === UploaderStates.success && newState.data) {
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: reader.result as string,
file,
mediaToken: newState.data || "",
};
setReferenceImages((prev) => [...prev, referenceImage]);
console.log("Reference image added:", referenceImage);
} else {
console.error("No data from uploadImage");
}
},
});
};
reader.readAsDataURL(file);
});
}
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleLibrarySelect = () => {
console.log("Opening library selector");
};
const handleAction = (action: string) => {
switch (action) {
case "upload":
handleUploadClick();
break;
case "library":
handleLibrarySelect();
break;
default:
console.log("Unknown action:", action);
}
};
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
e.preventDefault();
const pastedText = e.clipboardData.getData("text").trim();
setPrompt(pastedText);
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(e.target.value);
};
const handleEnqueue = async () => {
if (!prompt.trim()) return;
if (editorEngine) {
editorEngine.positive_prompt = prompt;
const engineApi = new EngineApi();
const snapshot = editorEngine.snapShotOfCurrentFrame(false);
const snapshotResult = await engineApi.uploadSceneSnapshot({
screenshot: snapshot || "",
sceneMediaToken: "",
});
await engineApi.enqueueImageGeneration({
prompt: prompt,
snapshotMediaToken: snapshotResult.data || "",
additionalImages: referenceImages.map((image) => image.mediaToken),
});
setisEnqueueing(true);
try {
// 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);
}
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleEnqueue();
}
};
// 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;
}
};
const handleSaveFrame = async () => {
if (editorEngine) {
const snapshot = editorEngine.snapShotOfCurrentFrame(false);
const engineApi = new EngineApi();
const result = await engineApi.uploadSceneSnapshot({
screenshot: snapshot || "",
sceneMediaToken: "",
});
console.log(result);
}
};
const handleDownloadFrame = () => {
if (editorEngine) {
editorEngine.snapShotOfCurrentFrame(true);
}
};
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 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",
disabled: true,
},
]}
onAction={handleAction}
showIconsInList
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="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}
onClick={handleDownloadFrame}
>
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}
onClick={handleSaveFrame}
>
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>
);
};
@@ -0,0 +1 @@
export * from "./PromptBox";
@@ -22,10 +22,10 @@ export const Letterbox = ({
}) => {
useSignals();
if (editorState.value !== EditorStates.CAMERA_VIEW || !isShowing) {
//case of Letterbox should not show
return;
}
// if (editorState.value !== EditorStates.CAMERA_VIEW || !isShowing) {
// //case of Letterbox should not show
// return;
// }
const matteOri = getMatteOrientation({
camAspect: cameraAspectRatio.value,
@@ -73,14 +73,14 @@ const Matte = ({
if (matteOri === MatteOrientation.TOP_BOTTOM) {
return (
<div
className="h-20 w-full bg-brand-secondary/50 brightness-75"
className="h-20 w-full bg-black/30 brightness-75"
style={{ height: `${height}px` }}
/>
);
}
return (
<div
className="h-full w-80 bg-brand-secondary/50 brightness-75"
className="h-full w-80 bg-black/30 brightness-75"
style={{ width: `${width}px` }}
/>
);
@@ -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;
}
@@ -1,10 +1,17 @@
import { ReactNode } from "react";
import { FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
import { faBrush, faCatSpace } from "@fortawesome/pro-solid-svg-icons";
import {
faAlienMonster,
faCatSpace,
faCloud,
faCube,
} from "@fortawesome/pro-solid-svg-icons";
import { TabTitles } from "~/enums";
import { StylizeTab } from "~/pages/PageEnigma/comps/SidePanelTabs/tabComps/StylizeTab/StylizeTab";
import { CartoonsTab } from "../SidePanelTabs/tabComps/CharactersComboTabs";
import { ObjectsTab } from "../SidePanelTabs/tabComps/ObjectsTab";
import { SkyboxesTab } from "../SidePanelTabs/tabComps/SkyboxesTab";
import { CreaturesTab } from "../SidePanelTabs/tabComps/CreaturesTab";
export interface TabItem {
icon: FontAwesomeIconProps["icon"];
@@ -19,9 +26,21 @@ export const tabList: TabItem[] = [
component: <CartoonsTab />,
},
// {
// icon: faBrush,
// title: TabTitles.STYLIZE,
// component: <StylizeTab />,
// }
{
icon: faCube,
title: TabTitles.OBJECTS,
component: <ObjectsTab />,
},
{
icon: faCloud,
title: TabTitles.SKYBOXES,
component: <SkyboxesTab />,
},
{
icon: faAlienMonster,
title: TabTitles.OBJECTS_CREATURES,
component: <CreaturesTab />,
},
];
@@ -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) => {
@@ -14,4 +14,9 @@
.filter-tab:disabled {
@apply border-transparent bg-ui-controls-button/50 hover:border-transparent disabled:text-white/30;
}
.glass {
@apply relative border border-white/10 shadow-lg backdrop-blur-xl transition-colors duration-300;
@apply bg-ui-controls/50 !important;
}
}
+12
View File
@@ -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",
+1
View File
@@ -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",
+20 -20
View File
@@ -28,21 +28,21 @@ export default {
"brand-primary": {
//sunglo
DEFAULT: "#e66462", //=500
"50": "#fdf3f3",
"100": "#fce4e4",
"200": "#facfce",
"300": "#f6acab",
"400": "#ef7c7a",
"500": "#e66462",
"600": "#cf3533",
"700": "#ae2927",
"800": "#902624",
"900": "#782524",
"950": "#410f0e",
DEFAULT: "#2d81ff", //=500
"50": "#eef6ff",
"100": "#d9ebff",
"200": "#bcdcff",
"300": "#8ec7ff",
"400": "#59a7ff",
"500": "#2d81ff",
"600": "#1b63f5",
"700": "#144ee1",
"800": "#173fb6",
"900": "#19398f",
"950": "#142457",
},
"brand-secondary": {
DEFAULT: "#2E2E2E",
DEFAULT: "#3E3E41",
"50": "#f6f6f6",
"100": "#e7e7e7",
"200": "#d1d1d1",
@@ -53,7 +53,7 @@ export default {
"700": "#4f4f4f",
"800": "#454545",
"900": "#3d3d3d",
"950": "#2e2e2e",
"950": "#3E3E41",
},
"brand-tertiary": {
//Aquamarine Blue
@@ -103,7 +103,7 @@ export default {
},
action: {
DEFAULT: "#2E2E2E",
DEFAULT: "#3E3E41",
"50": "#f6f6f6",
"100": "#e7e7e7",
"200": "#d1d1d1",
@@ -114,7 +114,7 @@ export default {
"700": "#4f4f4f",
"800": "#454545",
"900": "#3d3d3d",
"950": "#2e2e2e",
"950": "#3E3E41",
},
character: {
@@ -123,7 +123,7 @@ export default {
clip: "#7E92DA",
groupBg: "#2B3448",
titleBg: "#384763",
frame: "#6B83D8"
frame: "#6B83D8",
},
camera: {
unselected: "#466C7C",
@@ -159,7 +159,7 @@ export default {
},
assets: {
background: "#242424",
selectedTab: "#2E2E2E",
selectedTab: "#3E3E41",
},
dnd: {
canDrop: "#46936E",
@@ -174,8 +174,8 @@ export default {
background: "#242424",
panel: "#1F1F1F",
"panel-border": "#3F3F3F",
controls: "#2E2E2E",
"controls-button": "#2E2E2E",
controls: "#3E3E41",
"controls-button": "#3E3E41",
divider: "#515168",
},