mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
122 files changed
+3055
-1687
No files matched your search
@@ -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";
|
||||
|
||||
@@ -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" : "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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,
|
||||
}}>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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</>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+13
-15
@@ -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>
|
||||
))}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
};
|
||||
};
|
||||
+11
-4
@@ -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;
|
||||
}
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
+14
-10
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./assets";
|
||||
export * from "./message";
|
||||
export * from "./sidePanel";
|
||||
export * from "./track";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
+36
-128
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,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";
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
+20
-35
@@ -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,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);
|
||||
@@ -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
|
||||
});
|
||||
@@ -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);
|
||||
@@ -1,3 +1,5 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
|
||||
export const currentTime = signal(0);
|
||||
|
||||
export const currentScroll = signal(0);
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const kebabCase = (str:string) => str
|
||||
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.toLowerCase();
|
||||
Generated
+44
-18
@@ -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",
|
||||
|
||||
@@ -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
Reference in new issue
Block a user