Merge branch 'test' into feature/loading-saving-scene

This commit is contained in:
Michael Chung committed 2024-03-27 11:26:03 -04:00
commit 580bc86d7e
46 files changed
+1565 -575

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import { Dialog, Transition } from "@headlessui/react";
import { Button } from "~/components/Button";
import { Fragment } from "react";
interface Props {
text: string;
title: string;
open: boolean;
onClose: () => void;
onOk?: () => void;
okText?: string;
okColor?: string;
onCancel?: () => void;
cancelText?: string;
}
export const ConfirmationModal = ({
text,
title,
open,
onClose,
onOk,
okText = "OK",
okColor = "bg-brand-success",
onCancel,
cancelText = "Cancel",
}: Props) => {
return (
<Transition appear show={open} as={Fragment}>
<Dialog open={open} onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/40" />
</Transition.Child>
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h4"
className="mb-4 text-xl font-bold text-white"
>
{title}
</Dialog.Title>
<div className="mt-2">{text}</div>
<div className="mt-6 flex justify-end gap-2">
{!!onCancel && (
<Button
type="button"
onClick={onCancel}
className="rounded-lg px-3 py-2"
variant="secondary"
>
{cancelText}
</Button>
)}
{!!onOk && (
<Button
type="button"
onClick={onOk}
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
>
{okText}
</Button>
)}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
};
+16 -1
View File
@@ -1,16 +1,31 @@
import { twMerge } from "tailwind-merge";
import { H4 } from ".";
interface ItemPickerProps extends React.ImgHTMLAttributes<HTMLImageElement> {
label: string
selected: boolean;
onSelected : (val:string)=>void;
}
export const ItemPicker = ({
label,
selected = false,
onSelected,
...imgProps
}:ItemPickerProps) => {
const handleSelected = ()=>{
onSelected(label);
};
return (
<div className="relative border border-ui-border rounded-lg overflow-hidden">
<div
className={twMerge("relative border-2 rounded-lg overflow-hidden cursor-pointer 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 to-transparent absolute top-0 left-0 w-full h-full"/>
<H4 className="absolute bottom-0 left-2 drop-shadow-md">{label}</H4>
</div>
)
+71
View File
@@ -0,0 +1,71 @@
import { useState, useEffect } from 'react';
import { twMerge } from 'tailwind-merge';
interface LoadingDotsProps {
className?: string;
show?: boolean;
transition?: boolean;
transitionDuration?: number;
};
interface LoadingDotsInnerProps {
className?: string;
show?: boolean;
transition?: boolean;
transitionDuration?: number;
children: React.ReactNode;
};
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props}>
<div className="dot-typing"></div>
</LoadingDots>
)
}
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props}>
<div className="dot-bricks"></div>
</LoadingDots>
)
}
function LoadingDots({
className,
show=true,
transition = false,
transitionDuration = 1000,
children,
}: LoadingDotsInnerProps){
const [state, setState] = useState<'loading'|'loaded'|'completed'>('loading');
const transitionClasses =
(transition ? `transition-opacity duration-${transitionDuration} `: "")
+ (state === 'loading' ? 'opacity-100' : 'opacity-0');
const classNames = twMerge("w-full h-full flex justify-center items-center bg-ui-background ",transitionClasses, className);
useEffect(()=>{
if(!show){
if(transition){
setState((curr)=>{
if (curr === 'loading') return 'loaded'
else return curr;
});
setTimeout(()=>setState('completed'), 2000);
} else {
setState('completed');
}
}
}, [show, transition]);
if(state!== 'completed'){
return(
<div className={classNames}>
{children}
</div>
);
}else{
return null;
}
};
+8 -1
View File
@@ -7,11 +7,18 @@ export const Textarea = ({
}:TextareaHTMLAttributes<HTMLTextAreaElement>) => {
return(
<textarea
{...rest}
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}
/>
);
}
+3
View File
@@ -6,6 +6,7 @@ import { Input } from "./Input";
import { H4 } from "./H4";
import { ItemPicker } from "./ItemPicker";
import { Textarea } from "./Textarea";
import { LoadingDotsBricks, LoadingDotsTyping } from "./LoadingDots";
export {
Button,
@@ -16,4 +17,6 @@ export {
ItemPicker,
Textarea,
Input,
LoadingDotsBricks,
LoadingDotsTyping,
};
-128
View File
@@ -1,128 +0,0 @@
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import useUpdateCharacters from "~/contexts/TrackContext/utils/useUpdateCharacters";
import useUpdateCamera from "~/contexts/TrackContext/utils/useUpdateCamera";
import useUpdateAudio from "~/contexts/TrackContext/utils/useUpdateAudio";
import useUpdateObject from "~/contexts/TrackContext/utils/useUpdateObject";
import { AnimationClip, AudioClip } from "~/models/track";
interface Props {
children: ReactNode;
}
export const TrackProvider = ({ children }: Props) => {
const { characters, updateCharacters, toggleLipSyncMute } =
useUpdateCharacters();
const { camera, updateCamera } = useUpdateCamera();
const { audio, updateAudio, toggleAudioMute } = useUpdateAudio();
const { objects, updateObject } = useUpdateObject();
const [selectedClip, setSelectedClip] = useState<string | null>(null);
const selectClip = useCallback((clipId: string | null) => {
setSelectedClip(clipId);
}, []);
const [state, setState] = useState<{
dragType: "animations" | "lipSync" | null;
dragId: string | null;
}>({ dragType: null, dragId: null });
const [animationClips, setAnimationClips] = useState<AnimationClip[]>([]);
const [audioClips, setAudioClips] = useState<AudioClip[]>([]);
const startDrag = useCallback(
(type: "animations" | "lipSync", id: string) => {
setState({ dragId: id, dragType: type });
},
[],
);
const endDrag = useCallback(() => {
setState({ dragId: null, dragType: null });
}, []);
const [time, setTime] = useState(0);
const updateCurrentTime = useCallback((newTime: number) => {
setTime(newTime);
}, []);
const [canDrop, setCanDrop] = useState(false);
const [scale, setScale] = useState(1);
const [length, setLength] = useState(1);
const fullWidth = useMemo(() => {
return length * 60 * 4 * scale;
}, [length, scale]);
useEffect(() => {
setAnimationClips([
{
id: "ani-id1",
offset: 0,
length: 124,
name: "ani 11",
},
]);
setAudioClips([]);
setScale(1);
setLength(12);
}, []);
const values = useMemo(() => {
return {
characters,
camera,
audio,
objects,
updateCharacters,
updateCamera,
updateAudio,
updateObject,
selectClip,
selectedClip,
toggleLipSyncMute,
toggleAudioMute,
dragType: state.dragType,
dragId: state.dragId,
animationClips,
audioClips,
startDrag,
endDrag,
scale,
currentTime: time,
length,
updateCurrentTime,
canDrop,
setCanDrop,
fullWidth,
};
}, [
characters,
updateCharacters,
selectClip,
selectedClip,
camera,
updateCamera,
audio,
updateAudio,
objects,
updateObject,
toggleLipSyncMute,
toggleAudioMute,
state.dragId,
state.dragType,
animationClips,
audioClips,
startDrag,
endDrag,
canDrop,
updateCurrentTime,
setCanDrop,
time,
fullWidth,
length,
scale,
]);
return (
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
);
};
@@ -1,66 +0,0 @@
import { useCallback, useState } from "react";
import { AudioGroup } from "~/models/track";
export default function useUpdateAudio() {
const [audio, setAudio] = useState<AudioGroup>({
id: "AU1",
muted: false,
clips: [
{
id: "AU1-1",
length: 200,
offset: 0,
name: "audio 1",
},
{
id: "AU1-2",
length: 180,
offset: 300,
name: "audio 2",
},
],
});
const updateAudio = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setAudio((oldAudio) => {
const newClips = [...oldAudio.clips];
const clipIndex = newClips.findIndex((row) => row.id === id);
if (clipIndex === -1) {
return { ...oldAudio };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...oldAudio,
clips: newClips,
};
});
},
[],
);
const toggleAudioMute = useCallback(() => {
setAudio((oldAudio) => {
return {
...oldAudio,
muted: !oldAudio.muted,
};
});
}, []);
return {
audio,
updateAudio,
toggleAudioMute,
};
}
@@ -1,138 +0,0 @@
import { useCallback, useRef, useState } from "react";
import { CharacterGroup } from "~/models/track";
export default function useUpdateCharacters() {
const [characters, setCharacters] = useState<CharacterGroup[]>([
{
id: "CH1",
muted: false,
animationClips: [
{
id: "CH1-A1",
length: 200,
offset: 0,
name: "ani 1",
},
{
id: "CH1-A2",
length: 180,
offset: 300,
name: "ani 2",
},
],
positionClips: [
{
id: "CH1-P1",
length: 200,
offset: 100,
name: "pos 1",
},
{
id: "CH1-P2",
length: 180,
offset: 500,
name: "pos 2",
},
],
lipSyncClips: [
{
id: "CH1-L1",
length: 400,
offset: 150,
name: "sync 1",
},
],
},
]);
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.id === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...character,
animationClips: newAnimationClips,
};
});
});
return;
}
if (type === "positions") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newPositionClips = [...character.positionClips];
const clipIndex = newPositionClips.findIndex(
(row) => row.id === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newPositionClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...character,
positionClips: newPositionClips,
};
});
});
return;
}
if (type === "lipSync") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex((row) => row.id === id);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
});
}
},
[],
);
const toggleLipSyncMute = useCallback((characterId: string) => {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => ({
...character,
muted:
character.id === characterId ? !character.muted : character.muted,
}));
});
}, []);
return {
characters,
updateCharacters,
toggleLipSyncMute,
};
}
+23 -5
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import React, { useCallback, useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
interface LowerPanelPropsI {
timelineHeight: number;
@@ -6,10 +7,22 @@ interface LowerPanelPropsI {
}
export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
// const [open, setOpen] = useState(false);
const { setOverTimeline, updateCurrentTime, scale } =
useContext(TrackContext);
const onTimelineClick = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
updateCurrentTime((Math.round(event.clientX) - 92) / 4 / scale);
}
},
[updateCurrentTime, scale],
);
// const handleOpen = () => {
// setOpen(true);
// };
return (
<div
@@ -20,6 +33,11 @@ export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
"bg-ui-panel",
].join(" ")}
style={{ height: timelineHeight }}
onPointerOver={() => {
setOverTimeline(true);
}}
onPointerLeave={() => setOverTimeline(false)}
onPointerDown={onTimelineClick}
>
{children}
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
import { PageEnigmaComponent } from "./PageEnigmaComponent";
import { TrackProvider } from "~/contexts/TrackContext/TrackProvider";
import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackProvider";
export const PageEnigma = () => {
return (
+66 -66
View File
@@ -1,92 +1,92 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useReducer } from "react";
import { faSparkles } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "~/components";
import { Button, LoadingDotsBricks } from "~/components";
import { TopBarHelmet } from "~/modules/TopBarHelmet/TopBarHelmet";
import { SidePanel } from "~/modules/SidePanel";
import { Controls3D } from "./comps/Controls3D";
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
import { ControlsVideo } from "./comps/ControlsVideo";
import { PreviewWindow } from "./comps/PreviewWindow";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { Timeline } from "./comps/Timeline";
import { SidePanelTabs } from "./comps/SidePanelTabs";
import { EngineProvider } from "~/contexts/EngineProvider";
import { EngineProvider } from "./contexts/EngineProvider";
import { AppUIProvider } from "./contexts/AppUiContext";
import { reducer, initialState, ACTION_TYPES } from "./reducer";
import { VIEW_MODES } from "./reducer/types";
import { ViewSideBySide } from "./comps/ViewSideBySide";
export const PageEnigmaComponent = () => {
const [appUiState, dispatchAppUiState] = useReducer(reducer, initialState);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [timelineHeight, setTimelineHeight] = useState(260);
const timelineRef = useRef<HTMLDivElement | null>(null);
const updateTimelineHeight = useCallback(() => {
if (timelineRef.current) {
setTimelineHeight(timelineRef.current.offsetHeight);
}
}, []);
//for updating timeline/engine div height (for resizing)
useEffect(() => {
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
updateTimelineHeight();
}
});
if (timelineRef.current) {
observer.observe(timelineRef.current);
}
return () => observer.disconnect();
}, [updateTimelineHeight]);
useEffect(()=>{
setTimeout(()=>dispatchAppUiState({
type: ACTION_TYPES.HIDE_EDITOR_LOADER
}), 3000);
},[]);
return (
<div>
<TopBarHelmet>
<Button icon={faSparkles}>Generate Movie</Button>
</TopBarHelmet>
<EngineProvider>
<TopBarHelmet>
<Button icon={faSparkles}>Generate Movie</Button>
</TopBarHelmet>
<div style={{ height: "calc(100vh - 68px)" }}>
{/* Engine section/side panel */}
<div
id="CanvasUiWrapper"
className="flex"
// style={{ height: `calc(100% - ${timelineHeight}px)` }}
style={{ height: `calc(100% - 260px` }}
>
<div className="relative w-full overflow-hidden bg-gray-400">
<canvas
ref={canvasRef}
id="video-scene"
width="1280px"
height="720px"
/>
<AppUIProvider value={[appUiState, dispatchAppUiState]} >
<div style={{ height: "calc(100vh - 68px)" }}>
{/* Engine section/side panel */}
<div
id="CanvasUiWrapper"
className="flex"
style={{ height: `calc(100% - ${appUiState.timelineHeight}px)` }}
// style={{ height: `calc(100% - 260px` }}
>
<div className="relative w-full overflow-hidden bg-transparent">
<div className={(appUiState.viewMode === VIEW_MODES.SIDE_BY_SIDE) ? 'invisible' : ''}>
<canvas
ref={canvasRef}
id="video-scene"
width="1280px"
height="720px"
/>
{/* Top controls */}
<div className="absolute left-0 top-0 w-full">
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D />
{/* Top controls */}
<div className="absolute left-0 top-0 w-full">
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D />
</div>
</div>
{/* Bottom controls */}
<div className="absolute bottom-0 left-0 w-full">
<PreviewEngineCamera />
<ControlsVideo />
</div>
</div>
{
appUiState.viewMode === VIEW_MODES.SIDE_BY_SIDE &&
<ViewSideBySide />
}
<LoadingDotsBricks
className="absolute top-0 left-0"
show={appUiState.showEditorLoader}
transition
/>
</div>
{/* Bottom controls */}
<div className="absolute bottom-0 left-0 w-full">
<PreviewWindow />
<ControlsVideo />
</div>
{/* Side panel */}
<SidePanel>
<SidePanelTabs />
</SidePanel>
</div>
{/* Side panel */}
<SidePanel>
<SidePanelTabs />
</SidePanel>
{/* Timeline */}
<Timeline timelineHeight={appUiState.timelineHeight} />
</div>
{/* Timeline */}
<Timeline timelineHeight={timelineHeight} />
</div>
</AppUIProvider>
</EngineProvider>
</div>
);
@@ -11,7 +11,7 @@ import {
fa3dSphere,
} from "@awesome.me/kit-fde2be5eb0/icons/kit/custom";
import { ButtonIcon, ButtonIconSelect } from "~/components";
import { EngineContext } from "~/contexts/EngineContext";
import { EngineContext } from "../../contexts/EngineContext";
import { useContext } from "react";
export const Controls3D = () => {
@@ -1,7 +1,7 @@
import { useContext, useState } from "react";
import { Button, H4, Input } from "~/components";
import { ButtonDialogue } from "~/modules/ButtonDialogue";
import { EngineContext } from "~/contexts/EngineContext";
import { EngineContext } from "../../contexts/EngineContext";
export const ControlsTopButtons = () => {
const editorEngine = useContext(EngineContext);
@@ -0,0 +1,38 @@
import { useContext, useEffect, useState } from "react"
import { EngineContext } from "../../contexts/EngineContext"
import { LoadingDotsTyping } from "~/components";
export const PreviewEngineCamera = ()=>{
const editorEngine = useContext(EngineContext);
//take data from egine context
const [showLoader, setShowLoader] = useState<boolean>(true);
useEffect(()=>{
setTimeout(()=>setShowLoader(false), 5000);
},[]);
return (
<div
id="preview-engine-camera"
className="absolute bottom-0 w-30 m-4"
>
<div className="relative">
<div
className="text-white bg-ui-panel pt-1 px-2 pb-3 -z-10 rounded-t-lg -mb-2 w-fit"
>
<p>Camera View</p>
</div>
<div className="relative rounded-lg border border-white box overflow-hidden">
<img
//shoot that datat from engine context to this image
className="aspect-video max-h-40"
src="/resources/uiAssets/video_player_placeholder.gif"
/>
<div className="absolute w-full h-full top-0 left-0">
<LoadingDotsTyping show={showLoader} transition/>
</div>
</div>
</div>
</div>
)
}
@@ -1,30 +0,0 @@
import { useContext } from "react"
import { EngineContext } from "~/contexts/EngineContext"
export const PreviewWindow = ()=>{
const editorEngine = useContext(EngineContext);
//take data from egine context
return (
<div
id="preview-window"
className="absolute bottom-0 w-30 m-4"
>
<div className="relative">
<div
id="preview-window-label"
className="text-white bg-ui-panel pt-1 px-2 pb-3 -z-10 rounded-t-lg -mb-2 w-fit"
>
<p>Camera View</p>
</div>
<div className="rounded-lg border border-white box overflow-hidden">
<img
//shoot that datat from engine context to this image
className="aspect-video max-h-40"
src="/resources/uiAssets/video_player_placeholder.gif"
/>
</div>
</div>
</div>
)
}
@@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { BaseClip } from "~/models/track";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { BaseClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
interface Props {
clip: BaseClip;
@@ -8,8 +8,16 @@ interface Props {
}
export const ClipElement = ({ clip, type }: Props) => {
const { startDrag, dragId, endDrag, scale, canDrop, setCanDrop } =
useContext(TrackContext);
const {
startDrag,
dragId,
endDrag,
scale,
canDrop,
setCanDrop,
overTimeline,
dropId,
} = useContext(TrackContext);
const [initPosition, setInitPosition] = useState<{
initX: number;
initY: number;
@@ -36,6 +44,8 @@ export const ClipElement = ({ clip, type }: Props) => {
const onMouseMove = (event: MouseEvent) => {
if (dragId) {
event.stopPropagation();
event.preventDefault();
const deltaX = event.clientX - initX;
const deltaY = event.clientY - initY;
setCurrPosition({ x: deltaX, y: deltaY });
@@ -55,20 +65,24 @@ export const ClipElement = ({ clip, type }: Props) => {
const onPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
const box = document.getElementById(`ani-clip-${clip.id}`);
const position = box!.getBoundingClientRect();
startDrag(type, clip.id);
setCurrPosition({
x: 0,
y: 0,
x: position.x,
y: position.y + position.height + 1,
});
setInitPosition({
initX: position.x,
initY: position.y + position.height + 1,
});
setInitPosition({ initY: event.clientY, initX: event.clientX });
setCanDrop(false);
}
},
[type, clip.id, startDrag, setCanDrop],
);
// console.log("clip", canDrop);
return (
<div key={clip.id} className="relative h-16 w-16">
<div
@@ -82,16 +96,17 @@ export const ClipElement = ({ clip, type }: Props) => {
<div
id={`ani-dnd-${clip.id}`}
className={[
"absolute h-8 p-2",
"absolute p-2",
"rounded-lg",
!canDrop ? "bg-brand-primary" : "bg-brand-secondary-700",
dragId ? "block" : "hidden",
].join(" ")}
style={{
top: y,
left: x,
top: overTimeline ? y + 24 : y,
left: overTimeline ? x : x,
zIndex: 5000,
width: clip.length * 4 * scale,
width: overTimeline ? clip.length * 4 * scale : 64,
height: overTimeline ? 32 : 64,
}}
onPointerDown={onPointerDown}
>
@@ -1,6 +1,6 @@
import { useContext } from "react";
import { ClipElement } from "./ClipElement";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
export const ClipElements = () => {
const { animationClips } = useContext(TrackContext);
@@ -1,53 +0,0 @@
import {
H4,
ItemPicker,
Textarea
} from "~/components"
export const TabStyling = ()=>{
const handlePickingStyle = (picked:string)=>{
console.log(`Picked style: ${picked}`);
}
return(
<>
<H4>Select Base Style</H4>
<div className="flex gap-2 my-2">
<ItemPicker
label="Anime"
onClick={()=>handlePickingStyle("anime")}
src="/resources/avatars/0.webp"
/>
<ItemPicker
label="Pixel"
onClick={()=>handlePickingStyle("pixel")}
src="/resources/avatars/1.webp"
/>
<ItemPicker
label="Pixar"
onClick={()=>handlePickingStyle("pixar")}
src="/resources/avatars/2.webp"
/>
<ItemPicker
label="Stylized"
onClick={()=>handlePickingStyle("stylized")}
src="/resources/avatars/3.webp"
/>
</div>
<H4>Enter a Prompt</H4>
<div className="flex gap-2 my-2">
<Textarea className="w-full" />
</div>
<H4>Negative Prompt</H4>
<div className="flex gap-2 my-2">
<Textarea
className="w-full"
placeholder="Type here to filter out the things you don't want in the scene
"/>
</div>
</>
);
}
@@ -0,0 +1,87 @@
import { useContext, useState } from 'react';
import {
Button,
H4,
ItemPicker,
Textarea
} from "~/components"
import { AppUiContext } from '../../contexts/AppUiContext';
import { ACTION_TYPES } from '../../reducer';
import { VIEW_MODES } from '../../reducer/types';
export const TabStylization = ()=>{
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
const [ selection, setSelection ] = useState<string>("Anime");
const handlePickingStylizer = (picked:string)=>{
console.log(`Picked style: ${picked}`);
setSelection(picked);
}
const handleChangeViewMode = ()=>{
if(dispatchAppUiState!==null){
dispatchAppUiState({
type: ACTION_TYPES.ON_CHANGE_VIEW_MODE,
payload: {
viewMode: appUiState?.viewMode === VIEW_MODES.SIDE_BY_SIDE
? VIEW_MODES.EDITOR : VIEW_MODES.SIDE_BY_SIDE,
}
})
}
}
return(
<>
<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"
/>
</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"
/>
</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
"/>
</div>
<div className="flex gap-2 mt-6 justify-center">
<Button onClick={handleChangeViewMode}>
{
appUiState?.viewMode === VIEW_MODES.SIDE_BY_SIDE
? "Back to Scene" : "Preview Side by Side"
}
</Button>
</div>
</>
);
}
@@ -1,5 +1,5 @@
import { Tabs } from "~/modules/Tabs";
import { TabStyling } from "./TabStyling";
import { TabStylization} from "./TabStylization";
import { ClipElements } from "./ClipElements";
export const SidePanelTabs = () => {
@@ -19,8 +19,8 @@ export const SidePanelTabs = () => {
children: <p>Audio Tab</p>,
},
{
header: "Styling",
children: <TabStyling />,
header: "Stylization",
children: <TabStylization />,
},
]}
/>
@@ -1,5 +1,5 @@
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
export const Audio = () => {
@@ -9,19 +9,20 @@ export const Audio = () => {
return (
<div
className="bg-audio-groupBg block rounded-lg pb-5 pl-2 pr-4"
className="block rounded-lg bg-audio-groupBg pb-5 pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Global Audio
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-4">
<Track
id={audio!.id}
clips={clips}
title="Global Audio Track"
updateClip={updateAudio}
style="audio"
muted={audio?.muted}
muted={audio!.muted}
toggleMute={toggleAudioMute}
/>
</div>
@@ -1,5 +1,5 @@
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
export const Camera = () => {
@@ -8,12 +8,13 @@ export const Camera = () => {
return (
<div
className="bg-camera-groupBg block rounded-lg pb-5 pl-2 pr-4"
className="block rounded-lg bg-camera-groupBg pb-5 pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<div className="mb-5 pt-2 text-xs font-medium text-white">Camera</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-4">
<Track
id={camera!.id}
clips={clips}
title="Camera Position/Rotation"
style="camera"
@@ -1,5 +1,5 @@
import { useCallback, useContext, useMemo } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
function buildUpdaters(
@@ -61,8 +61,9 @@ export const Character = ({ characterId }: Props) => {
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Character
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-4">
<Track
id={character.id}
clips={animationClips}
title="Animation"
updateClip={updateClipAnimations}
@@ -70,6 +71,7 @@ export const Character = ({ characterId }: Props) => {
type="animations"
/>
<Track
id={character.id}
clips={positionClips}
title="Character Position/Rotation"
updateClip={updateClipPosition}
@@ -77,6 +79,7 @@ export const Character = ({ characterId }: Props) => {
type="positions"
/>
<Track
id={character.id}
clips={lipSyncClips}
title="Lipsync Audio Track"
updateClip={updateClipLipSync}
@@ -1,5 +1,5 @@
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { Track } from "~/pages/PageEnigma/comps/Timeline/Track";
export const Objects = () => {
@@ -7,15 +7,16 @@ export const Objects = () => {
return (
<div
className="bg-objects-groupBg block rounded-lg pb-5 pl-2 pr-4"
className="block rounded-lg bg-objects-groupBg pb-5 pl-2 pr-4"
style={{ width: fullWidth + 90 }}
>
<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-2">
<div key={object.id} className="flex flex-col gap-4">
<Track
id={object.id}
clips={object.clips}
title="Mask Position/Rotation"
updateClip={updateObject}
@@ -1,5 +1,5 @@
import { Fragment, useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { Fragment, useCallback, useContext, useEffect, useState } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { LowerPanel } from "~/modules/LowerPanel";
import { Character } from "./Character";
@@ -9,18 +9,63 @@ 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";
interface Props {
timelineHeight: number;
}
export const Timeline = ({ timelineHeight }: Props) => {
const { characters, objects, scale, length } = useContext(TrackContext);
const {
characters,
objects,
scale,
length,
selectedClip,
currentTime,
deleteCharacterClip,
deleteAudioClip,
deleteCameraClip,
deleteObjectClip,
} = useContext(TrackContext);
const { onPointerDown, time } = useMouseEventsAnimation();
const [dialogOpen, setDialogOpen] = useState(false);
const sectionWidth = 60 * 4 * scale;
const fullHeight =
characters.length * 200 + objects.objects.length * 80 + 248 + 96;
characters.length * 268 + objects.objects.length * 60 + 300 + 96;
const onDeleteAsk = useCallback(
(event: KeyboardEvent) => {
if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedClip) {
setDialogOpen(true);
}
},
[selectedClip],
);
const displayTime = time === -1 ? currentTime : time;
const onDelete = useCallback(() => {
deleteCharacterClip(selectedClip!);
deleteCameraClip(selectedClip!);
deleteAudioClip(selectedClip!);
deleteObjectClip(selectedClip!);
}, [
selectedClip,
deleteAudioClip,
deleteCharacterClip,
deleteCameraClip,
deleteObjectClip,
]);
useEffect(() => {
document.addEventListener("keydown", onDeleteAsk);
return () => {
document.addEventListener("keydown", onDeleteAsk);
};
}, [onDeleteAsk]);
return (
<>
@@ -44,7 +89,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="bg-ui-divider absolute block h-full"
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth + 88,
@@ -60,7 +105,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
00:{length < 10 ? "0" + length.toString() : length.toString()}
</div>
<div
className="bg-ui-divider absolute block h-full"
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: length * sectionWidth + 88,
@@ -84,7 +129,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
</div>
<div
className="absolute text-brand-primary"
style={{ top: 8, left: time * 4 * scale + 88 }}
style={{ top: 8, left: displayTime * 4 * scale + 88 }}
onPointerDown={onPointerDown}
>
<FontAwesomeIcon
@@ -102,6 +147,19 @@ export const Timeline = ({ timelineHeight }: Props) => {
/>
</div>
</LowerPanel>
<ConfirmationModal
title="Delete Clip"
text="Are you sure you want to delete the selected clip?"
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onOk={() => {
onDelete();
setDialogOpen(false);
}}
okText="Delete"
okColor="bg-brand-primary"
onCancel={() => setDialogOpen(false)}
/>
</>
);
};
+26 -8
View File
@@ -1,11 +1,12 @@
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { BaseClip } from "~/models/track";
import { useContext } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { BaseClip } from "~/pages/PageEnigma/models/track";
import { PointerEvent, useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
interface Props {
id: string;
clips: BaseClip[];
title: string;
style: "character" | "audio" | "camera" | "objects";
@@ -16,6 +17,7 @@ interface Props {
}
export const Track = ({
id,
clips,
toggleMute,
updateClip,
@@ -24,7 +26,8 @@ export const Track = ({
style,
type,
}: Props) => {
const { length, setCanDrop, dragType } = useContext(TrackContext);
const { length, scale, setCanDrop, dragType, setDropId, setDropOffset } =
useContext(TrackContext);
const trackType = type ?? style;
function onPointerOver() {
@@ -32,6 +35,19 @@ export const Track = ({
return;
}
setCanDrop(true);
setDropId(id);
}
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
if (dragType !== trackType) {
return;
}
const track = document.getElementById(`track-${trackType}-${id}`);
if (!track) {
return;
}
const position = track.getBoundingClientRect();
setDropOffset((event.clientX - position.x) / 4 / scale);
}
function onPointerLeave() {
if (dragType !== trackType) {
@@ -43,9 +59,11 @@ export const Track = ({
return (
<div className="pl-16">
<div
className={`rounded=lg relative mt-4 block h-9 w-full bg-${style}-unselected`}
id={`track-${trackType}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
>
{clips.map((clip, index) => (
<TrackClip
@@ -66,14 +84,14 @@ export const Track = ({
</div>
{!!toggleMute && (
<button
className="absolute text-xs text-white"
style={{ top: 6, left: -20 }}
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"
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
@@ -1,6 +1,6 @@
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
import { BaseClip } from "~/models/track";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { BaseClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useContext, useState } from "react";
interface Props {
@@ -51,7 +51,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
"rounded-l-lg",
"block h-full",
clip.id === selectedClip
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white"
? "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, cursor: "w-resize" }}
@@ -63,7 +63,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
...classes,
"block h-full",
clip.id === selectedClip
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white"
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
@@ -80,7 +80,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
"rounded-r-lg",
"block h-full",
clip.id === selectedClip
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white"
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from "react";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
export const useMouseEventsAnimation = () => {
const { currentTime, updateCurrentTime, length, scale } =
@@ -7,7 +7,7 @@ export const useMouseEventsAnimation = () => {
const [isActive, setIsActive] = useState("");
const [clientX, setClientX] = useState(0);
const [time, setTime] = useState(currentTime);
const [time, setTime] = useState(-1);
useEffect(() => {
const max = length * 60 * 4 * scale;
@@ -16,12 +16,15 @@ export const useMouseEventsAnimation = () => {
if (isActive) {
updateCurrentTime(Math.round(time));
setIsActive("");
setTime(-1);
}
};
const onMouseMove = (event: MouseEvent) => {
const delta = (event.clientX - clientX) / 4 / scale + currentTime;
if (isActive === "drag") {
event.stopPropagation();
event.preventDefault();
if (delta < 0 || delta > max) {
return;
}
@@ -37,15 +40,16 @@ export const useMouseEventsAnimation = () => {
window.removeEventListener("pointerup", onPointerUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [clientX, isActive, length, updateCurrentTime, scale, time]);
}, [clientX, isActive, length, updateCurrentTime, scale, time, currentTime]);
return {
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => {
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
setClientX(event.clientX);
setIsActive("drag");
setTime(currentTime);
}
},
}, []),
time,
};
};
@@ -5,8 +5,8 @@ import React, {
useEffect,
useRef,
} from "react";
import { BaseClip } from "~/models/track";
import { TrackContext } from "~/contexts/TrackContext/TrackContext";
import { BaseClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
export const useMouseEventsClip = (
clip: BaseClip,
@@ -89,6 +89,8 @@ export const useMouseEventsClip = (
type: string,
) => {
if (event.button === 0) {
event.stopPropagation();
event.preventDefault();
clientX.current = event.clientX;
isActive.current = type;
}
@@ -0,0 +1,49 @@
import {
useEffect,
useState,
} from "react";
import { LoadingDotsBricks } from "~/components";
export const ViewSideBySide = ()=>{
const [showLoader, setShowLoader] = useState(true);
useEffect(()=>{
setTimeout(()=>setShowLoader(false), 1000);
},[ ]);
return (
<div
id="view-side-by-side"
className="absolute top-0 left-0 w-full h-full flex justify-center items-center gap-2 p-2"
>
<div className="flex flex-col justify-center items-center">
<div className="text-white">
<label>Raw Preview</label>
</div>
<div className="relative">
<img
//shoot that datat from engine context to this image
className="aspect-video max-h-150"
src="/resources/uiAssets/video_player_placeholder.gif"
/>
</div>
</div>
<div className="flex flex-col justify-center items-center">
<div className="text-white">
<label>Stylized Preview</label>
</div>
<div className="relative">
<img
//shoot that datat from engine context to this image
className="aspect-video max-h-150"
src="/resources/uiAssets/video_player_placeholder.gif"
/>
</div>
</div>
<div className="absolute top-0 left-0 w-full h-full">
<LoadingDotsBricks show={showLoader} transition/>
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { createContext, Dispatch, ReactNode } from "react";
import { State, Action } from "../reducer";
export const AppUiContext = createContext<[State|null, Dispatch<Action>|null]>([null,null]);
export const AppUIProvider = ({
value,
children,
}:{
value:[State, Dispatch<Action>],
children: ReactNode
})=>{
return (
<AppUiContext.Provider value={value}>{children}</AppUiContext.Provider>
);
}
@@ -3,78 +3,155 @@ import {
AnimationClip,
AudioClip,
AudioGroup,
BaseClip,
CameraGroup,
CharacterGroup,
ObjectGroup,
} from "~/models/track";
} from "~/pages/PageEnigma/models/track";
export const TrackContext = createContext<{
// timeline characters and it's functions
characters: CharacterGroup[];
camera: CameraGroup | null;
audio: AudioGroup | null;
objects: ObjectGroup;
selectedClip: string | null;
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => void;
toggleLipSyncMute: (characterId: string) => void;
addCharacterAnimation: (options: {
clipId: string;
characterId: string;
animationClips: BaseClip[];
offset: number;
}) => void;
addCharacterAudio: (options: {
clipId: string;
characterId: string;
audioClips: BaseClip[];
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: BaseClip[],
offset: number,
) => void;
selectAudioClip: (clipId: string) => void;
deleteAudioClip: (clipId: string) => void;
// timeline objects group
objects: ObjectGroup;
updateObject: (options: {
id: string;
length: number;
offset: number;
}) => void;
selectObjectClip: (clipId: string) => void;
deleteObjectClip: (clipId: string) => void;
// current - only select one item - will be replaced
selectedClip: string | null;
selectClip: (clipId: string) => void;
toggleLipSyncMute: (characterId: string) => void;
toggleAudioMute: () => void;
// sidebar clips
animationClips: AnimationClip[];
audioClips: AudioClip[];
// drag and drop
dragType: "animations" | "lipSync" | null;
dragId: string | null;
startDrag: (type: "animations" | "lipSync", id: string) => void;
endDrag: () => void;
scale: number;
currentTime: number;
length: number;
updateCurrentTime: (time: number) => void;
canDrop: boolean;
setCanDrop: (can: boolean) => void;
overTimeline: boolean;
setOverTimeline: (over: boolean) => void;
dropId: string;
setDropId: (id: string) => void;
dropOffset: number;
setDropOffset: (offset: number) => void;
// scale of timeline displa
scale: number;
// current time position
currentTime: number;
updateCurrentTime: (time: number) => void;
// total length of the film clip
length: number;
// computed width length * 60 * 4 * scale
fullWidth: number;
}>({
characters: [],
camera: null,
audio: null,
objects: { id: "", objects: [] },
selectedClip: null,
updateCharacters: () => {},
updateCamera: () => {},
updateAudio: () => {},
updateObject: () => {},
selectClip: () => {},
toggleLipSyncMute: () => {},
addCharacterAnimation: () => {},
addCharacterAudio: () => {},
selectCharacterClip: () => {},
deleteCharacterClip: () => {},
camera: null,
updateCamera: () => {},
selectCameraClip: () => {},
deleteCameraClip: () => {},
audio: null,
updateAudio: () => {},
toggleAudioMute: () => {},
selectAudioClip: () => {},
addGlobalAudio: () => {},
deleteAudioClip: () => {},
objects: { id: "", objects: [] },
updateObject: () => {},
selectObjectClip: () => {},
deleteObjectClip: () => {},
selectedClip: null,
selectClip: () => {},
animationClips: [],
audioClips: [],
dragType: null,
dragId: null,
startDrag: () => {},
endDrag: () => {},
scale: 1,
currentTime: 0,
length: 12,
updateCurrentTime: () => {},
dropId: "",
setDropId: () => {},
canDrop: false,
setCanDrop: () => {},
overTimeline: false,
setOverTimeline: () => {},
dropOffset: 0,
setDropOffset: () => {},
scale: 1,
currentTime: 0,
updateCurrentTime: () => {},
length: 12,
fullWidth: 0,
});
@@ -0,0 +1,122 @@
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 { AnimationClip, AudioClip } from "~/pages/PageEnigma/models/track";
import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop";
interface Props {
children: ReactNode;
}
export const TrackProvider = ({ children }: Props) => {
const characters = useUpdateCharacters();
const camera = useUpdateCamera();
const audio = useUpdateAudio();
const objects = useUpdateObject();
const { endDrag, ...dragDrop } = useUpdateDragDrop();
const [selectedClip, setSelectedClip] = useState<string | null>(null);
const selectClip = useCallback((clipId: string | null) => {
setSelectedClip(clipId);
}, []);
const [animationClips, setAnimationClips] = useState<AnimationClip[]>([]);
const [audioClips, setAudioClips] = useState<AudioClip[]>([]);
const [scale, setScale] = useState(1);
const [length, setLength] = useState(1);
const [time, setTime] = useState(0);
const updateCurrentTime = useCallback((newTime: number) => {
setTime(newTime);
console.log("message", {
action: "UpdateCurrentTime",
id: "",
data: { currentTime: newTime },
});
}, []);
// cross group functions
const dropClip = useCallback(() => {
const { canDrop, dragType, dropId, dragId, dropOffset } = dragDrop;
if (canDrop) {
if (dragType === "animations") {
characters.addCharacterAnimation({
clipId: dragId!,
characterId: dropId,
animationClips,
offset: dropOffset,
});
}
}
endDrag();
}, [dragDrop, animationClips, characters, endDrag]);
const fullWidth = useMemo(() => {
return length * 60 * 4 * scale;
}, [length, scale]);
useEffect(() => {
setAnimationClips([
{
id: "ani-id1",
offset: 0,
length: 124,
name: "ani 11",
},
]);
setAudioClips([]);
setScale(1);
setLength(12);
}, []);
const values = useMemo(() => {
return {
...characters,
...camera,
...audio,
...objects,
selectClip,
selectedClip,
...dragDrop,
endDrag: dropClip,
animationClips,
audioClips,
scale,
currentTime: time,
updateCurrentTime,
length,
fullWidth,
};
}, [
characters,
camera,
audio,
objects,
selectClip,
selectedClip,
dragDrop,
dropClip,
animationClips,
audioClips,
updateCurrentTime,
time,
fullWidth,
length,
scale,
]);
return (
<TrackContext.Provider value={values}>{children}</TrackContext.Provider>
);
};
@@ -0,0 +1,129 @@
import { useCallback, useState } from "react";
import { AudioGroup, BaseClip } from "~/pages/PageEnigma/models/track";
export default function useUpdateAudio() {
const [audio, setAudio] = useState<AudioGroup>({
id: "AU1",
muted: false,
clips: [
{
id: "AU1-1",
length: 200,
offset: 0,
name: "audio 1",
},
{
id: "AU1-2",
length: 180,
offset: 300,
name: "audio 2",
},
],
});
const updateAudio = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setAudio((oldAudio) => {
const newClips = [...oldAudio.clips];
const clipIndex = newClips.findIndex((row) => row.id === id);
if (clipIndex === -1) {
return { ...oldAudio };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...oldAudio,
clips: newClips,
};
});
console.log("message", {
action: "UpdateGlobalAudio",
id,
data: { offset, length },
});
},
[],
);
const addGlobalAudio = useCallback(
(dragId: string, audioClips: BaseClip[], offset: number) => {
const clip = audioClips.find((row) => row.id === dragId);
if (!clip) {
return;
}
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [...oldAudio.clips, { ...clip, offset }],
};
});
console.log("message", {
action: "UpdateGlobalAudio",
id: dragId,
data: { offset },
});
},
[],
);
const toggleAudioMute = useCallback(() => {
setAudio((oldAudio) => {
return {
...oldAudio,
muted: !oldAudio.muted,
};
});
console.log("message", {
action: "ToggleGlobalAudioMute",
id: "",
});
}, []);
const selectAudioClip = useCallback((clipId: string) => {
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [
...oldAudio.clips.map((clip) => {
return {
...clip,
selected: clip.id === clipId ? !clip.selected : clip.selected,
};
}),
],
};
});
}, []);
const deleteAudioClip = useCallback((clipId: string) => {
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [...oldAudio.clips.filter((clip) => clip.id !== clipId)],
};
});
console.log("message", {
action: "UpdateGlobalAudio",
id: clipId,
});
}, []);
return {
audio,
updateAudio,
toggleAudioMute,
selectAudioClip,
addGlobalAudio,
deleteAudioClip,
};
}
@@ -1,5 +1,5 @@
import { useCallback, useState } from "react";
import { CameraGroup } from "~/models/track";
import { CameraGroup } from "~/pages/PageEnigma/models/track";
export default function useUpdateCamera() {
const [camera, setCamera] = useState<CameraGroup>({
@@ -48,8 +48,35 @@ export default function useUpdateCamera() {
[],
);
const selectCameraClip = useCallback((clipId: string) => {
setCamera((oldCamera) => {
return {
...oldCamera,
clips: [
...oldCamera.clips.map((clip) => {
return {
...clip,
selected: clip.id === clipId ? !clip.selected : clip.selected,
};
}),
],
};
});
}, []);
const deleteCameraClip = useCallback((clipId: string) => {
setCamera((oldCamera) => {
return {
...oldCamera,
clips: [...oldCamera.clips.filter((clip) => clip.id !== clipId)],
};
});
}, []);
return {
camera,
updateCamera,
selectCameraClip,
deleteCameraClip,
};
}
@@ -0,0 +1,249 @@
import { useCallback, useRef, useState } from "react";
import { BaseClip, CharacterGroup } from "~/pages/PageEnigma/models/track";
export default function useUpdateCharacters() {
const [characters, setCharacters] = useState<CharacterGroup[]>([
{
id: "CH1",
muted: false,
animationClips: [],
positionClips: [],
lipSyncClips: [
{
id: "CH1-L1",
length: 400,
offset: 150,
name: "sync 1",
},
],
},
]);
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.id === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...character,
animationClips: newAnimationClips,
};
});
});
console.log("message", {
action: "UpdateAnimation",
id,
data: { offset, length },
});
return;
}
if (type === "positions") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newPositionClips = [...character.positionClips];
const clipIndex = newPositionClips.findIndex(
(row) => row.id === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newPositionClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...character,
positionClips: newPositionClips,
};
});
});
return;
}
if (type === "lipSync") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex((row) => row.id === id);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length;
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
});
console.log("message", {
action: "UpdateLipSync",
id,
data: { offset, length },
});
}
},
[],
);
const addCharacterAnimation = useCallback(
({
clipId,
characterId,
animationClips,
offset,
}: {
clipId: string;
characterId: string;
animationClips: BaseClip[];
offset: number;
}) => {
const clip = animationClips.find((row) => row.id === clipId);
if (!clip) {
return;
}
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
animationClips: [...character.animationClips, { ...clip, offset }],
};
});
});
console.log("message", {
action: "AddAnimation",
id: clipId,
data: { offset },
});
},
[],
);
const addCharacterAudio = useCallback(
({
clipId,
characterId,
audioClips,
offset,
}: {
clipId: string;
characterId: string;
audioClips: BaseClip[];
offset: number;
}) => {
const clip = audioClips.find((row) => row.id === clipId);
if (!clip) {
return;
}
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
lipSyncClips: [...character.lipSyncClips, { ...clip, offset }],
};
});
});
console.log("message", {
action: "AddLipSync",
id: clipId,
data: { offset },
});
},
[],
);
const toggleLipSyncMute = useCallback((characterId: string) => {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => ({
...character,
muted:
character.id === characterId ? !character.muted : character.muted,
}));
});
console.log("message", {
action: "ToggleLipSync",
id: characterId,
});
}, []);
const selectCharacterClip = useCallback((clipId: string) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected: clip.id === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionClips.map((clip) => ({
...clip,
selected: clip.id === clipId ? !clip.selected : clip.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected: clip.id === clipId ? !clip.selected : clip.selected,
})),
})),
];
});
}, []);
const deleteCharacterClip = useCallback((clipId: string) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
animationClips: character.animationClips.filter(
(clip) => clip.id !== clipId,
),
positionClips: character.positionClips.filter(
(clip) => clip.id !== clipId,
),
lipSyncClips: character.lipSyncClips.filter(
(clip) => clip.id !== clipId,
),
})),
];
});
console.log("message", {
action: "DeleteCharacterClip",
id: clipId,
});
}, []);
return {
characters,
updateCharacters,
toggleLipSyncMute,
addCharacterAnimation,
addCharacterAudio,
selectCharacterClip,
deleteCharacterClip,
};
}
@@ -0,0 +1,38 @@
import { useCallback, useState } from "react";
export default function useUpdateDragDrop() {
const [state, setState] = useState<{
dragType: "animations" | "lipSync" | null;
dragId: string | null;
}>({ dragType: null, dragId: null });
const startDrag = useCallback(
(type: "animations" | "lipSync", id: string) => {
setState({ dragId: id, dragType: type });
},
[],
);
const [canDrop, setCanDrop] = useState(false);
const [overTimeline, setOverTimeline] = useState(false);
const endDrag = useCallback(() => {
setState({ dragId: null, dragType: null });
}, []);
const [dropId, setDropId] = useState("");
const [dropOffset, setDropOffset] = useState(0);
return {
dragId: state.dragId,
dragType: state.dragType,
startDrag,
endDrag,
canDrop,
setCanDrop,
overTimeline,
setOverTimeline,
dropId,
setDropId,
dropOffset,
setDropOffset,
};
}
@@ -1,5 +1,5 @@
import { useCallback, useState } from "react";
import { ObjectGroup } from "~/models/track";
import { ObjectGroup } from "~/pages/PageEnigma/models/track";
export default function useUpdateObject() {
const [objects, setObjects] = useState<ObjectGroup>({
@@ -46,8 +46,37 @@ export default function useUpdateObject() {
[],
);
const selectObjectClip = useCallback((clipId: string) => {
setObjects((oldObjectGroup) => {
return {
...oldObjectGroup,
objects: oldObjectGroup.objects.map((object) => ({
...object,
clips: object.clips.map((clip) => ({
...clip,
selected: clip.id === clipId ? !clip.selected : clip.selected,
})),
})),
};
});
}, []);
const deleteObjectClip = useCallback((clipId: string) => {
setObjects((oldObjectGroup) => {
return {
...oldObjectGroup,
objects: oldObjectGroup.objects.map((object) => ({
...object,
clips: object.clips.filter((clip) => clip.id !== clipId),
})),
};
});
}, []);
return {
objects,
updateObject,
selectObjectClip,
deleteObjectClip,
};
}
@@ -3,6 +3,7 @@ export interface BaseClip {
name: string;
offset: number;
length: number;
selected?: boolean;
}
export interface AnimationClip extends BaseClip {}
+15
View File
@@ -0,0 +1,15 @@
import {State, Action, ACTION_TYPES, VIEW_MODES} from './types';
import {reducer} from './reducer';
const initialState:State = {
timelineHeight: 260,
viewMode: VIEW_MODES.EDITOR,
showEditorLoader: true
}
export type {State, Action};
export {
ACTION_TYPES,
initialState,
reducer,
}
+34
View File
@@ -0,0 +1,34 @@
import { State, Action, ACTION_TYPES} from './types';
export function reducer(state: State, action: Action): State {
switch(action.type){
case ACTION_TYPES.SHOW_EDITOR_LOADER:{
return{
...state,
showEditorLoader: true,
}
}
case ACTION_TYPES.HIDE_EDITOR_LOADER:{
return{
...state,
showEditorLoader: false,
}
}
case ACTION_TYPES.ON_TIMELINE_RESIZE:{
return{
...state,
timelineHeight: action.payload.timelineHeight,
}
}
case ACTION_TYPES.ON_CHANGE_VIEW_MODE:{
console.log(action);
return{
...state,
viewMode: action.payload.viewMode
}
}
default:{
return state; //no change
}
}// end switch
}
+37
View File
@@ -0,0 +1,37 @@
export enum VIEW_MODES {
EDITOR = 'editor',
SIDE_BY_SIDE = 'side-by-side'
};
type ViewModes = VIEW_MODES.EDITOR | VIEW_MODES.SIDE_BY_SIDE;
export type State = {
viewMode: ViewModes;
timelineHeight: number;
showEditorLoader: boolean;
};
export enum ACTION_TYPES {
ON_TIMELINE_RESIZE = "on_timeline_resize",
ON_CHANGE_VIEW_MODE = "on_change_view_mode",
SHOW_EDITOR_LOADER = "show_editor_loader",
HIDE_EDITOR_LOADER = "hide_editor_loader",
};
export type Action =
| {type: ACTION_TYPES.SHOW_EDITOR_LOADER,}
| {type: ACTION_TYPES.HIDE_EDITOR_LOADER,}
| {
type: ACTION_TYPES.ON_TIMELINE_RESIZE,
payload: {
timelineHeight: number
}
}
| {
type: ACTION_TYPES.ON_CHANGE_VIEW_MODE,
payload: {
viewMode: ViewModes
}
}
;
+41 -2
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { LinksFunction } from "@remix-run/deno";
import {
Links,
@@ -12,6 +12,7 @@ import normalizeCss from "./styles/normalize.css?url";
import tailwindCss from "./styles/tailwind.css?url";
import baseCss from "./styles/base.css?url";
import { LoadingDotsBricks } from "~/components";
import { TopBar } from "./modules/TopBar";
import { TopBarInnerContext } from "~/contexts/TopBarInner";
@@ -45,7 +46,13 @@ export const links: LinksFunction = () => [
export default function App() {
const [topBarInnerComponent, setTopBarInnerComponent] =
useState<JSX.Element | null>(null);
useState<JSX.Element | null>(null);
const [isLoaded, setIsLoaded] = useState<'loading'|'loaded'|'completed'>('loading');
useEffect(()=>{
setIsLoaded('loaded');
setTimeout(()=>setIsLoaded('completed'), 2000);
},[]);
return (
<html lang="en">
@@ -56,6 +63,15 @@ export default function App() {
<Links />
</head>
<body className="overflow-hidden bg-ui-background">
{isLoaded !== 'completed' &&
<CompleteTakeoverLoadingScreen
className={
isLoaded === 'loading'
? "transition-opacity duration-1000 opacity-100"
: "transition-opacity duration-1000 opacity-0"
}
/>
}
<TopBarInnerContext.Provider
value={{
TopBarInner: topBarInnerComponent,
@@ -72,3 +88,26 @@ export default function App() {
</html>
);
}
function CompleteTakeoverLoadingScreen({className}:{className:string}){
return(
<div
id='complete-takeover-loading-screen'
className={className}
style={{
backgroundColor: '#1a1a27',
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
}}
>
<LoadingDotsBricks />
</div>
);
};
+112
View File
@@ -35,3 +35,115 @@ body {
user-select: none; /* Standard syntax */
pointer-events: none;
}
/*!
* three-dots - v0.3.2
* CSS loading animations made with single element
* https://nzbin.github.io/three-dots/
*
* Copyright (c) 2018 nzbin
* Released under MIT License
*/
/**
* ==============================================
* Dot Typing
* ==============================================
*/
.dot-typing {
position: relative;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #393948;
color: #393948;
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
animation: dot-typing 1.5s infinite linear;
}
@keyframes dot-typing {
0% {
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
}
16.667% {
box-shadow: 9984px -10px 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
}
33.333% {
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
}
50% {
box-shadow: 9984px 0 0 0 #393948, 9999px -10px 0 0 #393948, 10014px 0 0 0 #393948;
}
66.667% {
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
}
83.333% {
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px -10px 0 0 #393948;
}
100% {
box-shadow: 9984px 0 0 0 #393948, 9999px 0 0 0 #393948, 10014px 0 0 0 #393948;
}
}
/**
* ==============================================
* Dot Bricks
* ==============================================
*/
.dot-bricks {
position: relative;
top: 8px;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #393948;
color: #393948;
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
animation: dot-bricks 2s infinite ease;
}
@keyframes dot-bricks {
0% {
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
}
8.333% {
box-shadow: 10007px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
}
16.667% {
box-shadow: 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948, 10007px 0 0 0 #393948;
}
25% {
box-shadow: 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948;
}
33.333% {
box-shadow: 10007px 0 0 0 #393948, 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948;
}
41.667% {
box-shadow: 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948, 9991px 0 0 0 #393948;
}
50% {
box-shadow: 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948;
}
58.333% {
box-shadow: 9991px 0 0 0 #393948, 10007px -16px 0 0 #393948, 9991px -16px 0 0 #393948;
}
66.666% {
box-shadow: 9991px 0 0 0 #393948, 10007px 0 0 0 #393948, 9991px -16px 0 0 #393948;
}
75% {
box-shadow: 9991px 0 0 0 #393948, 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948;
}
83.333% {
box-shadow: 9991px -16px 0 0 #393948, 10007px 0 0 0 #393948, 10007px -16px 0 0 #393948;
}
91.667% {
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px -16px 0 0 #393948;
}
100% {
box-shadow: 9991px -16px 0 0 #393948, 9991px 0 0 0 #393948, 10007px 0 0 0 #393948;
}
}