Merge pull request #84 from storytold/feature/keyframes-continued

Feature/keyframes continued
This commit is contained in:
ArEnSc authored and GitHub committed 2024-04-04 17:18:19 -04:00
commit beb89e4e27
35 files changed
+391 -335

No files matched your search

+53
View File
@@ -3,12 +3,65 @@ import { TrackProvider } from "~/pages/PageEnigma/contexts/TrackContext/TrackPro
import { AppUIProvider } from "~/pages/PageEnigma/contexts/AppUiContext";
import { EngineProvider } from "~/pages/PageEnigma/contexts/EngineProvider";
import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragComponent";
import { toast, ToastBar, Toaster } from "react-hot-toast";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faTriangleExclamation,
faXmark,
} from "@fortawesome/pro-solid-svg-icons";
export const PageEnigma = () => {
return (
<TrackProvider>
<AppUIProvider>
<EngineProvider>
<Toaster
position="top-right"
toastOptions={{
style: {
border: "1px solid #39394D",
backgroundColor: "#39394D",
},
duration: 99999,
}}
containerStyle={{
top: 84,
right: 20,
}}
>
{(thisToast) => (
<ToastBar toast={thisToast}>
{({ message }) => {
return (
<div className="mr-3 flex justify-between rounded-lg">
<div>
{thisToast.type === "error" && (
<div className="ml-3 flex items-center gap-4">
<FontAwesomeIcon
icon={faTriangleExclamation}
className="text-keyframe-selected"
/>
<div className="text-base font-bold text-white">
Warning!
</div>
</div>
)}
<div className="text-left text-sm text-white opacity-50">
{message}
</div>
</div>
<button
onClick={() => toast.dismiss(thisToast.id)}
className="text-sm font-bold text-white opacity-50"
>
<FontAwesomeIcon icon={faXmark} />
</button>
</div>
);
}}
</ToastBar>
)}
</Toaster>
<PageEnigmaComponent />
</EngineProvider>
</AppUIProvider>
+12 -3
View File
@@ -1,5 +1,4 @@
import { useContext } from "react";
import { faSparkles } from "@fortawesome/pro-solid-svg-icons";
import { Button, LoadingBar, LoadingDots } from "~/components";
@@ -19,9 +18,12 @@ 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";
import { EngineContext } from "./contexts/EngineContext";
export const PageEnigmaComponent = () => {
useSignals();
const editorEngine = useContext(EngineContext);
const [appUiState] = useContext(AppUiContext);
//To prevent the click event from propagating to the canvas: TODO: HANDLE THIS BETTER?
@@ -29,13 +31,20 @@ export const PageEnigmaComponent = () => {
event.stopPropagation();
};
const handleGenerateMovieClick=()=> {
if ( editorEngine != null) {
editorEngine?.generateVideo()
} else {
console.log("Tried to generate movie but editor was null")
}
};
const lowerHeight = timelineHeight.value;
return (
<div>
<TopBarHelmet>
<Button icon={faSparkles}>Generate Movie</Button>
<Button icon={faSparkles} onClick={handleGenerateMovieClick}>Generate Movie</Button>
</TopBarHelmet>
<div style={{ height: "calc(100vh - 68px)" }}>
{/* Engine section/side panel */}
<div
@@ -24,68 +24,10 @@ export const ControlsTopButtons = () => {
const handleButtonTest = () => {
editorEngine?.testStylizeRequest();
// if(appUiState?.showEditorLoadingBar.isShowing){
//TO GO TO A HUNDRED AND DISAPPER
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
// payload:{
// showEditorLoadingBar: {
// progress: 100,
// }
// }
// });
// setTimeout(() => {
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.HIDE_EDITOR_LOADINGBAR,
// });
// }, 1000);
//END :TO GO TO A HUNDRED AND DISAPPER
//EAMPLE OF CHANING LOADING MESSAGE
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.UPDATE_EDITOR_LOADINGBAR,
// payload:{
// showEditorLoadingBar: {
// message: "new message",
// }
// }
// });
// }else{
// dispatchAppUiState({
// type: APPUI_ACTION_TYPES.SHOW_EDITOR_LOADINGBAR,
// payload:{
// showEditorLoadingBar: {
// message: 'display of LoadingBar triggered by Test Button',
// progress: 10,
// }
// }
// });
//}
};
const handleObjectPanelTest = () => {
if (appUiState?.controlPanel.isShowing) {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.HIDE_CONTROLPANELS_SCENEOBJECT,
});
} else {
dispatchAppUiState({
type: APPUI_ACTION_TYPES.SHOW_CONTROLPANELS_SCENEOBJECT,
payload: {
group: ClipGroup.OBJECT,
object_name: "TEST BUTTON",
object_uuid: "",
version: "1",
objectVectors: {
position: { x: 1, y: 2, z: 3 },
rotation: { x: 4, y: 5, z: 6 },
scale: { x: 7, y: 8, z: 9 },
},
},
});
}
const handleTestButton2 = () => {
console.log("Test Button 2")
};
const handleButtonCameraView = () => {
@@ -94,18 +36,9 @@ export const ControlsTopButtons = () => {
const handleButtonPlayBack = () => {
editorEngine?.startPlayback();
};
const handleButtonRender = () => {
editorEngine?.generateVideo();
};
const handleButtonTakeFrame = () => {
editorEngine?.take_timeline_cam_clip();
};
const handleButtonLoad = () => {
console.log("LOADING");
};
const handleButtonSingleFrame = () => {
editorEngine?.generateFrame();
};
@@ -172,18 +105,15 @@ export const ControlsTopButtons = () => {
Toggle Camera View
</Button>
<Button
onClick={handleObjectPanelTest}
onClick={handleTestButton2}
className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary"
>
Show Object Panel
Test Button 2
</Button>
</div>
<div className="flex gap-2">
<Button onClick={handleButtonSingleFrame}>Render Single Frame</Button>
<Button onClick={handleButtonTakeFrame}>Take Frame</Button>
<Button onClick={handleButtonRender}>Render</Button>
{/* <Button onClick={handleButtonLoad}>Load</Button>
<Button onClick={handleButtonRender}>Render</Button> */}
<Button
onClick={handleButtonTest}
className="bg-brand-tertiary hover:bg-brand-teriary-400 focus-visible:outline-brand-tertiary"
@@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect } from "react";
import { MediaClip } from "~/pages/PageEnigma/models/track";
import { ClipType, MediaClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import {
canDrop,
@@ -11,7 +11,7 @@ import { useSignals } from "@preact/signals-react/runtime";
interface Props {
clip: MediaClip;
type: "animations" | "audio";
type: ClipType;
}
export const AnimationElement = ({ clip, type }: Props) => {
@@ -3,6 +3,7 @@ import { AnimationElement } from "./AnimationElement";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useSignals } from "@preact/signals-react/runtime";
import { timelineHeight } from "~/pages/PageEnigma/store";
import { ClipType } from "~/pages/PageEnigma/models/track";
export const AnimationElements = () => {
const { animationClips } = useContext(TrackContext);
@@ -17,7 +18,11 @@ export const AnimationElements = () => {
<div className="flex flex-wrap gap-3 overflow-y-auto" style={{ height }}>
{animationClips.map((clip) => {
return (
<AnimationElement key={clip.media_id} clip={clip} type="animations" />
<AnimationElement
key={clip.media_id}
clip={clip}
type={ClipType.ANIMATION}
/>
);
})}
</div>
@@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect } from "react";
import { MediaClip } from "~/pages/PageEnigma/models/track";
import { ClipType, MediaClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import {
canDrop,
@@ -11,7 +11,7 @@ import { useSignals } from "@preact/signals-react/runtime";
interface Props {
clip: MediaClip;
type: "animations" | "audio";
type: ClipType;
}
export const AudioElement = ({ clip, type }: Props) => {
@@ -3,6 +3,7 @@ import { AnimationElement } from "./AnimationElement";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useSignals } from "@preact/signals-react/runtime";
import { timelineHeight } from "~/pages/PageEnigma/store";
import { ClipType } from "~/pages/PageEnigma/models/track";
export const AudioElements = () => {
const { audioClips } = useContext(TrackContext);
@@ -17,7 +18,11 @@ export const AudioElements = () => {
<div className="flex flex-wrap gap-3 overflow-y-auto" style={{ height }}>
{audioClips.map((clip) => {
return (
<AnimationElement key={clip.media_id} clip={clip} type="audio" />
<AnimationElement
key={clip.media_id}
clip={clip}
type={ClipType.AUDIO}
/>
);
})}
</div>
@@ -5,13 +5,14 @@ import {
toggleAudioMute,
updateAudio,
} from "~/pages/PageEnigma/store";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
export const Audio = () => {
const { clips } = audioGroup.value;
return (
<div
className="block rounded-lg bg-audio-groupBg pb-5 pl-2 pr-4"
className="bg-global_audio-groupBg block rounded-lg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
@@ -23,7 +24,7 @@ export const Audio = () => {
clips={clips}
title="Global Audio TrackClips"
updateClip={updateAudio}
style="audio"
group={ClipGroup.GLOBAL_AUDIO}
muted={audioGroup.value.muted}
toggleMute={toggleAudioMute}
/>
@@ -1,6 +1,7 @@
import { cameraGroup, fullWidth, updateCamera } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { useSignals } from "@preact/signals-react/runtime";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
export const Camera = () => {
useSignals();
@@ -17,7 +18,7 @@ export const Camera = () => {
id={cameraGroup.value.id}
keyframes={keyframes}
title="Camera Position/Rotation"
style="camera"
group={ClipGroup.CAMERA}
updateKeyframe={updateCamera}
/>
</div>
@@ -7,10 +7,11 @@ import {
updateCharacters,
} from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models/track";
function buildUpdaters(
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
type: ClipType;
id: string;
length: number;
offset: number;
@@ -21,17 +22,17 @@ function buildUpdaters(
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: "animations" });
updateCharacters({ ...options, type: ClipType.ANIMATION });
}
function updateClipPosition(options: { id: string; offset: number }) {
updateCharacters({ ...options, length: 0, type: "positions" });
updateCharacters({ ...options, length: 0, type: ClipType.TRANSFORM });
}
function updateClipLipSync(options: {
id: string;
length: number;
offset: number;
}) {
updateCharacters({ ...options, type: "lipSync" });
updateCharacters({ ...options, type: ClipType.AUDIO });
}
return { updateClipLipSync, updateClipPosition, updateClipAnimations };
}
@@ -68,15 +69,15 @@ export const Character = ({ characterId }: Props) => {
clips={animationClips}
title="Animation"
updateClip={updateClipAnimations}
style="character"
type="animations"
group={ClipGroup.CHARACTER}
type={ClipType.ANIMATION}
/>
<TrackKeyFrames
id={character.id}
keyframes={positionKeyframes}
title="Character Position/Rotation"
updateKeyframe={updateClipPosition}
style="character"
group={ClipGroup.CHARACTER}
/>
<TrackClips
id={character.id}
@@ -85,8 +86,8 @@ export const Character = ({ characterId }: Props) => {
updateClip={updateClipLipSync}
muted={character.muted}
toggleMute={toggleCharacterLipSyncMute}
style="character"
type="lipSync"
group={ClipGroup.CHARACTER}
type={ClipType.AUDIO}
/>
</div>
</div>
@@ -4,6 +4,7 @@ import { ObjectTrack } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
export const ObjectGroups = () => {
useSignals();
return (
<>
{objectGroup.value.objects.length > 0 && (
@@ -1,10 +1,11 @@
import { fullWidth, objectGroup, updateObject } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { ClipGroup } from "~/pages/PageEnigma/models/track";
export const ObjectTrack = () => {
return (
<div
className="block rounded-lg bg-objects-groupBg pb-5 pl-2 pr-4"
className="bg-object-groupBg block rounded-lg pb-5 pl-2 pr-4"
style={{ width: fullWidth.value + 90 }}
>
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
@@ -17,7 +18,7 @@ export const ObjectTrack = () => {
keyframes={object.keyframes}
title={`${object.name} Position/Rotation`}
updateKeyframe={updateObject}
style="objects"
group={ClipGroup.OBJECT}
/>
</div>
))}
@@ -13,7 +13,7 @@ import {
import { useQueueHandler } from "~/pages/PageEnigma/comps/Timeline/utils/useQueueHandler";
import { useSignals } from "@preact/signals-react/runtime";
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
import { Scrubber } from "~/pages/PageEnigma/comps/Scrubber/Scrubber";
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
@@ -1,17 +1,17 @@
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
import { useState } from "react";
import { scale, selectedItem } from "~/pages/PageEnigma/store";
import { Clip } from "~/pages/PageEnigma/models/track";
import { Clip, ClipGroup } from "~/pages/PageEnigma/models/track";
interface Props {
min: number;
max: number;
style: "character" | "camera" | "audio" | "objects";
group: ClipGroup;
clip: Clip;
updateClip: (options: { id: string; offset: number; length: number }) => void;
}
export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
export const TrackClip = ({ clip, min, max, group, updateClip }: Props) => {
const [state, setState] = useState({
length: clip.length,
offset: clip.offset,
@@ -31,8 +31,8 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
const classes = [
"absolute",
clip.clip_uuid === selectedClipId
? `bg-${style}-selected`
: `bg-${style}-clip`,
? `bg-${group}-selected`
: `bg-${group}-clip`,
];
return (
@@ -1,7 +1,7 @@
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { Clip } from "~/pages/PageEnigma/models/track";
import { Clip, ClipGroup, ClipType } from "~/pages/PageEnigma/models/track";
import { PointerEvent } from "react";
import {
canDrop,
@@ -17,13 +17,38 @@ interface Props {
id: string;
clips: Clip[];
title: string;
style: "character" | "audio" | "camera" | "objects";
type?: "animations" | "positions" | "lipSync";
group: ClipGroup;
type?: ClipType;
toggleMute?: () => void;
muted?: boolean;
updateClip: (options: { id: string; length: number; offset: number }) => void;
}
function getCanBuild({
dragType,
type,
group,
}: {
dragType: ClipType | null;
type?: ClipType;
group: ClipGroup;
}) {
if (dragType === ClipType.ANIMATION) {
if (dragType === type) {
return true;
}
}
if (dragType === ClipType.AUDIO) {
if (group === ClipGroup.CHARACTER && type === ClipType.AUDIO) {
return true;
}
if (group === ClipGroup.GLOBAL_AUDIO) {
return true;
}
}
return false;
}
export const TrackClips = ({
id,
clips,
@@ -31,36 +56,22 @@ export const TrackClips = ({
updateClip,
muted,
title,
style,
group,
type,
}: Props) => {
const trackType = type ?? style;
const trackType = (type ?? group) as ClipType;
function onPointerOver() {
if (dragType.value === "animations") {
if (dragType.value !== trackType) {
return;
}
if (getCanBuild({ dragType: dragType.value, type, group })) {
dropId.value = id;
}
if (dragType.value === "audio") {
if (["lipSync", "audio"].indexOf(trackType) === -1) {
return;
}
}
dropId.value = id;
}
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
if (dragType.value === "animations") {
if (dragType.value !== trackType) {
return;
}
}
if (dragType.value === "audio") {
if (["lipSync", "audio"].indexOf(trackType) === -1) {
return;
}
if (!getCanBuild({ dragType: dragType.value, type, group })) {
return;
}
const track = document.getElementById(`track-${trackType}-${id}`);
if (!track) {
return;
@@ -101,7 +112,7 @@ export const TrackClips = ({
<div className="pl-16">
<div
id={`track-${trackType}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
@@ -117,7 +128,7 @@ export const TrackClips = ({
? clips[index + 1].offset
: filmLength.value * 60
}
style={style}
group={group}
updateClip={updateClip}
clip={clip}
/>
@@ -1,44 +1,40 @@
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { useState } from "react";
import { scale, selectedItem } from "~/pages/PageEnigma/store";
import { filmLength, scale, selectedItem } from "~/pages/PageEnigma/store";
import { useMouseEventsKeyframe } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSortUpDown } from "@fortawesome/pro-solid-svg-icons";
import { useSignals } from "@preact/signals-react/runtime";
interface Props {
min: number;
max: number;
keyframe: Keyframe;
updateKeyframe: (options: { id: string; offset: number }) => void;
}
export const TrackKeyFrame = ({
keyframe,
min,
max,
updateKeyframe,
}: Props) => {
const [offset, setOffset] = useState(keyframe.offset);
const { onPointerDown } = useMouseEventsKeyframe({
export const TrackKeyFrame = ({ keyframe, updateKeyframe }: Props) => {
useSignals();
const { onPointerDown, offset } = useMouseEventsKeyframe({
keyframe,
max,
min,
max: filmLength.value * 60,
min: 0,
updateKeyframe,
setOffset,
});
const displayOffset = offset > -1 ? offset : keyframe.offset;
const selectedKeyframeId =
(selectedItem.value as Keyframe | null)?.keyframe_uuid ?? "";
return (
<button
className={["block rotate-45 cursor-ew-resize", "absolute"].join(" ")}
className={[
"block rotate-45 cursor-ew-resize",
"absolute",
keyframe.keyframe_uuid === selectedKeyframeId
? "bg-keyframe-selected"
: "bg-keyframe-unselected",
].join(" ")}
style={{
width: 18,
height: 18,
backgroundColor:
keyframe.keyframe_uuid === selectedKeyframeId ? "#FFDE67" : "#EEEEEE",
left: offset * 4 * scale.value - 6,
left: displayOffset * 4 * scale.value - 6,
top: 8,
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
@@ -1,15 +1,18 @@
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { filmLength } from "~/pages/PageEnigma/store";
import { ClipGroup, Keyframe } from "~/pages/PageEnigma/models/track";
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
interface Props {
id: string;
keyframes: Keyframe[];
title: string;
style: "character" | "audio" | "camera" | "objects";
group: ClipGroup;
toggleMute?: () => void;
muted?: boolean;
updateKeyframe: (options: { id: string; offset: number }) => void;
updateKeyframe: (options: {
id: string;
offset: number;
force?: boolean;
}) => void;
}
export const TrackKeyFrames = ({
@@ -17,23 +20,17 @@ export const TrackKeyFrames = ({
keyframes,
updateKeyframe,
title,
style,
group,
}: Props) => {
return (
<div className="pl-16">
<div
id={`track-${style}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
id={`track-${group}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
>
{keyframes.map((keyframe, index) => (
{keyframes.map((keyframe) => (
<TrackKeyFrame
key={keyframe.keyframe_uuid}
min={index > 0 ? keyframes[index - 1].offset + 1 : 0}
max={
index < keyframes.length - 1
? keyframes[index + 1].offset
: filmLength.value * 60
}
updateKeyframe={updateKeyframe}
keyframe={keyframe}
/>
@@ -37,13 +37,11 @@ export const useMouseEventsClip = (
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
return;
} else if (deltaOffset + currLength.current > max) {
currOffset.current = max;
} else {
currOffset.current = deltaOffset;
}
if (deltaOffset + currLength.current > max) {
currOffset.current = max - currLength.current;
return;
}
currOffset.current = deltaOffset;
}
if (isActive.current === "left") {
if (initLength.current - delta < 30 || deltaOffset < min) {
@@ -1,4 +1,4 @@
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Keyframe } from "~/pages/PageEnigma/models/track";
import { canDrop, scale } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
@@ -8,15 +8,14 @@ export const useMouseEventsKeyframe = ({
max,
min,
updateKeyframe,
setOffset,
}: {
keyframe: Keyframe;
max: number;
min: number;
updateKeyframe: (args: { id: string; offset: number }) => void;
setOffset: Dispatch<number>;
}) => {
useSignals();
const [offset, setOffset] = useState(-1);
const currOffset = useRef(keyframe.offset);
const initOffset = useRef(keyframe.offset);
const isActive = useRef("");
@@ -30,8 +29,9 @@ export const useMouseEventsKeyframe = ({
});
isActive.current = "";
canDrop.value = false;
setOffset(-1);
}
}, [updateKeyframe, keyframe.keyframe_uuid]);
}, [updateKeyframe, keyframe.keyframe_uuid, setOffset]);
const onMouseMove = useCallback(
(event: MouseEvent) => {
@@ -40,15 +40,13 @@ export const useMouseEventsKeyframe = ({
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
return;
}
if (deltaOffset > max) {
} else if (deltaOffset > max) {
currOffset.current = max;
return;
} else {
currOffset.current = deltaOffset;
}
currOffset.current = deltaOffset;
setOffset(currOffset.current);
}
setOffset(currOffset.current);
},
[max, min, setOffset],
);
@@ -78,7 +76,9 @@ export const useMouseEventsKeyframe = ({
event.preventDefault();
clientX.current = event.clientX;
isActive.current = type;
setOffset(keyframe.offset);
}
},
offset,
};
};
@@ -3,6 +3,7 @@ import {
MediaClip,
Keyframe,
QueueKeyframe,
ClipType,
} from "~/pages/PageEnigma/models/track";
export const TrackContext = createContext<{
@@ -15,7 +16,7 @@ export const TrackContext = createContext<{
audioClips: MediaClip[];
// drag and drop
startDrag: (type: "animations" | "audio", id: string, length: number) => void;
startDrag: (type: ClipType, id: string, length: number) => void;
endDrag: () => void;
}>({
addKeyframe: () => {},
@@ -48,7 +48,7 @@ export const TrackProvider = ({ children }: Props) => {
// cross group functions
const dropClip = useCallback(() => {
if (canDrop.value) {
if (dragType.value === "animations") {
if (dragType.value === ClipType.ANIMATION) {
addCharacterAnimation({
clipId: dragId.value!,
characterId: dropId.value,
@@ -56,7 +56,7 @@ export const TrackProvider = ({ children }: Props) => {
offset: dropOffset.value,
});
}
if (dragType.value === "audio") {
if (dragType.value === ClipType.AUDIO) {
addCharacterAudio({
clipId: dragId.value!,
characterId: dropId.value,
@@ -72,36 +72,14 @@ export const TrackProvider = ({ children }: Props) => {
}
}
endDrag();
}, [animationClips, endDrag]);
}, [animationClips, endDrag, audioClips]);
const fullWidth = useMemo(() => {
return length * 60 * 4 * scale;
}, [length, scale]);
useEffect(() => {
setAnimationClips([
{
version: 1,
media_id: "m_5q9s6esz8ymjqz0bheh8nf4crtj2kx",
type: ClipType.ANIMATION,
length: 25,
name: "Sit",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Idle",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Stand",
},
{
setAnimationClips([{
version: 1,
media_id: "m_w5t517krrf63f3tj2288vsffmj87zw",
type: ClipType.ANIMATION,
@@ -110,34 +88,13 @@ export const TrackProvider = ({ children }: Props) => {
},
]);
setAudioClips([
{
version: 1,
media_id: uuid.v4(),
type: ClipType.ANIMATION,
length: 25,
name: "Sing",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.AUDIO,
length: 25,
name: "Chatter",
},
{
version: 1,
media_id: "m_403phjvjkbbaxxbz8y7r6qjay07mfd",
type: ClipType.AUDIO,
length: 25,
name: "Talk",
},
{
version: 1,
media_id: uuid.v4(),
type: ClipType.AUDIO,
length: 25,
name: "Yell",
},
}
]);
setScale(1);
setLength(12);
@@ -5,10 +5,11 @@ import {
dragId,
dragType,
} from "~/pages/PageEnigma/store";
import { ClipType } from "~/pages/PageEnigma/models/track";
export default function useUpdateDragDrop() {
const startDrag = useCallback(
(type: "animations" | "audio", id: string, length: number) => {
(type: ClipType, id: string, length: number) => {
dragId.value = id;
dragType.value = type;
clipLength.value = length;
@@ -36,7 +36,6 @@ export default function useUpdateKeyframe() {
}, []);
const deleteKeyframe = useCallback((keyframe: Keyframe) => {
console.log("del", keyframe);
DELETE_KEYFRAME[keyframe.group](keyframe);
}, []);
@@ -1,47 +1,49 @@
// Clip offsets represent the state of the clip on the timeline as well as what type of clip it is.
// it is created from a media id.
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models/track";
export class ClipUI {
version: number
type: "transform" | "audio" | "animation" | "lipsync"
group: "character" | "object" | "camera" | "global_audio"
name: string
media_id: string
object_uuid: string
offset: number // in frames
length: number // in frames
version: number;
type: ClipType;
group: ClipGroup;
name: string;
media_id: string;
object_uuid: string;
offset: number; // in frames
length: number; // in frames
constructor(
version: number,
type: "transform" | "audio" | "animation" | "lipsync",
group: "character" | "object" | "camera" | "global_audio",
type: ClipType,
group: ClipGroup,
name: string,
media_id: string,
object_uuid: string,
offset: number,
length: number,
) {
this.version = version
this.group = group // Only needed for UI
this.version = version;
this.group = group; // Only needed for UI
this.name = name // UI
this.type = type // UI and Animation / Audio / Lipsync / : Engine
this.name = name; // UI
this.type = type; // UI and Animation / Audio / Lipsync / : Engine
this.object_uuid = object_uuid // Animation / Audio / Lipsync / : Engine
this.media_id = media_id // Animation / Audio / Lipsync / : Engine
this.offset = offset
this.length = length
this.object_uuid = object_uuid; // Animation / Audio / Lipsync / : Engine
this.media_id = media_id; // Animation / Audio / Lipsync / : Engine
this.offset = offset;
this.length = length;
}
toJSON(): string {
return JSON.stringify({
version: this.version,
group:this.group,
group: this.group,
name: this.name,
type: this.type,
object_uuid: this.object_uuid,
media_id: this.media_id,
start_offset: this.offset,
ending_offset: this.length
})
ending_offset: this.length,
});
}
}
+30 -30
View File
@@ -54,9 +54,9 @@ export enum Visibility {
*/
type Data = { [key: string]: any };
class APIManagerResponseSuccess {
public user_message: String;
public user_message: string;
public data: Data | null;
constructor(user_message: String = "", data: Data | null = null) {
constructor(user_message: string = "", data: Data | null = null) {
this.data = data;
this.user_message = user_message;
}
@@ -74,7 +74,7 @@ class APIManagerResponseError extends Error {
}
export class APIManager {
baseUrl: String;
baseUrl: string;
constructor() {
this.baseUrl = "https://api.fakeyou.com";
@@ -139,9 +139,9 @@ export class APIManager {
throw new APIManagerResponseError("Failed to download file");
}
// Convert the response from a blob to json text
let blob = await file_response.blob();
const blob = await file_response.blob();
const json_result: string = await new Promise((resolve, reject) => {
let reader = new FileReader();
const reader = new FileReader();
reader.onloadend = () => resolve(JSON.parse(reader.result as string));
reader.onerror = reject;
reader.readAsText(blob);
@@ -156,7 +156,7 @@ export class APIManager {
scene_glb_media_file_token,
);
console.log(`GLB ${media_bucket_path}`);
let glbLoader = new GLTFLoader();
const glbLoader = new GLTFLoader();
// promisify this
const loadGlb = (
bucket_path: string,
@@ -188,13 +188,13 @@ export class APIManager {
* @returns
*/
private async getMediaFile(media_file_token: string): Promise<string> {
let api_base_url = "https://api.fakeyou.com";
let url = `${api_base_url}/v1/media_files/file/${media_file_token}`;
let response = await fetch(url);
let json = await JSON.parse(await response.text());
let bucketPath = json["media_file"]["public_bucket_path"];
let media_base_url = "https://storage.googleapis.com/vocodes-public";
let media_url = `${media_base_url}${bucketPath}`; // gets you a bucket path
const api_base_url = "https://api.fakeyou.com";
const url = `${api_base_url}/v1/media_files/file/${media_file_token}`;
const response = await fetch(url);
const json = await JSON.parse(await response.text());
const bucketPath = json["media_file"]["public_bucket_path"];
const media_base_url = "https://storage.googleapis.com/vocodes-public";
const media_url = `${media_base_url}${bucketPath}`; // gets you a bucket path
return media_url;
}
@@ -218,7 +218,7 @@ export class APIManager {
scene_glb_media_file_token: string | null,
): Promise<string> {
const url = `${this.baseUrl}/v1/media_files/write/engine_asset`;
let uuid = uuidv4();
const uuid = uuidv4();
const form_data = new FormData();
form_data.append("uuid_idempotency_token", uuid);
@@ -257,7 +257,7 @@ export class APIManager {
timeline_state: TimelineDataState | null, // only for now.
): Promise<string> {
const url = `${this.baseUrl}/v1/media_files/write/scene_file`;
let uuid = uuidv4();
const uuid = uuidv4();
console.log(
`Saving Scene scene_media_file_token:${scene_media_file_token} | scene_glb_media_file_token:${scene_glb_media_file_token}`,
@@ -311,7 +311,7 @@ export class APIManager {
fileName: string,
): Promise<APIManagerResponseSuccess> {
const url = `${this.baseUrl}/v1/media_uploads/upload`;
let uuid = uuidv4();
const uuid = uuidv4();
const formData = new FormData();
formData.append("uuid_idempotency_token", uuid);
@@ -346,11 +346,11 @@ export class APIManager {
negative_prompt: string,
): Promise<string> {
const url = `https://funnel.tailce84f.ts.net/preview/`;
const payload = {
style: style,
positive_prompt: positive_prompt,
negative_prompt: negative_prompt
negative_prompt: negative_prompt,
};
const formData = new FormData();
@@ -393,14 +393,15 @@ export class APIManager {
return result;
}
public async stylizeVideo(media_token: string,
style: ArtStyle,
positive_prompt:string,
negative_prompt:string,
visibility:Visibility) {
let uuid = uuidv4();
public async stylizeVideo(
media_token: string,
style: ArtStyle,
positive_prompt: string,
negative_prompt: string,
visibility: Visibility,
) {
const uuid = uuidv4();
const data = {
uuid_idempotency_token: uuid,
style: style,
@@ -410,10 +411,10 @@ export class APIManager {
trim_start_millis: 0,
trim_end_millis: 3000,
enable_lipsync: true,
creator_set_visibility: visibility
}
creator_set_visibility: visibility,
};
const json_data = JSON.stringify(data)
const json_data = JSON.stringify(data);
const response = await fetch(`${this.baseUrl}/v1/video/enqueue_vst`, {
method: "POST",
@@ -435,4 +436,3 @@ export class APIManager {
return await response.json();
}
}
+7 -7
View File
@@ -11,7 +11,7 @@ import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "../Queue/QueueNames";
import { toEngineActions } from "../Queue/toEngineActions";
import { fromEngineActions } from "../Queue/fromEngineActions";
import { ClipGroup, ClipType } from "~/pages/PageEnigma/models/track";
// Every object uuid / entity has a track.
export class TimelineCurrentState {
is_editable: boolean;
@@ -160,7 +160,7 @@ export class TimeLine {
if (new_item) {
await this.addPlayableClip(new ClipUI(
data_json['version'],
"transform",
ClipType.TRANSFORM,
data_json['group'],
object_name,
"",
@@ -171,7 +171,7 @@ export class TimeLine {
}
public async addClip(data: any) {
let object_uuid = data["data"]["object_uuid"];
const object_uuid = data["data"]["object_uuid"];
const media_id = data["data"]["media_id"];
const name = data["data"]["name"];
const group = data["data"]["group"];
@@ -194,7 +194,7 @@ export class TimeLine {
this.addPlayableClip(
new ClipUI(
version,
"lipsync",
"lipsync",// TODO potiential bug ..
group,
name,
media_id,
@@ -366,7 +366,7 @@ export class TimeLine {
// element.play()
// remove the element from the list
const object = this.scene.get_object_by_uuid(element.object_uuid);
if (element.type == "transform") {
if (element.type === ClipType.TRANSFORM) {
if (object && this.transform_engine.clips[element.object_uuid]) {
this.transform_engine.clips[element.object_uuid].step(
object,
@@ -381,14 +381,14 @@ export class TimeLine {
} else {
this.audio_engine.playClip(element.media_id);
}
} else if (element.type == "lipsync") {
} else if (element.type == "lipsync") { // we will remove this when we know which group it will come from character + audio == lip sync audio.
if (this.scrubber_frame_position + 1 >= element.length) {
this.lipSync_engine.clips[element.object_uuid].stop();
} else if (object) {
await this.lipSync_engine.clips[element.object_uuid].play(object);
this.lipSync_engine.clips[element.object_uuid].step();
}
} else if (element.type == "animation") {
} else if (element.type === ClipType.ANIMATION) {
if (object) {
await this.animation_engine.clips[object.uuid].play(object);
this.animation_engine.clips[object.uuid].step(
+1
View File
@@ -38,6 +38,7 @@ export enum ClipType {
TRANSFORM = "transform",
AUDIO = "audio",
ANIMATION = "animation",
LIPSYNC = "lipsync"
}
export enum ClipGroup {
+16 -2
View File
@@ -8,11 +8,22 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
import { signal } from "@preact/signals-core";
import { toast } from "react-hot-toast";
export const cameraGroup = signal<CameraGroup>({ id: "CG1", keyframes: [] });
export function updateCamera({ id, offset }: { id: string; offset: number }) {
const oldCameraGroup = cameraGroup.value;
const existingKeyframe = oldCameraGroup.keyframes.some((row) => {
return row.offset === offset && row.keyframe_uuid !== id;
});
if (existingKeyframe) {
toast.error("There can only be one keyframe at this offset.");
return;
}
const newKeyframes = [...oldCameraGroup.keyframes];
const keyframe = newKeyframes.find((row) => row.keyframe_uuid === id);
if (!keyframe) {
@@ -33,7 +44,11 @@ export function updateCamera({ id, offset }: { id: string; offset: number }) {
}
export function addCameraKeyframe(keyframe: QueueKeyframe, offset: number) {
console.log("cam", keyframe);
const oldCameraGroup = cameraGroup.value;
if (oldCameraGroup.keyframes.some((row) => row.offset === offset)) {
toast.error("There can only be one keyframe at this offset.");
return;
}
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
@@ -46,7 +61,6 @@ export function addCameraKeyframe(keyframe: QueueKeyframe, offset: number) {
selected: false,
} as Keyframe;
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [...oldCameraGroup.keyframes, newKeyframe].sort(
+21 -8
View File
@@ -28,14 +28,16 @@ export function updateCharacters({
id,
offset,
length,
force,
}: {
type: "animations" | "positions" | "lipSync";
type: ClipType;
id: string;
length: number;
length?: number;
offset: number;
force?: boolean;
}) {
const oldCharacterGroups = characterGroups.value;
if (type === "animations") {
if (type === ClipType.ANIMATION) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
@@ -46,7 +48,7 @@ export function updateCharacters({
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length;
clip.length = length!;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
@@ -61,7 +63,7 @@ export function updateCharacters({
});
}
if (type === "positions") {
if (type === ClipType.TRANSFORM) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
@@ -85,7 +87,7 @@ export function updateCharacters({
};
});
}
if (type === "lipSync") {
if (type === ClipType.AUDIO) {
characterGroups.value = oldCharacterGroups.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
@@ -96,7 +98,7 @@ export function updateCharacters({
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length;
clip.length = length!;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
@@ -204,6 +206,18 @@ export function addCharacterAudio({
}
export function addCharacterKeyframe(keyframe: QueueKeyframe, offset: number) {
const oldCharacterGroups = characterGroups.value;
if (
oldCharacterGroups.some((characterGroup) => {
return characterGroup.positionKeyframes.some(
(row) => row.offset === offset,
);
})
) {
return;
}
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
@@ -216,7 +230,6 @@ export function addCharacterKeyframe(keyframe: QueueKeyframe, offset: number) {
selected: false,
} as Keyframe;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== keyframe.object_uuid) {
return { ...character };
+2 -1
View File
@@ -1,6 +1,7 @@
import { signal } from "@preact/signals-core";
import { ClipType } from "~/pages/PageEnigma/models/track";
export const dragType = signal<"animations" | "audio" | null>(null);
export const dragType = signal<ClipType | null>(null);
export const dragId = signal<string | null>(null);
export const canDrop = signal(false);
export const overTimeline = signal(false);
+40 -11
View File
@@ -8,23 +8,47 @@ import * as uuid from "uuid";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { toast } from "react-hot-toast";
export const objectGroup = signal<ObjectGroup>({
id: "OB1",
objects: [],
});
export function updateObject({ id, offset }: { id: string; offset: number }) {
const oldObject = objectGroup.value;
export function updateObject({
id,
offset,
}: {
id: string;
offset: number;
}): void {
const oldObjectGroup = objectGroup.value;
const obj = oldObjectGroup.objects.find((objectTrack) =>
objectTrack.keyframes.some((row) => row.keyframe_uuid === id),
);
if (!obj) {
return;
}
const existingKeyframe = obj.keyframes.some((row) => {
return row.offset === offset && row.keyframe_uuid !== id;
});
if (existingKeyframe) {
toast.error("There can only be one keyframe at this offset.");
return;
}
objectGroup.value = {
id: oldObject.id,
objects: oldObject.objects.map((object) => ({
id: oldObjectGroup.id,
objects: oldObjectGroup.objects.map((object) => ({
object_uuid: object.object_uuid,
name: object.name,
keyframes: object.keyframes.map((keyFrame) => {
if (keyFrame.keyframe_uuid !== id) {
keyframes: object.keyframes.map((keyframe) => {
if (keyframe.keyframe_uuid !== id) {
return {
...keyFrame,
...keyframe,
};
}
@@ -32,13 +56,13 @@ export function updateObject({ id, offset }: { id: string; offset: number }) {
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: {
...keyFrame,
...keyframe,
offset,
},
});
return {
...keyFrame,
...keyframe,
offset,
};
}),
@@ -52,6 +76,11 @@ export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
(row) => row.object_uuid === keyframe.object_uuid,
);
if (obj && obj.keyframes.some((row) => row.offset === offset)) {
toast.error("There can only be one keyframe at this offset.");
return;
}
const newObject = obj ?? {
object_uuid: keyframe.object_uuid,
name: keyframe.object_name ?? "unknown",
@@ -69,7 +98,7 @@ export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
selected: false,
} as Keyframe;
newObject.keyframes.push(newKeyframe);
console.log(newObject);
newObject.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
@@ -78,7 +107,7 @@ export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
(row) => row.object_uuid !== obj?.object_uuid,
),
newObject,
].sort((objA, objB) => (objA.object_uuid < objB.object_uuid ? -1 : 1)),
+24
View File
@@ -29,6 +29,7 @@
"react": "^18.2.0",
"react-cookie": "^7.1.4",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"tailwind-merge": "^2.2.2",
"three": "^0.162.0",
"uuid": "^9.0.1",
@@ -5950,6 +5951,14 @@
"node": "*"
}
},
"node_modules/goober": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz",
"integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -10416,6 +10425,21 @@
"react": "^18.2.0"
}
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
"dependencies": {
"goober": "^2.1.10"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+1
View File
@@ -35,6 +35,7 @@
"react": "^18.2.0",
"react-cookie": "^7.1.4",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"tailwind-merge": "^2.2.2",
"three": "^0.162.0",
"uuid": "^9.0.1",
+28 -24
View File
@@ -50,17 +50,17 @@ export default {
"brand-tertiary": {
//Aquamarine Blue
DEFAULT: "#1cb6be", //=300
'50': '#eefdfc',
'100': '#d4f9f8',
'200': '#aef3f3',
'300': '#62e4e6',
'400': '#38d3d8',
'500': '#1cb6be',
'600': '#1a94a0',
'700': '#1c7682',
'800': '#1f616b',
'900': '#1e505b',
'950': '#0e353e',
"50": "#eefdfc",
"100": "#d4f9f8",
"200": "#aef3f3",
"300": "#62e4e6",
"400": "#38d3d8",
"500": "#1cb6be",
"600": "#1a94a0",
"700": "#1c7682",
"800": "#1f616b",
"900": "#1e505b",
"950": "#0e353e",
},
danger: {
//copper rust
@@ -106,18 +106,22 @@ export default {
clip: "#5F949F",
groupBg: "#2B393E",
},
audio: {
global_audio: {
unselected: "#864C68",
selected: "#D460A6",
clip: "#E37BAD",
groupBg: "#382940",
},
objects: {
object: {
unselected: "#7C5646",
selected: "#EA8E5A",
clip: "#D49D75",
groupBg: "#372E32",
},
keyframe: {
unselected: "#EEEEEE",
selected: "#FFDE67",
},
"ui-background": "#1a1a27",
"ui-panel": "#242433",
@@ -126,11 +130,11 @@ export default {
"ui-controls-button": "#676781",
"ui-divider": "#515168",
"axis-x" :"#D33242",
"axis-y" :"#308752",
"axis-z" :"#2E70FF",
red: "#D33242",
green:"#308752",
"axis-x": "#D33242",
"axis-y": "#308752",
"axis-z": "#2E70FF",
red: "#D33242",
green: "#308752",
blue: "#2E70FF",
white: colors.white,
@@ -146,11 +150,11 @@ export default {
"bg-camera-selected",
"bg-camera-unselected",
"bg-camera-clip",
"bg-audio-selected",
"bg-audio-unselected",
"bg-audio-clip",
"bg-objects-selected",
"bg-objects-unselected",
"bg-objects-clip",
"bg-global_audio-selected",
"bg-global_audio-unselected",
"bg-global_audio-clip",
"bg-object-selected",
"bg-object-unselected",
"bg-object-clip",
],
} satisfies Config;