Merge pull request #115 from storytold/feature/terse

Feature/terse
This commit is contained in:
ArEnSc authored and GitHub committed 2024-04-08 16:44:01 -04:00
commit 33136a0a76
122 files changed
+3055 -1687

No files matched your search

+4 -1
View File
@@ -5,7 +5,7 @@ import { twMerge } from "tailwind-merge";
export interface ButtonPropsI
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon?: IconDefinition;
variant?: "primary" | "secondary";
variant?: "primary" | "secondary" | "action";
}
export const Button = ({
@@ -25,6 +25,9 @@ export const Button = ({
case "secondary": {
return " bg-brand-secondary hover:bg-brand-secondary-900 text-white focus-visible:outline-brand-secondary";
}
case "action": {
return " bg-action hover:bg-action-500 text-white focus-visible:outline-action";
}
case "primary":
default: {
return " bg-brand-primary hover:bg-brand-primary-400 text-white focus-visible:outline-brand-primary-600";
+10 -3
View File
@@ -2,17 +2,23 @@ import { twMerge } from "tailwind-merge";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Label } from "./Typography";
import { kebabCase } from "~/utilities";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
className?: string;
icon?: IconDefinition
}
export function Input({label, icon, className, ...rest }: InputProps) {
export function Input({
label, icon, className, id, ...rest
}: InputProps) {
return (
<div className={twMerge("flex flex-col gap-2", className)}>
{label && <Label>{label}</Label>}
{label &&
<Label htmlFor={id ? id : kebabCase(label)}>
{label}
</Label>
}
<div className="relative w-full">
{icon && (
@@ -22,6 +28,7 @@ export function Input({label, icon, className, ...rest }: InputProps) {
/>
)}
<input
id={id ? id : (label ? kebabCase(label) : undefined)}
className={twMerge(
"w-full h-10 rounded-md bg-brand-secondary p-3 text-white transition-all duration-150 ease-in-out outline-none outline-offset-0 focus:outline-brand-primary",
icon? "pl-12" : "",
+9 -1
View File
@@ -1,6 +1,7 @@
import { TextareaHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import { Label } from "./Typography";
import { kebabCase } from "~/utilities";
type ResizeType =
| "none"
@@ -15,6 +16,7 @@ export const Textarea = ({
className,
label,
resize = "vertical",
id,
...rest
}: TextareaHTMLAttributes<HTMLTextAreaElement> & {
label?: string;
@@ -22,8 +24,14 @@ export const Textarea = ({
}) => {
return (
<div className="flex flex-col">
{label && <Label>{label}</Label>}
{label &&
<Label htmlFor={id ? id : kebabCase(label)}>
{label}
</Label>
}
<textarea
id={id ? id : (label ? kebabCase(label) : undefined)}
className={twMerge(
"rounded-lg border border-ui-panel-border bg-ui-controls p-3",
className,
+10 -2
View File
@@ -1,4 +1,5 @@
import { NavLink, NavLinkProps } from "@remix-run/react";
import { LabelHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
interface TypoProps {
@@ -23,8 +24,15 @@ export const H4 = ({ className, children }: TypoProps) => (
<h4 className={twMerge(baseTypo, "text-base font-medium", className)}>{children}</h4>
);
export const Label = ({ className, children }: TypoProps) => (
<label className={twMerge(baseTypo, "mb-2 text-base font-medium", className)}>{children}</label>
export const Label = ({
className, children, ...rest
}: LabelHTMLAttributes<HTMLLabelElement>) => (
<label
className={twMerge(baseTypo, "mb-2 text-base font-medium", className)}
{...rest}
>
{children}
</label>
);
export const H5 = ({ className, children }: TypoProps) => (
+73 -42
View File
@@ -1,7 +1,7 @@
import {
createContext,
useEffect,
useCallback,
useEffect,
useState,
ReactNode,
Dispatch,
@@ -12,52 +12,43 @@ import { useCookies } from 'react-cookie';
import {
SessionResponse,
AuthState,
STORAGE_KEYS
} from "./types";
import { CreateSession, DestroySession, GetSession } from "./utilities";
export const AuthenticationContext = createContext<{
authState?: AuthState,
setAuthState?: Dispatch<SetStateAction<AuthState | undefined>>,
persistLoginOrOut?: ()=>void,
authState: AuthState,
setAuthState?: Dispatch<SetStateAction<AuthState>>,
loginAndGetUserInfo?: (
usernameOrEmail: string,
password:string,
failureCallback?:()=>void
)=>void;
logout?: ()=>void;
}>({});
}>({
authState:{}
});
export const AuthenticationProvider = ({children}:{children:ReactNode})=>{
const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']);
const [authState, setAuthState] = useState<AuthState | undefined>(undefined);
// console.log('auth provider rerender');
const [authCookies, setAuthCookie, removeAuthCookie] = useCookies([STORAGE_KEYS.USER_INFO]);
const [authState, setAuthState] = useState<AuthState>({});
const persistLoginOrOut = useCallback(()=>{
GetSession().then((
res: SessionResponse
)=>{
if(res.success && res.user){
setAuthCookie('userInfo', res.user);
setAuthState({
isLoggedIn: true,
userInfo: res.user
});
}else{
removeAuthCookie('userInfo');
}
});
}, []);
const loginAndGetUserInfo = (usernameOrEmail: string, password:string, failureCallback?:()=>void)=>{
const loginAndGetUserInfo = useCallback((
usernameOrEmail: string,
password:string,
failureCallback?:()=>void
)=>{
CreateSession({usernameOrEmail, password})
.then((respond)=>{
GetSession().then((
res: SessionResponse
)=>{
if(res.success && res.user && setAuthState){
setAuthCookie('userInfo', res.user);
setAuthState({
isLoggedIn: true,
sessionData: JSON.stringify(res.user),
userInfo: res.user
});
}else{
@@ -65,29 +56,69 @@ export const AuthenticationProvider = ({children}:{children:ReactNode})=>{
}
});
});
}
const logout = ()=>{
DestroySession().then((res)=>{
if (res && setAuthState) {
setAuthState({
isLoggedIn: false,
userInfo: undefined
});
}
});
}
useEffect(()=>{
if (authCookies !== undefined && authState===undefined){
persistLoginOrOut();
}
},[]);
const setLogoutState = useCallback(()=>{
setAuthState({
isLoggedIn: false,
sessionData: null,
userInfo: undefined
});
},[]);
const logout = useCallback(()=>{
DestroySession().then((res)=>{
if (res) setLogoutState();
});
},[]);
//Set Session upon Auth State Changes
useEffect(()=>{
//On First Load
if(authState.isLoggedIn === undefined){
//Persist Login if Session Data exist
const sessionData = localStorage.getItem(STORAGE_KEYS.USER_INFO);
// console.log(`Session Data: ${sessionData?.substring(0, 30)}${sessionData ? '...':''}`);
if (sessionData !== null){
// console.log('Getting session to persist login');
GetSession().then((
res: SessionResponse
)=>{
// console.log('Session Returned');
// console.log(res);
if(res.success && res.user){
// console.log('Setting Auth State');
setAuthState({
isLoggedIn: true,
sessionData: JSON.stringify(res.user),
userInfo: res.user
});
}else{
// console.log('Session expired, setting State');
setLogoutState();
}
});
}else{
setLogoutState();
}
}
// Propagate session data to localstorage
if(authState.sessionData !== null && authState.sessionData !== undefined){
localStorage.setItem(STORAGE_KEYS.USER_INFO, authState.sessionData);
setAuthCookie(STORAGE_KEYS.USER_INFO, authState.userInfo);
}
// Propagate Logout
else if (authState.sessionData === null){
localStorage.removeItem(STORAGE_KEYS.USER_INFO);
removeAuthCookie(STORAGE_KEYS.USER_INFO);
}
},[authState]);
return(
<AuthenticationContext.Provider value={{
authState,
setAuthState,
persistLoginOrOut,
loginAndGetUserInfo,
logout,
}}>
+5 -1
View File
@@ -1,3 +1,6 @@
export enum STORAGE_KEYS{
USER_INFO = "user_info",
}
export enum AUTH_STATUS {
LOGGED_IN = "logged_in",
LOGGING = "logging",
@@ -5,7 +8,8 @@ export enum AUTH_STATUS {
}
export type AuthState = {
isLoggedIn: boolean;
isLoggedIn?: boolean;
sessionData?: string | null;
userInfo?: UserInfo;
}
export interface UserInfo {
+20 -8
View File
@@ -1,7 +1,9 @@
import React, { useCallback } from "react";
import { useMouseEventsHeight } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsHeight";
import React, { useCallback, UIEvent } from "react";
import { useMouseEventsTimeline } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsTimeline";
import {
currentScroll,
currentTime,
dndTimelineHeight,
overTimeline,
scale,
timelineHeight,
@@ -15,14 +17,19 @@ interface LowerPanelPropsI {
}
export const LowerPanel = ({ children }: LowerPanelPropsI) => {
const { onPointerDown, height } = useMouseEventsHeight();
const { onPointerDown } = useMouseEventsTimeline();
const displayHeight = height > -1 ? height : timelineHeight.value;
const displayHeight =
dndTimelineHeight.value > -1
? dndTimelineHeight.value
: timelineHeight.value;
const onTimelineClick = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
const newTime = Math.round((event.clientX - 92) / 4 / scale.value);
const newTime = Math.round(
(event.clientX + currentScroll.value - 92) / 4 / scale.value,
);
if (newTime < 0) {
return;
}
@@ -37,11 +44,15 @@ export const LowerPanel = ({ children }: LowerPanelPropsI) => {
[],
);
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
currentScroll.value = event.currentTarget.scrollLeft;
}, []);
return (
<>
<div
className="w-full cursor-ns-resize bg-ui-panel-border"
style={{ height: 3, zIndex: 1000 }}
className="absolute w-full cursor-ns-resize bg-ui-panel-border"
style={{ height: 3, zIndex: 1000, bottom: displayHeight }}
onPointerDown={onPointerDown}
/>
<div
@@ -50,12 +61,13 @@ export const LowerPanel = ({ children }: LowerPanelPropsI) => {
"w-screen overflow-auto",
"bg-ui-panel",
].join(" ")}
style={{ height: displayHeight - 3 }}
style={{ height: displayHeight }}
onPointerOver={() => {
overTimeline.value = true;
}}
onPointerLeave={() => (overTimeline.value = false)}
onPointerDown={onTimelineClick}
onScroll={onScroll}
>
{children}
</div>
+54 -19
View File
@@ -1,31 +1,66 @@
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faChevronLeft,
faChevronRight,
} from "@fortawesome/pro-solid-svg-icons";
import {
dndSidePanelWidth,
lastSelectedTab,
selectedTab,
sidePanelHeight,
sidePanelVisible,
sidePanelWidth,
} from "~/pages/PageEnigma/store";
import { SidePanelTabs } from "~/pages/PageEnigma/comps/SidePanelTabs";
import { SidePanelMenu } from "~/pages/PageEnigma/comps/SidePanelTabs/SidePanelMenu";
import { useSignals } from "@preact/signals-react/runtime";
interface Props {
children: React.ReactNode;
}
export const SidePanel = () => {
useSignals();
export const SidePanel = ({ children }: Props) => {
const [isVisible, setIsVisible] = useState(true);
const displayWidth =
dndSidePanelWidth.value > -1
? dndSidePanelWidth.value
: sidePanelWidth.value;
return (
<div
className={
"relative h-full border-l border-l-ui-panel-border bg-ui-panel transition-all duration-300 ease-in-out" +
(isVisible ? " w-[23.5rem]" : " w-0")
}
>
<button
onClick={() => setIsVisible(!isVisible)}
className="absolute left-[-25px] top-1/2 flex rounded-l-lg bg-ui-controls px-2 py-3 align-middle text-sm text-white hover:bg-ui-controls-button"
<>
<div
className={[
"fixed",
"border-l border-l-ui-panel-border bg-ui-panel",
"flex",
"transition-all duration-300 ease-in-out",
].join(" ")}
style={{
top: 68,
right: 84,
width: sidePanelVisible.value ? displayWidth : 0,
}}
>
<FontAwesomeIcon icon={isVisible ? faChevronRight : faChevronLeft} />
</button>
<div className="relative h-full w-[23.5rem]">{children}</div>
</div>
<button
onClick={() => {
if (sidePanelVisible.value) {
lastSelectedTab.value = selectedTab.value;
selectedTab.value = null;
sidePanelVisible.value = false;
} else {
selectedTab.value = lastSelectedTab.value;
sidePanelVisible.value = true;
}
}}
className="absolute left-[-25px] flex rounded-l-lg bg-ui-controls px-2 py-3 align-middle text-sm text-white hover:bg-ui-controls-button"
style={{ top: sidePanelHeight.value / 2 - 10 }}
>
<FontAwesomeIcon
icon={sidePanelVisible.value ? faChevronRight : faChevronLeft}
/>
</button>
<div className="relative block h-full w-full bg-ui-panel">
<SidePanelTabs />
</div>
</div>
<SidePanelMenu />
</>
);
};
+26
View File
@@ -0,0 +1,26 @@
import { useContext, useEffect, ComponentType } from "react";
import { useNavigate, useLocation } from "@remix-run/react";
import { AuthenticationContext } from "~/contexts/Authentication";
export const withProtectionRoute = <P extends {}>(Component: ComponentType<P>) => (rest:P) => {
const { authState } = useContext(AuthenticationContext);
// console.log(authState);
if (authState.isLoggedIn === undefined ) {
return <>waiting</>;
}
if (authState.isLoggedIn === false) {
return <RedirectToLogin />
}
return (
<Component {...rest}/>
);
}
const RedirectToLogin = ()=>{
const navigate = useNavigate();
const {pathname} = useLocation();
useEffect(()=>{
navigate(`/login?redirect=${pathname}`);
},[]);
return <>redirecting</>
}
+79
View File
@@ -3,12 +3,91 @@ import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackPro
import { AppUIProvider } from "~/pages/PageEnigma/contexts/AppUiContext";
import { EngineProvider } from "~/pages/PageEnigma/contexts/EngineProvider";
import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragComponent";
// import { toast, ToastBar, Toaster } from "react-hot-toast";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import {
// faTriangleExclamation,
// faXmark,
// } from "@fortawesome/pro-solid-svg-icons";
import { useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import {
pageHeight,
pageWidth,
sidePanelWidth,
timelineHeight,
} from "~/pages/PageEnigma/store";
export const PageEnigma = () => {
useSignals();
useEffect(() => {
function setPage() {
// TODO address this issue with zooming
pageHeight.value = window.innerHeight;
pageWidth.value = window.outerWidth;
}
timelineHeight.value = window.innerHeight * 0.25;
sidePanelWidth.value = 443;
setPage();
window.addEventListener("resize", setPage);
return () => {
window.removeEventListener("resize", setPage);
};
}, []);
return (
<TrackProvider>
<AppUIProvider>
<EngineProvider>
{/* <Toaster
position="top-right"
toastOptions={{
style: {
border: "1px solid #39394D",
backgroundColor: "#39394D",
},
}}
containerStyle={{
top: 84,
right: 20,
}}
>
{(thisToast) => (
<ToastBar toast={thisToast}>
{({ message }) => {
return (
<div className="mr-3 flex justify-between rounded-lg">
<div>
{thisToast.type === "error" && (
<div className="ml-3 flex items-center gap-4">
<FontAwesomeIcon
icon={faTriangleExclamation}
className="text-keyframe-selected"
/>
<div className="text-base font-bold text-white">
Warning!
</div>
</div>
)}
<div className="text-left text-sm text-white opacity-50">
{message}
</div>
</div>
<button
onClick={() => toast.dismiss(thisToast.id)}
className="text-sm font-bold text-white opacity-50"
>
<FontAwesomeIcon icon={faXmark} />
</button>
</div>
);
}}
</ToastBar>
)}
</Toaster> */}
<PageEnigmaComponent />
</EngineProvider>
</AppUIProvider>
+54 -22
View File
@@ -1,5 +1,4 @@
import { useContext } from "react";
import { useContext, useState } from "react";
import { faSparkles } from "@fortawesome/pro-solid-svg-icons";
import { Button, LoadingBar, LoadingDots } from "~/components";
@@ -12,16 +11,26 @@ import { ControlsVideo } from "./comps/ControlsVideo";
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { ViewSideBySide } from "./comps/ViewSideBySide";
import { SidePanelTabs } from "./comps/SidePanelTabs";
import { Timeline } from "./comps/Timeline";
import { APPUI_VIEW_MODES } from "./reducers";
import { timelineHeight } from "~/pages/PageEnigma/store";
import {
timelineHeight,
sidePanelWidth,
pageWidth,
sidePanelVisible,
pageHeight,
dndSidePanelWidth,
dndTimelineHeight,
} from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
import { AppUiContext } from "~/pages/PageEnigma/contexts/AppUiContext";
import { EngineContext } from "./contexts/EngineContext";
export const PageEnigmaComponent = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const [appUiState] = useContext(AppUiContext);
//To prevent the click event from propagating to the canvas: TODO: HANDLE THIS BETTER?
@@ -29,19 +38,45 @@ export const PageEnigmaComponent = () => {
event.stopPropagation();
};
const lowerHeight = timelineHeight.value;
return (
<div>
<TopBarHelmet>
<Button icon={faSparkles}>Generate Movie</Button>
</TopBarHelmet>
const handleGenerateMovieClick = () => {
if (editorEngine != null) {
editorEngine?.generateVideo();
} else {
console.log("Tried to generate movie but editor was null");
}
};
<div style={{ height: "calc(100vh - 68px)" }}>
const dndWidth =
dndSidePanelWidth.value > -1
? dndSidePanelWidth.value
: sidePanelWidth.value;
const width = sidePanelVisible.value
? pageWidth.value - dndWidth - 66
: pageWidth.value - 66;
const height =
dndTimelineHeight.value > -1
? pageHeight.value - dndTimelineHeight.value - 68
: pageHeight.value - timelineHeight.value - 68;
return (
<div className="w-screen">
<TopBarHelmet>
<Button icon={faSparkles} onClick={handleGenerateMovieClick}>
Generate Movie
</Button>
</TopBarHelmet>
<div
className="relative flex w-screen"
style={{ height: "calc(100vh - 68px)" }}
>
{/* Engine section/side panel */}
<div
id="engine-n-panels-wrapper"
className="flex"
style={{ height: `calc(100% - ${lowerHeight}px)` }}
style={{
height,
width,
}}
>
<div className="relative w-full overflow-hidden bg-transparent">
<div
@@ -93,20 +128,17 @@ export const PageEnigmaComponent = () => {
progress={appUiState.showEditorLoadingBar.progress}
/>
</div>
{/* Side panel */}
<div onClick={handleOverlayClick}>
<SidePanel>
<SidePanelTabs />
</SidePanel>
</div>
</div>
{/* Timeline */}
{/* Side panel */}
<div onClick={handleOverlayClick}>
<Timeline />
<SidePanel />
</div>
</div>
{/* Timeline */}
<div onClick={handleOverlayClick}>
<Timeline />
</div>
</div>
);
};
+7 -6
View File
@@ -4,22 +4,23 @@ import {
QueueClip,
QueueKeyframe,
UpdateTime,
} from "~/pages/PageEnigma/models/track";
} from "~/pages/PageEnigma/models";
import { toTimelineActions } from "./toTimelineActions";
import { ClipUI } from "../datastructures/clips/clip_ui";
import { MediaItem } from "~/pages/PageEnigma/models";
class Queue {
private _queue: Record<
string,
{
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
data: QueueClip | UpdateTime | QueueKeyframe | ClipUI[] | MediaItem;
}[]
> = {};
private _subscribers: Record<
string,
(entry: {
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
data: QueueClip | UpdateTime | QueueKeyframe | ClipUI[] | MediaItem;
}) => void
> = {};
@@ -30,7 +31,7 @@ class Queue {
}: {
queueName: string;
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
data: QueueClip | UpdateTime | QueueKeyframe | ClipUI[] | MediaItem ;
}) {
if (!this._queue[queueName]) {
this._queue[queueName] = [];
@@ -47,7 +48,7 @@ class Queue {
queueName: string,
onMessage: (entry: {
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
data: QueueClip | UpdateTime | QueueKeyframe | ClipUI[] | MediaItem;
}) => void,
) {
this._subscribers[queueName] = onMessage;
@@ -1,4 +1,6 @@
export enum fromEngineActions {
UPDATE_TIME = "update_time",
UPDATE_TIME_LINE = "update_timeline",
UPDATE_CHARACTER_ID = "update_character_id",
// data={currentTime: number}
}
@@ -2,6 +2,11 @@ export enum toEngineActions {
ADD_CLIP = "add_clip",
// data=QueueClip
ADD_KEYFRAME = "add_keyframe",
ADD_CHARACTER = "add_character",
ADD_OBJECT = "add_object",
// data= QueueKeyframe
DELETE_CLIP = "delete_clip",
// data=QueueClip
@@ -17,26 +17,34 @@ import { ACTION_TYPES } from "../../reducers/appUiReducer/types";
import { QueueNames } from "../../Queue/QueueNames";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { toTimelineActions } from "../../Queue/toTimelineActions";
import { QueueKeyframe } from "~/pages/PageEnigma/models/track";
import { QueueKeyframe } from "~/pages/PageEnigma/models";
export const ControlPanelSceneObject = () => {
const editorEngine = useContext(EngineContext);
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const position =
appUiState.controlPanel.currentSceneObject?.objectVectors?.position;
const rotation =
appUiState.controlPanel.currentSceneObject?.objectVectors?.rotation;
const scale =
appUiState.controlPanel.currentSceneObject?.objectVectors?.scale;
const currentSceneObject = appUiState.controlPanel.currentSceneObject;
useEffect(() => {
// TODO this causes a subtle bug because it renders way too many times.
const vectors = appUiState.controlPanel.currentSceneObject.objectVectors;
editorEngine?.setSelectedObject(
vectors.position,
vectors.rotation,
vectors.scale,
);
}, [appUiState.controlPanel.currentSceneObject, editorEngine]);
if (!appUiState.controlPanel.currentSceneObject) {
return null;
}
const position =
appUiState.controlPanel.currentSceneObject.objectVectors.position;
const rotation =
appUiState.controlPanel.currentSceneObject.objectVectors.rotation;
const scale = appUiState.controlPanel.currentSceneObject.objectVectors.scale;
const currentSceneObject = appUiState.controlPanel.currentSceneObject;
useEffect(()=>{
const vectors = appUiState.controlPanel.currentSceneObject.objectVectors
editorEngine?.setSelectedObject(vectors.position, vectors.rotation, vectors.scale)
}, [appUiState.controlPanel.currentSceneObject]);
const handlePositionChange = (xyz: XYZ) => {
if (!currentSceneObject) {
@@ -141,9 +149,9 @@ export const ControlPanelSceneObject = () => {
}
};
const handleDeleteObject = () =>{
console.log("TELL EDITOR TO DELETE HERE");
}
const handleDeleteObject = () => {
editorEngine?.deleteObject(currentSceneObject.object_uuid);
};
return (
<Transition
@@ -159,7 +167,9 @@ export const ControlPanelSceneObject = () => {
<div className="flex justify-between">
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faCube} />
<p className="font-semibold">{appUiState.controlPanel.currentSceneObject.object_name}</p>
<p className="font-semibold">
{appUiState.controlPanel.currentSceneObject.object_name}
</p>
</div>
<div className="flex items-center gap-2 text-xs font-medium opacity-60">
<FontAwesomeIcon icon={faArrowRightArrowLeft} />
@@ -1,111 +1,45 @@
import { useContext, useState } from "react";
import { Button, Input } from "~/components";
import { ButtonDialogue } from "~/modules/ButtonDialogue";
import { AppUiContext } from "../../contexts/AppUiContext";
import { EngineContext } from "../../contexts/EngineContext";
import { APPUI_ACTION_TYPES } from "../../reducers";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
export const ControlsTopButtons = () => {
const editorEngine = useContext(EngineContext);
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const [sceneName, setSceneName] = useState<string>("");
const [sceneToken, setSceneToken] = useState<string>("");
// for testing
const [mediaToken, setMediaToken] = useState<string>("");
const handleButtonSave = () => {
console.log(`SceneName is ${sceneName}`);
editorEngine?.saveScene(sceneName);
};
const handleMediaToken = async () => {
await editorEngine?.loadMediaToken(mediaToken);
};
const handleButtonLoadScene = () => {
console.log(`Scene Token is ${sceneToken}`);
editorEngine?.loadScene(sceneToken);
};
const handleButtonTest = () => {
editorEngine?.testStylizeRequest();
const handleButtonTest = () => {};
// if(appUiState?.showEditorLoadingBar.isShowing){
//TO GO TO A HUNDRED AND DISAPPER
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
// payload:{
// showEditorLoadingBar: {
// progress: 100,
// }
// }
// });
// setTimeout(() => {
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADINGBAR,
// });
// }, 1000);
//END :TO GO TO A HUNDRED AND DISAPPER
//EAMPLE OF CHANING LOADING MESSAGE
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
// payload:{
// showEditorLoadingBar: {
// message: "new message",
// }
// }
// });
// }else{
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADINGBAR,
// payload:{
// showEditorLoadingBar: {
// message: 'display of LoadingBar triggered by Test Button',
// progress: 10,
// }
// }
// });
//}
};
const handleObjectPanelTest = () => {
if (appUiState?.controlPanel.isShowing) {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT,
});
} else {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
payload: {
group: ClipGroup.OBJECT,
object_name: "TEST BUTTON",
object_uuid: "",
version: "1",
objectVectors: {
position: { x: 1, y: 2, z: 3 },
rotation: { x: 4, y: 5, z: 6 },
scale: { x: 7, y: 8, z: 9 },
},
},
});
}
const handleTestButton2 = () => {
console.log("Test Button 2");
};
const handleButtonCameraView = () => {
editorEngine?.switchCameraView();
};
const handleButtonPlayBack = () => {
editorEngine?.startPlayback();
};
const handleButtonRender = () => {
editorEngine?.generateVideo();
};
// const handleButtonPlayBack = () => {
// editorEngine?.startPlayback();
// };
const handleButtonTakeFrame = () => {
editorEngine?.take_timeline_cam_clip();
// editorEngine?.take_timeline_cam_clip();
};
const handleButtonLoad = () => {
console.log("LOADING");
};
const handleButtonSingleFrame = () => {
editorEngine?.generateFrame();
};
@@ -153,6 +87,26 @@ export const ControlsTopButtons = () => {
/>
</ButtonDialogue>
<ButtonDialogue
buttonProps={{
variant: "secondary",
label: "Add Scene Object (Test)",
}}
confirmButtonProps={{
label: "Save",
disabled: mediaToken === "",
onClick: handleMediaToken,
}}
title="Add Scene Object via Media Token"
>
<Input
label="Please Enter a Media Token"
onChange={(e) => {
setMediaToken(e.target.value);
}}
/>
</ButtonDialogue>
<ButtonDialogue
buttonProps={{
variant: "secondary",
@@ -172,21 +126,18 @@ export const ControlsTopButtons = () => {
Toggle Camera View
</Button>
<Button
onClick={handleObjectPanelTest}
className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary"
onClick={handleTestButton2}
className="hover:bg-brand-teriary-400 bg-brand-tertiary focus-visible:outline-brand-tertiary"
>
Show Object Panel
Test Button 2
</Button>
</div>
<div className="flex gap-2">
<Button onClick={handleButtonSingleFrame}>Render Single Frame</Button>
<Button onClick={handleButtonTakeFrame}>Take Frame</Button>
<Button onClick={handleButtonRender}>Render</Button>
{/* <Button onClick={handleButtonLoad}>Load</Button>
<Button onClick={handleButtonRender}>Render</Button> */}
{/* <Button onClick={handleButtonSingleFrame}>Render Single Frame</Button>
<Button onClick={handleButtonTakeFrame}>Take Frame</Button> */}
<Button
onClick={handleButtonTest}
className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary"
className="hover:bg-brand-teriary-400 bg-brand-tertiary focus-visible:outline-brand-tertiary"
style={{ zIndex: 9001 }}
>
Test
@@ -16,13 +16,13 @@ export const ControlsVideo = () => {
console.log("Controls Video: Backward-Fast clicked");
};
const handleBackwardStep = () => {
console.log("Controls Video: Backward-Step clicked");
editorEngine?.stepFrame(-1);
};
const handlePlay = () => {
editorEngine?.startPlayback();
};
const handleForwardStep = () => {
console.log("Controls Video: Forward-Step clicked");
editorEngine?.stepFrame(1);
};
const handleForwardFast = () => {
console.log("Controls Video: Forward-Fast clicked");
@@ -1,8 +1,7 @@
import {
canDrop,
clipLength,
currPosition,
dragId,
dragItem,
overTimeline,
scale,
} from "~/pages/PageEnigma/store";
@@ -12,22 +11,53 @@ export const DragComponent = () => {
useSignals();
const { currX, currY } = currPosition.value;
if (!dragItem.value) {
return null;
}
return (
<div
id={`ani-dnd-${dragId.value}`}
className={[
"absolute p-2",
"rounded-lg",
!canDrop.value ? "bg-brand-primary" : "bg-brand-secondary-700",
dragId.value ? "block" : "hidden",
].join(" ")}
style={{
top: overTimeline.value ? currY - 16 : currY - 32,
left: currX + 1,
zIndex: 10000,
width: overTimeline.value ? clipLength.value * 4 * scale.value : 64,
height: overTimeline.value ? 32 : 64,
}}
/>
<>
{overTimeline.value ? (
<div
id={`ani-dnd-${dragItem.value.media_id}`}
className={[
"absolute p-2",
"rounded-lg",
!canDrop.value ? "bg-brand-primary" : "bg-brand-secondary-700",
"block",
].join(" ")}
style={{
top: currY - 16,
left: currX + 1,
zIndex: 10000,
width: (dragItem.value.length ?? 0) * 4 * scale.value,
height: 32,
}}
/>
) : (
<div
className="absolute rounded-lg"
style={{
width: 91,
height: 114,
top: currY - 57,
left: currX + 1,
zIndex: 10000,
}}
>
<img
src={dragItem.value.thumbnail}
alt={dragItem.value.name}
className="rounded-t-lg"
/>
<div
className="w-full rounded-b-lg py-1 text-center text-sm"
style={{ backgroundColor: "#39394D" }}
>
{dragItem.value.name}
</div>
</div>
)}
</>
);
};
@@ -1,78 +0,0 @@
import { useCallback, useContext, useEffect } from "react";
import { MediaClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import {
canDrop,
currPosition,
dragId,
initPosition,
} from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
interface Props {
clip: MediaClip;
type: "animations" | "audio";
}
export const AnimationElement = ({ clip, type }: Props) => {
useSignals();
const { startDrag, endDrag } = useContext(TrackContext);
const { initX, initY } = initPosition.value;
useEffect(() => {
const onPointerUp = () => {
if (dragId.value) {
endDrag();
}
};
const onMouseMove = (event: MouseEvent) => {
if (dragId) {
event.stopPropagation();
event.preventDefault();
const deltaX = event.pageX - initX;
const deltaY = event.pageY - initY;
currPosition.value = { currX: initX + deltaX, currY: initY + deltaY };
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [startDrag, endDrag, initX, initY]);
const onPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
startDrag(type, clip.media_id, clip.length);
currPosition.value = {
currX: event.pageX,
currY: event.pageY,
};
initPosition.value = {
initX: event.pageX,
initY: event.pageY,
};
canDrop.value = false;
}
},
[type, clip.media_id, startDrag, clip.length],
);
return (
<div className="relative h-16 w-16">
<div
id={`ani-clip-${clip.media_id}`}
className="absolute block h-16 w-16 rounded-lg bg-brand-secondary-700 p-2"
style={{ top: 0, left: 0 }}
onPointerDown={onPointerDown}
>
{clip.name}
</div>
</div>
);
};
@@ -1,25 +0,0 @@
import { useContext, useEffect, useState } from "react";
import { AnimationElement } from "./AnimationElement";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useSignals } from "@preact/signals-react/runtime";
import { timelineHeight } from "~/pages/PageEnigma/store";
export const AnimationElements = () => {
const { animationClips } = useContext(TrackContext);
useSignals();
const [height, setHeight] = useState(0);
useEffect(() => {
setHeight(window.outerHeight - timelineHeight.value);
}, []);
return (
<div className="flex flex-wrap gap-3 overflow-y-auto" style={{ height }}>
{animationClips.map((clip) => {
return (
<AnimationElement key={clip.media_id} clip={clip} type="animations" />
);
})}
</div>
);
};
@@ -1,25 +0,0 @@
import { useContext, useEffect, useState } from "react";
import { AnimationElement } from "./AnimationElement";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useSignals } from "@preact/signals-react/runtime";
import { timelineHeight } from "~/pages/PageEnigma/store";
export const AudioElements = () => {
const { audioClips } = useContext(TrackContext);
useSignals();
const [height, setHeight] = useState(0);
useEffect(() => {
setHeight(window.outerHeight - timelineHeight.value);
}, []);
return (
<div className="flex flex-wrap gap-3 overflow-y-auto" style={{ height }}>
{audioClips.map((clip) => {
return (
<AnimationElement key={clip.media_id} clip={clip} type="audio" />
);
})}
</div>
);
};
@@ -0,0 +1,60 @@
import { useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import {
selectedTab,
sidePanelHeight,
sidePanelVisible,
} from "~/pages/PageEnigma/store/sidePanel";
import { tabList } from "./tabList";
export const SidePanelMenu = () => {
useSignals();
useEffect(() => {
selectedTab.value = tabList[0];
}, []);
return (
<div
className={[
"fixed bg-assets-background",
"px-2 py-4",
"overflow-y-auto",
].join(" ")}
style={{
height: sidePanelHeight.value,
minWidth: 84,
maxWidth: 84,
right: 0,
top: 70,
}}
>
<div className="flex w-full flex-col gap-2">
{tabList.map((tab) => (
<button
key={tab.value}
className={[
"flex flex-col items-center rounded-lg px-2 py-3",
tab.value === selectedTab?.value?.value
? "bg-assets-selectedTab opacity-100"
: "opacity-50",
].join(" ")}
onClick={() => {
selectedTab.value = tab;
if (!sidePanelVisible.value) {
sidePanelVisible.value = true;
}
}}
>
<div>
<img src={tab.icon} alt={tab.title} width={20} height={20} />
</div>
<div className="" style={{ fontSize: 11 }}>
{tab.title}
</div>
</button>
))}
</div>
</div>
);
};
@@ -0,0 +1,31 @@
import { useSignals } from "@preact/signals-react/runtime";
import { selectedTab, sidePanelHeight } from "~/pages/PageEnigma/store";
import { tabList } from "~/pages/PageEnigma/comps/SidePanelTabs/tabList";
import { useMouseEventsSidePanel } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsSidePanel";
export const SidePanelTabs = () => {
useSignals();
const { onPointerDown } = useMouseEventsSidePanel();
return (
<>
<div style={{ height: sidePanelHeight.value, width: "100%" }}>
{tabList.map((tab) => (
<div
key={tab.value}
className={
tab.value === selectedTab.value?.value ? "h-full" : "hidden"
}
>
{tab.component}
</div>
))}
</div>
<div
className="absolute inset-0 block w-1 cursor-ew-resize"
style={{ height: sidePanelHeight.value }}
onPointerDown={onPointerDown}
/>
</>
);
};
@@ -60,7 +60,7 @@ export const TabStylization = () => {
};
return (
<div className="flex h-full w-full flex-col gap-3">
<div className="flex h-full w-full flex-col gap-3 p-4">
<div className="flex flex-col">
<Label>Select Base Style</Label>
<div className="grid grid-cols-4 gap-2">
@@ -98,6 +98,7 @@ export const TabStylization = () => {
<Textarea
label="Positive Prompt"
className="h-32 w-full"
name="positive-prompt"
placeholder="Type here to describe your scene"
onChange={onChangeHandlerPositive}
resize="none"
@@ -105,6 +106,7 @@ export const TabStylization = () => {
<Textarea
label="Negative Prompt"
className="h-32 w-full"
name="negative-prompt"
placeholder="Type here to filter out the things you don't want in the scene"
onChange={onChangeHandlerNegative}
resize="none"
@@ -1,29 +1 @@
import { Tabs } from "~/modules/Tabs";
import { TabStylization } from "./TabStylization";
import { AnimationElements } from "./AnimationElements";
import { AudioElements } from "~/pages/PageEnigma/comps/SidePanelTabs/AudioElements";
export const SidePanelTabs = () => {
return (
<Tabs
tabs={[
{
header: "Animation",
children: <AnimationElements />,
},
{
header: "Camera",
children: <p>Camera Tab</p>,
},
{
header: "Audio",
children: <AudioElements />,
},
{
header: "Stylization",
children: <TabStylization />,
},
]}
/>
);
};
export * from "./SidePanelTabs";
@@ -0,0 +1,71 @@
import { animationFilter, animationItems } from "~/pages/PageEnigma/store";
import { AssetFilterOption } from "~/pages/PageEnigma/models";
import { useSignals } from "@preact/signals-react/runtime";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button } from "~/components";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
export const AnimationTab = () => {
useSignals();
return (
<>
<div className="w-full overflow-x-auto">
<div className="mb-4 mt-4 flex justify-start gap-2 px-4">
<button
className={twMerge(
"filter-tab",
animationFilter.value === AssetFilterOption.ALL ? "active" : "",
"disabled",
)}
onClick={() => (animationFilter.value = AssetFilterOption.ALL)}
>
All
</button>
<button
className={twMerge(
"filter-tab",
animationFilter.value === AssetFilterOption.MINE ? "active" : "",
"disabled",
)}
onClick={() => (animationFilter.value = AssetFilterOption.MINE)}
disabled={!animationItems.value.some((item) => item.isMine)}
>
My Animations
</button>
<button
className={twMerge(
"filter-tab",
animationFilter.value === AssetFilterOption.BOOKMARKED
? "active"
: "",
"disabled",
)}
onClick={() =>
(animationFilter.value = AssetFilterOption.BOOKMARKED)
}
disabled={!animationItems.value.some((item) => item.isBookmarked)}
>
Bookmarked
</button>
</div>
</div>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
className="w-full py-3 text-sm font-medium"
>
Upload Animation
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4 pt-4">
<ItemElements
items={animationItems.value}
assetFilter={animationFilter.value}
/>
</div>
</>
);
};
@@ -0,0 +1,69 @@
import { audioFilter, audioItems } from "~/pages/PageEnigma/store";
import { AssetFilterOption } from "~/pages/PageEnigma/models";
import { useSignals } from "@preact/signals-react/runtime";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button } from "~/components";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
export const AudioTab = () => {
useSignals();
return (
<>
<div className="w-full overflow-x-auto">
<div className="mb-4 mt-4 flex justify-start gap-2 px-4">
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.ALL ? "active" : "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.ALL)}
>
All
</button>
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.MINE ? "active" : "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.MINE)}
disabled={!audioItems.value.some((item) => item.isMine)}
>
My Audios
</button>
<button
className={twMerge(
"filter-tab",
audioFilter.value === AssetFilterOption.BOOKMARKED
? "active"
: "",
"disabled",
)}
onClick={() => (audioFilter.value = AssetFilterOption.BOOKMARKED)}
disabled={!audioItems.value.some((item) => item.isBookmarked)}
>
Bookmarked
</button>
</div>
</div>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
className="w-full py-3 text-sm font-medium"
>
Generate Audio
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4 pt-4">
<ItemElements
items={audioItems.value}
assetFilter={audioFilter.value}
/>
</div>
</>
);
};
@@ -0,0 +1,69 @@
import { cameraFilter, cameraItems } from "~/pages/PageEnigma/store";
import { AssetFilterOption } from "~/pages/PageEnigma/models";
import { useSignals } from "@preact/signals-react/runtime";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button } from "~/components";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
export const CameraTab = () => {
useSignals();
return (
<>
<div className="w-full overflow-x-auto">
<div className="mb-4 mt-4 flex justify-start gap-2 px-4">
<button
className={twMerge(
"filter-tab",
cameraFilter.value === AssetFilterOption.ALL ? "active" : "",
"disabled",
)}
onClick={() => (cameraFilter.value = AssetFilterOption.ALL)}
>
All
</button>
<button
className={twMerge(
"filter-tab",
cameraFilter.value === AssetFilterOption.MINE ? "active" : "",
"disabled",
)}
onClick={() => (cameraFilter.value = AssetFilterOption.MINE)}
disabled={!cameraItems.value.some((item) => item.isMine)}
>
My Cameras
</button>
<button
className={twMerge(
"filter-tab",
cameraFilter.value === AssetFilterOption.BOOKMARKED
? "active"
: "",
"disabled",
)}
onClick={() => (cameraFilter.value = AssetFilterOption.BOOKMARKED)}
disabled={!cameraItems.value.some((item) => item.isBookmarked)}
>
Bookmarked
</button>
</div>
</div>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
className="w-full py-3 text-sm font-medium"
>
Create Camera Movement
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4 pt-4">
<ItemElements
items={cameraItems.value}
assetFilter={cameraFilter.value}
/>
</div>
</>
);
};
@@ -1,33 +1,32 @@
import { useCallback, useContext, useEffect } from "react";
import { MediaClip } from "~/pages/PageEnigma/models/track";
import { MediaItem } from "~/pages/PageEnigma/models";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import {
canDrop,
currPosition,
dragId,
dragItem,
initPosition,
} from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
interface Props {
clip: MediaClip;
type: "animations" | "audio";
item: MediaItem;
}
export const AudioElement = ({ clip, type }: Props) => {
export const ItemElement = ({ item }: Props) => {
useSignals();
const { startDrag, endDrag } = useContext(TrackContext);
const { initX, initY } = initPosition.value;
useEffect(() => {
const onPointerUp = () => {
if (dragId.value) {
if (dragItem.value) {
endDrag();
}
};
const onMouseMove = (event: MouseEvent) => {
if (dragId) {
if (dragItem.value) {
event.stopPropagation();
event.preventDefault();
const deltaX = event.pageX - initX;
@@ -48,7 +47,7 @@ export const AudioElement = ({ clip, type }: Props) => {
const onPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
startDrag(type, clip.media_id, clip.length);
startDrag(item);
currPosition.value = {
currX: event.pageX,
currY: event.pageY,
@@ -60,18 +59,17 @@ export const AudioElement = ({ clip, type }: Props) => {
canDrop.value = false;
}
},
[type, clip.media_id, startDrag, clip.length],
[item, startDrag],
);
return (
<div className="relative h-16 w-16">
<div className="relative w-full rounded-lg" onPointerDown={onPointerDown}>
<img src={item.thumbnail} alt={item.name} className="rounded-t-lg" />
<div
id={`ani-clip-${clip.media_id}`}
className="absolute block h-16 w-16 rounded-lg bg-brand-secondary-700 p-2"
style={{ top: 0, left: 0 }}
onPointerDown={onPointerDown}
className="w-full rounded-b-lg py-1 text-center text-sm"
style={{ backgroundColor: "#39394D" }}
>
{clip.name}
{item.name}
</div>
</div>
);
@@ -0,0 +1,47 @@
import { ItemElement } from "./ItemElement";
import { AssetFilterOption, MediaItem } from "~/pages/PageEnigma/models";
import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/store";
interface Props {
items: MediaItem[];
assetFilter: AssetFilterOption;
}
export const ItemElements = ({ items, assetFilter }: Props) => {
const displayWidth =
dndSidePanelWidth.value > -1
? dndSidePanelWidth.value
: sidePanelWidth.value;
const displayItems = items.filter((item) => {
if (assetFilter === AssetFilterOption.ALL) {
return true;
}
if (assetFilter === AssetFilterOption.MINE) {
return item.isMine;
}
return item.isBookmarked;
});
function getGridColumnsClass(displayWidth: number): string {
if (displayWidth <= 280) {
return "grid-cols-2";
} else if (displayWidth <= 360) {
return "grid-cols-3";
} else if (displayWidth <= 440) {
return "grid-cols-4";
} else {
return "grid-cols-4";
}
}
const gridColumnsClass = getGridColumnsClass(displayWidth);
return (
<div className={`grid ${gridColumnsClass} gap-3`}>
{displayItems.map((item) => (
<ItemElement key={item.media_id} item={item} />
))}
</div>
);
};
@@ -0,0 +1,79 @@
import { AssetFilterOption, AssetType } from "~/pages/PageEnigma/models";
import { useSignals } from "@preact/signals-react/runtime";
import {
characterFilter,
characterItems,
objectFilter,
objectItems,
} from "~/pages/PageEnigma/store";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { Button } from "~/components";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
interface Props {
type: AssetType;
}
export const ObjectsTab = ({ type }: Props) => {
useSignals();
const assetFilter =
type === AssetType.CHARACTER ? characterFilter : objectFilter;
const items = type === AssetType.CHARACTER ? characterItems : objectItems;
return (
<>
<div className="w-full overflow-x-auto">
<div className="mb-4 mt-4 flex justify-start gap-2 px-4">
<button
className={twMerge(
"filter-tab",
assetFilter.value === AssetFilterOption.ALL ? "active" : "",
"disabled",
)}
onClick={() => (assetFilter.value = AssetFilterOption.ALL)}
>
All
</button>
<button
className={twMerge(
"filter-tab",
assetFilter.value === AssetFilterOption.MINE ? "active" : "",
"disabled",
)}
onClick={() => (assetFilter.value = AssetFilterOption.MINE)}
disabled={!items.value.some((item) => item.isMine)}
>
My {type === AssetType.CHARACTER ? "Characters" : "Objects"}
</button>
<button
className={twMerge(
"filter-tab",
assetFilter.value === AssetFilterOption.BOOKMARKED
? "active"
: "",
"disabled",
)}
onClick={() => (assetFilter.value = AssetFilterOption.BOOKMARKED)}
disabled={!items.value.some((item) => item.isBookmarked)}
>
Bookmarked
</button>
</div>
</div>
<div className="w-full px-4">
<Button
icon={faCirclePlus}
variant="action"
className="w-full py-3 text-sm font-medium"
>
Upload {type === AssetType.CHARACTER ? "Character" : "Object"}
</Button>
</div>
<div className="h-full w-full overflow-y-auto px-4 pt-4">
<ItemElements items={items.value} assetFilter={assetFilter.value} />
</div>
</>
);
};
@@ -0,0 +1,19 @@
import { useSignals } from "@preact/signals-react/runtime";
import { ItemElements } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ItemElements";
import { shapeFilter, shapeItems } from "~/pages/PageEnigma/store";
export const ShapeTab = () => {
useSignals();
return (
<>
<div className="px-4 pt-4 text-base font-bold">Shapes</div>
<div className="w-full overflow-y-auto px-4 pt-4">
<ItemElements
items={shapeItems.value}
assetFilter={shapeFilter.value}
/>
</div>
</>
);
};
@@ -0,0 +1,51 @@
import { AssetType } from "~/pages/PageEnigma/models";
import { ObjectsTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ObjectsTab";
import { ShapeTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/ShapeTab";
import { AnimationTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/AnimationTab";
import { AudioTab } from "~/pages/PageEnigma/comps/SidePanelTabs/itemTabs/AudioTab";
import { TabStylization } from "~/pages/PageEnigma/comps/SidePanelTabs/TabStylization";
import { CameraTab } from "./itemTabs/CameraTab";
export const tabList = [
{
icon: "resources/icons/animations.png",
title: "Animation",
value: AssetType.ANIMATION,
component: <AnimationTab />,
},
{
icon: "resources/icons/objects.png",
title: "Objects",
value: AssetType.OBJECT,
component: <ObjectsTab type={AssetType.OBJECT} />,
},
{
icon: "resources/icons/shapes.png",
title: "Shapes",
value: AssetType.SHAPE,
component: <ShapeTab />,
},
{
icon: "resources/icons/characters.png",
title: "Characters",
value: AssetType.CHARACTER,
component: <ObjectsTab type={AssetType.CHARACTER} />,
},
{
icon: "resources/icons/camera.png",
title: "Camera",
value: AssetType.CAMERA,
component: <CameraTab />,
},
{
icon: "resources/icons/audios.png",
title: "Audio",
value: AssetType.AUDIO,
component: <AudioTab />,
},
{
icon: "resources/icons/objects.png",
title: "Style (Temporary being moved)",
value: AssetType.STYLE,
component: <TabStylization />,
},
];
@@ -5,13 +5,14 @@ import {
toggleAudioMute,
updateAudio,
} from "~/pages/PageEnigma/store";
import { ClipGroup } from "~/pages/PageEnigma/models";
export const Audio = () => {
const { clips } = audioGroup.value;
return (
<div
className="block rounded-lg bg-audio-groupBg pb-5 pl-2 pr-4"
className="bg-global_audio-groupBg block rounded-lg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
@@ -23,7 +24,7 @@ export const Audio = () => {
clips={clips}
title="Global Audio TrackClips"
updateClip={updateAudio}
style="audio"
group={ClipGroup.GLOBAL_AUDIO}
muted={audioGroup.value.muted}
toggleMute={toggleAudioMute}
/>
@@ -1,6 +1,7 @@
import { cameraGroup, fullWidth, updateCamera } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { useSignals } from "@preact/signals-react/runtime";
import { ClipGroup } from "~/pages/PageEnigma/models";
export const Camera = () => {
useSignals();
@@ -17,7 +18,7 @@ export const Camera = () => {
id={cameraGroup.value.id}
keyframes={keyframes}
title="Camera Position/Rotation"
style="camera"
group={ClipGroup.CAMERA}
updateKeyframe={updateCamera}
/>
</div>
@@ -7,10 +7,11 @@ import {
updateCharacters,
} from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models";
function buildUpdaters(
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
type: ClipType;
id: string;
length: number;
offset: number;
@@ -21,17 +22,17 @@ function buildUpdaters(
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: "animations" });
updateCharacters({ ...options, type: ClipType.ANIMATION });
}
function updateClipPosition(options: { id: string; offset: number }) {
updateCharacters({ ...options, length: 0, type: "positions" });
updateCharacters({ ...options, length: 0, type: ClipType.TRANSFORM });
}
function updateClipLipSync(options: {
id: string;
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: "lipSync" });
updateCharacters({ ...options, type: ClipType.AUDIO });
}
return { updateClipLipSync, updateClipPosition, updateClipAnimations };
}
@@ -68,15 +69,15 @@ export const Character = ({ characterId }: Props) => {
clips={animationClips}
title="Animation"
updateClip={updateClipAnimations}
style="character"
type="animations"
group={ClipGroup.CHARACTER}
type={ClipType.ANIMATION}
/>
<TrackKeyFrames
id={character.id}
keyframes={positionKeyframes}
title="Character Position/Rotation"
updateKeyframe={updateClipPosition}
style="character"
group={ClipGroup.CHARACTER}
/>
<TrackClips
id={character.id}
@@ -85,8 +86,8 @@ export const Character = ({ characterId }: Props) => {
updateClip={updateClipLipSync}
muted={character.muted}
toggleMute={toggleCharacterLipSyncMute}
style="character"
type="lipSync"
group={ClipGroup.CHARACTER}
type={ClipType.AUDIO}
/>
</div>
</div>
@@ -4,6 +4,7 @@ import { ObjectTrack } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
export const ObjectGroups = () => {
useSignals();
return (
<>
{objectGroup.value.objects.length > 0 && (
@@ -1,10 +1,11 @@
import { fullWidth, objectGroup, updateObject } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup } from "~/pages/PageEnigma/models";
export const ObjectTrack = () => {
return (
<div
className="block rounded-lg bg-objects-groupBg pb-5 pl-2 pr-4"
className="bg-object-groupBg block rounded-lg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
@@ -17,7 +18,7 @@ export const ObjectTrack = () => {
keyframes={object.keyframes}
title={`${object.name} Position/Rotation`}
updateKeyframe={updateObject}
style="objects"
group={ClipGroup.OBJECT}
/>
</div>
))}
@@ -1,12 +1,12 @@
import { currentTime, fullHeight, scale } from "~/pages/PageEnigma/store";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
import { useMouseEventsAnimation } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsAnimation";
import { useMouseEventsScrubber } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsScrubber";
import { useSignals } from "@preact/signals-react/runtime";
export const Scrubber = () => {
useSignals();
const { onPointerDown, time } = useMouseEventsAnimation();
const { onPointerDown, time } = useMouseEventsScrubber();
const displayTime = time === -1 ? currentTime.value : time;
return (
@@ -13,11 +13,11 @@ import {
import { useQueueHandler } from "~/pages/PageEnigma/comps/Timeline/utils/useQueueHandler";
import { useSignals } from "@preact/signals-react/runtime";
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
import { Scrubber } from "~/pages/PageEnigma/comps/Scrubber/Scrubber";
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
import { Clip, Keyframe } from "~/pages/PageEnigma/models/track";
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
function getItemType(item: Clip | Keyframe | null) {
if (!item) {
@@ -1,17 +1,17 @@
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
import { useState } from "react";
import { scale, selectedItem } from "~/pages/PageEnigma/store";
import { Clip } from "~/pages/PageEnigma/models/track";
import { Clip, ClipGroup } from "~/pages/PageEnigma/models";
interface Props {
min: number;
max: number;
style: "character" | "camera" | "audio" | "objects";
group: ClipGroup;
clip: Clip;
updateClip: (options: { id: string; offset: number; length: number }) => void;
}
export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
export const TrackClip = ({ clip, min, max, group, updateClip }: Props) => {
const [state, setState] = useState({
length: clip.length,
offset: clip.offset,
@@ -31,8 +31,8 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
const classes = [
"absolute",
clip.clip_uuid === selectedClipId
? `bg-${style}-selected`
: `bg-${style}-clip`,
? `bg-${group}-selected`
: `bg-${group}-clip`,
];
return (
@@ -1,12 +1,16 @@
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { Clip } from "~/pages/PageEnigma/models/track";
import {
AssetType,
Clip,
ClipGroup,
ClipType,
} from "~/pages/PageEnigma/models";
import { PointerEvent } from "react";
import {
canDrop,
clipLength,
dragType,
dragItem,
dropId,
dropOffset,
filmLength,
@@ -17,13 +21,38 @@ interface Props {
id: string;
clips: Clip[];
title: string;
style: "character" | "audio" | "camera" | "objects";
type?: "animations" | "positions" | "lipSync";
group: ClipGroup;
type?: ClipType;
toggleMute?: () => void;
muted?: boolean;
updateClip: (options: { id: string; length: number; offset: number }) => void;
}
function getCanBuild({
dragType,
type,
group,
}: {
dragType?: AssetType;
type?: ClipType;
group: ClipGroup;
}) {
if (dragType === AssetType.ANIMATION) {
if (type === ClipType.ANIMATION) {
return true;
}
}
if (dragType === AssetType.AUDIO) {
if (group === ClipGroup.CHARACTER && type === ClipType.AUDIO) {
return true;
}
if (group === ClipGroup.GLOBAL_AUDIO) {
return true;
}
}
return false;
}
export const TrackClips = ({
id,
clips,
@@ -31,36 +60,22 @@ export const TrackClips = ({
updateClip,
muted,
title,
style,
group,
type,
}: Props) => {
const trackType = type ?? style;
const trackType = (type ?? group) as ClipType;
function onPointerOver() {
if (dragType.value === "animations") {
if (dragType.value !== trackType) {
return;
}
if (getCanBuild({ dragType: dragItem.value?.type, type, group })) {
dropId.value = id;
}
if (dragType.value === "audio") {
if (["lipSync", "audio"].indexOf(trackType) === -1) {
return;
}
}
dropId.value = id;
}
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
if (dragType.value === "animations") {
if (dragType.value !== trackType) {
return;
}
}
if (dragType.value === "audio") {
if (["lipSync", "audio"].indexOf(trackType) === -1) {
return;
}
if (!getCanBuild({ dragType: dragItem.value?.type, type, group })) {
return;
}
const track = document.getElementById(`track-${trackType}-${id}`);
if (!track) {
return;
@@ -70,7 +85,7 @@ export const TrackClips = ({
const position = track.getBoundingClientRect();
const clipOffset = (event.clientX - position.x) / 4 / scale.value;
if (clipOffset + clipLength.value > filmLength.value * 60) {
if (clipOffset + (dragItem.value!.length ?? 0) > filmLength.value * 60) {
canDrop.value = false;
return;
}
@@ -83,7 +98,8 @@ export const TrackClips = ({
return true;
}
return (
clipOffset < clip.offset && clipOffset + clipLength.value >= clip.offset
clipOffset < clip.offset &&
clipOffset + (dragItem.value!.length ?? 0) >= clip.offset
);
});
@@ -91,7 +107,7 @@ export const TrackClips = ({
dropOffset.value = clipOffset;
}
function onPointerLeave() {
if (dragType.value !== trackType) {
if (canDrop.value) {
return;
}
canDrop.value = false;
@@ -101,7 +117,7 @@ export const TrackClips = ({
<div className="pl-16">
<div
id={`track-${trackType}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
@@ -117,7 +133,7 @@ export const TrackClips = ({
? clips[index + 1].offset
: filmLength.value * 60
}
style={style}
group={group}
updateClip={updateClip}
clip={clip}
/>
@@ -1,44 +1,40 @@
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { useState } from "react";
import { scale, selectedItem } from "~/pages/PageEnigma/store";
import { Keyframe } from "~/pages/PageEnigma/models";
import { filmLength, scale, selectedItem } from "~/pages/PageEnigma/store";
import { useMouseEventsKeyframe } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSortUpDown } from "@fortawesome/pro-solid-svg-icons";
import { useSignals } from "@preact/signals-react/runtime";
interface Props {
min: number;
max: number;
keyframe: Keyframe;
updateKeyframe: (options: { id: string; offset: number }) => void;
}
export const TrackKeyFrame = ({
keyframe,
min,
max,
updateKeyframe,
}: Props) => {
const [offset, setOffset] = useState(keyframe.offset);
const { onPointerDown } = useMouseEventsKeyframe({
export const TrackKeyFrame = ({ keyframe, updateKeyframe }: Props) => {
useSignals();
const { onPointerDown, offset } = useMouseEventsKeyframe({
keyframe,
max,
min,
max: filmLength.value * 60,
min: 0,
updateKeyframe,
setOffset,
});
const displayOffset = offset > -1 ? offset : keyframe.offset;
const selectedKeyframeId =
(selectedItem.value as Keyframe | null)?.keyframe_uuid ?? "";
return (
<button
className={["block rotate-45 cursor-ew-resize", "absolute"].join(" ")}
className={[
"block rotate-45 cursor-ew-resize",
"absolute",
keyframe.keyframe_uuid === selectedKeyframeId
? "bg-keyframe-selected"
: "bg-keyframe-unselected",
].join(" ")}
style={{
width: 18,
height: 18,
backgroundColor:
keyframe.keyframe_uuid === selectedKeyframeId ? "#FFDE67" : "#EEEEEE",
left: offset * 4 * scale.value - 6,
left: displayOffset * 4 * scale.value - 6,
top: 8,
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
@@ -1,15 +1,18 @@
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { filmLength } from "~/pages/PageEnigma/store";
import { ClipGroup, Keyframe } from "~/pages/PageEnigma/models";
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
interface Props {
id: string;
keyframes: Keyframe[];
title: string;
style: "character" | "audio" | "camera" | "objects";
group: ClipGroup;
toggleMute?: () => void;
muted?: boolean;
updateKeyframe: (options: { id: string; offset: number }) => void;
updateKeyframe: (options: {
id: string;
offset: number;
force?: boolean;
}) => void;
}
export const TrackKeyFrames = ({
@@ -17,23 +20,17 @@ export const TrackKeyFrames = ({
keyframes,
updateKeyframe,
title,
style,
group,
}: Props) => {
return (
<div className="pl-16">
<div
id={`track-${style}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
id={`track-${group}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
>
{keyframes.map((keyframe, index) => (
{keyframes.map((keyframe) => (
<TrackKeyFrame
key={keyframe.keyframe_uuid}
min={index > 0 ? keyframes[index - 1].offset + 1 : 0}
max={
index < keyframes.length - 1
? keyframes[index + 1].offset
: filmLength.value * 60
}
updateKeyframe={updateKeyframe}
keyframe={keyframe}
/>
@@ -1,5 +1,5 @@
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
import { Clip } from "~/pages/PageEnigma/models/track";
import { Clip } from "~/pages/PageEnigma/models";
import { canDrop, scale } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
@@ -37,13 +37,11 @@ export const useMouseEventsClip = (
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
return;
}
if (deltaOffset + currLength.current > max) {
} else if (deltaOffset + currLength.current > max) {
currOffset.current = max - currLength.current;
return;
} else {
currOffset.current = deltaOffset;
}
currOffset.current = deltaOffset;
}
if (isActive.current === "left") {
if (initLength.current - delta < 30 || deltaOffset < min) {
@@ -1,5 +1,5 @@
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
import { Keyframe } from "~/pages/PageEnigma/models/track";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Keyframe } from "~/pages/PageEnigma/models";
import { canDrop, scale } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
@@ -8,15 +8,14 @@ export const useMouseEventsKeyframe = ({
max,
min,
updateKeyframe,
setOffset,
}: {
keyframe: Keyframe;
max: number;
min: number;
updateKeyframe: (args: { id: string; offset: number }) => void;
setOffset: Dispatch<number>;
}) => {
useSignals();
const [offset, setOffset] = useState(-1);
const currOffset = useRef(keyframe.offset);
const initOffset = useRef(keyframe.offset);
const isActive = useRef("");
@@ -30,8 +29,9 @@ export const useMouseEventsKeyframe = ({
});
isActive.current = "";
canDrop.value = false;
setOffset(-1);
}
}, [updateKeyframe, keyframe.keyframe_uuid]);
}, [updateKeyframe, keyframe.keyframe_uuid, setOffset]);
const onMouseMove = useCallback(
(event: MouseEvent) => {
@@ -40,15 +40,13 @@ export const useMouseEventsKeyframe = ({
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
return;
}
if (deltaOffset > max) {
} else if (deltaOffset > max) {
currOffset.current = max;
return;
} else {
currOffset.current = deltaOffset;
}
currOffset.current = deltaOffset;
setOffset(currOffset.current);
}
setOffset(currOffset.current);
},
[max, min, setOffset],
);
@@ -78,7 +76,9 @@ export const useMouseEventsKeyframe = ({
event.preventDefault();
clientX.current = event.clientX;
isActive.current = type;
setOffset(keyframe.offset);
}
},
offset,
};
};
@@ -1,10 +1,15 @@
import React, { useCallback, useEffect, useState } from "react";
import { currentTime, filmLength, scale } from "~/pages/PageEnigma/store";
import {
currentScroll,
currentTime,
filmLength,
scale,
} from "~/pages/PageEnigma/store";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
export const useMouseEventsAnimation = () => {
export const useMouseEventsScrubber = () => {
const [isActive, setIsActive] = useState(false);
const [clientX, setClientX] = useState(0);
@@ -21,11 +26,13 @@ export const useMouseEventsAnimation = () => {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: Math.round(time) },
data: { currentTime: Math.round(time) + currentScroll.value },
});
}
};
console.log("scroll", currentScroll.value);
const onMouseMove = (event: MouseEvent) => {
const delta = Math.round(
(event.clientX - clientX) / 4 / scale.value + currentTime.value,
@@ -41,7 +48,7 @@ export const useMouseEventsAnimation = () => {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_TIME,
data: { currentTime: delta },
data: { currentTime: delta + currentScroll.value },
});
}
return delta;
@@ -0,0 +1,52 @@
import React, { useCallback, useEffect, useState } from "react";
import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/store";
export const useMouseEventsSidePanel = () => {
const [isActive, setIsActive] = useState(false);
const [clientX, setClientX] = useState(0);
useEffect(() => {
const onPointerUp = () => {
if (isActive) {
sidePanelWidth.value = Math.round(dndSidePanelWidth.value);
setIsActive(false);
dndSidePanelWidth.value = -1;
}
};
const onMouseMove = (event: MouseEvent) => {
if (isActive) {
const delta = event.clientX - clientX;
event.stopPropagation();
event.preventDefault();
if (sidePanelWidth.value - delta < 0) {
return;
}
if (sidePanelWidth.value - delta > 443) {
return;
}
dndSidePanelWidth.value = sidePanelWidth.value - delta;
return;
}
};
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientX, isActive]);
return {
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
event.stopPropagation();
setClientX(event.clientX);
setIsActive(true);
dndSidePanelWidth.value = sidePanelWidth.value;
}
}, []),
};
};
@@ -1,18 +1,20 @@
import React, { useCallback, useEffect, useState } from "react";
import { timelineHeight } from "~/pages/PageEnigma/store";
import {
dndTimelineHeight,
pageHeight,
timelineHeight,
} from "~/pages/PageEnigma/store";
export const useMouseEventsHeight = () => {
export const useMouseEventsTimeline = () => {
const [isActive, setIsActive] = useState(false);
const [clientY, setClientY] = useState(0);
const [height, setHeight] = useState(-1);
useEffect(() => {
const onPointerUp = () => {
if (isActive) {
timelineHeight.value = Math.round(height);
timelineHeight.value = Math.round(dndTimelineHeight.value);
setIsActive(false);
setHeight(-1);
dndTimelineHeight.value = -1;
}
};
@@ -24,7 +26,10 @@ export const useMouseEventsHeight = () => {
if (timelineHeight.value - delta < 30) {
return;
}
setHeight(timelineHeight.value - delta);
if (timelineHeight.value - delta > pageHeight.value * 0.5) {
return;
}
dndTimelineHeight.value = timelineHeight.value - delta;
return;
}
};
@@ -36,17 +41,16 @@ export const useMouseEventsHeight = () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientY, isActive, height]);
}, [clientY, isActive]);
return {
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
event.stopPropagation();
dndTimelineHeight.value = timelineHeight.value;
setClientY(event.clientY);
setIsActive(true);
setHeight(timelineHeight.value);
}
}, []),
height,
};
};
@@ -1,26 +1,43 @@
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
import { useSignals } from "@preact/signals-react/runtime";
import { useCallback, useContext, useEffect } from "react";
import { currentTime } from "~/pages/PageEnigma/store";
import {
addNewCharacter,
currentTime,
loadAudioData,
loadCameraData,
loadCharacterData,
loadObjectData,
} from "~/pages/PageEnigma/store";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import {
ClipGroup,
MediaItem,
QueueClip,
QueueKeyframe,
UpdateTime,
} from "~/pages/PageEnigma/models/track";
} from "~/pages/PageEnigma/models";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { toTimelineActions } from "~/pages/PageEnigma/Queue/toTimelineActions";
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
interface Arguments {
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
data: QueueClip | UpdateTime | QueueKeyframe | ClipUI[] | MediaItem;
}
const LOADING_FUNCTIONS: Record<ClipGroup, (item: ClipUI) => void> = {
[ClipGroup.CHARACTER]: loadCharacterData,
[ClipGroup.OBJECT]: loadObjectData,
[ClipGroup.CAMERA]: loadCameraData,
[ClipGroup.GLOBAL_AUDIO]: loadAudioData,
};
export function useQueueHandler() {
useSignals();
const { addKeyframe } = useContext(TrackContext);
const { addKeyframe, clearExistingData } = useContext(TrackContext);
const handleFromEngineActions = useCallback(({ action, data }: Arguments) => {
console.log("FROM ENGINE", action, data);
@@ -28,21 +45,34 @@ export function useQueueHandler() {
case fromEngineActions.UPDATE_TIME:
currentTime.value = (data as UpdateTime).currentTime;
break;
case fromEngineActions.UPDATE_TIME_LINE:
clearExistingData();
(data as ClipUI[]).forEach((item) => {
LOADING_FUNCTIONS[item.group](item);
});
break;
case fromEngineActions.UPDATE_CHARACTER_ID: {
addNewCharacter(data as MediaItem);
break;
}
default:
throw new Error(`Unknown action ${action}`);
}
}, []);
const handleToTimelineActions = useCallback(({ action, data }: Arguments) => {
console.log("TO TIMELINE", action, data);
switch (action) {
case toTimelineActions.ADD_KEYFRAME:
addKeyframe(data as QueueKeyframe, currentTime.value);
break;
default:
throw new Error(`Unknown action ${action}`);
}
}, []);
const handleToTimelineActions = useCallback(
({ action, data }: Arguments) => {
console.log("TO TIMELINE", action, data);
switch (action) {
case toTimelineActions.ADD_KEYFRAME:
addKeyframe(data as QueueKeyframe, currentTime.value);
break;
default:
throw new Error(`Unknown action ${action}`);
}
},
[addKeyframe],
);
useEffect(() => {
Queue.subscribe(QueueNames.FROM_ENGINE, handleFromEngineActions);
@@ -1,28 +1,22 @@
import { createContext } from "react";
import {
MediaClip,
Keyframe,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
import { MediaItem, Keyframe, QueueKeyframe } from "~/pages/PageEnigma/models";
export const TrackContext = createContext<{
// keyframes
addKeyframe: (keyframe: QueueKeyframe, offset: number) => void;
deleteKeyframe: (keyframe: Keyframe) => void;
// sidebar clips
animationClips: MediaClip[];
audioClips: MediaClip[];
// misc
clearExistingData: () => void;
// drag and drop
startDrag: (type: "animations" | "audio", id: string, length: number) => void;
startDrag: (item: MediaItem) => void;
endDrag: () => void;
}>({
addKeyframe: () => {},
deleteKeyframe: () => {},
animationClips: [],
audioClips: [],
clearExistingData: () => {},
startDrag: () => {},
endDrag: () => {},
@@ -1,18 +1,22 @@
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { ClipType, MediaClip } from "~/pages/PageEnigma/models/track";
import { ReactNode, useCallback, useMemo } from "react";
import { AssetType } from "~/pages/PageEnigma/models";
import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop";
import {
addCharacterAnimation,
addCharacterAudio,
addGlobalAudio,
addCharacter,
canDrop,
dragId,
dragType,
dragItem,
dropId,
dropOffset,
addObject,
characterGroups,
cameraGroup,
audioGroup,
objectGroup,
} from "~/pages/PageEnigma/store";
import * as uuid from "uuid";
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
interface Props {
@@ -24,159 +28,77 @@ export const TrackProvider = ({ children }: Props) => {
const { endDrag, ...dragDrop } = useUpdateDragDrop();
const [animationClips, setAnimationClips] = useState<MediaClip[]>([]);
const [audioClips, setAudioClips] = useState<MediaClip[]>([]);
const [scale, setScale] = useState(1);
const [length, setLength] = useState(1);
const [time, setTime] = useState(0);
const updateCurrentTime = useCallback((newTime: number) => {
setTime(newTime);
console.log("message", {
action: "UpdateCurrentTime",
id: "",
data: { currentTime: newTime },
});
}, []);
const [timelineHeight, setTimelineHeight] = useState(0);
useEffect(() => {
const windowHeight = window.outerHeight;
setTimelineHeight(windowHeight * 0.25);
}, []);
// cross group functions
const dropClip = useCallback(() => {
if (canDrop.value) {
if (dragType.value === "animations") {
if (dragItem.value) {
const mediaItem = dragItem.value;
if (mediaItem.type === AssetType.CHARACTER) {
addCharacter(dragItem.value);
}
// if (dragItem.value.type === AssetType.CAMERA) {
// console.log("Dragged In Camera Type")
// }
if (dragItem.value.type === AssetType.OBJECT) {
addObject(dragItem.value);
}
}
// if (dragItem.value.type === AssetType.SHAPE) {
// console.log("Dragged In Shape Type")
// }
if (canDrop.value && dragItem.value) {
if (dragItem.value.type === AssetType.ANIMATION) {
addCharacterAnimation({
clipId: dragId.value!,
dragItem: dragItem.value,
characterId: dropId.value,
animationClips,
offset: dropOffset.value,
});
}
if (dragType.value === "audio") {
if (dragItem.value.type === AssetType.AUDIO) {
console.log("add audio", dropId.value);
addCharacterAudio({
clipId: dragId.value!,
dragItem: dragItem.value,
characterId: dropId.value,
audioClips,
offset: dropOffset.value,
});
addGlobalAudio({
clipId: dragId.value!,
dragItem: dragItem.value,
audioId: dropId.value,
audioClips,
offset: dropOffset.value,
});
}
}
endDrag();
}, [animationClips, endDrag]);
}, [endDrag]);
const fullWidth = useMemo(() => {
return length * 60 * 4 * scale;
}, [length, scale]);
useEffect(() => {
setAnimationClips([
{
version: 1,
media_id: "m_5q9s6esz8ymjqz0bheh8nf4crtj2kx",
type: ClipType.ANIMATION,
length: 25,
name: "Sit",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Idle",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Stand",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Walk",
},
]);
setAudioClips([
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Sing",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.AUDIO,
length: 25,
name: "Chatter",
},
{
version: 1,
media_id: "m_403phjvjkbbaxxbz8y7r6qjay07mfd",
type: ClipType.AUDIO,
length: 25,
name: "Talk",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.AUDIO,
length: 25,
name: "Yell",
},
]);
setScale(1);
setLength(12);
const clearExistingData = useCallback(() => {
characterGroups.value = [];
cameraGroup.value = {
id: "CG1",
keyframes: [],
};
audioGroup.value = {
id: "AG-1",
clips: [],
muted: false,
};
objectGroup.value = {
id: "OG1",
objects: [],
};
}, []);
const values = useMemo(() => {
return {
...keyframes,
clearExistingData,
...dragDrop,
endDrag: dropClip,
animationClips,
audioClips,
scale,
currentTime: time,
updateCurrentTime,
length,
fullWidth,
timelineHeight,
setTimelineHeight,
};
}, [
keyframes,
}, [keyframes, dragDrop, dropClip, clearExistingData]);
dragDrop,
dropClip,
animationClips,
audioClips,
updateCurrentTime,
time,
fullWidth,
length,
scale,
timelineHeight,
]);
return (
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
);
@@ -1,24 +1,14 @@
import { useCallback } from "react";
import {
canDrop,
clipLength,
dragId,
dragType,
} from "~/pages/PageEnigma/store";
import { canDrop, dragItem } from "~/pages/PageEnigma/store";
import { MediaItem } from "~/pages/PageEnigma/models";
export default function useUpdateDragDrop() {
const startDrag = useCallback(
(type: "animations" | "audio", id: string, length: number) => {
dragId.value = id;
dragType.value = type;
clipLength.value = length;
},
[],
);
const startDrag = useCallback((item: MediaItem) => {
dragItem.value = item;
}, []);
const endDrag = useCallback(() => {
dragId.value = null;
dragType.value = null;
dragItem.value = null;
canDrop.value = false;
}, []);
@@ -1,9 +1,5 @@
import { useCallback } from "react";
import {
ClipGroup,
QueueKeyframe,
Keyframe,
} from "~/pages/PageEnigma/models/track";
import { ClipGroup, QueueKeyframe, Keyframe } from "~/pages/PageEnigma/models";
import {
addCameraKeyframe,
addCharacterKeyframe,
@@ -32,11 +28,11 @@ const DELETE_KEYFRAME: Record<ClipGroup, (keyframe: Keyframe) => void> = {
export default function useUpdateKeyframe() {
const addKeyframe = useCallback((keyframe: QueueKeyframe, offset: number) => {
console.log("k", keyframe);
ADD_KEYFRAME[keyframe.group](keyframe, offset);
}, []);
const deleteKeyframe = useCallback((keyframe: Keyframe) => {
console.log("del", keyframe);
DELETE_KEYFRAME[keyframe.group](keyframe);
}, []);
@@ -101,8 +101,8 @@ export class AnimationClip {
this.mixer?.setTime(0);
}
toJSON(): string {
return JSON.stringify({
toJSON(): any {
return {
version: this.version,
media_id: this.media_id,
object_uuid: this.object_uuid,
@@ -110,6 +110,6 @@ export class AnimationClip {
speed: this.speed,
length: this.length,
clip_name: this.clip_name,
})
};
}
}
@@ -52,12 +52,12 @@ export class AudioClip {
return new AudioData(audioContext, audioBuffer);
}
toJSON(): string {
return JSON.stringify({
toJSON(): any {
return {
version: this.version,
media_id: this.media_id,
type: this.type,
volume: this.volume,
});
};
}
}
@@ -1,47 +1,64 @@
// Clip offsets represent the state of the clip on the timeline as well as what type of clip it is.
// it is created from a media id.
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models";
export class ClipUI {
version: number
type: "transform" | "audio" | "animation" | "lipsync"
group: "character" | "object" | "camera" | "global_audio"
name: string
media_id: string
object_uuid: string
offset: number // in frames
length: number // in frames
version: number;
type: ClipType;
group: ClipGroup;
name: string;
media_id: string;
object_uuid: string;
object_name: string;
offset: number; // in frames
length: number; // in frames
should_play: boolean;
clip_uuid: string;
keyframe_offset: number;
constructor(
version: number,
type: "transform" | "audio" | "animation" | "lipsync",
group: "character" | "object" | "camera" | "global_audio",
type: ClipType,
group: ClipGroup,
name: string,
media_id: string,
clip_uuid: string,
object_uuid: string,
object_name: string,
offset: number,
length: number,
keyframe_offset: number = 0,
) {
this.version = version
this.group = group // Only needed for UI
this.version = version;
this.group = group; // Only needed for UI
this.name = name // UI
this.type = type // UI and Animation / Audio / Lipsync / : Engine
this.clip_uuid = clip_uuid;
this.object_uuid = object_uuid // Animation / Audio / Lipsync / : Engine
this.media_id = media_id // Animation / Audio / Lipsync / : Engine
this.offset = offset
this.length = length
this.name = name; // UI
this.type = type; // UI and Animation / Audio / Lipsync / : Engine
this.object_uuid = object_uuid; // Animation / Audio / Lipsync / : Engine
this.object_name = object_name;
this.media_id = media_id; // Animation / Audio / Lipsync / : Engine
this.offset = offset;
this.length = length;
this.should_play = true;
this.keyframe_offset = keyframe_offset;
}
toJSON(): string {
return JSON.stringify({
toJSON(): any {
return {
version: this.version,
group:this.group,
group: this.group,
name: this.name,
type: this.type,
clip_uuid: this.clip_uuid,
object_uuid: this.object_uuid,
object_name: this.object_name,
media_id: this.media_id,
start_offset: this.offset,
ending_offset: this.length
})
ending_offset: this.length,
keyframe_offset: this.keyframe_offset,
};
}
}
@@ -35,6 +35,7 @@ export class LipSyncClip {
audio_data: AudioData | undefined;
lipsync: LipSync;
blendshape_helper: BlendShapeHelper;
faces: THREE.Mesh[]
constructor(version: number, media_id: string, volume: number) {
this.version = version;
@@ -47,9 +48,7 @@ export class LipSyncClip {
this.blendshape_helper = new BlendShapeHelper(0, 0, 0);
// we might need 3 of these one for each character ...
this.lipsync = new LipSync();
console.log("Lip!")
this.faces = [];
}
// lip sync will be generated through TTS
@@ -84,8 +83,8 @@ export class LipSyncClip {
const blendShapeIndexA = c.morphTargetDictionary["aa"];
if (blendShapeIndexE != null) {
this.blendshape_helper = new BlendShapeHelper(blendShapeIndexA, blendShapeIndexE, blendShapeIndexO);
this.faces.push(c);
resolve(c);
return;
}
}
}
@@ -94,44 +93,48 @@ export class LipSyncClip {
}
async play(object: THREE.Object3D) {
if (this.audio_data?.audioBuffer == null) { await this.download_audio(); }
if (this.lipsync.face == null) {
this.lipsync = new LipSync(await this._detect_face(object));
console.log("Face", this.lipsync.face);
this.lipsync.startLipSyncFromAudioBuffer(this.audio_data?.audioBuffer);
async play(object: THREE.Object3D) {
if (this.audio_data?.audioBuffer == null) { await this.download_audio(); }
if (this.lipsync.face == null) {
this.lipsync = new LipSync(await this._detect_face(object));
this.lipsync.startLipSyncFromAudioBuffer(this.audio_data?.audioBuffer);
}
}
stop() {
if (this.lipsync == null) { return; }
this.lipsync.destroy();
}
setBlends(ah: number, ee: number, oh: number) {
this.faces.forEach((element: THREE.Mesh) => {
if (element.morphTargetInfluences) {
element.morphTargetInfluences[this.blendshape_helper.ee] = ee;
element.morphTargetInfluences[this.blendshape_helper.ah] = ah;
element.morphTargetInfluences[this.blendshape_helper.oh] = oh;
}
});
}
step() {
if (this.lipsync == null) { return; }
const positions = this.lipsync.update();
this.setBlends(positions["ee"], positions["ah"], positions["oh"]);
}
reset() {
if (this.lipsync.face != undefined) {
this.setBlends(0,0,0);
}
this.lipsync = new LipSync();
}
toJSON(): any {
return {
version: this.version,
media_id: this.media_id,
type: this.type,
volume: this.volume,
};
}
}
stop() {
if (this.lipsync == null) { return; }
this.lipsync.destroy();
}
step() {
if (this.lipsync == null) { return; }
const positions = this.lipsync.update();
console.log("Face", this.lipsync.face);
this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ee] = positions["ee"];
this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ah] = positions["ah"];
this.lipsync.face.morphTargetInfluences[this.blendshape_helper.oh] = positions["oh"];
}
reset() {
if (this.lipsync.face != undefined) {
this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ee] = 0;
this.lipsync.face.morphTargetInfluences[this.blendshape_helper.ah] = 0
this.lipsync.face.morphTargetInfluences[this.blendshape_helper.oh] = 0;
}
this.lipsync = new LipSync();
}
toJSON(): string {
return JSON.stringify({
version: this.version,
media_id: this.media_id,
type: this.type,
volume: this.volume,
});
}
}
@@ -85,7 +85,7 @@ export class TransformClip {
this.step_frame = frame - offset;
//if (this.step_frame < 0) { this.step_frame = 0; }
//if (this.step_frame >= this.length) { return; } // Reached max frames.
if (this.keyframes.length < 2) { return; } // If there are enough points in the scene.
if (this.keyframes.length < 1) { return; } // If there are enough points in the scene.
// Find the current and next keyframes based on time_frame
let { currentKeyframe, nextKeyframe } = this.findNextNumber(this.step_frame);
@@ -140,14 +140,14 @@ export class TransformClip {
keyframe_uuid));
}
toJSON(): string {
return JSON.stringify({
toJSON(): any {
return {
version: this.version,
media_id: this.media_id,
object_uuid: this.object_uuid,
length: this.length,
type: this.type,
keyframes: this.keyframes,
});
};
}
}
+2 -2
View File
@@ -11,14 +11,14 @@ export class AnimationEngine {
}
load_object(object_uuid: string, media_id:string, clip_name: string) {
this.clips[object_uuid] = new AnimationClip(
this.clips[object_uuid+media_id] = new AnimationClip(
this.version,
media_id,
"glb",
object_uuid,
1.0, 1.0,
clip_name);
this.clips[object_uuid]._load_animation();
this.clips[object_uuid+media_id]._load_animation();
}
}
+88 -89
View File
@@ -54,9 +54,9 @@ export enum Visibility {
*/
type Data = { [key: string]: any };
class APIManagerResponseSuccess {
public user_message: String;
public user_message: string;
public data: Data | null;
constructor(user_message: String = "", data: Data | null = null) {
constructor(user_message: string = "", data: Data | null = null) {
this.data = data;
this.user_message = user_message;
}
@@ -74,7 +74,7 @@ class APIManagerResponseError extends Error {
}
export class APIManager {
baseUrl: String;
baseUrl: string;
constructor() {
this.baseUrl = "https://api.fakeyou.com";
@@ -88,43 +88,43 @@ export class APIManager {
* @returns APIManagerResponseMessage
*/
public async saveSceneState(
scene: THREE.Scene,
save_json: string,
scene_name: string,
scene_glb_media_file_token: string | null = null,
scene_media_file_token: string | null = null,
timeline_state: TimelineDataState | null = null,
): Promise<APIManagerResponseSuccess> {
const file = await this.gltfExport(scene);
): Promise<string> {
const file = new File([save_json], `${scene_name}.glb`, {
type: "application/json",
});
// will overwrite the scene on db if token exists
const upload_glb_response = await this.uploadGLB(
const upload_glb_response = await this.uploadEngineAsset(
file,
scene_glb_media_file_token,
);
const result_scene_glb_media_file_token =
upload_glb_response["media_file_token"];
// now write the scene
const save_scene_timeline_response = await this.saveSceneAndTimeLineState(
result_scene_glb_media_file_token,
scene_media_file_token,
scene_name,
timeline_state,
);
//const result_scene_glb_media_file_token =
// upload_glb_response["media_file_token"];
//// now write the scene
//const save_scene_timeline_response = await this.saveSceneAndTimeLineState(
// result_scene_glb_media_file_token,
// scene_media_file_token,
// scene_name,
// timeline_state,
//);
//const result_scene_media_file_token =
// save_scene_timeline_response["media_file_token"];
//const data = {
// scene_glb_media_file_token: result_scene_glb_media_file_token,
// scene_media_file_token: result_scene_media_file_token,
//};
const result_scene_media_file_token =
save_scene_timeline_response["media_file_token"];
const data = {
scene_glb_media_file_token: result_scene_glb_media_file_token,
scene_media_file_token: result_scene_media_file_token,
};
return new APIManagerResponseSuccess("Scene Saved", data);
return upload_glb_response['media_file_token'];
}
public async loadSceneState(
scene_media_file_token: string | null,
): Promise<APIManagerResponseSuccess> {
): Promise<any> {
const api_base_url = "https://api.fakeyou.com";
const url = `${api_base_url}/v1/media_files/file/${scene_media_file_token}`;
const response = await fetch(url);
@@ -139,47 +139,47 @@ export class APIManager {
throw new APIManagerResponseError("Failed to download file");
}
// Convert the response from a blob to json text
let blob = await file_response.blob();
const blob = await file_response.blob();
const json_result: string = await new Promise((resolve, reject) => {
let reader = new FileReader();
const reader = new FileReader();
reader.onloadend = () => resolve(JSON.parse(reader.result as string));
reader.onerror = reject;
reader.readAsText(blob);
});
console.log(`loadSceneState: ${JSON.stringify(json_result)}`);
return json_result;
const scene_glb_media_file_token: string =
json_result["scene_glb_media_file_token"];
// const scene_glb_media_file_token: string =
// json_result["scene_glb_media_file_token"];
// const media_bucket_path = await this.getMediaFile(
// scene_glb_media_file_token,
// );
// console.log(`GLB ${media_bucket_path}`);
// const glbLoader = new GLTFLoader();
// // promisify this
// const loadGlb = (
// bucket_path: string,
// ): Promise<APIManagerResponseSuccess> => {
// return new Promise((resolve, reject) => {
// glbLoader.load(bucket_path, (glb) => {
// if (glb) {
// const scene: THREE.Scene = glb.scene;
// const data = {
// scene_glb_media_file_token: scene_glb_media_file_token,
// scene_media_file_token: scene_media_file_token,
// scene: scene,
// };
// console.log(`Data: ${data}`);
// resolve(new APIManagerResponseSuccess("Success Loaded", data));
// } else {
// throw new APIManagerResponseError("Failed to Load GLB Scene");
// }
// });
// });
// };
const media_bucket_path = await this.getMediaFile(
scene_glb_media_file_token,
);
console.log(`GLB ${media_bucket_path}`);
let glbLoader = new GLTFLoader();
// promisify this
const loadGlb = (
bucket_path: string,
): Promise<APIManagerResponseSuccess> => {
return new Promise((resolve, reject) => {
glbLoader.load(bucket_path, (glb) => {
if (glb) {
const scene: THREE.Scene = glb.scene;
const data = {
scene_glb_media_file_token: scene_glb_media_file_token,
scene_media_file_token: scene_media_file_token,
scene: scene,
};
console.log(`Data: ${data}`);
resolve(new APIManagerResponseSuccess("Success Loaded", data));
} else {
throw new APIManagerResponseError("Failed to Load GLB Scene");
}
});
});
};
return await loadGlb(media_bucket_path);
//return await loadGlb(media_bucket_path);
}
/**
@@ -188,13 +188,13 @@ export class APIManager {
* @returns
*/
private async getMediaFile(media_file_token: string): Promise<string> {
let api_base_url = "https://api.fakeyou.com";
let url = `${api_base_url}/v1/media_files/file/${media_file_token}`;
let response = await fetch(url);
let json = await JSON.parse(await response.text());
let bucketPath = json["media_file"]["public_bucket_path"];
let media_base_url = "https://storage.googleapis.com/vocodes-public";
let media_url = `${media_base_url}${bucketPath}`; // gets you a bucket path
const api_base_url = "https://api.fakeyou.com";
const url = `${api_base_url}/v1/media_files/file/${media_file_token}`;
const response = await fetch(url);
const json = await JSON.parse(await response.text());
const bucketPath = json["media_file"]["public_bucket_path"];
const media_base_url = "https://storage.googleapis.com/vocodes-public";
const media_url = `${media_base_url}${bucketPath}`; // gets you a bucket path
return media_url;
}
@@ -213,18 +213,18 @@ export class APIManager {
return file;
}
private async uploadGLB(
private async uploadEngineAsset(
file: File,
scene_glb_media_file_token: string | null,
): Promise<string> {
media_file_token: string | null,
): Promise<any> {
const url = `${this.baseUrl}/v1/media_files/write/engine_asset`;
let uuid = uuidv4();
const uuid = uuidv4();
const form_data = new FormData();
form_data.append("uuid_idempotency_token", uuid);
// update existing scene otherwise create new glb scene and use it's media_file_id
if (scene_glb_media_file_token != null) {
form_data.append("media_file_token", scene_glb_media_file_token);
if (media_file_token != null) {
form_data.append("media_file_token", media_file_token);
}
form_data.append("file", file);
@@ -245,7 +245,6 @@ export class APIManager {
throw new Error("Failed to Send Data");
} else {
const json_data = await response.json();
console.log(`uploadGLB: ${JSON.stringify(json_data)}`);
return json_data; // or handle the response as appropriate
}
}
@@ -257,7 +256,7 @@ export class APIManager {
timeline_state: TimelineDataState | null, // only for now.
): Promise<string> {
const url = `${this.baseUrl}/v1/media_files/write/scene_file`;
let uuid = uuidv4();
const uuid = uuidv4();
console.log(
`Saving Scene scene_media_file_token:${scene_media_file_token} | scene_glb_media_file_token:${scene_glb_media_file_token}`,
@@ -310,8 +309,8 @@ export class APIManager {
blob: any,
fileName: string,
): Promise<APIManagerResponseSuccess> {
const url = `${this.baseUrl}/v1/media_uploads/upload`;
let uuid = uuidv4();
const url = `${this.baseUrl}/v1/media_files/upload`;
const uuid = uuidv4();
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid);
@@ -346,11 +345,11 @@ export class APIManager {
negative_prompt: string,
): Promise<string> {
const url = `https://funnel.tailce84f.ts.net/preview/`;
const payload = {
style: style,
positive_prompt: positive_prompt,
negative_prompt: negative_prompt
negative_prompt: negative_prompt,
};
const formData = new FormData();
@@ -393,14 +392,15 @@ export class APIManager {
return result;
}
public async stylizeVideo(media_token: string,
style: ArtStyle,
positive_prompt:string,
negative_prompt:string,
visibility:Visibility) {
let uuid = uuidv4();
public async stylizeVideo(
media_token: string,
style: ArtStyle,
positive_prompt: string,
negative_prompt: string,
visibility: Visibility,
) {
const uuid = uuidv4();
const data = {
uuid_idempotency_token: uuid,
style: style,
@@ -410,10 +410,10 @@ export class APIManager {
trim_start_millis: 0,
trim_end_millis: 3000,
enable_lipsync: true,
creator_set_visibility: visibility
}
creator_set_visibility: visibility,
};
const json_data = JSON.stringify(data)
const json_data = JSON.stringify(data);
const response = await fetch(`${this.baseUrl}/v1/video/enqueue_vst`, {
method: "POST",
@@ -435,4 +435,3 @@ export class APIManager {
return await response.json();
}
}
+213 -394
View File
@@ -15,7 +15,7 @@ import { BokehPass } from "three/addons/postprocessing/BokehPass.js";
import { createFFmpeg, fetchFile, FFmpeg } from "@ffmpeg/ffmpeg";
import AudioEngine from "./audio_engine.js";
import TransformEngine from "./transform_engine.js";
import { TimeLine, TimelineDataState } from "./timeline.js";
import { TimeLine } from "./timeline.js";
import { ClipUI } from "../datastructures/clips/clip_ui.js";
import { LipSyncEngine } from "./lip_sync_engine.js";
import { AnimationEngine } from "./animation_engine.js";
@@ -24,22 +24,11 @@ import { PointerLockControls } from "three/addons/controls/PointerLockControls.j
import { APPUI_ACTION_TYPES } from "../reducers";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
import { XYZ } from "../../datastructures/common";
class EditorState {
// {
// action: "ShowLoadingIndicator"
// source: "Editor"
// data: { "message" : "saving scene" }
// }
import { XYZ } from "../datastructures/common";
import { StoryTellerProxyScene } from "../proxy/storyteller_proxy_scene";
import { StoryTellerProxyTimeline } from "../proxy/storyteller_proxy_timeline";
selected_object: THREE.Object3D | undefined;
is_loading: boolean;
constructor() {
this.selected_object;
this.is_loading = false;
}
}
// Main editor class that will call everything else all you need to call is " initialize() ".
class Editor {
version: number;
@@ -91,6 +80,7 @@ class Editor {
cam_obj: THREE.Object3D | undefined;
renderPass: RenderPass | undefined;
generating_preview: boolean;
frames: number;
camera_person_mode: boolean;
current_scene_media_token: string | null;
@@ -104,7 +94,13 @@ class Editor {
positive_prompt: string;
negative_prompt: string;
art_style: ArtStyle;
last_scrub: number;
// Default params.
// scene proxy for serialization
storyteller_proxy_scene: StoryTellerProxyScene;
constructor() {
console.log(
"If you see this message twice! then it rendered twice, if you see it once it's all good.",
@@ -169,6 +165,8 @@ class Editor {
this.playback = false;
this.playback_location = 0;
this.max_length = 10;
this.last_scrub = 0;
this.frames = 0;
// Audio Engine Test.
this.render_width = 1280;
@@ -203,6 +201,9 @@ class Editor {
"((masterpiece, best quality, 8K, detailed)), colorful, epic, fantasy, (fox, red fox:1.2), no humans, 1other, ((koi pond)), outdoors, pond, rocks, stones, koi fish, ((watercolor))), lilypad, fish swimming around.";
this.negative_prompt = "";
this.art_style = ArtStyle.Anime2DFlat;
this.storyteller_proxy_scene = new StoryTellerProxyScene(this.version, this.activeScene.scene)
}
initialize(config: any) {
@@ -287,7 +288,22 @@ class Editor {
window.addEventListener("mousemove", this.onMouseMove.bind(this), false);
window.addEventListener("click", this.onMouseClick.bind(this), false);
// Base control and debug stuff remove debug in prod.
this._initializeControl();
if (this.control == undefined) {
return;
}
this.control.addEventListener("change", this.renderScene.bind(this));
this.control.addEventListener("dragging-changed", (event: any) => {
if (this.orbitControls == undefined) {
return;
}
this.orbitControls.enabled = !event.value;
this.updateSelectedUI();
// this.update_properties()
});
this.control.setSize(0.5); // Good default value for visuals.
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.activeScene.scene.add(this.control);
// Resets canvas size.
this.onWindowResize();
// Creates the main update loop.
@@ -307,42 +323,12 @@ class Editor {
this.onMouseUp.bind(this),
false,
);
this.renderer.domElement.addEventListener(
"onContextMenu",
this.onContextMenu.bind(this),
false,
);
// saving state of the scene
this.current_scene_media_token = null;
this.current_scene_glb_media_token = null;
this.renderer.domElement.addEventListener(
"mousedown",
this.onMouseDown.bind(this),
false,
);
this.renderer.domElement.addEventListener(
"mouseup",
this.onMouseUp.bind(this),
false,
);
this.renderer.domElement.addEventListener(
"onContextMenu",
this.onContextMenu.bind(this),
false,
);
this.cam_obj = this.activeScene.get_object_by_name("::CAM::");
if (this.cam_obj) {
this.addTransformClipBase(
"Camera Object",
"camera",
this.cam_obj,
0,
150,
);
}
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
@@ -367,71 +353,27 @@ class Editor {
console.log(result);
}
public async testTestTimelineEvents() {}
public async testStylizeRequest() {
const result = await this.api_manager
.stylizeVideo(
"mu_6wy1570a0c3c0tpkkncf4tsvb5234",
this.art_style,
this.positive_prompt,
this.negative_prompt,
Visibility.Public,
)
.catch((error) => {
console.log(error);
});
console.log(result);
}
public async testTestTimelineEvents() { }
public async loadScene(scene_media_token: string) {
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADER,
});
if (scene_media_token != null) {
this.current_scene_media_token = scene_media_token;
}
this.current_scene_media_token = scene_media_token;
const load_scene_state_response = await this.api_manager.loadSceneState(
const scene_json = await this.api_manager.loadSceneState(
this.current_scene_media_token,
);
console.log(load_scene_state_response);
if (load_scene_state_response.data == null) {
console.log("load_scene_state_response Missing Data");
return;
}
let proxyScene = new StoryTellerProxyScene(this.version, this.activeScene);
await proxyScene.loadFromSceneJson(scene_json['scene']);
this.cam_obj = this.activeScene.get_object_by_name("::CAM::");
const loaded_scene = load_scene_state_response.data["scene"];
let proxyTimeline = new StoryTellerProxyTimeline(this.version, this.timeline, this.transform_engine, this.animation_engine, this.audio_engine, this.lipsync_engine);
await proxyTimeline.loadFromJson(scene_json['timeline']);
if (load_scene_state_response.data == null) {
return;
}
// Load these so you can rewrite the scene glb using it's token.
this.current_scene_media_token =
load_scene_state_response.data["scene_media_file_token"];
this.current_scene_glb_media_token =
load_scene_state_response.data["scene_glb_media_file_token"];
console.log(
`loadScene => SceneMediaToken:${this.current_scene_media_token} SceneGLBMediaToken:${this.current_scene_glb_media_token}`,
);
this.activeScene.scene.children = loaded_scene.children;
this.activeScene.scene.children.forEach((child: THREE.Object3D) => {
child.parent = this.activeScene.scene;
if (child.type == "DirectionalLight") {
const pos = child.position;
const rot = child.rotation;
const light = this.activeScene._create_base_lighting();
light.position.set(pos.x, pos.y, pos.z);
light.rotation.set(rot.x, rot.y, rot.z);
this.activeScene.scene.remove(child);
}
});
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADER,
@@ -439,9 +381,9 @@ class Editor {
}
// TO UPDATE selected objects in the scene might want to add to the scene ...
async setSelectedObject(position:XYZ,rotation:XYZ,scale:XYZ) {
if (this.selected !=null) {
async setSelectedObject(position: XYZ, rotation: XYZ, scale: XYZ) {
if (this.selected != undefined || this.selected != null) {
//console.log(`triggering setSelectedObject`)
this.selected.position.x = position.x
this.selected.position.y = position.y
this.selected.position.z = position.z
@@ -455,7 +397,7 @@ class Editor {
this.selected.scale.z = scale.z
}
}
}
public async saveScene(name: string) {
@@ -464,31 +406,24 @@ class Editor {
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADER,
});
console.log(
`saveScene => SceneMediaToken:${this.current_scene_media_token} SceneGLBMediaToken:${this.current_scene_glb_media_token}`,
);
let proxyScene = new StoryTellerProxyScene(this.version, this.activeScene);
let scene_json = await proxyScene.saveToScene();
let proxyTimeline = new StoryTellerProxyTimeline(this.version, this.timeline, this.transform_engine, this.animation_engine, this.audio_engine, this.lipsync_engine);
let timeline_json = await proxyTimeline.saveToJson();
let save_data = { scene: scene_json, timeline: timeline_json };
// TODO turn scene information into and object ...
const result = await this.api_manager.saveSceneState(
this.activeScene.scene,
JSON.stringify(save_data),
name,
this.current_scene_glb_media_token,
this.current_scene_media_token,
new TimelineDataState(),
);
if (result.data == null) {
return;
}
const scene_media_token = result.data["scene_media_file_token"];
if (scene_media_token != null) {
this.current_scene_media_token = scene_media_token;
}
const scene_glb_media_token = result.data["scene_glb_media_file_token"];
if (scene_glb_media_token != null) {
this.current_scene_glb_media_token = scene_glb_media_token;
}
console.log(result);
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADER,
@@ -509,82 +444,24 @@ class Editor {
return;
}
this.last_selected = this.selected;
this.selected = undefined;
this.control.detach();
this.activeScene.scene.remove(this.control);
this.outlinePass.selectedObjects = [];
}
async _serialize_timeline() {
// note the database from the server is the source of truth for all the data.
// Test code here
const object: THREE.Object3D = await this.activeScene.load_glb(
"./resources/models/fox/fox.glb",
);
// load object into the engine for lip syncing
this.lipsync_engine.load_object(
object.uuid,
"m_f1jxx4zwy4da2zn0cvdqhha7kqkj72",
);
// create the clip with the same id for a reference to the media
this.timeline.addPlayableClip(
new ClipUI(
1.0,
"lipsync",
"character",
"clip1",
"m_f1jxx4zwy4da2zn0cvdqhha7kqkj72",
object.uuid,
150,
400,
),
);
// media id for this is up in the air but when a path is created you should be able to store and delete it
this.timeline.addPlayableClip(
new ClipUI(
1.0,
"transform",
"character",
"clip2",
object.uuid,
object.uuid,
0,
150,
),
);
// media id for this as well it can be downloaded
this.timeline.addPlayableClip(
new ClipUI(
1.0,
"animation",
"character",
"clip3",
"/resources/models/fox/fox_idle.glb",
object.uuid,
0,
400,
),
);
this.animation_engine.load_object(
object.uuid,
"/resources/models/fox/fox_idle.glb",
"clip3",
);
}
switchCameraView() {
this.camera_person_mode = !this.camera_person_mode;
console.log("camera");
console.log(this.camera_person_mode);
if (this.cam_obj) {
if (this.camera_person_mode) {
this.last_cam_pos.copy(this.camera.position);
this.last_cam_rot.copy(this.camera.rotation);
this.camera.position.copy(this.cam_obj.position);
this.camera.rotation.copy(this.cam_obj.rotation);
if (this.orbitControls) {
this.orbitControls.enabled = false;
}
@@ -595,11 +472,11 @@ class Editor {
this.cameraViewControls.enabled = true;
}
this.cam_obj.scale.set(0, 0, 0);
if (this.activeScene.hot_items) {
this.activeScene.hot_items.forEach((element) => {
element.visible = false;
});
}
this.removeTransformControls();
this.selected = this.cam_obj;
this.dispatchAppUiState({ type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT });
this.updateSelectedUI();
} else {
this.camera.position.copy(this.last_cam_pos);
this.camera.rotation.copy(this.last_cam_rot);
@@ -618,93 +495,31 @@ class Editor {
element.visible = true;
});
}
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT,
});
}
}
}
async addTransformClipBase(
name: string = "New Clip",
group: "object" | "character" | "camera" | "global_audio",
object: THREE.Object3D,
offset: number,
length: number,
) {
this.timeline.addPlayableClip(
new ClipUI(
1.0,
"transform",
group,
name,
object.uuid, // object id here ...
object.uuid, // Need to change to media id ....
offset,
length,
),
);
this.transform_engine.loadObject(object.uuid, length);
public async loadMediaToken(media_file_token: string) {
this.activeScene.load_glb(media_file_token);
}
async _test_demo() {
// note the database from the server is the source of truth for all the data.
// Test code here
const object: THREE.Object3D = await this.activeScene.load_glb(
"./resources/models/fox/fox.glb",
"m_4wva09qznapzk5rcvbxy671d1qx2pr",
);
object.uuid = "CH1";
// Load timeline creates the the clips from the datastructure and loads them in here.
// load object into the engine for lip syncing
//this.lipsync_engine.load_object(
// object.uuid,
// "m_f1jxx4zwy4da2zn0cvdqhha7kqkj72",
//);
//this.animation_engine.load_object(
// object.uuid,
// "/resources/models/fox/fox_idle.glb",
// "clip3",
//);
// then it creates clip ui to load the playable clips
// then refreshes the timeline.
// create the clip with the same id for a reference to the media
//this.timeline.addPlayableClip(
// new ClipUI(
// 1.0,
// "lipsync",
// "character",
// "clip1",
// "m_f1jxx4zwy4da2zn0cvdqhha7kqkj72",
// object.uuid,
// 150,
// 400,
// ),
//);
//// media id for this is up in the air but when a path is created you should be able to store and delete it
//this.timeline.addPlayableClip(
// new ClipUI(1.0, "transform", "character", "clip2", object.uuid, object.uuid, 0, 150),
//);
// media id for this as well it can be downloaded
//this.timeline.addPlayableClip(
// new ClipUI(
// 1.0,
// "animation",
// "character",
// "clip3",
// "/resources/models/fox/fox_idle.glb",
// object.uuid,
// 0,
// 400,
// ),
//);
//this.audio_engine.loadClip("m_h33vytxs5eqqqf07nsy14qzrf9ww4v");
//await this.timeline.addPlayableClip(new ClipUI(
// 1.0,
// "audio",
// "global_audio",
// "AudioClip",
// "m_h33vytxs5eqqqf07nsy14qzrf9ww4v",
// "",
// 0,
// 50));
// Stick Open Pose Man: m_9f3d3z94kk6m25zywyz6an3p43fjtw
// XBot: m_r7w1tmkx2jg8nznr3hyzj4k6zhfh7d
// YBot: m_9sqg0evpr23587jnr8z3zsvav1x077
// Shrek: m_fmxy8wjnep1hdaz7qdg4n7y15d2bsp
}
// Configure post processing.
@@ -771,6 +586,17 @@ class Editor {
this.composer.addPass(this.outputPass);
}
deleteObject(uuid: string) {
let obj = this.activeScene.get_object_by_uuid(uuid);
if (obj) {
this.activeScene.scene.remove(obj);
}
this.removeTransformControls();
this.selected = undefined;
this.dispatchAppUiState({ type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT });
this.timeline.deleteObject(uuid)
}
create_parim(name: string) {
const uuid = this.activeScene.instantiate(name);
}
@@ -780,51 +606,42 @@ class Editor {
this.activeScene.renderMode(this.rendering);
}
save() {
//console.log(this.control)
//if(this.selected != null){ this.control.detach(this.selected) }
//this.activeScene.scene.remove(this.control)
//this.activeScene.scene.remove(this.activeScene.gridHelper)
//this.save_manager.save(this.activeScene.scene, this._save_to_cloud.bind(this), this.audio_manager, this.timeline, this.activeScene.animations)
//this.activeScene._createGrid()
//this.audio_engine.playClip("m_f7jnwt3d1ddchatdk5vaqt0n4mb1hg")
//console.log(this.selected)
if (this.selected == null) {
return;
}
this.transform_engine.addFrame(this.selected);
console.log("Frame taken.");
stepFrame(frames: number) {
this.timeline.stepFrame(frames);
}
take_timeline_cam_clip() {
if (this.cam_obj == null) {
return;
// Render the scene to the camera, this is called in the update.
renderScene() {
if (this.composer != null && !this.rendering && this.rawRenderer) {
this.composer.render();
this.rawRenderer.render(this.activeScene.scene, this.render_camera);
} else if (this.renderer && this.render_camera) {
this.renderer.setSize(this.render_width, this.render_height);
this.renderer.render(this.activeScene.scene, this.render_camera);
} else {
console.error("Could not render to canvas no render or composer!");
}
if (!this.camera_person_mode) {
return;
if (this.rendering && this.renderer && this.clock) {
this.frames += 1;
this.playback_location++;
const imgData = this.renderer.domElement.toDataURL();
this.frame_buffer.push(imgData);
this.render_timer += this.clock.getDelta();
if (this.timeline.is_playing == false) {
this.stopPlayback();
this.playback_location = 0;
this.rendering = false;
this.switchCameraView();
this.onWindowResize();
}
}
this.transform_engine.addFrame(
this.cam_obj,
this.transform_engine.clips[this.cam_obj.uuid].length,
);
console.log("Camera frame taken.");
this.activeScene.createPoint(this.cam_obj.position, false);
}
// Basicly Unity 3D's update loop.
updateLoop(time: number) {
async updateLoop(time: number) {
if (this.cam_obj == undefined) {
this.cam_obj = this.activeScene.get_object_by_name("::CAM::");
if (this.cam_obj) {
this.addTransformClipBase(
"Camera Object",
"camera",
this.cam_obj,
0,
150,
);
}
}
// Updates debug stats.
@@ -841,25 +658,43 @@ class Editor {
if (this.cameraViewControls && this.camera_person_mode) {
this.cameraViewControls.update(5 * delta_time);
if (this.cam_obj) {
if (this.timeline.is_playing == false) {
if (this.last_scrub != this.timeline.scrubber_frame_position) {
this.camera.position.copy(this.cam_obj.position);
this.camera.rotation.copy(this.cam_obj.rotation);
} else if (this.timeline.is_playing == false) {
this.cam_obj.position.copy(this.camera.position);
this.cam_obj.rotation.copy(this.camera.rotation);
} else {
this.camera.position.copy(this.cam_obj.position);
this.camera.rotation.copy(this.cam_obj.rotation);
}
this.cam_obj.visible = false;
const min = new THREE.Vector3(-12, -1, -12);
const max = new THREE.Vector3(12, 24, 12);
this.camera.position.copy(this.camera.position.clamp(min, max));
}
}
else if (this.cam_obj) {
this.cam_obj.visible = true;
}
if (this.render_camera && this.cam_obj) {
this.render_camera.position.copy(this.cam_obj.position);
this.render_camera.rotation.copy(this.cam_obj.rotation);
this.cam_obj.scale.copy(new THREE.Vector3(1, 1, 1));
}
if(this.timeline.is_playing){
this.timeline.update();
if (this.timeline.is_playing) {
await this.timeline.update(this.rendering);
}
else if (this.last_scrub == this.timeline.scrubber_frame_position) {
this.updateSelectedUI();
}
this.last_scrub = this.timeline.scrubber_frame_position;
this.renderScene();
}
@@ -871,12 +706,6 @@ class Editor {
this.transform_interaction = true;
}
async loadWavAsBlob(url: string) {
const response = await fetch(url);
const blob = await response.blob();
return blob;
}
async convertAudioClip(itteration: number, ffmpeg: FFmpeg, clip: ClipUI) {
const video_og = itteration + "tmp.mp4";
const wav_name = itteration + "tmp.wav";
@@ -928,10 +757,10 @@ class Editor {
audioSegment,
"-filter_complex",
"[1:a]adelay=" +
startTime * 1000 +
"|" +
startTime * 1000 +
"[a1];[0:a][a1]amix=inputs=2[a]",
startTime * 1000 +
"|" +
startTime * 1000 +
"[a1];[0:a][a1]amix=inputs=2[a]",
"-map",
"[a]",
`${itteration}final_tmp.wav`,
@@ -957,7 +786,12 @@ class Editor {
}
async stopPlayback(compile_audio: boolean = true) {
console.log(this.frames, this.frame_buffer.length);
this.renderMode();
if (this.generating_preview) { return; }
this.generating_preview = true;
const ffmpeg = createFFmpeg({ log: true });
await ffmpeg.load();
for (let index = 0; index < this.frame_buffer.length; index++) {
@@ -970,7 +804,7 @@ class Editor {
}
await ffmpeg.run(
"-framerate",
"" + this.cap_fps,
"" + this.cap_fps / 2,
"-i",
"image%d.png",
"-f",
@@ -984,6 +818,10 @@ class Editor {
"-c:a",
"aac", // Specify audio codec (optional, but recommended for MP4)
"-shortest", // Ensure output duration matches the shortest stream (video or audio)
"-pix_fmt",
"yuv420p",
"-f",
"mp4",
"0tmp.mp4",
);
@@ -998,22 +836,39 @@ class Editor {
}
}
const output = await ffmpeg.FS("readFile", itteration + "tmp.mp4");
const output = ffmpeg.FS("readFile", itteration + "tmp.mp4");
ffmpeg.exit();
this.generating_preview = false;
// Create a Blob from the output file for downloading
const blob = new Blob([output.buffer], { type: "video/mp4" });
const url = URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = "render.mp4";
document.body.appendChild(downloadLink);
downloadLink.click();
// Clean up
URL.revokeObjectURL(url);
document.body.removeChild(downloadLink);
const data = await this.api_manager.uploadMedia(blob, "tmp.wav");
console.log(data);
// Create a link to download the file
const data: any = await this.api_manager.uploadMedia(blob, "render.mp4");
console.log("data", data)
if (data == null) { return; }
let upload_token = data['media_file_token'];
console.log(upload_token);
// Create a link to download the file stylize video using api ..
//{"success":true,"upload_token":"mu_x9kr5cfafn512pjbygdszvbdpktrr"} payload
const result = await this.api_manager
.stylizeVideo(
upload_token,
this.art_style,
this.positive_prompt,
this.negative_prompt,
Visibility.Public,
)
.catch((error) => {
console.log(error);
});
// {"success":true,"inference_job_token":"jinf_j3nbqbd15wqxb0xcks13qh3f3bz"}
console.log(result);
}
async generateFrame() {
@@ -1054,9 +909,9 @@ class Editor {
const url = await this.api_manager.uploadMediaFrameGeneration(
blob,
"render.mp4",
"anime_ghibli",
"((masterpiece, best quality, 8K, detailed)), colorful, epic, fantasy, (fox, red fox:1.2), no humans, 1other, ((koi pond)), outdoors, pond, rocks, stones, koi fish, ((watercolor))), lilypad, fish swimming around.",
"",
this.art_style,
this.positive_prompt,
this.negative_prompt,
);
console.log(url);
@@ -1083,15 +938,16 @@ class Editor {
}
}
// This initializes the generation of a video render scene is where the core work happens
generateVideo() {
console.log("Generating video...");
if (this.rendering) {
if (this.rendering || this.generating_preview) {
return;
}
this.rendering = true; // has to go first to debounce
this.startPlayback();
this.frame_buffer = [];
this.render_timer = 0;
this.rendering = true;
this.activeScene.renderMode(this.rendering);
if (this.activeScene.hot_items) {
this.activeScene.hot_items.forEach((element) => {
@@ -1103,69 +959,28 @@ class Editor {
startPlayback() {
this.timeline.is_playing = true;
this.timeline.scrubber_frame_position = 0;
if (this.activeScene.hot_items) {
this.activeScene.hot_items.forEach((element) => {
element.visible = false;
});
}
if (!this.camera_person_mode) {
this.switchCameraView();
}
}
// Initializes transform x y z changes.
_initializeControl() {
if (this.control == undefined) {
return;
}
this.control.addEventListener("change", this.renderScene.bind(this));
this.control.addEventListener("dragging-changed", (event: any) => {
if (this.orbitControls == undefined) {
return;
}
this.orbitControls.enabled = !event.value;
this.updateSelectedUI();
// this.update_properties()
});
this.control.setSize(0.5); // Good default value for visuals.
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.activeScene.scene.add(this.control);
}
// Render the scene to the camera.
renderScene() {
if (this.composer != null && !this.rendering && this.rawRenderer) {
this.composer.render();
this.rawRenderer.render(this.activeScene.scene, this.render_camera);
} else if (this.renderer && this.render_camera) {
this.renderer.setSize(this.render_width, this.render_height);
this.renderer.render(this.activeScene.scene, this.render_camera);
} else {
console.error("Could not render to canvas no render or composer!");
}
if (this.rendering && this.renderer && this.clock) {
this.playback_location++;
const imgData = this.renderer.domElement.toDataURL();
this.frame_buffer.push(imgData);
this.render_timer += this.clock.getDelta();
if (this.timeline.is_playing == false) {
this.stopPlayback();
this.playback_location = 0;
this.rendering = false;
this.switchCameraView();
this.onWindowResize();
}
}
}
updateSelectedUI() {
if (this.selected == undefined) {
return;
}
const pos = this.selected.position;
const rot = this.selected.rotation;
const scale = this.selected.scale;
// TODO this is a bug we need to only show when clicked on and use UPDATE when updating.
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
type: APPUI_ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
group:
this.selected.name === "::CAM::"
@@ -1176,23 +991,24 @@ class Editor {
version: this.version,
objectVectors: {
position: {
x: parseFloat(pos.x.toFixed(2)),
y: parseFloat(pos.y.toFixed(2)),
z: parseFloat(pos.z.toFixed(2)),
x: parseFloat(pos.x.toFixed(6)),
y: parseFloat(pos.y.toFixed(6)),
z: parseFloat(pos.z.toFixed(6)),
},
rotation: {
x: parseFloat(rot.x.toFixed(2)),
y: parseFloat(rot.y.toFixed(2)),
z: parseFloat(rot.z.toFixed(2)),
x: parseFloat(rot.x.toFixed(6)),
y: parseFloat(rot.y.toFixed(6)),
z: parseFloat(rot.z.toFixed(6)),
},
scale: {
x: parseFloat(scale.x.toFixed(2)),
y: parseFloat(scale.y.toFixed(2)),
z: parseFloat(scale.z.toFixed(2)),
x: parseFloat(scale.x.toFixed(6)),
y: parseFloat(scale.y.toFixed(6)),
z: parseFloat(scale.z.toFixed(6)),
},
},
},
});
}
// Automaticly resize scene.
@@ -1223,10 +1039,6 @@ class Editor {
this.render_camera.updateProjectionMatrix();
}
onContextMenu(event: any) {
return false;
}
onMouseDown(event: any) {
if (event.button === 1 && this.camera_person_mode) {
this.lockControls?.lock();
@@ -1255,7 +1067,8 @@ class Editor {
this.raycaster == undefined ||
this.mouse == undefined ||
this.control == undefined ||
this.outlinePass == undefined
this.outlinePass == undefined ||
this.camera_person_mode
) {
return;
}
@@ -1263,8 +1076,9 @@ class Editor {
this.raycaster.setFromCamera(this.mouse, this.camera);
const interactable: any[] = [];
this.activeScene.scene.children.forEach((child: THREE.Object3D) => {
// console.log(child);
if (child.name != "") {
if (child.type == "Mesh" || child.type == "Object3D") {
if (child.type == "Mesh" || child.type == "Object3D" || child.type == "Group") {
interactable.push(child);
}
}
@@ -1278,17 +1092,22 @@ class Editor {
currentObject = currentObject.parent;
}
this.selected = currentObject;
// Show panel here
if (this.selected.type == "Scene") {
this.selected = intersects[0].object;
}
this.activeScene.selected = this.selected;
this.updateSelectedUI();
// this.update_properties()
this.activeScene.scene.add(this.control);
this.control.attach(this.selected);
this.outlinePass.selectedObjects = [this.selected];
this.transform_interaction = true;
// Contact react land
this.dispatchAppUiState({ type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT });
this.updateSelectedUI();
}
} else if (this.transform_interaction == false) {
this.removeTransformControls();
+67 -5
View File
@@ -46,6 +46,7 @@ class Scene {
let obj = new THREE.Mesh(geometry, material);
obj.receiveShadow = true;
obj.castShadow = true;
obj.userData["media_id"] = "Parim";
//obj.type = "Object3D";
obj.name = name;
this.scene.add(obj);
@@ -60,16 +61,33 @@ class Scene {
return this.scene.getObjectByName(name);
}
createPoint(pos: THREE.Vector3, visable: boolean = true) {
createPoint(pos: THREE.Vector3, keyframe_uuid: string): THREE.Object3D {
let geometry = new THREE.SphereGeometry(0.1, 18, 12);
let material = new THREE.MeshBasicMaterial({ color: 0x05C3DD });
let obj = new THREE.Mesh(geometry, material);
obj.position.copy(pos);
obj.receiveShadow = visable;
obj.receiveShadow = false;
obj.castShadow = false;
obj.userData['media_id'] = "Point::" + keyframe_uuid;
if (this.hot_items != undefined) {
this.hot_items.push(obj);
}
this.scene.add(obj);
return obj;
}
deletePoint(keyframe_uuid: string) {
this.scene.traverse((object) => {
if (object.userData.media_id) {
let obj_keyframe_uuid = object.userData.media_id.replace("Point::", "");
console.log(obj_keyframe_uuid);
if (obj_keyframe_uuid === keyframe_uuid) {
console.log("Found!", object);
this.scene.remove(object);
return;
}
}
});
}
_disable_skybox() {
@@ -77,7 +95,7 @@ class Scene {
}
_create_camera_obj() {
this.load_glb("/resources/models/camera/camera.glb", false).then((cam_obj) => {
this.load_glb("m_cxh4asqhapdz10j880755dg4yevshb", false).then((cam_obj) => {
cam_obj.userData["name"] = "::CAM::";
cam_obj.name = "::CAM::";
cam_obj.position.set(0, 0.6, 1.5);
@@ -97,7 +115,50 @@ class Scene {
}
}
async load_glb(filepath: string, auto_add: boolean = true): Promise<THREE.Object3D> { //: Promise<THREE.Object3D> {
async getMediaURL(media_id: string) {
//This is for prod when we have the proper info on the url.
let api_base_url = "https://api.fakeyou.com";
let url = `${api_base_url}/v1/media_files/file/${media_id}`
let responce = await fetch(url);
let json = await JSON.parse(await responce.text());
let bucketPath = json["media_file"]["public_bucket_path"];
let media_base_url = "https://storage.googleapis.com/vocodes-public"
let media_url = `${media_base_url}${bucketPath}`
return media_url;
}
async load_glb(media_id: string, auto_add: boolean = true): Promise<THREE.Object3D> { //: Promise<THREE.Object3D> {
return new Promise(async (resolve) => {
let glbLoader = new GLTFLoader();
glbLoader.load(await this.getMediaURL(media_id), (glb) => {
glb.scene.children.forEach(child => {
child.traverse((c: THREE.Object3D) => {
if (c instanceof THREE.Mesh) {
c.material.metalness = 0.0;
c.material.specular = 0.5;
c.castShadow = true;
c.receiveShadow = true;
c.frustumCulled = false;
c.material.transparent = false;
}
});
//console.log(child);
//if (child.type == "Group") {
// if (auto_add) { child.children.forEach(element => {
// this.scene.add(element);
// }); }
// resolve(child);
//}
child.frustumCulled = false;
child.userData["media_id"] = media_id;
if (auto_add) { this.scene.add(child); }
resolve(child);
});
});
});
}
async load_glb_absolute(filepath: string, auto_add: boolean = true): Promise<THREE.Object3D> { //: Promise<THREE.Object3D> {
return new Promise((resolve) => {
let glbLoader = new GLTFLoader();
glbLoader.load(filepath, (glb) => {
@@ -161,8 +222,9 @@ class Scene {
directional_light.shadow.mapSize.width = 2048;
directional_light.shadow.mapSize.height = 2048;
directional_light.shadow.map = null;
directional_light.castShadow = true;
directional_light.castShadow = false;
directional_light.shadow.bias = 0.00004;
directional_light.userData["media_id"] = "DirectionalLight";
this.scene.add(directional_light);
this.scene.add(directional_light.target);
+130 -140
View File
@@ -11,17 +11,9 @@ import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "../Queue/QueueNames";
import { toEngineActions } from "../Queue/toEngineActions";
import { fromEngineActions } from "../Queue/fromEngineActions";
import { ClipType } from "~/pages/PageEnigma/models/track";
// Every object uuid / entity has a track.
export class TimelineCurrentState {
is_editable: boolean;
selected_object_ID: number;
constructor() {
this.is_editable = true; // can add clips to it
this.selected_object_ID = 0;
}
}
export class TimelineDataState {
timeline_items: ClipUI[];
scrubber_frame_position: number;
@@ -50,6 +42,7 @@ export class TimeLine {
animation_engine: AnimationEngine;
// lip sync engine
lipSync_engine: LipSyncEngine;
characters: {[key: string]: string};
scene: Scene;
@@ -63,8 +56,9 @@ export class TimeLine {
scene: Scene,
) {
this.timeline_items = [];
this.characters = {};
this.absolute_end = 60 * 12;
this.timeline_limit = this.absolute_end; // 5 seconds
this.timeline_limit = 0; // 5 seconds
this.is_playing = false;
this.scrubber_frame_position = 0; // in frames into the tl
@@ -83,11 +77,14 @@ export class TimeLine {
);
this.current_time = 0;
// TODO: How to move the timeline should put in update.
// setInterval(()=> {
// this.current_time +=1
// this.pushEvent(fromEngineActions.UPDATE_TIME, { currentTime: this.current_time })
// },50)
}
public async updateUI() {
Queue.publish({
queueName: QueueNames.FROM_ENGINE,
action: fromEngineActions.UPDATE_TIME_LINE,
data: this.timeline_items,
});
}
public async pushEvent(action: fromEngineActions, data: any) {
@@ -124,13 +121,52 @@ export class TimeLine {
await this.scrub(data);
break;
case toEngineActions.MUTE:
await this.mute(data);
await this.mute(data, false);
break;
case toEngineActions.UNMUTE:
await this.mute(data, true);
break;
case toEngineActions.ADD_CHARACTER:
await this.addCharacter(data);
break;
case toEngineActions.ADD_OBJECT:
await this.addObject(data);
break;
default:
console.log("Action Not Wired");
console.log("Action Not Wired", action);
}
}
public async addCharacter(data: any) {
let media_id = data.data['media_id'];
let name = data.data['name'];
let type = data.data['type'];
let obj = await this.scene.load_glb(media_id);
obj.userData['name'] = name;
obj.name = name;
let object_uuid = obj.uuid;
console.log("data.data", data.data)
this.characters[object_uuid] = type;
data.data['object_uuid'] = object_uuid;
Queue.publish({
queueName: QueueNames.FROM_ENGINE,
action: fromEngineActions.UPDATE_CHARACTER_ID,
data: data.data,
});
}
public async addObject(data: any) {
let media_id = data.data['media_id'];
let name = data.data['name'];
let obj = await this.scene.load_glb(media_id);
obj.userData['name'] = name;
obj.name = name;
}
public async addKeyFrame(data: any) {
// KeyFrame Object
// version: number;
@@ -144,12 +180,21 @@ export class TimeLine {
// selected?: boolean;
let data_json = data['data'];
let uuid = data_json['object_uuid'];
let keyframe_uuid = data_json['keyframe_uuid'];
let object_name = this.scene.get_object_by_uuid(uuid)?.name;
if (object_name == undefined) {
object_name = "undefined"
}
for (let key in this.characters) {
let element = this.characters[key];
if (key == uuid) {
data_json['group'] = element;
break;
}
}
let new_item = this.transform_engine.addFrame(uuid,
this.absolute_end,
data_json['position'],
@@ -160,27 +205,28 @@ export class TimeLine {
if (new_item) {
await this.addPlayableClip(new ClipUI(
data_json['version'],
"transform",
ClipType.TRANSFORM,
data_json['group'],
object_name,
"",
keyframe_uuid,
uuid,
object_name,
0,
this.absolute_end))
this.absolute_end,
data_json['offset']))
}
this.scene.createPoint(data_json['position'], data_json['keyframe_uuid']);
}
public deleteObject(object_uuid: string) {
this.timeline_items = this.timeline_items.filter(element => element.object_uuid !== object_uuid);
// Update react land here.
}
public async addClip(data: any) {
let object_uuid = data["data"]["object_uuid"];
// This is for testing for selected object instead of actual object given by timeline.
//if (object_uuid.length < 6) {
// object_uuid = this.scene.selected?.uuid; //data['data']['object_uuid'];
// if (object_uuid == undefined) {
// return;
// }
//}
const object_uuid = data["data"]["object_uuid"];
const media_id = data["data"]["media_id"];
const name = data["data"]["name"];
const group = data["data"]["group"];
@@ -188,6 +234,13 @@ export class TimeLine {
const type = data["data"]["type"];
const offset = data["data"]["offset"];
const end_offset = data["data"]["length"] + offset;
let object_name = this.scene.get_object_by_uuid(object_uuid)?.name;
const clip_uuid = data["data"]['clip_uuid'];
console.log(data);
if(object_name == undefined) {
object_name = "Undefined."
}
switch (type) {
case "animation":
@@ -203,16 +256,17 @@ export class TimeLine {
this.addPlayableClip(
new ClipUI(
version,
"lipsync",
"lipsync", // TODO potiential bug ..
group,
name,
media_id,
clip_uuid,
object_uuid,
object_name,
offset,
end_offset));
return;
} else {
console.log("Audio!")
this.audio_engine.loadClip(media_id);
}
break;
@@ -226,7 +280,9 @@ export class TimeLine {
group,
name,
media_id,
clip_uuid,
object_uuid,
object_name,
offset,
end_offset,
),
@@ -237,6 +293,7 @@ export class TimeLine {
let keyframe_uuid = data['data']["keyframe_uuid"];
let object_uuid = data['data']['object_uuid'];
this.transform_engine.clips[object_uuid].removeKeyframe(keyframe_uuid);
this.scene.deletePoint(keyframe_uuid);
}
public async updateKeyFrame(data: any) {
@@ -248,8 +305,6 @@ export class TimeLine {
public async updateClip(data: any) {
// only length and offset changes here.
console.log(data);
let object_uuid = data["data"]["object_uuid"];
const media_id = data["data"]["media_id"];
const offset = data["data"]["offset"];
@@ -262,19 +317,37 @@ export class TimeLine {
}
}
}
public async deleteClip(data: any) {
console.log(data);
let json_data = data['data'];
let object_uuid = data["data"]["object_uuid"];
let media_id = data["data"]["media_id"];
let type = data['type']
for (let i = 0; i < this.timeline_items.length; i++) {
const element = this.timeline_items[i];
if (element.media_id == media_id && element.object_uuid == object_uuid) {
this.timeline_items.splice(i, 1);
break;
}
}
}
public async scrubberUpdate(data: any) {
console.log(data);
}
public async mute(data: any) {
console.log(data);
public async mute(data: any, isMute: boolean) {
this.timeline_items.forEach(element => {
if (element.group == data.data['group']) {
element.should_play = isMute;
console.log(element);
}
});
}
public async addPlayableClip(clip: ClipUI): Promise<void> {
console.log("clip", clip)
this.timeline_items.push(clip);
}
@@ -304,6 +377,14 @@ export class TimeLine {
this.setScrubberPosition(data["data"]["currentTime"]);
}
public async stepFrame(frames: number) {
this.update();
this.scrubber_frame_position += frames;
this.pushEvent(fromEngineActions.UPDATE_TIME, {
currentTime: this.scrubber_frame_position,
});
}
public async scrubberDidStop(offset_frame: number) { }
// public streaming events into the timeline from
public async setScrubberPosition(offset: number) {
@@ -317,7 +398,6 @@ export class TimeLine {
}
private async resetScene() {
this.timeline_limit = this.getEndPoint();
for (const element of this.timeline_items) {
if (element.type == "transform") {
const object = this.scene.get_object_by_uuid(element.object_uuid);
@@ -327,7 +407,7 @@ export class TimeLine {
} else if (element.type == "audio") {
this.audio_engine.loadClip(element.media_id);
} else if (element.type == "animation") {
this.animation_engine.clips[element.object_uuid].stop();
this.animation_engine.clips[element.object_uuid + element.media_id].stop();
} else if (element.type == "lipsync") {
this.lipSync_engine.clips[element.object_uuid].reset();
} else {
@@ -348,9 +428,9 @@ export class TimeLine {
}
// called by the editor update loop on each frame
public async update() {
public async update(isRendering = false) {
//if (this.is_playing == false) return; // start and stop
this.timeline_limit = this.getEndPoint();
if (this.is_playing) {
this.current_time += 1;
this.pushEvent(fromEngineActions.UPDATE_TIME, {
@@ -369,13 +449,14 @@ export class TimeLine {
for (const element of this.timeline_items) {
if (
element.offset <= this.scrubber_frame_position &&
this.scrubber_frame_position <= element.length
this.scrubber_frame_position <= element.length &&
element.should_play == true
) {
// run async
// element.play()
// remove the element from the list
const object = this.scene.get_object_by_uuid(element.object_uuid);
if (element.type == "transform") {
if (element.type === ClipType.TRANSFORM) {
if (object && this.transform_engine.clips[element.object_uuid]) {
this.transform_engine.clips[element.object_uuid].step(
object,
@@ -390,18 +471,20 @@ export class TimeLine {
} else {
this.audio_engine.playClip(element.media_id);
}
} else if (element.type == "lipsync") {
} else if (element.type == "lipsync") { // we will remove this when we know which group it will come from character + audio == lip sync audio.
if (this.scrubber_frame_position + 1 >= element.length) {
this.lipSync_engine.clips[element.object_uuid].stop();
} else if (object) {
await this.lipSync_engine.clips[element.object_uuid].play(object);
this.lipSync_engine.clips[element.object_uuid].step();
}
} else if (element.type == "animation") {
} else if (element.type === ClipType.ANIMATION) {
if (object) {
await this.animation_engine.clips[object.uuid].play(object);
this.animation_engine.clips[object.uuid].step(
this.scrubber_frame_position / 60,
await this.animation_engine.clips[object.uuid + element.media_id].play(object);
let fps = 120;
if (isRendering) { fps = 60; }
this.animation_engine.clips[object.uuid + element.media_id].step(
this.scrubber_frame_position / fps, // Double FPS for best result.
);
}
} else {
@@ -428,96 +511,3 @@ export class TimeLine {
}
}
// How much timeline precision we have
//const percision = 100 // using a fake update loop mock
//
//class AudioEngineMock {
// async play(media_id:string) {
// console.log("Audio Playing {media_id}")
// }
//}
//
//// Visual verification tests.
//function CheckIfBasicAudioClipWorks() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI(1.0,'audio',1,0))
// timeline.play()
//}
//
//function CheckIfBasicTransformClipWorks() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI(1.0,'transform',2,0))
// timeline.play()
//}
//
//function CheckIfBasicClipWorks() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI(1.0,'animation',3,0))
// timeline.play()
//}
// function CheckIfBasicClipWorks3SecondsAfterTimelineStops() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip1", 0, 10000))
// timeline.play()
// }
// function CheckIfTwoClipsAtTheSameTimeWorks() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip1", 0, 1000))
// timeline.play()
// }
// function CheckIfTwoClipsOneAfterAnotherWorks() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip1", 0, 1000))
// timeline.addPlayableClip(new ClipUI("clip2", 0, 2000))
// timeline.play()
// }
// function CheckIfTimeLineStopBeforeClipPlays() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip3",0,1000))
// timeline.play()
// setInterval(async ()=> {
// timeline.stop()
// },1100)
// console.log("Stopped")
// }
// function CheckIfTimeLineStartAfterClipPlays() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip3",0,1000))
// timeline.play()
// setInterval(async ()=> {
// timeline.stop()
// },500)
// console.log("Stopped")
// }
// function CheckIfClipsPlayAllTogetherConcurrently() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip1",0,1000))
// timeline.addPlayableClip(new ClipUI("clip2",0,1000))
// timeline.addPlayableClip(new ClipUI("clip3",0,1000))
// timeline.addPlayableClip(new ClipUI("clip4",0,1000))
// timeline.play()
// }
// function CheckIfClipsPlayAllTogether() {
// const timeline = new TimeLine()
// timeline.addPlayableClip(new TrackClip("clip1",0,1000))
// timeline.addPlayableClip(new TrackClip("clip2",0,1000))
// timeline.addPlayableClip(new TrackClip("clip3",0,1000))
// timeline.addPlayableClip(new TrackClip("clip4",0,1000))
// timeline.play()
// }
//CheckIfBasicClipWorks()
//CheckIfTwoClipsAtTheSameTimeWorks()
// CheckIfTwoClipsOneAfterAnotherWorks()
// CheckIfTimeLineStopBeforeClipPlays()
// CheckIfTimeLineStartAfterClipPlays()
// CheckIfClipsPlayAllTogetherConcurrently()
// CheckIfClipsPlayAllTogether()
+27
View File
@@ -0,0 +1,27 @@
export interface MediaItem {
version: number;
type: AssetType;
media_id: string;
object_uuid?: string;
name: string;
length?: number;
thumbnail?: string;
isMine?: boolean;
isBookmarked?: boolean;
}
export enum AssetType {
CHARACTER = "character",
OBJECT = "object",
AUDIO = "audio",
ANIMATION = "animation",
CAMERA = "camera",
SHAPE = "shape",
STYLE = "style", // TODO Remove
}
export enum AssetFilterOption {
ALL,
MINE,
BOOKMARKED,
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./assets";
export * from "./message";
export * from "./sidePanel";
export * from "./track";
+28
View File
@@ -0,0 +1,28 @@
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models";
import { XYZ } from "~/pages/PageEnigma/datastructures/common";
export interface QueueClip {
version: number;
type: ClipType;
group: ClipGroup;
object_uuid?: string;
clip_uuid?: string;
media_id?: string;
name?: string;
offset?: number;
length?: number;
selected?: boolean;
}
export interface QueueKeyframe {
version: number;
keyframe_uuid?: string;
group: ClipGroup;
object_uuid: string;
object_name?: string;
offset?: number;
position: XYZ;
rotation: XYZ;
scale: XYZ;
selected?: boolean;
}
+9
View File
@@ -0,0 +1,9 @@
import { AssetType } from "~/pages/PageEnigma/models/assets";
import { ReactNode } from "react";
export interface Tab {
icon: string;
title: string;
value: AssetType;
component: ReactNode;
}
+1 -34
View File
@@ -1,26 +1,5 @@
import { XYZ } from "../datastructures/common";
export interface QueueClip {
version: number;
type: ClipType;
group: ClipGroup;
object_uuid?: string;
clip_uuid?: string;
media_id?: string;
name?: string;
offset?: number;
length?: number;
selected?: boolean;
}
export interface MediaClip {
version: number;
type: ClipType;
media_id: string;
name: string;
length: number;
}
export interface Clip {
version: number;
clip_uuid: string;
@@ -59,21 +38,9 @@ export interface Keyframe {
selected?: boolean;
}
export interface QueueKeyframe {
version: number;
keyframe_uuid?: string;
group: ClipGroup;
object_uuid: string;
object_name?: string;
offset?: number;
position: XYZ;
rotation: XYZ;
scale: XYZ;
selected?: boolean;
}
export interface CharacterGroup {
id: string;
name: string;
muted: boolean;
animationClips: Clip[];
positionKeyframes: Keyframe[];
@@ -0,0 +1,89 @@
import * as THREE from "three";
export interface ObjectJSON {
version: number;
position: {
x: number;
y: number;
z: number;
};
rotation: {
x: number;
y: number;
z: number;
};
scale: {
x: number;
y: number;
z: number;
};
object_name: string;
object_uuid: string;
object_user_data_name: string;
media_file_token: string;
}
export class StoryTellerProxy3DObject {
version: number;
position: THREE.Vector3;
rotation: THREE.Euler;
scale: THREE.Vector3;
object_uuid: string;
object_name: string;
object_user_data_name: string; // changable name
media_file_token: string;
constructor(
version: number,
media_file_token: string,
) {
this.version = version;
this.media_file_token = media_file_token;
this.position = new THREE.Vector3(0.0, 0.0, 0.0);
this.rotation = new THREE.Euler(0.0, 0.0, 0.0);
this.scale = new THREE.Vector3(1.0, 1.0, 1.0);
this.object_name = "";
this.object_user_data_name = "";
this.object_uuid = "";
}
public async initialize(object: THREE.Object3D) {
this.position = object.position;
this.rotation = object.rotation;
this.scale = object.scale;
this.object_name = object.name;
this.object_user_data_name = object.userData.name;
this.object_uuid = object.uuid;
}
public async toJSON(): Promise<ObjectJSON> {
const json:ObjectJSON = {
version: this.version,
position: {
x: this.position.x,
y: this.position.y,
z: this.position.z,
},
rotation: {
x: this.rotation.x,
y: this.rotation.y,
z: this.rotation.z,
},
scale: {
x: this.scale.x,
y: this.scale.y,
z: this.scale.z,
},
object_name: this.object_name,
object_uuid: this.object_uuid,
object_user_data_name: this.object_user_data_name,
media_file_token: this.media_file_token,
};
return json;
}
}
@@ -0,0 +1,90 @@
import * as THREE from "three";
import { GLTFLoader, GLTF } from "three/addons/loaders/GLTFLoader.js";
import { StoryTellerProxy3DObject, ObjectJSON } from "./storyteller_proxy_3d_object"
import Scene from "../js/scene";
interface LookUpDictionary {
[key: string]: StoryTellerProxy3DObject;
}
export class StoryTellerProxyScene {
sceneItemProxy: StoryTellerProxy3DObject[];
scene: Scene;
glbLoader: GLTFLoader;
lookUpDictionary: LookUpDictionary;
version: number;
constructor(version: number, scene: Scene) {
this.version = version
this.scene = scene;
this.glbLoader = new GLTFLoader();
this.lookUpDictionary = {}
this.sceneItemProxy = []
}
public async saveToScene(): Promise<any> {
let results: ObjectJSON[] = [];
if (this.scene.scene != null) {
for (const child of this.scene.scene.children) {
if (child.userData['media_id'] != undefined) {
if (this.lookUpDictionary[child.uuid] == null) {
this.lookUpDictionary[child.uuid] = new StoryTellerProxy3DObject(this.version, child.userData['media_id']);
}
let proxyObject3D: StoryTellerProxy3DObject = this.lookUpDictionary[child.uuid];
proxyObject3D.position.copy(child.position);
proxyObject3D.rotation.copy(child.rotation);
proxyObject3D.scale.copy(child.scale);
proxyObject3D.object_user_data_name = child.userData.name;
proxyObject3D.object_name = child.name;
proxyObject3D.object_uuid = child.uuid;
const json_data = await proxyObject3D.toJSON();
results.push(json_data);
}
}
} else {
console.log("Scene doesn't exist needs to be assigned");
}
return results;
}
public async loadFromSceneJson(scene_json: ObjectJSON[]) {
if (scene_json != null && this.scene != null) {
while (this.scene.scene.children.length > 0) {
this.scene.scene.remove(this.scene.scene.children[0]);
}
for (const json_object of scene_json) {
let token: string = json_object.media_file_token;
let obj;
switch (token) {
case 'Parim':
let prim_uuid = this.scene.instantiate(json_object.object_name);
obj = this.scene.get_object_by_uuid(prim_uuid);
break;
case 'DirectionalLight':
obj = this.scene._create_base_lighting();
break;
default:
if(token.includes('m_')){
obj = await this.scene.load_glb(token);
} else if(token.includes('Point::')){
let keyframe_uuid = token.replace("Point::", "");
obj = this.scene.createPoint(new THREE.Vector3(0,0,0), keyframe_uuid);
}
break;
}
if (obj) {
obj.position.copy(json_object.position);
obj.rotation.copy(new THREE.Euler(json_object.rotation.x, json_object.rotation.y, json_object.rotation.z));
obj.scale.copy(json_object.scale);
obj.name = json_object.object_name;
obj.userData.name = json_object.object_user_data_name;
obj.uuid = json_object.object_uuid;
obj.userData['media_id'] = json_object.media_file_token;
}
}
this.scene._createGrid();
}
}
};
@@ -0,0 +1,137 @@
import { AnimationClip } from "../datastructures/clips/animation_clip";
import { AudioClip } from "../datastructures/clips/audio_clip";
import { ClipUI } from "../datastructures/clips/clip_ui";
import { LipSyncClip } from "../datastructures/clips/lipsync_clip";
import { TransformClip } from "../datastructures/clips/transform_clip";
import AnimationEngine from "../js/animation_engine";
import AudioEngine from "../js/audio_engine";
import LipSyncEngine from "../js/lip_sync_engine";
import { TimeLine } from "../js/timeline";
import TransformEngine from "../js/transform_engine";
export class StoryTellerProxyTimeline {
timeline: TimeLine
transform_engine: TransformEngine
animation_engine: AnimationEngine
audio_engine: AudioEngine
lipsync_engine: LipSyncEngine
constructor(version: number, timeline: TimeLine, transform_engine: TransformEngine, animation_engine: AnimationEngine, audio_engine: AudioEngine, lipsync_engine: LipSyncEngine) {
this.timeline = timeline;
this.transform_engine = transform_engine;
this.animation_engine = animation_engine;
this.audio_engine = audio_engine;
this.lipsync_engine = lipsync_engine;
}
private async getItems(items: any[]): Promise<any[]> {
let timeline_items_data = [];
for (let index = 0; index < items.length; index++) {
const element = items[index];
timeline_items_data.push(element.toJSON());
};
return timeline_items_data;
}
private async getItemsDict(items: { [key: string]: any }): Promise<{ [key: string]: any }> {
let timeline_items_data: { [key: string]: any } = {};
for (const key in items) {
if (items.hasOwnProperty(key)) {
const element = items[key];
timeline_items_data[key] = element.toJSON();
}
}
return timeline_items_data;
}
public async saveToJson(): Promise<any> {
let timeline_json = {
timeline: await this.getItems(this.timeline.timeline_items),
transform: await this.getItemsDict(this.transform_engine.clips),
animation: await this.getItemsDict(this.animation_engine.clips),
audio: await this.getItemsDict(this.audio_engine.clips),
lipsync: await this.getItemsDict(this.lipsync_engine.clips),
}
console.log(timeline_json)
return timeline_json;
}
private async loadTimelineClips(timeline_clips: any[]): Promise<any[]> {
let new_clips = [];
if (timeline_clips) {
for (let index = 0; index < timeline_clips.length; index++) {
const element = timeline_clips[index];
let clip = new ClipUI(
element.version, element.type,
element.group, element.name,
element.media_id, element.clip_uuid,
element.object_uuid,
element.object_name,
element.start_offset, element.ending_offset,
element.keyframe_offset);
new_clips.push(clip);
}
}
return new_clips;
}
private async loadTransformClips(items: { [key: string]: any }): Promise<{ [key: string]: any }> {
let timeline_items_data: { [key: string]: any } = {};
for (const key in items) {
if (items.hasOwnProperty(key)) {
const element = items[key];
timeline_items_data[key] = new TransformClip(element.version, element.object_uuid, element.length, element.media_id);
timeline_items_data[key].keyframes = element.keyframes;
}
}
return timeline_items_data;
}
private async loadAudioClips(items: { [key: string]: any }): Promise<{ [key: string]: any }> {
let timeline_items_data: { [key: string]: any } = {};
for (const key in items) {
if (items.hasOwnProperty(key)) {
const element = items[key];
timeline_items_data[key] = new AudioClip(element.version, element.media_id, element.volume);
}
}
return timeline_items_data;
}
private async loadAnimationClips(items: { [key: string]: any }): Promise<{ [key: string]: any }> {
let timeline_items_data: { [key: string]: any } = {};
for (const key in items) {
if (items.hasOwnProperty(key)) {
const element = items[key];
timeline_items_data[key] = new AnimationClip(element.version, element.media_id, "remote", element.object_uuid, element.speed, element.length, element.clip_name);
}
}
return timeline_items_data;
}
private async loadLipsyncClips(items: { [key: string]: any }): Promise<{ [key: string]: any }> {
let timeline_items_data: { [key: string]: any } = {};
for (const key in items) {
if (items.hasOwnProperty(key)) {
const element = items[key];
timeline_items_data[key] = new LipSyncClip(element.version, element.media_id, element.volume);
}
}
return timeline_items_data;
}
public async loadFromJson(timeline: any) {
this.timeline.timeline_items = await this.loadTimelineClips(timeline['timeline']);
this.transform_engine.clips = await this.loadTransformClips(timeline['transform']);
this.audio_engine.clips = await this.loadAudioClips(timeline['audio']);
this.lipsync_engine.clips = await this.loadLipsyncClips(timeline['lipsync']);
this.animation_engine.clips = await this.loadAnimationClips(timeline['animation']);
console.log(this.timeline.timeline_items)
this.timeline.updateUI()
}
}
@@ -1,5 +1,5 @@
import { Simple3DVector } from "../../datastructures/common";
import { ClipGroup } from "../../models/track";
import { AssetType, ClipGroup } from "../../models";
export enum VIEW_MODES {
EDITOR = "editor",
SIDE_BY_SIDE = "side-by-side",
@@ -1,5 +1,5 @@
import { State, VIEW_MODES } from "./types";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
import { ClipGroup } from "~/pages/PageEnigma/models";
export const initialStateValues: State = {
timelineHeight: 260,
+26 -12
View File
@@ -3,13 +3,14 @@ import {
Clip,
ClipGroup,
ClipType,
MediaClip,
} from "~/pages/PageEnigma/models/track";
MediaItem,
} from "~/pages/PageEnigma/models";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
import { signal } from "@preact/signals-core";
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
export const audioGroup = signal<AudioGroup>({
id: "AG-1",
@@ -49,31 +50,27 @@ export function updateAudio({
}
export function addGlobalAudio({
clipId,
dragItem,
audioId,
audioClips,
offset,
}: {
clipId: string;
dragItem: MediaItem;
audioId: string;
audioClips: MediaClip[];
offset: number;
}) {
if (audioGroup.value.id !== audioId) {
return;
}
const clip = audioClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
version: 1,
media_id: dragItem.media_id,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
offset,
length: dragItem.length,
name: dragItem.name,
clip_uuid,
} as Clip;
@@ -144,3 +141,20 @@ export function deleteAudioClip(clip: Clip) {
],
};
}
export function loadAudioData(item: ClipUI) {
const existingAudio = audioGroup.value;
const newItem = {
version: item.version,
clip_uuid: item.clip_uuid,
type: item.type,
group: item.group,
object_uuid: item.object_uuid,
media_id: item.media_id,
name: item.name,
offset: item.offset,
length: item.length,
} as Clip;
existingAudio.clips.push(newItem);
existingAudio.clips.sort((clipA, clipB) => clipA.offset - clipB.offset);
}
+33 -3
View File
@@ -2,17 +2,29 @@ import {
CameraGroup,
Keyframe,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
} from "~/pages/PageEnigma/models";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
import { signal } from "@preact/signals-core";
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
// import { toast } from "react-hot-toast";
export const cameraGroup = signal<CameraGroup>({ id: "CG1", keyframes: [] });
export function updateCamera({ id, offset }: { id: string; offset: number }) {
const oldCameraGroup = cameraGroup.value;
const existingKeyframe = oldCameraGroup.keyframes.some((row) => {
return row.offset === offset && row.keyframe_uuid !== id;
});
if (existingKeyframe) {
//toast.error("There can only be one keyframe at this offset.");
return;
}
const newKeyframes = [...oldCameraGroup.keyframes];
const keyframe = newKeyframes.find((row) => row.keyframe_uuid === id);
if (!keyframe) {
@@ -33,7 +45,11 @@ export function updateCamera({ id, offset }: { id: string; offset: number }) {
}
export function addCameraKeyframe(keyframe: QueueKeyframe, offset: number) {
console.log("cam", keyframe);
const oldCameraGroup = cameraGroup.value;
if (oldCameraGroup.keyframes.some((row) => row.offset === offset)) {
//toast.error("There can only be one keyframe at this offset.");
return;
}
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
@@ -46,7 +62,6 @@ export function addCameraKeyframe(keyframe: QueueKeyframe, offset: number) {
selected: false,
} as Keyframe;
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [...oldCameraGroup.keyframes, newKeyframe].sort(
@@ -98,3 +113,18 @@ export function deleteCameraKeyframe(deleteKeyframe: Keyframe) {
],
};
}
export function loadCameraData(item: ClipUI) {
const existingCamera = cameraGroup.value;
const newKeyframe = {
version: item.version,
keyframe_uuid: item.clip_uuid,
group: item.group,
object_uuid: item.object_uuid,
offset: item.keyframe_offset,
} as Keyframe;
existingCamera.keyframes.push(newKeyframe);
existingCamera.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
}
@@ -0,0 +1,28 @@
import { CharacterGroup, MediaItem } from "~/pages/PageEnigma/models";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { characterGroups } from "~/pages/PageEnigma/store";
export function addCharacter(character: MediaItem) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CHARACTER,
data: character,
});
}
export function addNewCharacter(data: MediaItem) {
const newCharacter = {
id: data.object_uuid,
name: data.name,
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
} as CharacterGroup;
characterGroups.value = [...characterGroups.value, newCharacter].sort(
(charA, charB) => (charA.id < charB.id ? -1 : 1),
);
}
@@ -4,136 +4,36 @@ import {
ClipGroup,
ClipType,
Keyframe,
MediaClip,
MediaItem,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
} from "~/pages/PageEnigma/models";
import { signal } from "@preact/signals-core";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
export const characterGroups = signal<CharacterGroup[]>([
{
id: "CH1",
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
},
]);
export function updateCharacters({
type,
id,
offset,
length,
}: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) {
const oldCharacterGroups = characterGroups.value;
if (type === "animations") {
characterGroups.value = oldCharacterGroups.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
animationClips: newAnimationClips,
};
});
}
if (type === "positions") {
characterGroups.value = oldCharacterGroups.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
(row) => row.keyframe_uuid === id,
);
if (keyframeIndex === -1) {
return { ...character };
}
const keyframe = newPositionKeyframes[keyframeIndex];
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: keyframe,
});
return {
...character,
positionKeyframes: newPositionKeyframes,
};
});
}
if (type === "lipSync") {
characterGroups.value = oldCharacterGroups.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
}
}
export const characterGroups = signal<CharacterGroup[]>([]);
export function addCharacterAnimation({
clipId,
dragItem,
characterId,
animationClips,
offset,
}: {
clipId: string;
dragItem: MediaItem;
characterId: string;
animationClips: MediaClip[];
offset: number;
}) {
const clip = animationClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
version: 1,
media_id: dragItem.media_id,
group: ClipGroup.CHARACTER,
type: ClipType.ANIMATION,
offset,
length: dragItem.length,
clip_uuid,
name: dragItem.name,
object_uuid: characterId,
} as Clip;
@@ -158,29 +58,25 @@ export function addCharacterAnimation({
}
export function addCharacterAudio({
clipId,
dragItem,
characterId,
audioClips,
offset,
}: {
clipId: string;
dragItem: MediaItem;
characterId: string;
audioClips: MediaClip[];
offset: number;
}) {
const clip = audioClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
version: 1,
media_id: dragItem.media_id,
group: ClipGroup.CHARACTER,
type: ClipType.AUDIO,
offset,
length: dragItem.length,
clip_uuid,
object_uuid: characterId,
name: dragItem.name,
} as Clip;
const oldCharacterGroups = characterGroups.value;
@@ -188,6 +84,13 @@ export function addCharacterAudio({
if (character.id !== characterId) {
return { ...character };
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
@@ -195,15 +98,21 @@ export function addCharacterAudio({
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function addCharacterKeyframe(keyframe: QueueKeyframe, offset: number) {
const oldCharacterGroups = characterGroups.value;
if (
oldCharacterGroups.some((characterGroup) => {
return characterGroup.positionKeyframes.some(
(row) => row.offset === offset,
);
})
) {
return;
}
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
@@ -216,7 +125,6 @@ export function addCharacterKeyframe(keyframe: QueueKeyframe, offset: number) {
selected: false,
} as Keyframe;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== keyframe.object_uuid) {
return { ...character };
@@ -0,0 +1,4 @@
export * from "./addCharacter";
export * from "./characterGroups";
export * from "./loadCharacterData";
export * from "./updateCharacters";
@@ -0,0 +1,89 @@
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
import {
CharacterGroup,
Clip,
ClipType,
Keyframe,
} from "~/pages/PageEnigma/models";
import { characterGroups } from "~/pages/PageEnigma/store";
function getAddCharacter(item: ClipUI) {
const existingCharacter = characterGroups.value.find(
(character) => character.id === item.object_uuid,
);
if (existingCharacter) {
return existingCharacter;
}
const newCharacter = {
id: item.object_uuid,
name: item.object_name,
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
} as CharacterGroup;
characterGroups.value = [
...characterGroups.value.filter(
(character) => character.id !== item.object_uuid,
),
newCharacter,
].sort((charA, charB) => (charA.id < charB.id ? -1 : 1));
return characterGroups.value.find(
(character) => character.id === item.object_uuid,
) as CharacterGroup;
}
export function loadCharacterData(item: ClipUI) {
const existingCharacter = getAddCharacter(item);
if (item.type === ClipType.ANIMATION) {
const newItem = {
version: item.version,
clip_uuid: item.clip_uuid,
type: item.type,
group: item.group,
object_uuid: item.object_uuid,
media_id: item.media_id,
name: item.name,
offset: item.offset,
length: item.length,
} as Clip;
existingCharacter.animationClips.push(newItem);
existingCharacter.animationClips.sort(
(clipA, clipB) => clipA.offset - clipB.offset,
);
}
if (item.type === ClipType.TRANSFORM) {
const newKeyframe = {
version: item.version,
keyframe_uuid: item.clip_uuid,
group: item.group,
object_uuid: item.object_uuid,
offset: item.keyframe_offset,
} as Keyframe;
existingCharacter.positionKeyframes.push(newKeyframe);
existingCharacter.positionKeyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
}
if (item.type === ClipType.AUDIO) {
const newItem = {
version: item.version,
clip_uuid: item.clip_uuid,
type: item.type,
group: item.group,
object_uuid: item.object_uuid,
media_id: item.media_id,
name: item.name,
offset: item.offset,
length: item.length,
} as Clip;
existingCharacter.lipSyncClips.push(newItem);
existingCharacter.lipSyncClips.sort(
(clipA, clipB) => clipA.offset - clipB.offset,
);
}
}
@@ -0,0 +1,94 @@
import { ClipType } from "~/pages/PageEnigma/models";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { characterGroups } from "~/pages/PageEnigma/store";
export function updateCharacters({
type,
id,
offset,
length,
}: {
type: ClipType;
id: string;
length?: number;
offset: number;
}) {
const oldCharacterGroups = characterGroups.value;
if (type === ClipType.ANIMATION) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length!;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
animationClips: newAnimationClips,
};
});
}
if (type === ClipType.TRANSFORM) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
(row) => row.keyframe_uuid === id,
);
if (keyframeIndex === -1) {
return { ...character };
}
const keyframe = newPositionKeyframes[keyframeIndex];
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: keyframe,
});
return {
...character,
positionKeyframes: newPositionKeyframes,
};
});
}
if (type === ClipType.AUDIO) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length!;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
}
}
+5 -3
View File
@@ -1,12 +1,14 @@
import { signal } from "@preact/signals-core";
import { MediaItem } from "~/pages/PageEnigma/models";
export const dragType = signal<"animations" | "audio" | null>(null);
export const dragId = signal<string | null>(null);
export const dragItem = signal<MediaItem | null>(null);
export const canDrop = signal(false);
export const overTimeline = signal(false);
export const dropId = signal("");
export const dropOffset = signal(0);
export const clipLength = signal(0);
export const dndTimelineHeight = signal(-1);
export const initPosition = signal<{ initX: number; initY: number }>({
initX: 0,
initY: 0,
+2
View File
@@ -2,7 +2,9 @@ export * from "./audioGroup";
export * from "./cameraGroup";
export * from "./characterGroups";
export * from "./dragAndDrop";
export * from "./media";
export * from "./objectGroup";
export * from "./selectedItem";
export * from "./sidePanel";
export * from "./sizing";
export * from "./timing";
+150
View File
@@ -0,0 +1,150 @@
import { signal } from "@preact/signals-core";
import { AssetType, MediaItem } from "~/pages/PageEnigma/models";
import * as uuid from "uuid";
export const animationItems = signal<MediaItem[]>([
{
version: 1,
media_id: "m_yzreg1494d08aepezj74607d2ascep",
type: AssetType.ANIMATION,
length: 100,
name: "Sit",
thumbnail: "resources/characters/img04.png",
isMine: true,
},
{
version: 1,
media_id: "m_qepzjytd4ahd5askdt2ngrwzj2aa3f",
type: AssetType.ANIMATION,
length: 100,
name: "Idle",
thumbnail: "resources/characters/img07.png",
isBookmarked: true,
},
{
version: 1,
media_id: "m_s7d4ems68sb2vqj4pdje1rc15q3ycp",
type: AssetType.ANIMATION,
length: 100,
name: "Walk",
thumbnail: "resources/characters/img07.png",
},
{
version: 1,
media_id: "m_v06h821hj4dvst0mzm575jdka31f41",
type: AssetType.ANIMATION,
length: 100,
name: "Punch",
thumbnail: "resources/characters/img08.png",
},
{
version: 1,
media_id: "m_q92keqa05gds69zyd8mkep7z9338t8",
type: AssetType.ANIMATION,
length: 100,
name: "Jump",
thumbnail: "resources/characters/img06.png",
},
{
version: 1,
media_id: "m_0qmgsyxh5ng9c9ac3m0snzdqqs5q60",
type: AssetType.ANIMATION,
length: 100,
name: "Dance",
thumbnail: "resources/characters/img11.png",
},
]);
export const audioItems = signal<MediaItem[]>([
{
version: 1,
media_id: "m_403phjvjkbbaxxbz8y7r6qjay07mfd",
type: AssetType.AUDIO,
length: 25,
name: "Talk (Demo Sounds)",
thumbnail: "resources/placeholders/audio_placeholder.png"
},
{
version: 1,
media_id: "m_w5nn3kjh1fbkmjrdac5b2qaba0pmyt",
type: AssetType.AUDIO,
length: 25,
name: "NCS Song",
thumbnail: "resources/placeholders/audio_placeholder.png"
}
]);
export const cameraItems = signal<MediaItem[]>([
{
version: 1,
media_id: uuid.v4(),
type: AssetType.CAMERA,
name: "Portrait Zoom Out",
thumbnail: "resources/placeholders/placeholder.png",
},
{
version: 1,
media_id: uuid.v4(),
type: AssetType.CAMERA,
name: "Pan Left and Right",
thumbnail: "resources/placeholders/placeholder.png",
},
]);
export const characterItems = signal<MediaItem[]>([
{
version: 1,
media_id: "m_fmxy8wjnep1hdaz7qdg4n7y15d2bsp",
type: AssetType.CHARACTER,
name: "Shrek",
thumbnail: "resources/placeholders/placeholder.png",
},
{
version: 1,
media_id: "m_9f3d3z94kk6m25zywyz6an3p43fjtw",
type: AssetType.CHARACTER,
name: "Stick Man",
thumbnail: "resources/placeholders/placeholder.png",
},
{
version: 1,
media_id: "m_r7w1tmkx2jg8nznr3hyzj4k6zhfh7d ",
type: AssetType.CHARACTER,
name: "Female Doll",
thumbnail: "resources/characters/img03.png",
},
{
version: 1,
media_id: "m_9sqg0evpr23587jnr8z3zsvav1x077 ",
type: AssetType.CHARACTER,
name: "Male Doll",
thumbnail: "resources/characters/img03.png",
},
{
version: 1,
media_id: "m_4wva09qznapzk5rcvbxy671d1qx2pr ",
type: AssetType.CHARACTER,
name: "Story Girl",
thumbnail: "resources/characters/img13.png",
},
]);
export const objectItems = signal<MediaItem[]>([
{
version: 1,
media_id: "m_0xfrmekc56satjxn66wt6c9dkw7dxe",
type: AssetType.OBJECT,
name: "Pikachu Statue",
thumbnail: "resources/placeholders/placeholder.png",
},
]);
// In the future these will have shape id's
export const shapeItems = signal<MediaItem[]>([
{
version: 1,
media_id: "",
type: AssetType.SHAPE,
name: "Cube",
thumbnail: "resources/placeholders/placeholder.png",
},
]);
@@ -0,0 +1,3 @@
export * from "./loadObjectData";
export * from "./objectGroup";
export * from "./updateObjectGroup";
@@ -0,0 +1,48 @@
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
import { Keyframe, ObjectTrack } from "~/pages/PageEnigma/models";
import { objectGroup } from "~/pages/PageEnigma/store";
function getAddObject(item: ClipUI) {
const existingObject = objectGroup.value.objects.find(
(obj) => obj.object_uuid === item.object_uuid,
);
if (existingObject) {
return existingObject;
}
const newObject = {
object_uuid: item.object_uuid,
name: item.object_name,
keyframes: [] as Keyframe[],
} as ObjectTrack;
objectGroup.value = {
id: "OB1",
objects: [
...objectGroup.value.objects.filter(
(obj) => obj.object_uuid !== item.object_uuid,
),
newObject,
].sort((objA, objB) => (objA.object_uuid < objB.object_uuid ? -1 : 1)),
};
return objectGroup.value.objects.find(
(obj) => obj.object_uuid === item.object_uuid,
) as ObjectTrack;
}
export function loadObjectData(item: ClipUI) {
const existingObject = getAddObject(item);
const newKeyframe = {
version: item.version,
keyframe_uuid: item.clip_uuid,
group: item.group,
object_uuid: item.object_uuid,
offset: item.keyframe_offset,
} as Keyframe;
existingObject.keyframes.push(newKeyframe);
existingObject.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
}
@@ -1,57 +1,34 @@
import { signal } from "@preact/signals-core";
import {
Keyframe,
MediaItem,
ObjectGroup,
ObjectTrack,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
} from "~/pages/PageEnigma/models";
import * as uuid from "uuid";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { ClipUI } from "~/pages/PageEnigma/datastructures/clips/clip_ui";
// import { toast } from "react-hot-toast";
export const objectGroup = signal<ObjectGroup>({
id: "OB1",
objects: [],
});
export function updateObject({ id, offset }: { id: string; offset: number }) {
const oldObject = objectGroup.value;
objectGroup.value = {
id: oldObject.id,
objects: oldObject.objects.map((object) => ({
object_uuid: object.object_uuid,
name: object.name,
keyframes: object.keyframes.map((keyFrame) => {
if (keyFrame.keyframe_uuid !== id) {
return {
...keyFrame,
};
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: {
...keyFrame,
offset,
},
});
return {
...keyFrame,
offset,
};
}),
})),
};
}
export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
const oldObjectGroup = objectGroup.value;
const obj = oldObjectGroup.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
if (obj && obj.keyframes.some((row) => row.offset === offset)) {
//toast.error("There can only be one keyframe at this offset.");
return;
}
const newObject = obj ?? {
object_uuid: keyframe.object_uuid,
name: keyframe.object_name ?? "unknown",
@@ -69,7 +46,7 @@ export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
selected: false,
} as Keyframe;
newObject.keyframes.push(newKeyframe);
console.log(newObject);
newObject.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
@@ -78,7 +55,7 @@ export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
(row) => row.object_uuid !== obj?.object_uuid,
),
newObject,
].sort((objA, objB) => (objA.object_uuid < objB.object_uuid ? -1 : 1)),
@@ -137,3 +114,11 @@ export function deleteObjectKeyframe(keyframe: Keyframe) {
],
};
}
export function addObject(object: MediaItem) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_OBJECT,
data: object,
});
}
@@ -0,0 +1,59 @@
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { objectGroup } from "~/pages/PageEnigma/store";
export function updateObject({
id,
offset,
}: {
id: string;
offset: number;
}): void {
const oldObjectGroup = objectGroup.value;
const obj = oldObjectGroup.objects.find((objectTrack) =>
objectTrack.keyframes.some((row) => row.keyframe_uuid === id),
);
if (!obj) {
return;
}
const existingKeyframe = obj.keyframes.some((row) => {
return row.offset === offset && row.keyframe_uuid !== id;
});
if (existingKeyframe) {
//toast.error("There can only be one keyframe at this offset.");
return;
}
objectGroup.value = {
id: oldObjectGroup.id,
objects: oldObjectGroup.objects.map((object) => ({
object_uuid: object.object_uuid,
name: object.name,
keyframes: object.keyframes.map((keyframe) => {
if (keyframe.keyframe_uuid !== id) {
return {
...keyframe,
};
}
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: {
...keyframe,
offset,
},
});
return {
...keyframe,
offset,
};
}),
})),
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { signal } from "@preact/signals-core";
import { Clip, Keyframe } from "~/pages/PageEnigma/models/track";
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
export const selectedItem = signal<Clip | Keyframe | null>(null);
+21
View File
@@ -0,0 +1,21 @@
import { computed, signal } from "@preact/signals-core";
import { AssetFilterOption, Tab } from "~/pages/PageEnigma/models";
import { pageHeight, timelineHeight } from "~/pages/PageEnigma/store/sizing";
export const sidePanelVisible = signal(true);
export const dndSidePanelWidth = signal(-1);
export const selectedTab = signal<Tab | null>(null);
export const lastSelectedTab = signal<Tab | null>(null);
export const animationFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const audioFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const cameraFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const characterFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const objectFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const shapeFilter = signal<AssetFilterOption>(AssetFilterOption.ALL);
export const sidePanelHeight = computed(() => {
return pageHeight.value - timelineHeight.value - 68; // header
});
+11 -3
View File
@@ -2,12 +2,13 @@ import { signal, computed } from "@preact/signals-core";
import { characterGroups } from "~/pages/PageEnigma/store/characterGroups";
import { objectGroup } from "~/pages/PageEnigma/store/objectGroup";
export const pageHeight = signal(0);
export const pageWidth = signal(0);
// timeline
export const scale = signal(1);
export const filmLength = signal(12);
export const timelineHeight = signal(0);
export const fullWidth = computed(() => {
return filmLength.value * 60 * 4 * scale.value;
});
export const fullHeight = computed(() => {
return (
@@ -17,3 +18,10 @@ export const fullHeight = computed(() => {
96
);
});
export const fullWidth = computed(() => {
return filmLength.value * 60 * 4 * scale.value;
});
// side panel
export const sidePanelWidth = signal(0);
+2
View File
@@ -1,3 +1,5 @@
import { signal } from "@preact/signals-core";
export const currentTime = signal(0);
export const currentScroll = signal(0);
+27
View File
@@ -0,0 +1,27 @@
import { json } from "@remix-run/router";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { faChevronLeft } from "@fortawesome/pro-solid-svg-icons";
import { P, H1, ButtonLink } from "~/components";
export const loader = () => {
return json(null, { status: 404 });
};
const Page404 = withProtectionRoute(()=>{
return(
<div
className="fixed w-full overflow-scroll"
style={{height: "calc(100% - 72px)"}}
>
<div
className='bg-ui-panel w-10/12 max-w-7xl h-1/2 mx-auto my-24 rounded-lg p-6 flex flex-col items-center justify-center gap-8 text-center'
>
<P>Oops, seems like this is not a page that exists!</P>
<H1>Server Error: 404</H1>
<ButtonLink to="/" icon={faChevronLeft}>Back to Dashboard</ButtonLink>
</div>
</div>
)
});
export default Page404;
+4 -4
View File
@@ -1,5 +1,5 @@
import { PageEnigma } from "../pages/PageEnigma";
import { withProtectionRoute } from "~/modules/withProtectedRoute";
import { PageEnigma } from "~/pages/PageEnigma";
export default function IdealEnigma() {
return <PageEnigma />;
}
const IdealEnigma = withProtectionRoute(()=><PageEnigma />);
export default IdealEnigma;
+6 -14
View File
@@ -1,29 +1,19 @@
import { useContext, useEffect, useState, useRef } from "react";
import { useNavigate } from "@remix-run/react";
import { useCookies } from 'react-cookie';
import { useNavigate, useSearchParams } from "@remix-run/react";
import { faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
import { AuthenticationContext } from "~/contexts/Authentication";
import {
Button,
H1,
Input,
Link,
P,
} from '~/components';
import { Button,H1,Input,Link,P } from '~/components';
import { LoadingDots } from "~/components";
export default function LoginScreen() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [showLoader, setShowLoader] = useState<string|undefined>(undefined);
const formRef = useRef<HTMLFormElement | null>(null);
const [authCookies, setAuthCookie, removeAuthCookie] = useCookies(['userInfo']);
const {authState, loginAndGetUserInfo} = useContext(AuthenticationContext);
const handleOnSumbit = (ev: React.FormEvent<HTMLFormElement>)=>{
ev.preventDefault();
if(formRef.current){
@@ -43,8 +33,9 @@ export default function LoginScreen() {
}// end handleOnSubmit
useEffect(()=>{
const redirectPath = searchParams.get('redirect');
if(authState && authState.isLoggedIn)
navigate("/");
navigate(redirectPath ? redirectPath:'/');
},[authState]);
return (
@@ -64,6 +55,7 @@ export default function LoginScreen() {
icon={faUser}
name="usernameOrEmail"
placeholder="Username or Email"
autoComplete="username"
required
/>
<br />
+14
View File
@@ -1,3 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.filter-tab {
@apply text-nowrap rounded-lg border-2 border-assets-background bg-assets-background px-3 py-2 text-sm font-medium transition-all duration-150 hover:border-assets-background/0 hover:bg-assets-background/80;
}
.filter-tab.active {
@apply border-2 border-brand-primary hover:border-brand-primary hover:bg-assets-background;
}
.filter-tab.disabled {
@apply hover:bg-assets-background disabled:text-white/50;
}
}
+4
View File
@@ -0,0 +1,4 @@
export const kebabCase = (str:string) => str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/[\s_]+/g, '-')
.toLowerCase();
+44 -18
View File
@@ -30,6 +30,7 @@
"react-cookie": "^7.1.4",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"webm-writer": "^1.0.0"
@@ -1494,14 +1495,13 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
"dev": true,
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
@@ -1511,31 +1511,36 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"dev": true,
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -2771,7 +2776,6 @@
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -11975,6 +11979,28 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/terser": {
"version": "5.30.3",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
"integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+2
View File
@@ -7,6 +7,7 @@
"build": "remix vite:build",
"staging-build": "remix vite:build",
"dev": "remix vite:dev",
"staging-build": "remix vite:build",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "netlify serve",
"typecheck": "tsc",
@@ -37,6 +38,7 @@
"react-cookie": "^7.1.4",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"webm-writer": "^1.0.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more