Merge branch 'test' into feature/login

This commit is contained in:
wil committed 2024-04-01 19:11:01 -04:00
commit 11bf308bca
50 files changed
+2403 -2043

No files matched your search

+1 -2
View File
@@ -1,8 +1,7 @@
import { twMerge } from "tailwind-merge";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Label } from ".";
import { Label } from "./Typography";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
+21 -15
View File
@@ -1,32 +1,38 @@
import { twMerge } from "tailwind-merge";
import { H4 } from ".";
import { ArtStyle } from "~/pages/PageEnigma/js/api_manager";
interface ItemPickerProps extends React.ImgHTMLAttributes<HTMLImageElement> {
label: string
label: string;
type: ArtStyle;
selected: boolean;
onSelected : (val:string)=>void;
onSelected: (picked: ArtStyle) => void;
}
export const ItemPicker = ({
label,
type,
selected = false,
onSelected,
...imgProps
}:ItemPickerProps) => {
const handleSelected = ()=>{
onSelected(label);
}: ItemPickerProps) => {
const handleSelected = () => {
onSelected(type);
};
return (
<div
className={twMerge("relative border-2 rounded-lg overflow-hidden cursor-pointer transition-colors ease-in-out", selected ? "border-brand-primary" : "border-ui-border")}
<button
className={twMerge(
"relative cursor-pointer overflow-hidden rounded-lg border-2 transition-colors ease-in-out",
selected ? "border-brand-primary" : "border-ui-border",
)}
onClick={handleSelected}
>
<img className="aspect-square min-w-20" {...imgProps} />
<div className="bg-gradient-to-t from-gray-700 absolute top-0 left-0 w-full h-full"/>
<H4 className="absolute bottom-0 left-2 drop-shadow-md">{label}</H4>
</div>
)
}
<img className="aspect-square" {...imgProps} alt="style" />
<div className="absolute left-0 top-0 h-full w-full bg-gradient-to-t from-gray-700" />
<H4 className="absolute bottom-[1px] left-[6px] text-sm drop-shadow-md">
{label}
</H4>
</button>
);
};
+36 -18
View File
@@ -1,24 +1,42 @@
import { TextareaHTMLAttributes } from "react"
import { TextareaHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import { Label } from "./Typography";
type ResizeType =
| "none"
| "both"
| "horizontal"
| "vertical"
| "block"
| "inline"
| undefined;
export const Textarea = ({
className,
label,
resize = "vertical",
...rest
}:TextareaHTMLAttributes<HTMLTextAreaElement>) => {
return(
<textarea
className={twMerge(
"rounded-lg bg-ui-controls border border-ui-panel-border py-2 px-4"
,className
)}
style={{
outline: "2px solid transparent",
transition: "outline-color 0.15s ease-in-out",
}}
onFocus={(e) => (e.currentTarget.style.outlineColor = "#e66462")}
onBlur={(e) => (e.currentTarget.style.outlineColor = "transparent")}
{...rest}
/>
}: TextareaHTMLAttributes<HTMLTextAreaElement> & {
label?: string;
resize?: ResizeType;
}) => {
return (
<div className="flex flex-col">
{label && <Label>{label}</Label>}
<textarea
className={twMerge(
"rounded-lg border border-ui-panel-border bg-ui-controls p-3",
className,
)}
style={{
outline: "2px solid transparent",
transition: "outline-color 0.15s ease-in-out",
resize: resize,
}}
onFocus={(e) => (e.currentTarget.style.outlineColor = "#e66462")}
onBlur={(e) => (e.currentTarget.style.outlineColor = "transparent")}
{...rest}
/>
</div>
);
}
};
+45 -17
View File
@@ -1,32 +1,59 @@
import { NavLink, NavLinkProps } from "@remix-run/react";
import { twMerge } from "tailwind-merge";
interface TypogProps {
interface TypoProps {
className?: string;
children: React.ReactNode;
}
const baseTypo = "text-white";
export const H1 = ({className,children,}:TypogProps)=>
<h1 className={twMerge(baseTypo, "font-medium text-2xl",className)}>{children}</h1>;
export const H1 = ({ className, children }: TypoProps) => (
<h1 className={twMerge(baseTypo, "text-2xl font-medium", className)}>
{children}
</h1>
);
export const H2 = ({className,children,}:TypogProps)=>
<h2 className={twMerge(baseTypo, "font-medium text-xl", className)}> {children} </h2>;
export const H2 = ({ className, children }: TypoProps) => (
<h2 className={twMerge(baseTypo, "text-xl font-medium", className)}>
{" "}
{children}{" "}
</h2>
);
export const H3 = ({className,children,}:TypogProps)=>
<h3 className={twMerge(baseTypo, "font-medium text-lg", className)}> {children} </h3>;
export const H3 = ({ className, children }: TypoProps) => (
<h3 className={twMerge(baseTypo, "text-lg font-medium", className)}>
{" "}
{children}{" "}
</h3>
);
export const H4 = ({className,children,}:TypogProps)=>
<h4 className={twMerge(baseTypo, "font-medium text-base", className)}> {children} </h4>;
export const H4 = ({ className, children }: TypoProps) => (
<h4 className={twMerge(baseTypo, "text-base font-medium", className)}>
{" "}
{children}{" "}
</h4>
);
export const Label = ({className,children,}:TypogProps)=>
<label className={twMerge(baseTypo, "font-medium text-base", className)}> {children} </label>;
export const Label = ({ className, children }: TypoProps) => (
<label className={twMerge(baseTypo, "mb-2 text-base font-medium", className)}>
{" "}
{children}{" "}
</label>
);
export const H5 = ({className,children,}:TypogProps)=>
<h5 className={twMerge(baseTypo, "font-medium text-sm",className)}> {children} </h5>;
export const H5 = ({ className, children }: TypoProps) => (
<h5 className={twMerge(baseTypo, "text-sm font-medium", className)}>
{" "}
{children}{" "}
</h5>
);
export const H6 = ({className,children,}:TypogProps)=>
<h6 className={twMerge(baseTypo, "font-light text-sm",className)}> {children} </h6>;
export const H6 = ({ className, children }: TypoProps) => (
<h6 className={twMerge(baseTypo, "text-sm font-light", className)}>
{" "}
{children}{" "}
</h6>
);
export const Link = ({className, ...rest}:NavLinkProps)=>
<NavLink
@@ -37,5 +64,6 @@ export const Link = ({className, ...rest}:NavLinkProps)=>
{...rest}
/>
export const P = ({className,children,}:TypogProps)=>
<p className={twMerge(baseTypo, className)}> {children} </p>;
export const P = ({ className, children }: TypoProps) => (
<p className={twMerge(baseTypo, className)}> {children} </p>
);
+20 -15
View File
@@ -23,6 +23,9 @@ export const LowerPanel = ({ children }: LowerPanelPropsI) => {
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
const newTime = Math.round((event.clientX - 92) / 4 / scale.value);
if (newTime < 0) {
return;
}
currentTime.value = newTime;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
@@ -35,25 +38,27 @@ export const LowerPanel = ({ children }: LowerPanelPropsI) => {
);
return (
<div
className={[
"absolute bottom-0",
"w-screen overflow-auto",
"bg-ui-panel",
].join(" ")}
style={{ height: displayHeight }}
onPointerOver={() => {
overTimeline.value = true;
}}
onPointerLeave={() => (overTimeline.value = false)}
onPointerDown={onTimelineClick}
>
<>
<div
className="w-full cursor-ns-resize bg-ui-panel-border"
style={{ height: 3, zIndex: 1000 }}
onPointerDown={onPointerDown}
/>
{children}
</div>
<div
className={[
"absolute bottom-0",
"w-screen overflow-auto",
"bg-ui-panel",
].join(" ")}
style={{ height: displayHeight - 3 }}
onPointerOver={() => {
overTimeline.value = true;
}}
onPointerLeave={() => (overTimeline.value = false)}
onPointerDown={onTimelineClick}
>
{children}
</div>
</>
);
};
+1 -1
View File
@@ -15,7 +15,7 @@ export const SidePanel = ({ children }: Props) => {
return (
<div
className={
"relative border-l border-l-ui-panel-border bg-ui-panel transition-all duration-300 ease-in-out" +
"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")
}
>
+3 -5
View File
@@ -19,7 +19,7 @@ export const Tabs = ({ tabs: tabProps }: { tabs: TabType[] }) => {
);
return (
<div className="w-full h-full px-2 sm:px-0 flex flex-col">
<div className="flex h-full w-full flex-col px-2 sm:px-0">
<Tab.Group defaultIndex={3}>
<Tab.List className="flex">
{tabs.map((tab, idx) => (
@@ -31,11 +31,9 @@ export const Tabs = ({ tabs: tabProps }: { tabs: TabType[] }) => {
</Tab>
))}
</Tab.List>
<Tab.Panels className="grow p-4">
<Tab.Panels className="grow overflow-auto p-4">
{tabs.map((tab, idx) => (
<Tab.Panel key={idx}>
{tab.children}
</Tab.Panel>
<Tab.Panel key={idx}>{tab.children}</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
+9 -1
View File
@@ -1,10 +1,18 @@
import { PageEnigmaComponent } from "./PageEnigmaComponent";
import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackProvider";
import { AppUIProvider } from "~/pages/PageEnigma/contexts/AppUiContext";
import { EngineProvider } from "~/pages/PageEnigma/contexts/EngineProvider";
import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragComponent";
export const PageEnigma = () => {
return (
<TrackProvider>
<PageEnigmaComponent />
<AppUIProvider>
<EngineProvider>
<PageEnigmaComponent />
</EngineProvider>
</AppUIProvider>
<DragComponent />
</TrackProvider>
);
};
+67 -105
View File
@@ -1,4 +1,4 @@
import { useReducer } from "react";
import { useContext } from "react";
import { faSparkles } from "@fortawesome/pro-solid-svg-icons";
@@ -15,32 +15,14 @@ import { ViewSideBySide } from "./comps/ViewSideBySide";
import { SidePanelTabs } from "./comps/SidePanelTabs";
import { Timeline } from "./comps/Timeline";
import { EngineProvider } from "./contexts/EngineProvider";
import { AppUIProvider } from "./contexts/AppUiContext";
import {
appUiReducer,
appUiInitialStateValues,
APPUI_VIEW_MODES,
} from "./reducers";
import {
canDrop,
clipLength,
currPosition,
dragId,
overTimeline,
scale,
timelineHeight,
} from "~/pages/PageEnigma/store";
import { APPUI_VIEW_MODES } from "./reducers";
import { timelineHeight } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
import { AppUiContext } from "~/pages/PageEnigma/contexts/AppUiContext";
export const PageEnigmaComponent = () => {
useSignals();
const { currX, currY } = currPosition.value;
const [appUiState, dispatchAppUiState] = useReducer(
appUiReducer,
appUiInitialStateValues,
);
const [appUiState] = useContext(AppUiContext);
//To prevent the click event from propagating to the canvas: TODO: HANDLE THIS BETTER?
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -54,97 +36,77 @@ export const PageEnigmaComponent = () => {
<Button icon={faSparkles}>Generate Movie</Button>
</TopBarHelmet>
<AppUIProvider value={[appUiState, dispatchAppUiState]}>
<EngineProvider>
<div style={{ height: "calc(100vh - 68px)" }}>
{/* Engine section/side panel */}
<div style={{ height: "calc(100vh - 68px)" }}>
{/* Engine section/side panel */}
<div
id="engine-n-panels-wrapper"
className="flex"
style={{ height: `calc(100% - ${lowerHeight}px)` }}
>
<div className="relative w-full overflow-hidden bg-transparent">
<div
id="engine-n-panels-wrapper"
className="flex"
style={{ height: `calc(100% - ${lowerHeight}px)` }}
className={
appUiState.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? "invisible"
: ""
}
>
<div className="relative w-full overflow-hidden bg-transparent">
<div
className={
appUiState.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? "invisible"
: ""
}
>
<canvas id="video-scene" width="1280px" height="720px" />
<canvas id="video-scene" width="1280px" height="720px" />
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
onClick={handleOverlayClick}
>
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D />
</div>
</div>
{/* Bottom controls */}
<div
className="absolute bottom-0 left-0 w-full"
onClick={handleOverlayClick}
>
<PreviewEngineCamera />
<ControlsVideo />
<ControlPanelSceneObject />
</div>
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
onClick={handleOverlayClick}
>
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D />
</div>
{appUiState.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE && (
<ViewSideBySide />
)}
<LoadingDots
className="absolute left-0 top-0"
isShowing={appUiState.showEditorLoader.isShowing}
type="bricks"
message={appUiState.showEditorLoader.message}
/>
<LoadingBar
id="editor-loading-bar"
wrapperClassName="absolute top-0 left-0"
innerWrapperClassName="max-w-screen-sm"
isShowing={appUiState.showEditorLoadingBar.isShowing}
message={appUiState.showEditorLoadingBar.message}
label={appUiState.showEditorLoadingBar.label}
progress={appUiState.showEditorLoadingBar.progress}
/>
</div>
{/* Side panel */}
<div onClick={handleOverlayClick}>
<SidePanel>
<SidePanelTabs />
</SidePanel>
{/* Bottom controls */}
<div
className="absolute bottom-0 left-0 w-full"
onClick={handleOverlayClick}
>
<PreviewEngineCamera />
<ControlsVideo />
<ControlPanelSceneObject />
</div>
</div>
{/* Timeline */}
<div onClick={handleOverlayClick}>
<Timeline />
</div>
{appUiState.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE && (
<ViewSideBySide />
)}
<LoadingDots
className="absolute left-0 top-0"
isShowing={appUiState.showEditorLoader.isShowing}
type="bricks"
message={appUiState.showEditorLoader.message}
/>
<LoadingBar
id="editor-loading-bar"
wrapperClassName="absolute top-0 left-0"
innerWrapperClassName="max-w-screen-sm"
isShowing={appUiState.showEditorLoadingBar.isShowing}
message={appUiState.showEditorLoadingBar.message}
label={appUiState.showEditorLoadingBar.label}
progress={appUiState.showEditorLoadingBar.progress}
/>
</div>
</EngineProvider>
</AppUIProvider>
<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,
}}
/>
{/* Side panel */}
<div onClick={handleOverlayClick}>
<SidePanel>
<SidePanelTabs />
</SidePanel>
</div>
</div>
{/* Timeline */}
<div onClick={handleOverlayClick}>
<Timeline />
</div>
</div>
</div>
);
};
+10 -7
View File
@@ -1,7 +1,10 @@
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { QueueClip } from "~/pages/PageEnigma/models/track";
import { XYZ } from "../datastructures/common";
import {
QueueClip,
QueueKeyframe,
UpdateTime,
} from "~/pages/PageEnigma/models/track";
import { toTimelineActions } from "./toTimelineActions";
class Queue {
@@ -9,14 +12,14 @@ class Queue {
string,
{
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | { currentTime: number } | { position: XYZ, rotation: XYZ, scale: XYZ };
data: QueueClip | UpdateTime | QueueKeyframe;
}[]
> = {};
private _subscribers: Record<
string,
(entry: {
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | { currentTime: number } | { position: XYZ, rotation: XYZ, scale: XYZ };
data: QueueClip | UpdateTime | QueueKeyframe;
}) => void
> = {};
@@ -26,8 +29,8 @@ class Queue {
data,
}: {
queueName: string;
action: fromEngineActions | toEngineActions | toTimelineActions ;
data: QueueClip | { currentTime: number } | { position: XYZ, rotation: XYZ, scale: XYZ };
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
}) {
if (!this._queue[queueName]) {
this._queue[queueName] = [];
@@ -44,7 +47,7 @@ class Queue {
queueName: string,
onMessage: (entry: {
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | { currentTime: number } | { position: XYZ, rotation: XYZ, scale: XYZ };
data: QueueClip | UpdateTime | QueueKeyframe;
}) => void,
) {
this._subscribers[queueName] = onMessage;
@@ -1,8 +1,12 @@
export enum toEngineActions {
ADD_CLIP = "add_clip",
// data=QueueClip
ADD_KEYFRAME = "add_keyframe",
// data= QueueKeyframe
DELETE_CLIP = "delete_clip",
// data=QueueClip
DELETE_KEYFRAME = "delete_keyframe",
// data=QueueKeyframe
MUTE = "mute",
// data={version, type, group, object_uuid} if lipsync
// data={version, type, group} if global_audio
@@ -13,8 +17,8 @@ export enum toEngineActions {
// data={version, type, group} if global_audio
UPDATE_CLIP = "update_clip",
// data=QueueClip
UPDATE_KEYFRAME = "update_keyframe",
// data=QueueKeyframe
UPDATE_TIME = "update_time",
ADD_KEYFRAME = "add_keyframe",
// data={currentTime: number}
}
@@ -8,7 +8,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppUiContext } from "../../contexts/AppUiContext";
import { Button, H4, H5, H6, InputVector } from "~/components";
import { Button, H5, InputVector } from "~/components";
import { XYZ } from "../../datastructures/common";
import { ACTION_TYPES } from "../../reducers/appUiReducer/types";
@@ -16,86 +16,143 @@ 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";
export const ControlPanelSceneObject = () => {
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const position = appUiState?.currentSceneObject.objectVectors.position;
const rotation = appUiState?.currentSceneObject.objectVectors.rotation;
const scalar = appUiState?.currentSceneObject.objectVectors.scale;
const handlePositionChange = (xyz: XYZ) => {
if (appUiState)
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
objectVectors: {
position: { ...xyz },
rotation: appUiState.currentSceneObject.objectVectors.rotation,
scale: appUiState.currentSceneObject.objectVectors.scale,
},
},
if (!currentSceneObject) {
console.log("Missing Scene Object Position");
return;
}
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: currentSceneObject.version,
objectVectors: {
position: { ...xyz },
rotation: appUiState?.controlPanel.currentSceneObject?.objectVectors
?.rotation ?? { x: 0, y: 0, z: 0 },
scale: appUiState?.controlPanel.currentSceneObject?.objectVectors
?.scale ?? { x: 0, y: 0, z: 0 },
},
});
},
});
};
const handleRotationChange = (xyz: XYZ) => {
if (appUiState)
if (appUiState) {
if (currentSceneObject == null) {
console.log("Missing Scene Object Rotation");
return;
}
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
objectVectors: {
position: appUiState.currentSceneObject.objectVectors.position,
rotation: { ...xyz },
scale: appUiState.currentSceneObject.objectVectors.scale,
},
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: currentSceneObject.version,
objectVectors: {
position:
appUiState.controlPanel.currentSceneObject.objectVectors.position,
rotation: { ...xyz },
scale:
appUiState.controlPanel.currentSceneObject.objectVectors.scale,
},
},
});
}
};
const handleScalarChange = (xyz: XYZ) => {
if (appUiState)
const handleScaleChange = (xyz: XYZ) => {
if (appUiState) {
if (currentSceneObject == null) {
console.log("Missing Scene Object Scale");
return;
}
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
objectVectors: {
position: appUiState.currentSceneObject.objectVectors.position,
rotation: appUiState.currentSceneObject.objectVectors.rotation,
scale: { ...xyz },
},
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: currentSceneObject.version,
objectVectors: {
position:
appUiState.controlPanel.currentSceneObject.objectVectors.position,
rotation:
appUiState.controlPanel.currentSceneObject.objectVectors.rotation,
scale: { ...xyz },
},
},
});
}
};
const handleOnAddKeyFrame = () => {
if (appUiState) {
console.log(`${appUiState.currentSceneObject.objectVectors.position.x}`)
console.log(`${appUiState.currentSceneObject.objectVectors.position.y}`)
console.log(`${appUiState.currentSceneObject.objectVectors.position.z}`)
if (
position == null ||
rotation == null ||
scale == null ||
currentSceneObject == null
) {
return;
}
console.log(`${appUiState.currentSceneObject.objectVectors.rotation.x}`)
console.log(`${appUiState.currentSceneObject.objectVectors.rotation.y}`)
console.log(`${appUiState.currentSceneObject.objectVectors.rotation.z}`)
// console.log(`Position ${position.x}`)
// console.log(`Position ${position.y}`)
// console.log(`Position ${position.z}`)
console.log(`${appUiState.currentSceneObject.objectVectors.scale.x}`)
console.log(`${appUiState.currentSceneObject.objectVectors.scale.y}`)
console.log(`${appUiState.currentSceneObject.objectVectors.scale.z}`)
// console.log(`Rotation ${rotation.x}`)
// console.log(`Rotation ${rotation.y}`)
// console.log(`Rotation ${rotation.z}`)
Queue.publish({queueName:QueueNames.TO_TIMELINE, action: toTimelineActions.ADD_KEYFRAME,
data: {
position: appUiState.currentSceneObject.objectVectors.position,
rotation: appUiState.currentSceneObject.objectVectors.rotation,
scale:appUiState.currentSceneObject.objectVectors.scale
}
})
// console.log(`Scale ${scale.x}`)
// console.log(`Scale ${scale.y}`)
// console.log(`Scale ${scale.z}`)
console.log(`Group ${currentSceneObject.group}`);
console.log(`Object UUID ${currentSceneObject.object_uuid}`);
console.log(`Object Name ${currentSceneObject.object_name}`);
console.log(`Object Version ${currentSceneObject.version}`);
Queue.publish({
queueName: QueueNames.TO_TIMELINE,
action: toTimelineActions.ADD_KEYFRAME,
data: {
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: 1,
position: currentSceneObject.objectVectors.position,
rotation: currentSceneObject.objectVectors.rotation,
scale: currentSceneObject.objectVectors.scale,
} as QueueKeyframe,
});
}
};
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;
return (
<Transition
show={appUiState?.currentSceneObject.isShowing}
show={appUiState?.controlPanel.isShowing}
className="absolute bottom-0 right-0 m-4 flex h-fit w-fit flex-col gap-2 rounded-lg border border-ui-panel-border bg-ui-panel p-4 text-white"
enter="transition-opacity duration-100"
enterFrom="opacity-0"
@@ -139,18 +196,20 @@ export const ControlPanelSceneObject = () => {
<div className="flex flex-col gap-1">
<H5>Scale</H5>
<InputVector
x={scalar?.x || 0}
y={scalar?.y || 0}
z={scalar?.z || 0}
onChange={handleScalarChange}
x={scale?.x || 0}
y={scale?.y || 0}
z={scale?.z || 0}
onChange={handleScaleChange}
/>
</div>
<span className="h-2" />
<div className="flex gap-2">
<Button variant="secondary"
className="grow"
onClick={handleOnAddKeyFrame}>
<Button
variant="secondary"
className="grow"
onClick={handleOnAddKeyFrame}
>
Add Keyframe (K)
</Button>
<Button variant="secondary" icon={faTrash} />
@@ -4,10 +4,11 @@ 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 [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const [sceneName, setSceneName] = useState<string>("");
const [sceneToken, setSceneToken] = useState<string>("");
@@ -22,8 +23,8 @@ export const ControlsTopButtons = () => {
};
const handleButtonTest = () => {
editorEngine?.testStylizeRequest()
editorEngine?.testStylizeRequest();
// if(appUiState?.showEditorLoadingBar.isShowing){
//TO GO TO A HUNDRED AND DISAPPER
// dispatchAppUiState({
@@ -41,15 +42,15 @@ export const ControlsTopButtons = () => {
// }, 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",
// }
// }
// });
//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,
@@ -59,31 +60,33 @@ export const ControlsTopButtons = () => {
// progress: 10,
// }
// }
// });
//}
};
const handleObjectPanelTest = () => {
if (appUiState?.currentSceneObject.isShowing) {
if (appUiState?.controlPanel.isShowing) {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT,
});
}else{
} else {
dispatchAppUiState({
type:APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
payload:{
currentSceneObject:{
objectVectors: {
position: {x:1,y:2,z:3},
rotation: {x:4,y:5,z:6},
scale: {x:7,y:8,z:9},
}
}
}
type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
payload: {
group: ClipGroup.OBJECT,
object_name: "",
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 handleButtonCameraView = () => {
editorEngine?.switchCameraView();
@@ -111,40 +114,44 @@ export const ControlsTopButtons = () => {
<div className="flex flex-col gap-2 pl-3 pt-3">
<div className="flex gap-2">
<ButtonDialogue
buttonProps={{
variant: "secondary",
label: "Load Scene",
}}
title="Load Scene"
confirmButtonProps={{
label: "Load",
disabled: sceneToken === "" ? true : false,
onClick: handleButtonLoadScene,
}}
>
buttonProps={{
variant: "secondary",
label: "Load Scene",
}}
title="Load Scene"
confirmButtonProps={{
label: "Load",
disabled: sceneToken === "",
onClick: handleButtonLoadScene,
}}
>
<Input
label="Please provide the Token of the scene you wished to load:"
onChange={(e)=>{setSceneToken(e.target.value)}}
onChange={(e) => {
setSceneToken(e.target.value);
}}
/>
</ButtonDialogue>
<ButtonDialogue
buttonProps={{
variant: "secondary",
label: "Save Scene",
}}
title="Save Scene"
confirmButtonProps={{
label: "Save",
disabled: sceneName === "" ? true : false,
onClick: handleButtonSave,
}}
>
<Input
label="Please Enter a name for your scene"
onChange={(e)=>{setSceneName(e.target.value)}}
/>
</ButtonDialogue>
<ButtonDialogue
buttonProps={{
variant: "secondary",
label: "Save Scene",
}}
title="Save Scene"
confirmButtonProps={{
label: "Save",
disabled: sceneName === "",
onClick: handleButtonSave,
}}
>
<Input
label="Please Enter a name for your scene"
onChange={(e) => {
setSceneName(e.target.value);
}}
/>
</ButtonDialogue>
<ButtonDialogue
buttonProps={{
@@ -172,7 +179,6 @@ export const ControlsTopButtons = () => {
</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>
@@ -181,7 +187,7 @@ export const ControlsTopButtons = () => {
<Button
onClick={handleButtonTest}
className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary"
style={{zIndex:9001}}
style={{ zIndex: 9001 }}
>
Test
</Button>
@@ -0,0 +1,33 @@
import {
canDrop,
clipLength,
currPosition,
dragId,
overTimeline,
scale,
} from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
export const DragComponent = () => {
useSignals();
const { currX, currY } = currPosition.value;
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,
}}
/>
);
};
@@ -0,0 +1,31 @@
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 { useSignals } from "@preact/signals-react/runtime";
export const Scrubber = () => {
useSignals();
const { onPointerDown, time } = useMouseEventsAnimation();
const displayTime = time === -1 ? currentTime.value : time;
return (
<div
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
style={{ top: 8, left: displayTime * 4 * scale.value + 84 }}
onPointerDown={onPointerDown}
>
<div>
<FontAwesomeIcon icon={faSortDown} className="h-5 text-brand-primary" />
</div>
<div
className="block bg-brand-primary"
style={{
width: 2,
marginTop: -5,
height: fullHeight.value,
}}
/>
</div>
);
};
@@ -1,122 +1,121 @@
import { useContext, useState } from 'react';
import { useContext, useState } from "react";
import {
Button,
H4,
ItemPicker,
Textarea
} from "~/components"
import { Button, ItemPicker, Label, Textarea } from "~/components";
import { AppUiContext } from '../../contexts/AppUiContext';
import { APPUI_ACTION_TYPES, APPUI_VIEW_MODES } from '../../reducers';
import { EngineContext } from '../../contexts/EngineContext';
import { ArtStyle } from '../../js/api_manager';
export const TabStylization = ()=>{
import { AppUiContext } from "../../contexts/AppUiContext";
import { APPUI_ACTION_TYPES, APPUI_VIEW_MODES } from "../../reducers";
import { EngineContext } from "../../contexts/EngineContext";
import { ArtStyle } from "../../js/api_manager";
export const TabStylization = () => {
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const [ selection, setSelection ] = useState<string>("Anime");
const [selection, setSelection] = useState<ArtStyle>(ArtStyle.Anime2DFlat);
const editorEngine = useContext(EngineContext);
const handlePickingStylizer = (picked:string)=>{
const handlePickingStylizer = (picked: ArtStyle) => {
console.log(`Picked style: ${picked}`);
let selected = ArtStyle.Anime2DFlat
switch (picked) {
case "Anime":
selected = ArtStyle.Anime2DFlat
break;
case "Pixel":
selected = ArtStyle.PixelArt
break;
case "Pixar":
selected = ArtStyle.Cartoon3D
break;
case "Stylized":
selected = ArtStyle.ComicBook
break;
if (editorEngine == null) {
console.log("Editor is null");
return;
}
if (editorEngine == null) { console.log("Editor is null"); return;}
editorEngine.art_style = selected
console.log(`actual style: ${selected}`);
editorEngine.art_style = picked;
setSelection(picked);
}
};
const onChangeHandlerNegative = (e:React.ChangeEvent<HTMLInputElement>) => {
if (editorEngine == null) { console.log("Editor is null"); return;}
editorEngine.negative_prompt = e.target.value
console.log(e.target.value)
}
const onChangeHandlerNegative = (
e: React.ChangeEvent<HTMLTextAreaElement>,
) => {
if (editorEngine == null) {
console.log("Editor is null");
return;
}
editorEngine.negative_prompt = e.target.value;
console.log(e.target.value);
};
const onChangeHandlerPositive = (e:React.ChangeEvent<HTMLInputElement>) => {
if (editorEngine == null) { console.log("Editor is null"); return;}
editorEngine.positive_prompt = e.target.value
console.log(e.target.value)
}
const handleChangeViewMode = ()=>{
if(dispatchAppUiState!==null){
const onChangeHandlerPositive = (
e: React.ChangeEvent<HTMLTextAreaElement>,
) => {
if (editorEngine == null) {
console.log("Editor is null");
return;
}
editorEngine.positive_prompt = e.target.value;
console.log(e.target.value);
};
const handleChangeViewMode = () => {
if (dispatchAppUiState !== null) {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.ON_CHANGE_VIEW_MODE,
payload: {
viewMode: appUiState?.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? APPUI_VIEW_MODES.EDITOR : APPUI_VIEW_MODES.SIDE_BY_SIDE,
}
})
viewMode:
appUiState?.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? APPUI_VIEW_MODES.EDITOR
: APPUI_VIEW_MODES.SIDE_BY_SIDE,
},
});
}
}
return(
<div className="gap-2 w-full h-full overflow-scroll">
<H4>Select Base Stylizer</H4>
<div className="flex gap-2 my-2">
<ItemPicker
label="Anime"
selected={(selection === "Anime")}
onSelected={handlePickingStylizer}
src="/resources/avatars/0.webp"
/>
<ItemPicker
label="Pixel"
selected={selection === "Pixel"}
onSelected={handlePickingStylizer}
src="/resources/avatars/1.webp"
/>
<ItemPicker
label="Pixar"
selected={selection === "Pixar"}
onSelected={handlePickingStylizer}
src="/resources/avatars/2.webp"
/>
<ItemPicker
label="Stylized"
selected={selection === "Stylized"}
onSelected={handlePickingStylizer}
src="/resources/avatars/3.webp"
/>
};
return (
<div className="flex h-full w-full flex-col gap-3">
<div className="flex flex-col">
<Label>Select Base Style</Label>
<div className="grid grid-cols-4 gap-2">
<ItemPicker
label="Anime"
type={ArtStyle.Anime2DFlat}
selected={selection === ArtStyle.Anime2DFlat}
onSelected={handlePickingStylizer}
src="/resources/avatars/0.webp"
/>
<ItemPicker
label="Pixel"
type={ArtStyle.PixelArt}
selected={selection === ArtStyle.PixelArt}
onSelected={handlePickingStylizer}
src="/resources/avatars/1.webp"
/>
<ItemPicker
label="Pixar"
type={ArtStyle.Cartoon3D}
selected={selection === ArtStyle.Cartoon3D}
onSelected={handlePickingStylizer}
src="/resources/avatars/2.webp"
/>
<ItemPicker
label="Stylized"
type={ArtStyle.ComicBook}
selected={selection === ArtStyle.ComicBook}
onSelected={handlePickingStylizer}
src="/resources/avatars/3.webp"
/>
</div>
</div>
<H4>Enter a Prompt</H4>
<div className="flex gap-2 my-2">
<Textarea
className="w-full h-32"
placeholder="Type here to describe your scene"
onChange={onChangeHandlerPositive}
/>
</div>
<H4>Negative Prompt</H4>
<div className="flex gap-2 my-2">
<Textarea
className="w-full h-32"
placeholder="Type here to filter out the things you don't want in the scene"
onChange={onChangeHandlerNegative}
/>
</div>
<div className="flex gap-2 mt-6 justify-center">
<Textarea
label="Positive Prompt"
className="h-32 w-full"
placeholder="Type here to describe your scene"
onChange={onChangeHandlerPositive}
resize="none"
/>
<Textarea
label="Negative Prompt"
className="h-32 w-full"
placeholder="Type here to filter out the things you don't want in the scene"
onChange={onChangeHandlerNegative}
resize="none"
/>
<div className="mt-3 flex justify-center gap-2">
<Button onClick={handleChangeViewMode}>
{
appUiState?.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? "Back to Scene" : "Preview Side by Side"
}
{appUiState?.viewMode === APPUI_VIEW_MODES.SIDE_BY_SIDE
? "Back to Scene"
: "Preview Side by Side"}
</Button>
</div>
</div>
);
}
};
@@ -1,11 +1,13 @@
import { useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { fullWidth } from "~/pages/PageEnigma/store";
import {
audioGroup,
fullWidth,
toggleAudioMute,
updateAudio,
} from "~/pages/PageEnigma/store";
export const Audio = () => {
const { audio, updateAudio, toggleAudioMute } = useContext(TrackContext);
const { clips } = audio!;
const { clips } = audioGroup.value;
return (
<div
@@ -17,12 +19,12 @@ export const Audio = () => {
</div>
<div className="flex flex-col gap-4">
<TrackClips
id={audio!.id}
id={audioGroup.value.id}
clips={clips}
title="Global Audio TrackClips"
updateClip={updateAudio}
style="audio"
muted={audio!.muted}
muted={audioGroup.value.muted}
toggleMute={toggleAudioMute}
/>
</div>
+7 -10
View File
@@ -1,11 +1,8 @@
import { useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { fullWidth } from "~/pages/PageEnigma/store";
import { cameraGroup, fullWidth, updateCamera } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
export const Camera = () => {
const { camera, updateCamera } = useContext(TrackContext);
const { clips } = camera!;
const { keyframes } = cameraGroup.value!;
return (
<div
@@ -14,12 +11,12 @@ export const Camera = () => {
>
<div className="mb-5 pt-2 text-xs font-medium text-white">Camera</div>
<div className="flex flex-col gap-4">
<TrackClips
id={camera!.id}
clips={clips}
<TrackKeyFrames
id={cameraGroup.value.id}
keyframes={keyframes}
title="Camera Position/Rotation"
style="camera"
updateClip={updateCamera}
updateKeyframe={updateCamera}
/>
</div>
</div>
@@ -1,7 +1,12 @@
import { useCallback, useContext, useMemo } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useCallback, useMemo } from "react";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { fullWidth } from "~/pages/PageEnigma/store";
import {
characterGroups,
fullWidth,
toggleLipSyncMute,
updateCharacters,
} from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
function buildUpdaters(
updateCharacters: (options: {
@@ -18,12 +23,8 @@ function buildUpdaters(
}) {
updateCharacters({ ...options, type: "animations" });
}
function updateClipPosition(options: {
id: string;
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: "positions" });
function updateClipPosition(options: { id: string; offset: number }) {
updateCharacters({ ...options, length: 0, type: "positions" });
}
function updateClipLipSync(options: {
id: string;
@@ -39,20 +40,19 @@ interface Props {
}
export const Character = ({ characterId }: Props) => {
const { characters, updateCharacters, toggleLipSyncMute } =
useContext(TrackContext);
const character = characters.find((row) => (row.id = characterId));
const character = characterGroups.value.find((row) => (row.id = characterId));
const { updateClipLipSync, updateClipPosition, updateClipAnimations } =
useMemo(() => buildUpdaters(updateCharacters), [updateCharacters]);
useMemo(() => buildUpdaters(updateCharacters), []);
const toggleCharacterLipSyncMute = useCallback(() => {
toggleLipSyncMute(character?.id ?? "");
}, []);
}, [character?.id]);
if (!character) {
return false;
}
const { animationClips, positionClips, lipSyncClips } = character;
const { animationClips, positionKeyframes, lipSyncClips } = character;
return (
<div
@@ -71,13 +71,12 @@ export const Character = ({ characterId }: Props) => {
style="character"
type="animations"
/>
<TrackClips
<TrackKeyFrames
id={character.id}
clips={positionClips}
keyframes={positionKeyframes}
title="Character Position/Rotation"
updateClip={updateClipPosition}
updateKeyframe={updateClipPosition}
style="character"
type="positions"
/>
<TrackClips
id={character.id}
@@ -0,0 +1,14 @@
import { characterGroups } from "~/pages/PageEnigma/store";
import { Character } from "~/pages/PageEnigma/comps/Timeline/Character";
import { useSignals } from "@preact/signals-react/runtime";
export const Characters = () => {
useSignals();
return (
<>
{characterGroups.value.map((character) => (
<Character key={character.id} characterId={character.id} />
))}
</>
);
};
@@ -0,0 +1,16 @@
import { objectGroup } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
import { ObjectTrack } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
export const ObjectGroups = () => {
useSignals();
return (
<>
{objectGroup.value.objects.length > 0 && (
<div className="p-4">
<ObjectTrack />
</div>
)}
</>
);
};
@@ -1,11 +1,7 @@
import { useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { fullWidth } from "~/pages/PageEnigma/store";
import { fullWidth, objectGroup, updateObject } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
export const Objects = () => {
const { objects, updateObject } = useContext(TrackContext);
export const ObjectTrack = () => {
return (
<div
className="block rounded-lg bg-objects-groupBg pb-5 pl-2 pr-4"
@@ -14,13 +10,13 @@ export const Objects = () => {
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Objects
</div>
{objects.objects.map((object) => (
<div key={object.id} className="flex flex-col gap-4">
{objectGroup.value.objects.map((object) => (
<div key={object.object_uuid} className="flex flex-col gap-4">
<TrackKeyFrames
id={object.id}
keyFrames={object.keyFrames}
id={object.object_uuid}
keyframes={object.keyframes}
title="Mask Position/Rotation"
updateClip={updateObject}
updateKeyframe={updateObject}
style="objects"
/>
</div>
+40 -112
View File
@@ -1,62 +1,60 @@
import { Fragment, useCallback, useContext, useEffect, useState } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useCallback, useEffect, useState } from "react";
import { LowerPanel } from "~/modules/LowerPanel";
import { Character } from "./Character";
import { Camera } from "./Camera";
import { Audio } from "./Audio";
import { Objects } from "./Objects";
import { useMouseEventsAnimation } from "./utils/useMouseEventsAnimation";
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ConfirmationModal } from "~/components/ConfirmationModal";
import {
scale,
currentTime,
filmLength,
timelineHeight,
deleteAudioClip,
deleteCharacterClip,
} from "~/pages/PageEnigma/store";
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 { 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 { selectedItem } from "~/pages/PageEnigma/store/selectedItem";
function getItemType(item: Clip | Keyframe | null) {
if (!item) {
return "";
}
return (item as Clip).clip_uuid ? "clip" : "keyframe";
}
export const Timeline = () => {
const {
characters,
objects,
selectedClip,
deleteCharacterClip,
deleteAudioClip,
deleteCameraClip,
} = useContext(TrackContext);
const { onPointerDown, time } = useMouseEventsAnimation();
useSignals();
const [dialogOpen, setDialogOpen] = useState(false);
const { deleteKeyframe } = useUpdateKeyframe();
// implement the code to handle incoming messages from the Engine
useQueueHandler();
const sectionWidth = 60 * 4 * scale.value;
const fullHeight =
characters.length * 268 + objects.objects.length * 60 + 300 + 96;
useEffect(() => {
timelineHeight.value = window.outerHeight * 0.25;
}, []);
const onDeleteAsk = useCallback(
(event: KeyboardEvent) => {
if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedClip) {
setDialogOpen(true);
}
},
[selectedClip],
);
const displayTime = time === -1 ? currentTime.value : time;
const onDeleteAsk = useCallback((event: KeyboardEvent) => {
if (
["Backspace", "Delete"].indexOf(event.key) > -1 &&
selectedItem.value !== null
) {
event.stopPropagation();
event.preventDefault();
setDialogOpen(true);
}
}, []);
const onDelete = useCallback(() => {
deleteCharacterClip(selectedClip!);
deleteCameraClip(selectedClip!);
deleteAudioClip(selectedClip!);
}, [selectedClip, deleteAudioClip, deleteCharacterClip, deleteCameraClip]);
deleteCharacterClip(selectedItem.value as Clip);
deleteAudioClip(selectedItem.value as Clip);
deleteKeyframe(selectedItem.value as Keyframe);
selectedItem.value = null;
}, [deleteKeyframe]);
useEffect(() => {
document.addEventListener("keydown", onDeleteAsk);
@@ -69,56 +67,9 @@ export const Timeline = () => {
return (
<>
<LowerPanel>
<div
className={[
"prevent-select mt-4",
"flex h-3",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
>
{Array(filmLength.value)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute ps-1 pt-1"
style={{ left: index * sectionWidth + 92 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth + 88,
height: fullHeight,
}}
/>
</Fragment>
))}
<div
className="absolute"
style={{ left: filmLength.value * sectionWidth + 92 }}
>
00:
{filmLength.value < 10
? "0" + filmLength.value.toString()
: filmLength.value.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: filmLength.value * sectionWidth + 88,
height: fullHeight,
}}
/>
</div>
<TimerGrid />
<div className="p-4">
{characters.map((character) => (
<Character key={character.id} characterId={character.id} />
))}
<Characters />
</div>
<div className="p-4">
<Camera />
@@ -126,35 +77,12 @@ export const Timeline = () => {
<div className="p-4">
<Audio />
</div>
{objects.objects.length > 0 && (
<div className="p-4">
<Objects />
</div>
)}
<div
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
style={{ top: 8, left: displayTime * 4 * scale.value + 84 }}
onPointerDown={onPointerDown}
>
<div>
<FontAwesomeIcon
icon={faSortDown}
className="h-5 text-brand-primary"
/>
</div>
<div
className="block bg-brand-primary"
style={{
width: 2,
marginTop: -5,
height: fullHeight,
}}
/>
</div>
<ObjectGroups />
<Scrubber />
</LowerPanel>
<ConfirmationModal
title="Delete Clip"
text="Are you sure you want to delete the selected clip?"
text={`Are you sure you want to delete the selected ${getItemType(selectedItem.value)}?`}
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onOk={() => {
@@ -1,19 +1,18 @@
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
import { BaseClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useContext, useState } from "react";
import { useState } from "react";
import { scale } from "~/pages/PageEnigma/store";
import { Clip } from "~/pages/PageEnigma/models/track";
import { selectedItem } from "~/pages/PageEnigma/store/selectedItem";
interface Props {
min: number;
max: number;
style: "character" | "camera" | "audio" | "objects";
clip: BaseClip;
clip: Clip;
updateClip: (options: { id: string; offset: number; length: number }) => void;
}
export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
const { selectClip, selectedClip } = useContext(TrackContext);
const [state, setState] = useState({
length: clip.length,
offset: clip.offset,
@@ -26,11 +25,15 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
setState,
);
const selectedClipId = (selectedItem.value as Clip | null)?.clip_uuid ?? "";
const { length, offset } = state;
const classes = [
"absolute",
clip.id === selectedClip ? `bg-${style}-selected` : `bg-${style}-clip`,
clip.clip_uuid === selectedClipId
? `bg-${style}-selected`
: `bg-${style}-clip`,
];
return (
@@ -51,7 +54,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
...classes,
"rounded-l-lg",
"block h-full",
clip.id === selectedClip
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -61,13 +64,13 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
cursor: "w-resize",
}}
onPointerDown={(event) => onPointerDown(event, "left")}
onClick={() => selectClip(clip.id)}
onClick={() => (selectedItem.value = clip)}
/>
<button
className={[
...classes,
"block h-full",
clip.id === selectedClip
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -77,14 +80,14 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
cursor: "move",
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
onClick={() => selectClip(clip.id)}
onClick={() => (selectedItem.value = clip)}
/>
<button
className={[
...classes,
"rounded-r-lg",
"block h-full",
clip.id === selectedClip
clip.clip_uuid === selectedClipId
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -94,7 +97,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
cursor: "e-resize",
}}
onPointerDown={(event) => onPointerDown(event, "right")}
onClick={() => selectClip(clip.id)}
onClick={() => (selectedItem.value = clip)}
/>
</>
);
@@ -1,106 +1,64 @@
import { BaseKeyFrame } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useContext, useState } from "react";
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { useState } from "react";
import { scale } 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 { selectedItem } from "~/pages/PageEnigma/store/selectedItem";
interface Props {
min: number;
max: number;
style: "character" | "camera" | "audio" | "objects";
keyFrame: BaseKeyFrame;
updateClip: (options: { id: string; offset: number; length: number }) => void;
keyframe: Keyframe;
updateKeyframe: (options: { id: string; offset: number }) => void;
}
export const TrackKeyFrame = ({
keyFrame,
// min,
// max,
keyframe,
min,
max,
style,
// updateClip,
updateKeyframe,
}: Props) => {
const { selectedClip } = useContext(TrackContext);
// const { selectClip, selectedClip } = useContext(TrackContext);
const [state, setState] = useState({
offset: keyFrame.offset,
const [offset, setOffset] = useState(keyframe.offset);
const { onPointerDown } = useMouseEventsKeyframe({
keyframe,
max,
min,
updateKeyframe,
setOffset,
});
// const { onPointerDown } = useMouseEventsClip(
// keyFrame,
// max,
// min,
// updateClip,
// setState,
// );
const { offset } = state;
const selectedKeyframeId =
(selectedItem.value as Keyframe | null)?.keyframe_uuid ?? "";
const classes = [
"absolute",
keyFrame.id === selectedClip ? `bg-${style}-selected` : `bg-${style}-clip`,
keyframe.keyframe_uuid === selectedKeyframeId
? `bg-${style}-selected`
: `bg-${style}-clip`,
];
return (
<>
<div
className={[
...classes,
"px-[6px] py-[3px]",
"rounded",
"prevent-select text-xs font-medium text-white",
].join(" ")}
style={{ top: -28, left: offset * 4 * scale.value + 2 }}
>
{keyFrame.name}
</div>
<button
className={[
...classes,
"rounded-l-lg",
"block h-full",
keyFrame.id === selectedClip
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
width: 15,
left: offset * 4 * scale.value,
cursor: "w-resize",
}}
// onPointerDown={(event) => onPointerDown(event, "left")}
// onClick={() => selectClip(clip.id)}
/>
<button
className={[
...classes,
"block h-full",
keyFrame.id === selectedClip
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
width: length * 4 * scale.value - 30,
left: offset * 4 * scale.value + 15,
cursor: "move",
}}
// onPointerDown={(event) => onPointerDown(event, "drag")}
// onClick={() => selectClip(clip.id)}
/>
<button
className={[
...classes,
"rounded-r-lg",
"block h-full",
keyFrame.id === selectedClip
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
width: 15,
left: offset * 4 * scale.value + length * 4 * scale.value - 15,
cursor: "e-resize",
}}
// onPointerDown={(event) => onPointerDown(event, "right")}
// onClick={() => selectClip(clip.id)}
/>
</>
<button
className={[
...classes,
"rounded-l-lg",
"block h-full",
keyframe.keyframe_uuid === selectedKeyframeId
? "border-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
width: 15,
left: offset * 4 * scale.value,
cursor: "w-resize",
}}
onPointerDown={(event) => onPointerDown(event, "left")}
onClick={() => (selectedItem.value = keyframe)}
>
<FontAwesomeIcon icon={faSortUpDown} />
</button>
);
};
@@ -1,118 +1,47 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { BaseKeyFrame } from "~/pages/PageEnigma/models/track";
import { PointerEvent } from "react";
import {
canDrop,
clipLength,
dragType,
dropId,
dropOffset,
filmLength,
scale,
} from "~/pages/PageEnigma/store";
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { filmLength } from "~/pages/PageEnigma/store";
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
interface Props {
id: string;
keyFrames: BaseKeyFrame[];
keyframes: Keyframe[];
title: string;
style: "character" | "audio" | "camera" | "objects";
type?: "animations" | "positions" | "lipSync";
toggleMute?: () => void;
muted?: boolean;
updateClip: (options: { id: string; length: number; offset: number }) => void;
updateKeyframe: (options: { id: string; offset: number }) => void;
}
export const TrackKeyFrames = ({
id,
keyFrames,
toggleMute,
updateClip,
muted,
keyframes,
updateKeyframe,
title,
style,
type,
}: Props) => {
const trackType = type ?? style;
function onPointerOver() {
if (dragType.value !== trackType) {
return;
}
dropId.value = id;
}
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
if (dragType.value !== trackType) {
return;
}
const track = document.getElementById(`track-${trackType}-${id}`);
if (!track) {
return;
}
// Now check the the clip fits
const position = track.getBoundingClientRect();
const clipOffset = (event.clientX - position.x) / 4 / scale.value;
if (clipOffset + clipLength.value > filmLength.value * 60) {
canDrop.value = false;
return;
}
canDrop.value = true;
dropOffset.value = clipOffset;
}
function onPointerLeave() {
if (dragType.value !== trackType) {
return;
}
canDrop.value = false;
}
return (
<div className="pl-16">
<div
id={`track-${trackType}-${id}`}
id={`track-${style}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
>
{keyFrames.map((keyFrame, index) => (
{keyframes.map((keyframe, index) => (
<TrackKeyFrame
key={keyFrame.id}
min={index > 0 ? keyFrames[index - 1].offset + 1 : 0}
key={keyframe.keyframe_uuid}
min={index > 0 ? keyframes[index - 1].offset + 1 : 0}
max={
index < keyFrames.length - 1
? keyFrames[index + 1].offset
index < keyframes.length - 1
? keyframes[index + 1].offset
: filmLength.value * 60
}
style={style}
updateClip={updateClip}
keyFrame={keyFrame}
updateKeyframe={updateKeyframe}
keyframe={keyframe}
/>
))}
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
{title}
</div>
{!!toggleMute && (
<button
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
style={{ top: 6, left: -28 }}
onClick={toggleMute}
>
{muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
)}
</div>
</div>
);
@@ -0,0 +1,84 @@
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { canDrop, scale } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
export const useMouseEventsKeyframe = ({
keyframe,
max,
min,
updateKeyframe,
setOffset,
}: {
keyframe: Keyframe;
max: number;
min: number;
updateKeyframe: (args: { id: string; offset: number }) => void;
setOffset: Dispatch<number>;
}) => {
useSignals();
const currOffset = useRef(keyframe.offset);
const initOffset = useRef(keyframe.offset);
const isActive = useRef("");
const clientX = useRef(0);
const onPointerUp = useCallback(() => {
if (isActive.current) {
updateKeyframe({
id: keyframe.keyframe_uuid,
offset: Math.round(currOffset.current),
});
isActive.current = "";
canDrop.value = false;
}
}, [updateKeyframe, keyframe.keyframe_uuid]);
const onMouseMove = useCallback(
(event: MouseEvent) => {
const delta = (event.clientX - clientX.current) / 4 / scale.value;
const deltaOffset = delta + initOffset.current;
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
return;
}
if (deltaOffset > max) {
currOffset.current = max;
return;
}
currOffset.current = deltaOffset;
}
setOffset(currOffset.current);
},
[max, min, setOffset],
);
useEffect(() => {
currOffset.current = keyframe.offset;
initOffset.current = keyframe.offset;
}, [keyframe.offset]);
useEffect(() => {
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [onPointerUp, onMouseMove]);
return {
onPointerDown: (
event: React.PointerEvent<HTMLButtonElement>,
type: string,
) => {
if (event.button === 0) {
event.stopPropagation();
event.preventDefault();
clientX.current = event.clientX;
isActive.current = type;
}
},
};
};
@@ -1,25 +1,29 @@
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
import { useSignals } from "@preact/signals-react/runtime";
import { useCallback, useEffect } from "react";
import { useCallback, useContext, useEffect } from "react";
import { currentTime } 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 { QueueClip } from "~/pages/PageEnigma/models/track";
interface UpdateTime {
currentTime: number;
}
import {
QueueClip,
QueueKeyframe,
UpdateTime,
} from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { toTimelineActions } from "~/pages/PageEnigma/Queue/toTimelineActions";
interface Arguments {
action: fromEngineActions | toEngineActions;
data: QueueClip | UpdateTime;
action: fromEngineActions | toEngineActions | toTimelineActions;
data: QueueClip | UpdateTime | QueueKeyframe;
}
export function useQueueHandler() {
useSignals();
const handleActions = useCallback(({ action, data }: Arguments) => {
console.log(`FROM ENGINE:${action} ${data}`)
const { addKeyframe } = useContext(TrackContext);
const handleFromEngineActions = useCallback(({ action, data }: Arguments) => {
console.log("FROM ENGINE", action, data);
switch (action) {
case fromEngineActions.UPDATE_TIME:
currentTime.value = (data as UpdateTime).currentTime;
@@ -29,7 +33,19 @@ export function useQueueHandler() {
}
}, []);
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}`);
}
}, []);
useEffect(() => {
Queue.subscribe(QueueNames.FROM_ENGINE, handleActions);
}, [handleActions]);
Queue.subscribe(QueueNames.FROM_ENGINE, handleFromEngineActions);
Queue.subscribe(QueueNames.TO_TIMELINE, handleToTimelineActions);
}, [handleFromEngineActions, handleToTimelineActions]);
}
@@ -0,0 +1,55 @@
import { filmLength, fullHeight, scale } from "~/pages/PageEnigma/store";
import { Fragment } from "react";
export const TimerGrid = () => {
const sectionWidth = 60 * 4 * scale.value;
return (
<div
className={[
"prevent-select mt-4",
"flex h-3",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
>
{Array(filmLength.value)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute ps-1 pt-1"
style={{ left: index * sectionWidth + 92 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth + 88,
height: fullHeight.value,
}}
/>
</Fragment>
))}
<div
className="absolute"
style={{ left: filmLength.value * sectionWidth + 92 }}
>
00:
{filmLength.value < 10
? "0" + filmLength.value.toString()
: filmLength.value.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: filmLength.value * sectionWidth + 88,
height: fullHeight.value,
}}
/>
</div>
);
};
+15 -14
View File
@@ -1,19 +1,20 @@
import { createContext, Dispatch, ReactNode } from "react";
import { AppUiState, AppUiAction } from "../reducers";
import { createContext, Dispatch, ReactNode, useReducer } from "react";
import {
AppUiState,
AppUiAction,
appUiReducer,
appUiInitialStateValues,
} from "../reducers";
export const AppUiContext = createContext<[
AppUiState | null,
Dispatch<AppUiAction>
]>([ null ,()=>{}]);
export const AppUiContext = createContext<[AppUiState, Dispatch<AppUiAction>]>([
appUiInitialStateValues,
() => {},
]);
export const AppUIProvider = ({ children }: { children: ReactNode }) => {
const value = useReducer(appUiReducer, appUiInitialStateValues);
export const AppUIProvider = ({
value,
children,
}:{
value:[AppUiState, Dispatch<AppUiAction>],
children: ReactNode
})=>{
return (
<AppUiContext.Provider value={value}>{children}</AppUiContext.Provider>
);
}
};
@@ -1,72 +1,15 @@
import { createContext } from "react";
import {
AudioGroup,
CameraGroup,
CharacterGroup,
ObjectGroup,
ObjectTrack,
MediaClip,
Keyframe,
QueueKeyframe,
Clip,
} from "~/pages/PageEnigma/models/track";
export const TrackContext = createContext<{
// timeline characters and it's functions
characters: CharacterGroup[];
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => void;
toggleLipSyncMute: (characterId: string) => void;
addCharacterAnimation: (options: {
clipId: string;
characterId: string;
animationClips: MediaClip[];
offset: number;
}) => void;
addCharacterAudio: (options: {
clipId: string;
characterId: string;
audioClips: MediaClip[];
offset: number;
}) => void;
selectCharacterClip: (clipId: string) => void;
deleteCharacterClip: (clipId: string) => void;
// timeline camera group
camera: CameraGroup | null;
updateCamera: (options: {
id: string;
length: number;
offset: number;
}) => void;
selectCameraClip: (clipId: string) => void;
deleteCameraClip: (clipId: string) => void;
// timeline global audio group
audio: AudioGroup | null;
updateAudio: (options: {
id: string;
length: number;
offset: number;
}) => void;
toggleAudioMute: () => void;
addGlobalAudio: (
clipId: string,
animationClips: MediaClip[],
offset: number,
) => void;
selectAudioClip: (clipId: string) => void;
deleteAudioClip: (clipId: string) => void;
// timeline objects group
objects: ObjectGroup;
updateObject: (options: { id: string; offset: number }) => void;
addObject: (options: ObjectTrack) => void;
// current - only select one item - will be replaced
selectedClip: string | null;
selectClip: (clipId: string) => void;
// keyframes
addKeyframe: (keyframe: QueueKeyframe, offset: number) => void;
deleteKeyframe: (keyframe: Keyframe) => void;
// sidebar clips
animationClips: MediaClip[];
@@ -80,32 +23,8 @@ export const TrackContext = createContext<{
) => void;
endDrag: () => void;
}>({
characters: [],
updateCharacters: () => {},
toggleLipSyncMute: () => {},
addCharacterAnimation: () => {},
addCharacterAudio: () => {},
selectCharacterClip: () => {},
deleteCharacterClip: () => {},
camera: null,
updateCamera: () => {},
selectCameraClip: () => {},
deleteCameraClip: () => {},
audio: null,
updateAudio: () => {},
toggleAudioMute: () => {},
selectAudioClip: () => {},
addGlobalAudio: () => {},
deleteAudioClip: () => {},
objects: { id: "", objects: [] },
updateObject: () => {},
addObject: () => {},
selectedClip: null,
selectClip: () => {},
addKeyframe: () => {},
deleteKeyframe: () => {},
animationClips: [],
audioClips: [],
@@ -1,12 +1,14 @@
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters";
import useUpdateCamera from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera";
import useUpdateAudio from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio";
import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject";
import { ClipType, MediaClip } from "~/pages/PageEnigma/models/track";
import {
ClipType,
Keyframe,
Clip,
MediaClip,
} from "~/pages/PageEnigma/models/track";
import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop";
import {
addCharacterAnimation,
canDrop,
dragId,
dragType,
@@ -14,24 +16,17 @@ import {
dropOffset,
} from "~/pages/PageEnigma/store";
import * as uuid from "uuid";
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
interface Props {
children: ReactNode;
}
export const TrackProvider = ({ children }: Props) => {
const characters = useUpdateCharacters();
const camera = useUpdateCamera();
const audio = useUpdateAudio();
const objects = useUpdateObject();
const keyframes = useUpdateKeyframe();
const { endDrag, ...dragDrop } = useUpdateDragDrop();
const [selectedClip, setSelectedClip] = useState<string | null>(null);
const selectClip = useCallback((clipId: string | null) => {
setSelectedClip(clipId);
}, []);
const [animationClips, setAnimationClips] = useState<MediaClip[]>([]);
const [audioClips, setAudioClips] = useState<MediaClip[]>([]);
@@ -57,7 +52,7 @@ export const TrackProvider = ({ children }: Props) => {
const dropClip = useCallback(() => {
if (canDrop.value) {
if (dragType.value === "animations") {
characters.addCharacterAnimation({
addCharacterAnimation({
clipId: dragId.value!,
characterId: dropId.value,
animationClips,
@@ -66,7 +61,7 @@ export const TrackProvider = ({ children }: Props) => {
}
}
endDrag();
}, [animationClips, characters, endDrag]);
}, [animationClips, endDrag]);
const fullWidth = useMemo(() => {
return length * 60 * 4 * scale;
@@ -103,20 +98,43 @@ export const TrackProvider = ({ children }: Props) => {
name: "Walk",
},
]);
setAudioClips([]);
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: uuid.v4(),
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 values = useMemo(() => {
return {
...characters,
...camera,
...audio,
...objects,
selectClip,
selectedClip,
...keyframes,
...dragDrop,
endDrag: dropClip,
@@ -133,13 +151,7 @@ export const TrackProvider = ({ children }: Props) => {
setTimelineHeight,
};
}, [
characters,
camera,
audio,
objects,
selectClip,
selectedClip,
keyframes,
dragDrop,
dropClip,
@@ -1,153 +0,0 @@
import { useCallback, useState } from "react";
import {
AudioGroup,
Clip,
ClipGroup,
ClipType,
MediaClip,
} from "~/pages/PageEnigma/models/track";
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 default function useUpdateAudio() {
const [audio, setAudio] = useState<AudioGroup>({
id: "AU1",
muted: false,
clips: [],
});
const updateAudio = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setAudio((oldAudio) => {
const newClips = [...oldAudio.clips];
const clipIndex = newClips.findIndex((row) => row.clip_uuid === id);
if (clipIndex === -1) {
return { ...oldAudio };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...oldAudio,
clips: newClips,
};
});
},
[],
);
const addGlobalAudio = useCallback(
(dragId: string, audioClips: MediaClip[], offset: number) => {
const clip = audioClips.find((row) => row.media_id === dragId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
offset,
clip_uuid,
} as Clip;
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [...oldAudio.clips, newClip],
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
},
[],
);
const toggleAudioMute = useCallback(() => {
setAudio((oldAudio) => {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: oldAudio?.muted ? toEngineActions.UNMUTE : toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
},
});
return {
...oldAudio,
muted: !oldAudio.muted,
};
});
}, []);
const selectAudioClip = useCallback((clipId: string) => {
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [
...oldAudio.clips.map((clip) => {
return {
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
};
}),
],
};
});
}, []);
const deleteAudioClip = useCallback((clipId: string) => {
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [
...oldAudio.clips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
],
};
});
}, []);
return {
audio,
updateAudio,
toggleAudioMute,
selectAudioClip,
addGlobalAudio,
deleteAudioClip,
};
}
@@ -1,92 +0,0 @@
import { useCallback, useState } from "react";
import { CameraGroup } from "~/pages/PageEnigma/models/track";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
export default function useUpdateCamera() {
const [camera, setCamera] = useState<CameraGroup>({
id: "CA1",
clips: [],
});
const updateCamera = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setCamera((oldCamera) => {
const newClips = [...oldCamera.clips];
const clipIndex = newClips.findIndex((row) => row.clip_uuid === id);
if (clipIndex === -1) {
return { ...oldCamera };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...oldCamera,
clips: newClips,
};
});
},
[],
);
const selectCameraClip = useCallback((clipId: string) => {
setCamera((oldCamera) => {
return {
...oldCamera,
clips: [
...oldCamera.clips.map((clip) => {
return {
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
};
}),
],
};
});
}, []);
const deleteCameraClip = useCallback((clipId: string) => {
setCamera((oldCamera) => {
return {
...oldCamera,
clips: [
...oldCamera.clips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
],
};
});
}, []);
return {
camera,
updateCamera,
selectCameraClip,
deleteCameraClip,
};
}
@@ -1,328 +0,0 @@
import { useCallback, useState } from "react";
import {
Clip,
CharacterGroup,
ClipGroup,
ClipType,
MediaClip,
} from "~/pages/PageEnigma/models/track";
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";
export default function useUpdateCharacters() {
const [characters, setCharacters] = useState<CharacterGroup[]>([
{
id: "CH1",
muted: false,
animationClips: [],
positionClips: [],
lipSyncClips: [],
},
]);
const updateCharacters = useCallback(
({
type,
id,
offset,
length,
}: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => {
if (type === "animations") {
setCharacters((oldCharacters) => {
return oldCharacters.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,
};
});
});
return;
}
if (type === "positions") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newPositionClips = [...character.positionClips];
const clipIndex = newPositionClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newPositionClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
positionClips: newPositionClips,
};
});
});
return;
}
if (type === "lipSync") {
setCharacters((oldCharacters) => {
return oldCharacters.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,
};
});
});
}
},
[],
);
const addCharacterAnimation = useCallback(
({
clipId,
characterId,
animationClips,
offset,
}: {
clipId: string;
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,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
animationClips: [...character.animationClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
},
[],
);
const addCharacterAudio = useCallback(
({
clipId,
characterId,
audioClips,
offset,
}: {
clipId: string;
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,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
},
[],
);
const toggleLipSyncMute = useCallback((characterId: string) => {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id === characterId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: character?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
object_uuid: characterId,
},
});
}
return {
...character,
muted:
character.id === characterId ? !character.muted : character.muted,
};
});
});
}, []);
const selectCharacterClip = useCallback((clipId: string) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionClips.map((clip) => ({
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
})),
];
});
}, []);
const deleteCharacterClip = useCallback((clipId: string) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
animationClips: character.animationClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
positionClips: character.positionClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
lipSyncClips: character.lipSyncClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
})),
];
});
}, []);
return {
characters,
updateCharacters,
toggleLipSyncMute,
addCharacterAnimation,
addCharacterAudio,
selectCharacterClip,
deleteCharacterClip,
};
}
@@ -0,0 +1,47 @@
import { useCallback } from "react";
import {
ClipGroup,
QueueKeyframe,
Keyframe,
} from "~/pages/PageEnigma/models/track";
import {
addCameraKeyframe,
addCharacterKeyframe,
addObjectKeyframe,
deleteCameraKeyframe,
deleteCharacterKeyframe,
deleteObjectKeyframe,
} from "~/pages/PageEnigma/store";
const ADD_KEYFRAME: Record<
ClipGroup,
(keyframe: QueueKeyframe, offset: number) => void
> = {
[ClipGroup.CAMERA]: addCameraKeyframe,
[ClipGroup.CHARACTER]: addCharacterKeyframe,
[ClipGroup.OBJECT]: addObjectKeyframe,
[ClipGroup.GLOBAL_AUDIO]: () => {},
};
const DELETE_KEYFRAME: Record<ClipGroup, (keyframe: Keyframe) => void> = {
[ClipGroup.CAMERA]: deleteCameraKeyframe,
[ClipGroup.CHARACTER]: deleteCharacterKeyframe,
[ClipGroup.OBJECT]: deleteObjectKeyframe,
[ClipGroup.GLOBAL_AUDIO]: () => {},
};
export default function useUpdateKeyframe() {
const addKeyframe = useCallback((keyframe: QueueKeyframe, offset: number) => {
ADD_KEYFRAME[keyframe.group](keyframe, offset);
}, []);
const deleteKeyframe = useCallback((keyframe: Keyframe) => {
console.log("del", keyframe);
DELETE_KEYFRAME[keyframe.group](keyframe);
}, []);
return {
addKeyframe,
deleteKeyframe,
};
}
@@ -1,53 +0,0 @@
import { useCallback, useState } from "react";
import { ObjectGroup, ObjectTrack } from "~/pages/PageEnigma/models/track";
export default function useUpdateObject() {
const [objects, setObjects] = useState<ObjectGroup>({
id: "OB1",
objects: [
// {
// id: "OB1-O1",
// keyFrames: [
// {
// id: "OB1-O1-1",
// offset: 0,
// name: "obj 1",
// },
// ],
// },
],
});
const updateObject = useCallback(
({ id, offset }: { id: string; offset: number }) => {
setObjects((oldObject) => {
return {
id: oldObject.id,
objects: oldObject.objects.map((object) => ({
id: object.id,
keyFrames: object.keyFrames.map((keyFrame) => ({
...keyFrame,
offset: keyFrame.id === id ? offset : keyFrame.offset,
})),
})),
};
});
},
[],
);
const addObject = useCallback((obj: ObjectTrack) => {
setObjects((oldObjectGroup) => {
return {
...oldObjectGroup,
objects: [...oldObjectGroup.objects, obj],
};
});
}, []);
return {
objects,
updateObject,
addObject,
};
}
+214 -123
View File
@@ -3,7 +3,7 @@ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { FreeCam } from "./free_cam";
import { TransformControls } from "three/addons/controls/TransformControls.js";
import Scene from "./scene.js";
import { APIManager, ArtStyle, Visibility } from "./api_manager.js";
import { APIManager, ArtStyle, Visibility } from "./api_manager.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
@@ -101,7 +101,7 @@ class Editor {
positive_prompt: string;
negative_prompt: string;
art_style:ArtStyle;
art_style: ArtStyle;
// Default params.
constructor() {
console.log(
@@ -111,9 +111,8 @@ class Editor {
// Special async react lifecycle fix
// For making sure the editor only gets created onece.
this.can_initialize = false;
let one_element = document.getElementById("created-one-element");
this.can_initialize = true;
let newElement = document.createElement("div");
const newElement = document.createElement("div");
newElement.id = "created-one-element";
document.body.appendChild(newElement);
// life cycle fix
@@ -198,9 +197,10 @@ class Editor {
this.current_scene_glb_media_token = null;
// stylization parameters
this.positive_prompt = "((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.positive_prompt =
"((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;
}
initialize(config: any) {
@@ -212,7 +212,6 @@ class Editor {
// type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADER
// });
if (this.can_initialize == false) {
console.log("Editor Already Initialized");
return;
@@ -224,15 +223,20 @@ class Editor {
this.canvasRenderCamReference = document.getElementById("camera-view");
// Base width and height.
let width = this.canvReference.width;
let height = this.canvReference.height;
const width = this.canvReference.width;
const height = this.canvReference.height;
// Sets up camera and base position.
this.camera = new THREE.PerspectiveCamera(70, width / height, 0.15, 30);
this.camera.position.z = 3;
this.camera.position.y = 3;
this.camera.position.x = -3;
this.render_camera = new THREE.PerspectiveCamera(70, width / height, 0.15, 30);
this.render_camera = new THREE.PerspectiveCamera(
70,
width / height,
0.15,
30,
);
// Base WebGL render and clock for delta time.
this.renderer = new THREE.WebGLRenderer({
@@ -291,9 +295,21 @@ class Editor {
this._test_demo();
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.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,
);
// saving state of the scene
this.current_scene_media_token = null;
@@ -317,7 +333,13 @@ class Editor {
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.addTransformClipBase(
"Camera Object",
"camera",
this.cam_obj,
0,
150,
);
}
console.log(this.cam_obj);
@@ -338,22 +360,27 @@ class Editor {
// Token comes in from the front end to load the scene from the site.
public async testBatchRequest() {
const result = await this.api_manager.getMediaBatch(["m_8fmp9hrvsqcryzka1fra597kg42s50", "m_z4jzbst3xfh64h0qn4bqh4afenfps9"]);
const result = await this.api_manager.getMediaBatch([
"m_8fmp9hrvsqcryzka1fra597kg42s50",
"m_z4jzbst3xfh64h0qn4bqh4afenfps9",
]);
console.log(result);
}
public async testTestTimelineEvents() {
}
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);
});
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);
}
@@ -378,7 +405,9 @@ class Editor {
const loaded_scene = load_scene_state_response.data["scene"];
if (load_scene_state_response.data == null) { return; }
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"];
@@ -395,9 +424,9 @@ class Editor {
child.parent = this.activeScene.scene;
if (child.type == "DirectionalLight") {
let pos = child.position;
let rot = child.rotation;
let light = this.activeScene._create_base_lighting();
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);
@@ -468,7 +497,7 @@ class Editor {
async _serialize_timeline() {
// note the database from the server is the source of truth for all the data.
// Test code here
let object: any = await this.activeScene.load_glb(
const object: any = await this.activeScene.load_glb(
"./resources/models/fox/fox.glb",
);
@@ -494,7 +523,16 @@ class Editor {
// 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),
new ClipUI(
1.0,
"transform",
"character",
"clip2",
object.uuid,
object.uuid,
0,
150,
),
);
// media id for this as well it can be downloaded
@@ -519,7 +557,7 @@ class Editor {
switchCameraView() {
this.camera_person_mode = !this.camera_person_mode;
console.log("camera")
console.log("camera");
if (this.cam_obj) {
if (this.camera_person_mode) {
this.last_cam_pos.copy(this.camera.position);
@@ -536,12 +574,11 @@ class Editor {
if (this.cameraViewControls) {
this.cameraViewControls.enabled = true;
}
this.cam_obj.scale.set(0,0,0);
this.cam_obj.scale.set(0, 0, 0);
if (this.activeScene.hot_items) {
this.activeScene.hot_items.forEach((element) => {
element.visible = false;
});
}
} else {
this.camera.position.copy(this.last_cam_pos);
@@ -555,7 +592,7 @@ class Editor {
if (this.cameraViewControls) {
this.cameraViewControls.enabled = false;
}
this.cam_obj.scale.set(1,1,1);
this.cam_obj.scale.set(1, 1, 1);
if (this.activeScene.hot_items) {
this.activeScene.hot_items.forEach((element) => {
element.visible = true;
@@ -579,7 +616,7 @@ class Editor {
group,
name,
object.uuid, // object id here ...
object.uuid,// Need to change to media id ....
object.uuid, // Need to change to media id ....
offset,
length,
),
@@ -590,7 +627,7 @@ class Editor {
async _test_demo() {
// note the database from the server is the source of truth for all the data.
// Test code here
let object: any = await this.activeScene.load_glb(
const object: any = await this.activeScene.load_glb(
"./resources/models/fox/fox.glb",
);
@@ -651,8 +688,8 @@ class Editor {
// Configure post processing.
_configurePostProcessing() {
let width = this.canvReference.width;
let height = this.canvReference.height;
const width = this.canvReference.width;
const height = this.canvReference.height;
if (this.renderer == undefined || this.camera == undefined) {
return;
@@ -714,7 +751,7 @@ class Editor {
}
create_parim(name: string) {
let uuid = this.activeScene.instantiate(name);
const uuid = this.activeScene.instantiate(name);
}
renderMode() {
@@ -756,11 +793,16 @@ class Editor {
// Basicly Unity 3D's update loop.
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)
this.addTransformClipBase(
"Camera Object",
"camera",
this.cam_obj,
0,
150,
);
}
}
@@ -773,7 +815,7 @@ class Editor {
return;
}
let delta_time = this.clock.getDelta();
const delta_time = this.clock.getDelta();
if (this.cameraViewControls && this.camera_person_mode) {
this.cameraViewControls.update(5 * delta_time);
@@ -813,13 +855,12 @@ class Editor {
}
async convertAudioClip(itteration: number, ffmpeg: FFmpeg, clip: ClipUI) {
let video_og = itteration + 'tmp.mp4';
let wav_name = itteration + 'tmp.wav';
let new_video = (itteration + 1) + 'tmp.mp4';
const video_og = itteration + "tmp.mp4";
const wav_name = itteration + "tmp.wav";
const new_video = itteration + 1 + "tmp.mp4";
let startFrame = clip.offset;
let endFrame = clip.length;
if (endFrame > this.timeline.timeline_limit) {
endFrame = this.timeline.timeline_limit;
}
@@ -829,42 +870,72 @@ class Editor {
const startTime = startFrame / this.cap_fps;
const endTime = endFrame / this.cap_fps;
let end = endTime - startTime;
const end = endTime - startTime;
let duration = this.timeline.timeline_limit / this.cap_fps;
let audioSegment = "as_" + wav_name;
await ffmpeg.FS('writeFile', wav_name, await fetchFile(await this.api_manager.getMediaFile(clip.media_id)));
await ffmpeg.run('-i',
const audioSegment = "as_" + wav_name;
await ffmpeg.FS(
"writeFile",
wav_name,
'-ss', '0',
'-to', '' + end,
"-max_muxing_queue_size", "999999",
audioSegment);
await ffmpeg.run('-i', video_og, "-max_muxing_queue_size", "999999", `${itteration}empty_tmp.wav`);
await fetchFile(await this.api_manager.getMediaFile(clip.media_id)),
);
await ffmpeg.run(
"-i",
wav_name,
"-ss",
"0",
"-to",
"" + end,
"-max_muxing_queue_size",
"999999",
audioSegment,
);
await ffmpeg.run(
'-i', `${itteration}empty_tmp.wav`,
'-i', audioSegment,
'-filter_complex', "[1:a]adelay=" + startTime * 1000 + "|" + startTime * 1000 + "[a1];[0:a][a1]amix=inputs=2[a]",
"-map", "[a]",
`${itteration}final_tmp.wav`)
"-i",
video_og,
"-max_muxing_queue_size",
"999999",
`${itteration}empty_tmp.wav`,
);
await ffmpeg.run(
'-i', video_og,
'-i', `${itteration}final_tmp.wav`,
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v:0',
'-map', '1:a:0',
'-strict', 'experimental',
new_video)
"-i",
`${itteration}empty_tmp.wav`,
"-i",
audioSegment,
"-filter_complex",
"[1:a]adelay=" +
startTime * 1000 +
"|" +
startTime * 1000 +
"[a1];[0:a][a1]amix=inputs=2[a]",
"-map",
"[a]",
`${itteration}final_tmp.wav`,
);
await ffmpeg.run(
"-i",
video_og,
"-i",
`${itteration}final_tmp.wav`,
"-c:v",
"copy",
"-c:a",
"aac",
"-map",
"0:v:0",
"-map",
"1:a:0",
"-strict",
"experimental",
new_video,
);
}
async stopPlayback(compile_audio: boolean = true) {
this.renderMode();
let ffmpeg = createFFmpeg({ log: true });
const ffmpeg = createFFmpeg({ log: true });
await ffmpeg.load();
for (let index = 0; index < this.frame_buffer.length; index++) {
const element = this.frame_buffer[index];
@@ -890,10 +961,9 @@ class Editor {
"-c:a",
"aac", // Specify audio codec (optional, but recommended for MP4)
"-shortest", // Ensure output duration matches the shortest stream (video or audio)
"0tmp.mp4"
"0tmp.mp4",
);
let itteration = 0;
if (compile_audio) {
@@ -902,10 +972,10 @@ class Editor {
await this.convertAudioClip(itteration, ffmpeg, clip);
itteration += 1;
}
};
}
}
let output = await ffmpeg.FS("readFile", itteration + "tmp.mp4");
const output = await ffmpeg.FS("readFile", itteration + "tmp.mp4");
// Create a Blob from the output file for downloading
const blob = new Blob([output.buffer], { type: "video/mp4" });
const url = URL.createObjectURL(blob);
@@ -918,11 +988,9 @@ class Editor {
URL.revokeObjectURL(url);
document.body.removeChild(downloadLink);
let data = await this.api_manager.uploadMedia(blob, "tmp.wav");
const data = await this.api_manager.uploadMedia(blob, "tmp.wav");
console.log(data);
// Create a link to download the file
}
async generateFrame() {
@@ -931,7 +999,7 @@ class Editor {
this.generating_preview = true;
this.activeScene.renderMode(true);
if (this.activeScene.hot_items) {
this.activeScene.hot_items.forEach(element => {
this.activeScene.hot_items.forEach((element) => {
element.visible = false;
});
}
@@ -943,37 +1011,41 @@ class Editor {
this.renderer.setSize(this.render_width, this.render_height);
this.renderer.render(this.activeScene.scene, this.render_camera);
let imgData = this.renderer.domElement.toDataURL();
const imgData = this.renderer.domElement.toDataURL();
this.activeScene.renderMode(false);
this.onWindowResize();
const rawPreview: HTMLVideoElement | null = document.getElementById("raw-preview") as HTMLVideoElement;
const rawPreview: HTMLVideoElement | null = document.getElementById(
"raw-preview",
) as HTMLVideoElement;
if (rawPreview) {
rawPreview.src = imgData;
let ffmpeg = createFFmpeg({ log: false });
const ffmpeg = createFFmpeg({ log: false });
await ffmpeg.load();
await ffmpeg.FS(
"writeFile",
`render.png`,
await fetchFile(imgData),
);
await ffmpeg.run('-i', `render.png`, 'render.mp4');
let output = await ffmpeg.FS("readFile", 'render.mp4');
await ffmpeg.FS("writeFile", `render.png`, await fetchFile(imgData));
await ffmpeg.run("-i", `render.png`, "render.mp4");
const output = await ffmpeg.FS("readFile", "render.mp4");
const blob = new Blob([output.buffer], { type: "video/mp4" });
let 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.", "");
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.",
"",
);
console.log(url);
const stylePreview: HTMLVideoElement | null = document.getElementById("stylized-preview") as HTMLVideoElement;
const stylePreview: HTMLVideoElement | null = document.getElementById(
"stylized-preview",
) as HTMLVideoElement;
if (stylePreview) {
stylePreview.src = url;
stylePreview.width = rawPreview.width;
stylePreview.height = rawPreview.height;
}
else {
console.log("No style preview window.")
} else {
console.log("No style preview window.");
}
this.generating_preview = false;
@@ -981,9 +1053,8 @@ class Editor {
return new Promise((resolve, reject) => {
resolve(url);
});
}
else {
console.log("No raw preview window.")
} else {
console.log("No raw preview window.");
}
this.generating_preview = false;
}
@@ -991,14 +1062,16 @@ class Editor {
generateVideo() {
console.log("Generating video...");
if (this.rendering) { return; }
if (this.rendering) {
return;
}
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 => {
this.activeScene.hot_items.forEach((element) => {
element.visible = false;
});
}
@@ -1046,7 +1119,7 @@ class Editor {
if (this.rendering && this.renderer && this.clock) {
this.playback_location++;
let imgData = this.renderer.domElement.toDataURL();
const imgData = this.renderer.domElement.toDataURL();
this.frame_buffer.push(imgData);
this.render_timer += this.clock.getDelta();
if (this.timeline.is_playing == false) {
@@ -1060,31 +1133,47 @@ class Editor {
}
updateSelectedUI() {
if (this.selected == undefined) { return; }
let pos = this.selected.position;
let rot = this.selected.rotation;
let scale = this.selected.scale;
if (this.selected == undefined) {
return;
}
const pos = this.selected.position;
const rot = this.selected.rotation;
const scale = this.selected.scale;
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
objectVectors: {
position: { x: parseFloat(pos.x.toFixed(2)), y: parseFloat(pos.y.toFixed(2)), z: parseFloat(pos.z.toFixed(2)) },
rotation: { x: parseFloat(rot.x.toFixed(2)), y: parseFloat(rot.y.toFixed(2)), z: parseFloat(rot.z.toFixed(2)) },
scalar: { x: parseFloat(scale.x.toFixed(2)), y: parseFloat(scale.y.toFixed(2)), z: parseFloat(scale.z.toFixed(2)) },
}
}
}
group: "object", // TODO: add meta data to determine what it is a camera or a object or a character into prefab clips
object_uuid: this.selected.uuid,
object_name: this.selected.name,
version: this.version,
objectVectors: {
position: {
x: parseFloat(pos.x.toFixed(2)),
y: parseFloat(pos.y.toFixed(2)),
z: parseFloat(pos.z.toFixed(2)),
},
rotation: {
x: parseFloat(rot.x.toFixed(2)),
y: parseFloat(rot.y.toFixed(2)),
z: parseFloat(rot.z.toFixed(2)),
},
scale: {
x: parseFloat(scale.x.toFixed(2)),
y: parseFloat(scale.y.toFixed(2)),
z: parseFloat(scale.z.toFixed(2)),
},
},
},
});
}
// Automaticly resize scene.
onWindowResize() {
// Calculate the maximum possible dimensions while maintaining the aspect ratio
let width = window.innerWidth; // / aspect_adjust
let height = window.innerHeight; // / aspectRatio
const width = window.innerWidth; // / aspect_adjust
const height = window.innerHeight; // / aspectRatio
if (this.camera == undefined || this.renderer == undefined) {
return;
@@ -1099,7 +1188,9 @@ class Editor {
this.composer.setSize(width, height);
}
if (this.render_camera == undefined) { return; }
if (this.render_camera == undefined) {
return;
}
//this.renderer.setSize(this.render_width, this.render_height);
this.render_camera.aspect = this.render_width / this.render_height;
@@ -1144,7 +1235,7 @@ class Editor {
}
this.raycaster.setFromCamera(this.mouse, this.camera);
let interactable: any[] = [];
const interactable: any[] = [];
this.activeScene.scene.children.forEach((child: THREE.Object3D) => {
if (child.name != "") {
if (child.type == "Mesh" || child.type == "Object3D") {
@@ -1152,7 +1243,7 @@ class Editor {
}
}
});
let intersects = this.raycaster.intersectObjects(interactable, true);
const intersects = this.raycaster.intersectObjects(interactable, true);
if (intersects.length > 0) {
if (intersects[0].object.type != "GridHelper") {
@@ -1176,7 +1267,7 @@ class Editor {
} else if (this.transform_interaction == false) {
this.removeTransformControls();
this.dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT,
});
} else {
this.transform_interaction = false;
+307 -287
View File
@@ -10,323 +10,344 @@ import AnimationEngine from "./animation_engine";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "../Queue/QueueNames";
import { toEngineActions } from "../Queue/toEngineActions";
import { Action } from "@remix-run/router";
import { fromEngineActions } from "../Queue/fromEngineActions";
import { currentTime } from "../store";
// 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
}
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
constructor(timeline_items: ClipUI[] = [],
scrubber_frame_position: number = 0) {
this.timeline_items = timeline_items
this.scrubber_frame_position = scrubber_frame_position
}
timeline_items: ClipUI[];
scrubber_frame_position: number;
constructor(
timeline_items: ClipUI[] = [],
scrubber_frame_position: number = 0,
) {
this.timeline_items = timeline_items;
this.scrubber_frame_position = scrubber_frame_position;
}
}
export class TimeLine {
timeline_items: ClipUI[];
timeline_items: ClipUI[]
timeline_limit: number;
scrubber_frame_position: number;
is_playing: boolean;
timeline_limit: number
scrubber_frame_position: number
is_playing: boolean
// plays audio
audio_engine: AudioEngine;
// key framing
transform_engine: TransformEngine;
// animation engine
animation_engine: AnimationEngine;
// lip sync engine
lipSync_engine: LipSyncEngine;
// plays audio
audio_engine: AudioEngine
// key framing
transform_engine: TransformEngine
// animation engine
animation_engine: AnimationEngine
// lip sync engine
lipSync_engine: LipSyncEngine
scene: Scene;
scene: Scene
current_time: number;
// ensure that the elements are loaded first.
constructor(
audio_engine: AudioEngine,
transform_engine: TransformEngine,
lipsync_engine: LipSyncEngine,
animation_engine: AnimationEngine,
scene: Scene,
) {
this.timeline_items = [];
this.timeline_limit = 60 * 5; // 5 seconds
current_time: number
// ensure that the elements are loaded first.
constructor(audio_engine: AudioEngine,
transform_engine: TransformEngine,
lipsync_engine: LipSyncEngine,
animation_engine: AnimationEngine,
scene: Scene) {
this.is_playing = false;
this.scrubber_frame_position = 0; // in frames into the tl
this.timeline_items = []
this.timeline_limit = 60 * 5 // 5 seconds
// this will be used to play the audio clips
this.audio_engine = audio_engine;
this.transform_engine = transform_engine;
this.lipSync_engine = lipsync_engine;
this.animation_engine = animation_engine;
this.is_playing = false
this.scrubber_frame_position = 0 // in frames into the tl
this.scene = scene;
// this will be used to play the audio clips
this.audio_engine = audio_engine
this.transform_engine = transform_engine
this.lipSync_engine = lipsync_engine
this.animation_engine = animation_engine
Queue.subscribe(
QueueNames.TO_ENGINE,
this.handleTimelineActions.bind(this),
);
this.scene = scene;
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)
}
Queue.subscribe(QueueNames.TO_ENGINE, this.handleTimelineActions.bind(this));
public async pushEvent(action: fromEngineActions, data: any) {
this.current_time += 1;
Queue.publish({
queueName: QueueNames.FROM_ENGINE,
action: fromEngineActions.UPDATE_TIME,
data: data,
});
}
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 handleTimelineActions(data: any) {
const action = data["action"];
switch (action) {
case toEngineActions.ADD_KEYFRAME:
await this.addKeyFrame(data);
break;
case toEngineActions.ADD_CLIP:
await this.addClip(data);
break;
case toEngineActions.DELETE_CLIP:
await this.deleteClip(data);
break;
case toEngineActions.UPDATE_CLIP:
await this.updateClip(data);
break;
case toEngineActions.UPDATE_TIME:
await this.scrub(data);
break;
case toEngineActions.MUTE:
await this.mute(data);
break;
default:
console.log("Action Not Wired");
}
}
public async addKeyFrame(data: any) {
// KeyFrame Object
// version: number;
// clip_uuid: string;
// group: ClipGroup;
// object_uuid?: string;
// offset: number;
// position: XYZ
// rotation: XYZ;
// scale: XYZ;
// selected?: boolean;
}
public async addClip(data: any) {
let object_uuid = data["data"]["object_uuid"];
if (object_uuid.length < 6) {
object_uuid = this.scene.selected?.uuid; //data['data']['object_uuid'];
if (object_uuid == undefined) {
return;
}
}
public async pushEvent(action: fromEngineActions, data: any) {
this.current_time += 1
Queue.publish({
queueName: QueueNames.FROM_ENGINE,
action: fromEngineActions.UPDATE_TIME,
data: data,
});
}
const media_id = data["data"]["media_id"];
const name = data["data"]["name"];
const group = data["data"]["group"];
const version = data["data"]["group"];
const type = data["data"]["type"];
const offset = data["data"]["offset"];
const end_offset = data["data"]["length"] + offset;
public async handleTimelineActions(data: any) {
const action = data["action"]
switch (action) {
case toEngineActions.ADD_KEYFRAME:
await this.addKeyFrame(data)
break
case toEngineActions.ADD_CLIP:
await this.addClip(data)
break
case toEngineActions.DELETE_CLIP:
await this.deleteClip(data)
break
case toEngineActions.UPDATE_CLIP:
await this.updateClip(data)
break
case toEngineActions.UPDATE_TIME:
await this.scrub(data)
break
case toEngineActions.MUTE:
await this.mute(data)
break
default:
console.log("Action Not Wired")
// media id for this as well it can be downloaded
this.addPlayableClip(
new ClipUI(
version,
type,
group,
name,
media_id,
object_uuid,
offset,
end_offset,
),
);
switch (type) {
case "animation":
this.animation_engine.load_object(object_uuid, media_id, name);
break;
case "transform":
this.transform_engine.loadObject(object_uuid, data["data"]["length"]);
break;
}
}
public async updateClip(data: any) {
// only length and offset changes here.
console.log(data);
}
public async deleteClip(data: any) {
console.log(data);
}
public async scrubberUpdate(data: any) {
console.log(data);
}
public async mute(data: any) {
console.log(data);
}
public async addPlayableClip(clip: ClipUI): Promise<void> {
this.timeline_items.push(clip);
}
// when given a media id item it will create the clip.
// Then the clip will be loaded by the engines, if they come from outside of the loaded scene.
public async createClipOffset(
media_id: string,
object_uuid: string,
type: string,
): Promise<void> {
// use engine to load based off media id and type animation | transform |
}
// this will update the state of the clips based off uuid easing?
public async updatePlayableClip(
clip_uuid: string,
updates: AnyJson,
): Promise<void> {}
public async deletePlayableClip(clip_uuid: string): Promise<void> {}
public async scrub(data: any): Promise<void> {
if (this.is_playing) {
return;
}
this.setScrubberPosition(data["data"]["currentTime"]);
}
public async scrubberDidStop(offset_frame: number) {}
// public streaming events into the timeline from
public async setScrubberPosition(offset: number) {
this.scrubber_frame_position = offset; // in ms
}
// should play from the clip that is closest to the to scrubber
public async play(): Promise<void> {
console.log(`Starting Timeline`);
this.is_playing = true;
}
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);
if (object && this.transform_engine.clips[element.object_uuid]) {
this.transform_engine.clips[element.object_uuid].reset(object);
}
} 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();
} else if (element.type == "lipsync") {
this.lipSync_engine.clips[element.object_uuid].reset();
} else {
this.stop();
throw "Error New Type of element in the timeline";
}
}
}
private getEndPoint(): number {
let longest = 0;
for (const element of this.timeline_items) {
if (longest < element.length) {
longest = element.length;
}
}
return longest;
}
// called by the editor update loop on each frame
public async update() {
//if (this.is_playing == false) return; // start and stop
if (this.is_playing) {
this.current_time += 1;
this.pushEvent(fromEngineActions.UPDATE_TIME, {
currentTime: this.current_time,
});
this.scrubber_frame_position = this.current_time;
}
public async addKeyFrame(data: any) {
// KeyFrame Object
// version: number;
// clip_uuid: string;
// group: ClipGroup;
// object_uuid?: string;
// offset: number;
// position: XYZ
// rotation: XYZ;
// scale: XYZ;
// selected?: boolean;
if (this.scrubber_frame_position <= 0) {
await this.resetScene();
}
public async addClip(data: any) {
let object_uuid = data['data']['object_uuid'];
if (object_uuid.length < 6){
object_uuid = this.scene.selected?.uuid;//data['data']['object_uuid'];
if(object_uuid == undefined) { return; }
//this.scrubber_frame_position += 1;
//2. allow stopping.
//3. smallest unit is a frame and it is set by the scene and is in fps, our videos will be 60fps but we can reprocess them using the pipeline.
for (const element of this.timeline_items) {
if (
element.offset <= this.scrubber_frame_position &&
this.scrubber_frame_position <= element.length
) {
// 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 (object && this.transform_engine.clips[element.object_uuid]) {
this.transform_engine.clips[element.object_uuid].length =
element.length - element.offset;
this.transform_engine.clips[element.object_uuid].step(
object,
element.offset,
this.scrubber_frame_position,
);
}
} else if (element.type == "audio") {
if (this.scrubber_frame_position + 1 >= element.length) {
this.audio_engine.stopClip(element.media_id);
} else {
this.audio_engine.playClip(element.media_id);
}
} else if (element.type == "lipsync") {
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") {
if (object) {
await this.animation_engine.clips[object.uuid].play(object);
this.animation_engine.clips[object.uuid].step(
this.scrubber_frame_position / 60,
);
}
} else {
this.stop();
throw "Error New Type of element in the timeline";
}
let media_id = data['data']['media_id'];
let name = data['data']['name'];
let group = data['data']['group'];
let version = data['data']['group'];
let type = data['data']['type'];
let offset = data['data']['offset'];
let end_offset = data['data']['length']+offset;
// media id for this as well it can be downloaded
this.addPlayableClip(
new ClipUI(version, type,
group, name,
media_id, object_uuid,
offset, end_offset));
switch(type) {
case "animation":
this.animation_engine.load_object(object_uuid, media_id, name);
break
case "transform":
this.transform_engine.loadObject(object_uuid, data['data']['length']);
break
}
}
public async updateClip(data: any) {
// only length and offset changes here.
console.log(data)
}
public async deleteClip(data: any) {
console.log(data)
//this.timelineItems = this.timelineItems.filter(item => item !== element)
}
}
public async scrubberUpdate(data: any) {
console.log(data)
if (
this.scrubber_frame_position >= this.timeline_limit &&
this.is_playing
) {
await this.stop();
}
}
public async mute(data: any) {
console.log(data)
}
public async addPlayableClip(clip: ClipUI): Promise<void> {
this.timeline_items.push(clip)
}
// when given a media id item it will create the clip.
// Then the clip will be loaded by the engines, if they come from outside of the loaded scene.
public async createClipOffset(media_id: string, object_uuid: string, type: string): Promise<void> {
// use engine to load based off media id and type animation | transform |
}
// this will update the state of the clips based off uuid easing?
public async updatePlayableClip(clip_uuid: string, updates: AnyJson): Promise<void> {
}
public async deletePlayableClip(clip_uuid: string): Promise<void> {
}
public async scrub(data: any): Promise<void> {
if (this.is_playing) { return }
this.setScrubberPosition(data['data']['currentTime']);
}
public async scrubberDidStop(offset_frame: number) {
}
// public streaming events into the timeline from
public async setScrubberPosition(offset: number) {
this.scrubber_frame_position = offset // in ms
}
// should play from the clip that is closest to the to scrubber
public async play(): Promise<void> {
console.log(`Starting Timeline`)
this.is_playing = true
}
private async resetScene() {
this.timeline_limit = this.getEndPoint();
for (const element of this.timeline_items) {
if (element.type == "transform") {
let object = this.scene.get_object_by_uuid(element.object_uuid);
if (object && this.transform_engine.clips[element.object_uuid]) { this.transform_engine.clips[element.object_uuid].reset(object); }
}
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();
}
else if (element.type == "lipsync") {
this.lipSync_engine.clips[element.object_uuid].reset();
}
else {
this.stop()
throw "Error New Type of element in the timeline"
}
}
}
private getEndPoint(): number {
let longest = 0;
for (const element of this.timeline_items) {
if(longest < element.length) {
longest = element.length;
}
}
return longest;
}
// called by the editor update loop on each frame
public async update(deltatime: number) {
//if (this.is_playing == false) return; // start and stop
if(this.is_playing) {
this.current_time += 1;
this.pushEvent(fromEngineActions.UPDATE_TIME, { currentTime: this.current_time })
this.scrubber_frame_position = this.current_time;
}
if (this.scrubber_frame_position <= 0) {
await this.resetScene();
}
//this.scrubber_frame_position += 1;
//2. allow stopping.
//3. smallest unit is a frame and it is set by the scene and is in fps, our videos will be 60fps but we can reprocess them using the pipeline.
for (const element of this.timeline_items) {
if (element.offset <= this.scrubber_frame_position && this.scrubber_frame_position <= element.length) {
// run async
// element.play()
// remove the element from the list
let object = this.scene.get_object_by_uuid(element.object_uuid)
if (element.type == "transform") {
if (object && this.transform_engine.clips[element.object_uuid]) {
this.transform_engine.clips[element.object_uuid].length = (element.length - element.offset);
this.transform_engine.clips[element.object_uuid].step(object, element.offset, this.scrubber_frame_position);
}
}
else if (element.type == "audio") {
if (this.scrubber_frame_position + 1 >= element.length) {
this.audio_engine.stopClip(element.media_id);
}
else {
this.audio_engine.playClip(element.media_id);
}
}
else if (element.type == "lipsync") {
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") {
if (object) {
await this.animation_engine.clips[object.uuid].play(object);
this.animation_engine.clips[object.uuid].step(this.scrubber_frame_position/60);
}
}
else {
this.stop()
throw "Error New Type of element in the timeline"
}
//this.timelineItems = this.timelineItems.filter(item => item !== element)
}
}
if (this.scrubber_frame_position >= this.timeline_limit && this.is_playing) {
await this.stop();
}
}
private async stop(): Promise<void> {
await this.resetScene();
this.is_playing = false
console.log(`Stopping Timeline`)
this.current_time = 0;
this.pushEvent(fromEngineActions.UPDATE_TIME, { currentTime: this.current_time })
}
private async stop(): Promise<void> {
await this.resetScene();
this.is_playing = false;
console.log(`Stopping Timeline`);
this.current_time = 0;
this.pushEvent(fromEngineActions.UPDATE_TIME, {
currentTime: this.current_time,
});
}
}
// How much timeline precision we have
@@ -387,7 +408,7 @@ export class TimeLine {
// }
// function CheckIfTimeLineStartAfterClipPlays() {
// const timeline = new TimeLine()
// const timeline = new TimeLine()
// timeline.addPlayableClip(new ClipUI("clip3",0,1000))
// timeline.play()
// setInterval(async ()=> {
@@ -397,7 +418,7 @@ export class TimeLine {
// }
// function CheckIfClipsPlayAllTogetherConcurrently() {
// const timeline = new TimeLine()
// 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))
@@ -406,7 +427,7 @@ export class TimeLine {
// }
// function CheckIfClipsPlayAllTogether() {
// const timeline = new TimeLine()
// 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))
@@ -422,4 +443,3 @@ export class TimeLine {
// CheckIfClipsPlayAllTogetherConcurrently()
// CheckIfClipsPlayAllTogether()
+31 -8
View File
@@ -1,3 +1,5 @@
import { XYZ } from "../datastructures/common";
export interface QueueClip {
version: number;
type: ClipType;
@@ -45,10 +47,28 @@ export enum ClipGroup {
OBJECT = "object",
}
export interface BaseKeyFrame {
id: string;
name: string;
export interface Keyframe {
version: number;
keyframe_uuid: string;
group: ClipGroup;
object_uuid: string;
offset: number;
position: XYZ;
rotation: XYZ;
scale: XYZ;
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;
}
@@ -56,13 +76,13 @@ export interface CharacterGroup {
id: string;
muted: boolean;
animationClips: Clip[];
positionClips: Clip[];
positionKeyframes: Keyframe[];
lipSyncClips: Clip[];
}
export interface CameraGroup {
id: string;
clips: Clip[];
keyframes: Keyframe[];
}
export interface AudioGroup {
@@ -77,8 +97,11 @@ export interface ObjectGroup {
}
export interface ObjectTrack {
id: string;
keyFrames: ObjectKeyFrame[];
object_uuid: string;
name: string;
keyframes: Keyframe[];
}
export interface ObjectKeyFrame extends BaseKeyFrame {}
export interface UpdateTime {
currentTime: number;
}
@@ -1,94 +1,95 @@
import { State, Action, ACTION_TYPES} from './types';
import { State, Action, ACTION_TYPES } from "./types";
export function reducer(state: State, action: Action): State {
switch(action.type){
case ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT:{
return{
switch (action.type) {
case ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT: {
console.log("pay", action.payload);
return {
...state,
currentSceneObject: {
controlPanel: {
isShowing: true,
objectVectors: {...action.payload.currentSceneObject.objectVectors},
currentSceneObject: action.payload,
},
}
};
}
case ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT:{
return{
case ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT: {
return {
...state,
currentSceneObject: {
isShowing: state.currentSceneObject.isShowing,
objectVectors: {...action.payload.currentSceneObject.objectVectors},
controlPanel: {
isShowing: state.controlPanel.isShowing,
currentSceneObject: action.payload,
},
}
};
}
case ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT:{
return{
case ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT: {
return {
...state,
currentSceneObject: {
controlPanel: {
...state.controlPanel,
isShowing: false,
objectVectors: state.currentSceneObject.objectVectors,
},
}
};
}
case ACTION_TYPES.SHOW_EDITOR_LOADER:{
return{
case ACTION_TYPES.SHOW_EDITOR_LOADER: {
return {
...state,
showEditorLoader: {
isShowing: true,
message: action.payload?.showEditorLoader.message,
},
}
};
}
case ACTION_TYPES.HIDE_EDITOR_LOADER:{
return{
case ACTION_TYPES.HIDE_EDITOR_LOADER: {
return {
...state,
showEditorLoader: {
isShowing: false,
message: state.showEditorLoader.message
message: state.showEditorLoader.message,
},
}
};
}
case ACTION_TYPES.SHOW_EDITOR_LOADINGBAR:{
return{
case ACTION_TYPES.SHOW_EDITOR_LOADINGBAR: {
return {
...state,
showEditorLoadingBar: {
...action.payload?.showEditorLoadingBar,
isShowing: true,
},
}
};
}
case ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR:{
return{
case ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR: {
return {
...state,
showEditorLoadingBar: {
...state.showEditorLoadingBar,
...action.payload?.showEditorLoadingBar,
},
}
};
}
case ACTION_TYPES.HIDE_EDITOR_LOADINGBAR:{
return{
case ACTION_TYPES.HIDE_EDITOR_LOADINGBAR: {
return {
...state,
showEditorLoadingBar: {
...state.showEditorLoadingBar,
isShowing: false,
},
}
};
}
case ACTION_TYPES.ON_TIMELINE_RESIZE:{
return{
case ACTION_TYPES.ON_TIMELINE_RESIZE: {
return {
...state,
timelineHeight: action.payload.timelineHeight,
}
};
}
case ACTION_TYPES.ON_CHANGE_VIEW_MODE:{
case ACTION_TYPES.ON_CHANGE_VIEW_MODE: {
console.log(action);
return{
return {
...state,
viewMode: action.payload.viewMode
}
viewMode: action.payload.viewMode,
};
}
default:{
default: {
return state; //no change
}
}// end switch
} // end switch
}
@@ -1,32 +1,41 @@
import { Simple3DVector } from "../../datastructures/common";
import { ClipGroup } from "../../models/track";
export enum VIEW_MODES {
EDITOR = 'editor',
SIDE_BY_SIDE = 'side-by-side'
};
EDITOR = "editor",
SIDE_BY_SIDE = "side-by-side",
}
type ViewModes = VIEW_MODES.EDITOR | VIEW_MODES.SIDE_BY_SIDE;
export type ControlPanel = {
isShowing: boolean;
currentSceneObject: SceneObject;
};
export type SceneObject = {
group: ClipGroup; // TODO: add meta data to determine what it is a camera or a object or a character into prefab clips
object_uuid: string;
object_name: string;
version: string;
objectVectors: Simple3DVector;
};
export type State = {
viewMode: ViewModes;
timelineHeight: number;
showEditorLoader: {
isShowing:boolean;
message?:string;
isShowing: boolean;
message?: string;
};
showEditorLoadingBar:{
showEditorLoadingBar: {
isShowing: boolean;
label?: string;
message?: string;
progress?: number;
useFakeTimer?: number;
}
currentSceneObject:{
isShowing: boolean;
objectVectors: Simple3DVector;
}
};
controlPanel: ControlPanel;
};
export enum ACTION_TYPES {
ON_TIMELINE_RESIZE = "on_timeline_resize",
ON_CHANGE_VIEW_MODE = "on_change_view_mode",
@@ -38,67 +47,61 @@ export enum ACTION_TYPES {
SHOW_CONTROLPANELS_SCENEOBJECT = "show_controlpanels_sceneobject",
UPDATE_CONTROLPANELS_SCENEOBJECT = "update_controlpanels_sceneobject",
HIDE_CONTROLPANELS_SCENEOBJECT = "hide_controlpanels_sceneobject",
};
}
export type Action =
| {type: ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT}
| {
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload:{
currentSceneObject: {
objectVectors: Simple3DVector;
};
}
// Not sure why we need this but type script won't stop complaning fix wil.
type: ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT;
}
| {
type: ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
payload:{
currentSceneObject: {
objectVectors: Simple3DVector;
};
}
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT;
payload: SceneObject;
}
| {type: ACTION_TYPES.HIDE_EDITOR_LOADER,}
| {
type: ACTION_TYPES.SHOW_EDITOR_LOADER,
payload?:{
type: ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT;
payload: SceneObject;
}
| { type: ACTION_TYPES.HIDE_EDITOR_LOADER }
| {
type: ACTION_TYPES.SHOW_EDITOR_LOADER;
payload?: {
showEditorLoader: {
message:string|undefined;
}
}
message: string | undefined;
};
};
}
| {type: ACTION_TYPES.HIDE_EDITOR_LOADINGBAR,}
| { type: ACTION_TYPES.HIDE_EDITOR_LOADINGBAR }
| {
type: ACTION_TYPES.SHOW_EDITOR_LOADINGBAR,
payload?:{
type: ACTION_TYPES.SHOW_EDITOR_LOADINGBAR;
payload?: {
showEditorLoadingBar: {
label?: string;
message?: string;
progress?: number;
useFakeTimer?: number;
}
}
};
};
}
| {
type: ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
payload:{
type: ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR;
payload: {
showEditorLoadingBar: {
label?: string;
message?: string;
progress?: number;
}
}
};
};
}
| {
type: ACTION_TYPES.ON_TIMELINE_RESIZE,
type: ACTION_TYPES.ON_TIMELINE_RESIZE;
payload: {
timelineHeight: number
}
timelineHeight: number;
};
}
| {
type: ACTION_TYPES.ON_CHANGE_VIEW_MODE,
payload: {
viewMode: ViewModes
}
}
;
type: ACTION_TYPES.ON_CHANGE_VIEW_MODE;
payload: {
viewMode: ViewModes;
};
};
@@ -1,23 +1,30 @@
import {State, VIEW_MODES} from './types';
import { State, VIEW_MODES } from "./types";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
export const initialStateValues : State = {
export const initialStateValues: State = {
timelineHeight: 260,
viewMode: VIEW_MODES.EDITOR,
showEditorLoader: {
isShowing:false,
message: 'Loading Editor Engine 🦊'
isShowing: false,
message: "Loading Editor Engine 🦊",
},
showEditorLoadingBar: {
isShowing:true,
progress:5,
message: 'Loading Editor Engine 🦊'
isShowing: true,
progress: 5,
message: "Loading Editor Engine 🦊",
},
currentSceneObject:{
isShowing:false,
objectVectors:{
position:{x:0,y:0,z:0},
rotation:{x:0,y:0,z:0},
scale:{x:0,y:0,z:0},
}
}
}
controlPanel: {
isShowing: false,
currentSceneObject: {
group: ClipGroup.OBJECT,
object_name: "",
object_uuid: "",
version: "1",
objectVectors: {
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 },
scale: { x: 0, y: 0, z: 0 },
},
},
},
};
+136
View File
@@ -0,0 +1,136 @@
import {
AudioGroup,
Clip,
ClipGroup,
ClipType,
MediaClip,
} from "~/pages/PageEnigma/models/track";
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";
export const audioGroup = signal<AudioGroup>({
id: "AG-1",
clips: [],
muted: false,
});
export function updateAudio({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) {
const oldAudioGroup = audioGroup.value;
const newClips = [...oldAudioGroup.clips];
const clipIndex = newClips.findIndex((row) => row.clip_uuid === id);
if (clipIndex === -1) {
return { ...oldAudioGroup };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
audioGroup.value = {
...oldAudioGroup,
clips: newClips,
};
}
export function addGlobalAudio(
dragId: string,
audioClips: MediaClip[],
offset: number,
) {
const clip = audioClips.find((row) => row.media_id === dragId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
offset,
clip_uuid,
} as Clip;
const oldAudioGroup = audioGroup.value;
audioGroup.value = {
...oldAudioGroup,
clips: [...oldAudioGroup.clips, newClip],
};
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function toggleAudioMute() {
const oldAudioGroup = audioGroup.value;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: oldAudioGroup?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
},
});
audioGroup.value = {
...oldAudioGroup,
muted: !oldAudioGroup.muted,
};
}
export function selectAudioClip(clipId: string) {
const oldAudioGroup = audioGroup.value;
audioGroup.value = {
...oldAudioGroup,
clips: [
...oldAudioGroup.clips.map((clip) => {
return {
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
};
}),
],
};
}
export function deleteAudioClip(clip: Clip) {
const oldAudioGroup = audioGroup.value;
audioGroup.value = {
...oldAudioGroup,
clips: [
...oldAudioGroup.clips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
],
};
}
+99
View File
@@ -0,0 +1,99 @@
import {
CameraGroup,
Keyframe,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
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";
export const cameraGroup = signal<CameraGroup>({ id: "CG1", keyframes: [] });
export function updateCamera({ id, offset }: { id: string; offset: number }) {
const oldCameraGroup = cameraGroup.value;
const newKeyframes = [...oldCameraGroup.keyframes];
const keyframe = newKeyframes.find((row) => row.keyframe_uuid === id);
if (!keyframe) {
return;
}
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: keyframe,
});
cameraGroup.value = {
...oldCameraGroup,
keyframes: newKeyframes,
};
}
export function addCameraKeyframe(keyframe: QueueKeyframe, offset: number) {
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [...oldCameraGroup.keyframes, newKeyframe].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
}
export function selectCameraKeyframe(keyframeId: string) {
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [
...oldCameraGroup.keyframes.map((keyframe) => {
return {
...keyframe,
selected:
keyframe.keyframe_uuid === keyframeId
? !keyframe.selected
: keyframe.selected,
};
}),
],
};
}
export function deleteCameraKeyframe(deleteKeyframe: Keyframe) {
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [
...oldCameraGroup.keyframes.filter((keyframe) => {
if (keyframe.keyframe_uuid === deleteKeyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: keyframe,
});
return false;
}
return true;
}),
],
};
}
@@ -0,0 +1,331 @@
import {
CharacterGroup,
Clip,
ClipGroup,
ClipType,
Keyframe,
MediaClip,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
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,
positionClips: 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 function addCharacterAnimation({
clipId,
characterId,
animationClips,
offset,
}: {
clipId: string;
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,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
animationClips: [...character.animationClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function addCharacterAudio({
clipId,
characterId,
audioClips,
offset,
}: {
clipId: string;
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,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function addCharacterKeyframe(keyframe: QueueKeyframe, offset: number) {
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== keyframe.object_uuid) {
return { ...character };
}
return {
...character,
positionKeyframes: [...character.positionKeyframes, newKeyframe].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
}
export function toggleLipSyncMute(characterId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id === characterId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: character?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
object_uuid: characterId,
},
});
}
return {
...character,
muted: character.id === characterId ? !character.muted : character.muted,
};
});
}
export function selectCharacterClip(clipId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionKeyframes.map((keyframe) => ({
...keyframe,
selected:
keyframe.keyframe_uuid === clipId
? !keyframe.selected
: keyframe.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
}));
}
export function deleteCharacterClip(clip: Clip) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
animationClips: character.animationClips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
lipSyncClips: character.lipSyncClips.filter((row) => {
if (row.clip_uuid === clip.clip_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: row,
});
return false;
}
return true;
}),
}));
}
export function deleteCharacterKeyframe(keyframe: Keyframe) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
positionClips: character.positionKeyframes.filter((row) => {
if (row.keyframe_uuid === keyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: row,
});
return false;
}
return true;
}),
}));
}
+4
View File
@@ -1,3 +1,7 @@
export * from "./audioGroup";
export * from "./cameraGroup";
export * from "./characterGroups";
export * from "./dragAndDrop";
export * from "./objectGroup";
export * from "./sizing";
export * from "./timing";
+121
View File
@@ -0,0 +1,121 @@
import { signal } from "@preact/signals-core";
import {
Keyframe,
ObjectGroup,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
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";
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) => ({
...keyFrame,
offset: keyFrame.keyframe_uuid === id ? offset : 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,
);
const newObject = obj ?? {
object_uuid: keyframe.object_uuid,
name: keyframe.object_name ?? "unknown",
keyframes: [] as Keyframe[],
};
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
newObject.keyframes.push(newKeyframe);
console.log(newObject);
newObject.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
objectGroup.value = {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
),
newObject,
].sort((objA, objB) => (objA.object_uuid < objB.object_uuid ? -1 : 1)),
};
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
}
export function deleteObjectKeyframe(keyframe: Keyframe) {
const oldObjectGroup = objectGroup.value;
const obj = oldObjectGroup.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
console.log("delete", obj);
if (!obj) {
return oldObjectGroup;
}
const newKeyframes = [
...obj.keyframes.filter((row) => {
if (row.keyframe_uuid === keyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: row,
});
return false;
}
return true;
}),
];
if (newKeyframes.length) {
return {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.map((object) => ({
...object,
...(object.object_uuid === keyframe.object_uuid
? { keyframes: newKeyframes }
: {}),
})),
],
};
}
objectGroup.value = {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
),
],
};
}
@@ -0,0 +1,4 @@
import { signal } from "@preact/signals-core";
import { Clip, Keyframe } from "~/pages/PageEnigma/models/track";
export const selectedItem = signal<Clip | Keyframe | null>(null);
+11
View File
@@ -1,4 +1,6 @@
import { signal, computed } from "@preact/signals-core";
import { characterGroups } from "~/pages/PageEnigma/store/characterGroups";
import { objectGroup } from "~/pages/PageEnigma/store/objectGroup";
export const scale = signal(1);
export const filmLength = signal(12);
@@ -6,3 +8,12 @@ export const timelineHeight = signal(0);
export const fullWidth = computed(() => {
return filmLength.value * 60 * 4 * scale.value;
});
export const fullHeight = computed(() => {
return (
characterGroups.value.length * 268 +
objectGroup.value.objects.length * 60 +
300 +
96
);
});