Keyframe and speed ups.

This commit is contained in:
FromTheLOLAshes committed 2024-04-01 09:18:02 +07:00
1 parent c7849f9a3e
commit 9347782812
27 files changed
+989 -1143

No files matched your search

+3 -27
View File
@@ -22,21 +22,12 @@ import {
appUiInitialStateValues,
APPUI_VIEW_MODES,
} from "./reducers";
import {
canDrop,
clipLength,
currPosition,
dragId,
overTimeline,
scale,
timelineHeight,
} from "~/pages/PageEnigma/store";
import { timelineHeight } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragComponent";
export const PageEnigmaComponent = () => {
useSignals();
const { currX, currY } = currPosition.value;
const [appUiState, dispatchAppUiState] = useReducer(
appUiReducer,
appUiInitialStateValues,
@@ -129,22 +120,7 @@ export const PageEnigmaComponent = () => {
</div>
</EngineProvider>
</AppUIProvider>
<div
id={`ani-dnd-${dragId.value}`}
className={[
"absolute p-2",
"rounded-lg",
!canDrop.value ? "bg-brand-primary" : "bg-brand-secondary-700",
dragId.value ? "block" : "hidden",
].join(" ")}
style={{
top: overTimeline.value ? currY - 16 : currY - 32,
left: currX + 1,
zIndex: 10000,
width: overTimeline.value ? clipLength.value * 4 * scale.value : 64,
height: overTimeline.value ? 32 : 64,
}}
/>
<DragComponent />
</div>
);
};
@@ -8,7 +8,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppUiContext } from "../../contexts/AppUiContext";
import { Button, H4, H5, H6, InputVector } from "~/components";
import { Button, H5, InputVector } from "~/components";
import { XYZ } from "../../datastructures/common";
import { ACTION_TYPES } from "../../reducers/appUiReducer/types";
@@ -16,6 +16,7 @@ import { ACTION_TYPES } from "../../reducers/appUiReducer/types";
import { QueueNames } from "../../Queue/QueueNames";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { toTimelineActions } from "../../Queue/toTimelineActions";
import { QueueKeyframe } from "~/pages/PageEnigma/models/track";
export const ControlPanelSceneObject = () => {
const [appUiState, dispatchAppUiState] = useContext(AppUiContext);
@@ -24,24 +25,21 @@ export const ControlPanelSceneObject = () => {
const scale = appUiState?.currentSceneObject.objectVectors.scale;
const currentSceneObject = appUiState?.currentSceneObject;
const handlePositionChange = (xyz: XYZ) => {
if (appUiState) {
if (currentSceneObject == null) {
console.log("Missing Scene Object Position");
return;
console.log("Missing Scene Object Position");
return;
}
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
group:currentSceneObject.group,
object_uuid:currentSceneObject.object_uuid,
object_name:currentSceneObject.object_name,
version:currentSceneObject.version,
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: currentSceneObject.version,
objectVectors: {
position: { ...xyz },
rotation: appUiState.currentSceneObject.objectVectors.rotation,
@@ -54,20 +52,19 @@ export const ControlPanelSceneObject = () => {
};
const handleRotationChange = (xyz: XYZ) => {
if (appUiState) {
if (currentSceneObject == null) {
console.log("Missing Scene Object Rotation");
return;
}
if (currentSceneObject == null) {
console.log("Missing Scene Object Rotation");
return;
}
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
group:currentSceneObject.group,
object_uuid:currentSceneObject.object_uuid,
object_name:currentSceneObject.object_name,
version:currentSceneObject.version,
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: currentSceneObject.version,
objectVectors: {
position: appUiState.currentSceneObject.objectVectors.position,
rotation: { ...xyz },
@@ -80,19 +77,18 @@ export const ControlPanelSceneObject = () => {
};
const handleScaleChange = (xyz: XYZ) => {
if (appUiState) {
if (currentSceneObject == null) {
console.log("Missing Scene Object Scale");
return;
console.log("Missing Scene Object Scale");
return;
}
dispatchAppUiState({
type: ACTION_TYPES.UPDATE_CONTROLPANELS_SCENEOBJECT,
payload: {
currentSceneObject: {
group:currentSceneObject.group,
object_uuid:currentSceneObject.object_uuid,
object_name:currentSceneObject.object_name,
version:currentSceneObject.version,
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: currentSceneObject.version,
objectVectors: {
position: appUiState.currentSceneObject.objectVectors.position,
rotation: appUiState.currentSceneObject.objectVectors.rotation,
@@ -101,52 +97,53 @@ export const ControlPanelSceneObject = () => {
},
},
});
}
}
};
const handleOnAddKeyFrame = () => {
if (appUiState) {
if (
position == null ||
rotation == null ||
scale == null ||
currentSceneObject == null
) {
return;
}
if (position == null ||
rotation == null ||
scale == null ||
currentSceneObject == null)
{ return; }
// console.log(`Position ${position.x}`)
// console.log(`Position ${position.y}`)
// console.log(`Position ${position.z}`)
// console.log(`Position ${position.x}`)
// console.log(`Position ${position.y}`)
// console.log(`Position ${position.z}`)
// console.log(`Rotation ${rotation.x}`)
// console.log(`Rotation ${rotation.y}`)
// console.log(`Rotation ${rotation.z}`)
// console.log(`Rotation ${rotation.x}`)
// console.log(`Rotation ${rotation.y}`)
// console.log(`Rotation ${rotation.z}`)
// console.log(`Scale ${scale.x}`)
// console.log(`Scale ${scale.y}`)
// console.log(`Scale ${scale.z}`)
// console.log(`Scale ${scale.x}`)
// console.log(`Scale ${scale.y}`)
// console.log(`Scale ${scale.z}`)
console.log(`Group ${currentSceneObject.group}`)
console.log(`Object UUID ${currentSceneObject.object_uuid}`)
console.log(`Object Name ${currentSceneObject.object_name}`)
console.log(`Object Version ${currentSceneObject.version}`)
console.log(`Group ${currentSceneObject.group}`);
console.log(`Object UUID ${currentSceneObject.object_uuid}`);
console.log(`Object Name ${currentSceneObject.object_name}`);
console.log(`Object Version ${currentSceneObject.version}`);
Queue.publish({queueName:QueueNames.TO_TIMELINE, action: toTimelineActions.ADD_KEYFRAME,
data: {
group:currentSceneObject.group,
object_uuid:currentSceneObject.object_uuid,
object_name:currentSceneObject.object_name,
version:currentSceneObject.version,
Queue.publish({
queueName: QueueNames.TO_TIMELINE,
action: toTimelineActions.ADD_KEYFRAME,
data: {
group: currentSceneObject.group,
object_uuid: currentSceneObject.object_uuid,
object_name: currentSceneObject.object_name,
version: 1,
position: currentSceneObject.objectVectors.position,
rotation: currentSceneObject.objectVectors.rotation,
scale:currentSceneObject.objectVectors.scale
}
})
position: currentSceneObject.objectVectors.position,
rotation: currentSceneObject.objectVectors.rotation,
scale: currentSceneObject.objectVectors.scale,
} as QueueKeyframe,
});
}
}
};
return (
<Transition
@@ -203,9 +200,11 @@ export const ControlPanelSceneObject = () => {
<span className="h-2" />
<div className="flex gap-2">
<Button variant="secondary"
className="grow"
onClick={handleOnAddKeyFrame}>
<Button
variant="secondary"
className="grow"
onClick={handleOnAddKeyFrame}
>
Add Keyframe (K)
</Button>
<Button variant="secondary" icon={faTrash} />
@@ -0,0 +1,33 @@
import {
canDrop,
clipLength,
currPosition,
dragId,
overTimeline,
scale,
} from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
export const DragComponent = () => {
useSignals();
const { currX, currY } = currPosition.value;
return (
<div
id={`ani-dnd-${dragId.value}`}
className={[
"absolute p-2",
"rounded-lg",
!canDrop.value ? "bg-brand-primary" : "bg-brand-secondary-700",
dragId.value ? "block" : "hidden",
].join(" ")}
style={{
top: overTimeline.value ? currY - 16 : currY - 32,
left: currX + 1,
zIndex: 10000,
width: overTimeline.value ? clipLength.value * 4 * scale.value : 64,
height: overTimeline.value ? 32 : 64,
}}
/>
);
};
@@ -0,0 +1,29 @@
import { currentTime, fullHeight, scale } from "~/pages/PageEnigma/store";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
import { useMouseEventsAnimation } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsAnimation";
export const Scrubber = () => {
const { onPointerDown, time } = useMouseEventsAnimation();
const displayTime = time === -1 ? currentTime.value : time;
return (
<div
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
style={{ top: 8, left: displayTime * 4 * scale.value + 84 }}
onPointerDown={onPointerDown}
>
<div>
<FontAwesomeIcon icon={faSortDown} className="h-5 text-brand-primary" />
</div>
<div
className="block bg-brand-primary"
style={{
width: 2,
marginTop: -5,
height: fullHeight.value,
}}
/>
</div>
);
};
@@ -1,11 +1,13 @@
import { useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { fullWidth } from "~/pages/PageEnigma/store";
import {
audioGroup,
fullWidth,
toggleAudioMute,
updateAudio,
} from "~/pages/PageEnigma/store";
export const Audio = () => {
const { audio, updateAudio, toggleAudioMute } = useContext(TrackContext);
const { clips } = audio!;
const { clips } = audioGroup.value;
return (
<div
@@ -17,12 +19,12 @@ export const Audio = () => {
</div>
<div className="flex flex-col gap-4">
<TrackClips
id={audio!.id}
id={audioGroup.value.id}
clips={clips}
title="Global Audio TrackClips"
updateClip={updateAudio}
style="audio"
muted={audio!.muted}
muted={audioGroup.value.muted}
toggleMute={toggleAudioMute}
/>
</div>
@@ -1,11 +1,8 @@
import { useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { fullWidth } from "~/pages/PageEnigma/store";
import { cameraGroup, fullWidth, updateCamera } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
export const Camera = () => {
const { camera, updateCamera } = useContext(TrackContext);
const { keyframes } = camera!;
const { keyframes } = cameraGroup.value!;
return (
<div
@@ -15,7 +12,7 @@ export const Camera = () => {
<div className="mb-5 pt-2 text-xs font-medium text-white">Camera</div>
<div className="flex flex-col gap-4">
<TrackKeyFrames
id={camera!.id}
id={cameraGroup.value.id}
keyframes={keyframes}
title="Camera Position/Rotation"
style="camera"
@@ -1,7 +1,11 @@
import { useCallback, useContext, useMemo } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useCallback, useMemo } from "react";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { fullWidth } from "~/pages/PageEnigma/store";
import {
characterGroups,
fullWidth,
toggleLipSyncMute,
updateCharacters,
} from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
function buildUpdaters(
@@ -36,16 +40,14 @@ interface Props {
}
export const Character = ({ characterId }: Props) => {
const { characters, updateCharacters, toggleLipSyncMute } =
useContext(TrackContext);
const character = characters.find((row) => (row.id = characterId));
const character = characterGroups.value.find((row) => (row.id = characterId));
const { updateClipLipSync, updateClipPosition, updateClipAnimations } =
useMemo(() => buildUpdaters(updateCharacters), [updateCharacters]);
useMemo(() => buildUpdaters(updateCharacters), []);
const toggleCharacterLipSyncMute = useCallback(() => {
toggleLipSyncMute(character?.id ?? "");
}, [character?.id, toggleLipSyncMute]);
}, [character?.id]);
if (!character) {
return false;
@@ -0,0 +1,14 @@
import { characterGroups } from "~/pages/PageEnigma/store";
import { Character } from "~/pages/PageEnigma/comps/Timeline/Character";
import { useSignals } from "@preact/signals-react/runtime";
export const Characters = () => {
useSignals();
return (
<>
{characterGroups.value.map((character) => (
<Character key={character.id} characterId={character.id} />
))}
</>
);
};
@@ -0,0 +1,16 @@
import { objectGroup } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
import { ObjectTrack } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
export const ObjectGroups = () => {
useSignals();
return (
<>
{objectGroup.value.objects.length > 0 && (
<div className="p-4">
<ObjectTrack />
</div>
)}
</>
);
};
@@ -1,11 +1,7 @@
import { useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { fullWidth } from "~/pages/PageEnigma/store";
import { fullWidth, objectGroup, updateObject } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
export const Objects = () => {
const { objects, updateObject } = useContext(TrackContext);
export const ObjectTrack = () => {
return (
<div
className="block rounded-lg bg-objects-groupBg pb-5 pl-2 pr-4"
@@ -14,7 +10,7 @@ export const Objects = () => {
<div className="prevent-select mb-5 pt-2 text-xs font-medium text-white">
Objects
</div>
{objects.objects.map((object) => (
{objectGroup.value.objects.map((object) => (
<div key={object.object_uuid} className="flex flex-col gap-4">
<TrackKeyFrames
id={object.object_uuid}
+16 -100
View File
@@ -1,42 +1,30 @@
import { Fragment, useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useState } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { LowerPanel } from "~/modules/LowerPanel";
import { Character } from "./Character";
import { Camera } from "./Camera";
import { Audio } from "./Audio";
import { Objects } from "./Objects";
import { useMouseEventsAnimation } from "./utils/useMouseEventsAnimation";
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ConfirmationModal } from "~/components/ConfirmationModal";
import {
scale,
currentTime,
filmLength,
timelineHeight,
deleteAudioClip,
deleteCharacterClip,
} from "~/pages/PageEnigma/store";
import { useQueueHandler } from "~/pages/PageEnigma/comps/Timeline/utils/useQueueHandler";
import { useSignals } from "@preact/signals-react/runtime";
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
import { Scrubber } from "~/pages/PageEnigma/comps/Scrubber/Scrubber";
import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
export const Timeline = () => {
const {
characters,
objects,
selectedItem,
deleteCharacterClip,
deleteAudioClip,
deleteCameraClip,
} = useContext(TrackContext);
const { onPointerDown, time } = useMouseEventsAnimation();
useSignals();
const { selectedItem } = useContext(TrackContext);
const [dialogOpen, setDialogOpen] = useState(false);
// implement the code to handle incoming messages from the Engine
useQueueHandler();
const sectionWidth = 60 * 4 * scale.value;
const fullHeight =
characters.length * 268 + objects.objects.length * 60 + 300 + 96;
useEffect(() => {
timelineHeight.value = window.outerHeight * 0.25;
}, []);
@@ -50,13 +38,10 @@ export const Timeline = () => {
[selectedItem],
);
const displayTime = time === -1 ? currentTime.value : time;
const onDelete = useCallback(() => {
deleteCharacterClip(selectedItem!);
deleteCameraClip(selectedItem!);
deleteAudioClip(selectedItem!);
}, [selectedItem, deleteAudioClip, deleteCharacterClip, deleteCameraClip]);
}, [selectedItem]);
useEffect(() => {
document.addEventListener("keydown", onDeleteAsk);
@@ -66,59 +51,13 @@ export const Timeline = () => {
};
}, [onDeleteAsk]);
console.log("timeline");
return (
<>
<LowerPanel>
<div
className={[
"prevent-select mt-4",
"flex h-3",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
>
{Array(filmLength.value)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute ps-1 pt-1"
style={{ left: index * sectionWidth + 92 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth + 88,
height: fullHeight,
}}
/>
</Fragment>
))}
<div
className="absolute"
style={{ left: filmLength.value * sectionWidth + 92 }}
>
00:
{filmLength.value < 10
? "0" + filmLength.value.toString()
: filmLength.value.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: filmLength.value * sectionWidth + 88,
height: fullHeight,
}}
/>
</div>
<TimerGrid />
<div className="p-4">
{characters.map((character) => (
<Character key={character.id} characterId={character.id} />
))}
<Characters />
</div>
<div className="p-4">
<Camera />
@@ -126,31 +65,8 @@ export const Timeline = () => {
<div className="p-4">
<Audio />
</div>
{objects.objects.length > 0 && (
<div className="p-4">
<Objects />
</div>
)}
<div
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
style={{ top: 8, left: displayTime * 4 * scale.value + 84 }}
onPointerDown={onPointerDown}
>
<div>
<FontAwesomeIcon
icon={faSortDown}
className="h-5 text-brand-primary"
/>
</div>
<div
className="block bg-brand-primary"
style={{
width: 2,
marginTop: -5,
height: fullHeight,
}}
/>
</div>
<ObjectGroups />
<Scrubber />
</LowerPanel>
<ConfirmationModal
title="Delete Clip"
@@ -116,19 +116,19 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
onPointerDown={(event) => onPointerDown(event, "right")}
onClick={() => selectItem(clip.clip_uuid)}
/>
{selectedItem === clip.clip_uuid && (
<button
className="absolute flex h-full items-center justify-center"
style={{
width: length * 4 * scale.value,
left: offset * 4 * scale.value,
zIndex: 2000,
}}
onPointerDown={onPlayClick}
>
<FontAwesomeIcon icon={faPlay} />
</button>
)}
{/*{selectedItem === clip.clip_uuid && (*/}
{/* <button*/}
{/* className="absolute flex h-full items-center justify-center"*/}
{/* style={{*/}
{/* width: length * 4 * scale.value,*/}
{/* left: offset * 4 * scale.value,*/}
{/* zIndex: 2000,*/}
{/* }}*/}
{/* onPointerDown={onPlayClick}*/}
{/* >*/}
{/* <FontAwesomeIcon icon={faPlay} />*/}
{/* </button>*/}
{/*)}*/}
</>
);
};
@@ -23,7 +23,7 @@ export function useQueueHandler() {
const { addKeyframe } = useContext(TrackContext);
const handleFromEngineActions = useCallback(({ action, data }: Arguments) => {
console.log(`FROM ENGINE:${action} ${data}`);
console.log("FROM ENGINE", action, data);
switch (action) {
case fromEngineActions.UPDATE_TIME:
currentTime.value = (data as UpdateTime).currentTime;
@@ -34,10 +34,11 @@ export function useQueueHandler() {
}, []);
const handleToTimelineActions = useCallback(({ action, data }: Arguments) => {
console.log(`FROM ENGINE:${action} ${data}`);
console.log("TO TIMELINE", action, data);
switch (action) {
case toTimelineActions.ADD_KEYFRAME:
addKeyframe(data as QueueKeyframe);
console.log(1);
addKeyframe(data as QueueKeyframe, currentTime.value);
break;
default:
throw new Error(`Unknown action ${action}`);
@@ -0,0 +1,55 @@
import { filmLength, fullHeight, scale } from "~/pages/PageEnigma/store";
import { Fragment } from "react";
export const TimerGrid = () => {
const sectionWidth = 60 * 4 * scale.value;
return (
<div
className={[
"prevent-select mt-4",
"flex h-3",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
>
{Array(filmLength.value)
.fill(0)
.map((_, index) => (
<Fragment key={index}>
<div
className="absolute ps-1 pt-1"
style={{ left: index * sectionWidth + 92 }}
>
00:{index < 10 ? "0" + index.toString() : index.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: index * sectionWidth + 88,
height: fullHeight.value,
}}
/>
</Fragment>
))}
<div
className="absolute"
style={{ left: filmLength.value * sectionWidth + 92 }}
>
00:
{filmLength.value < 10
? "0" + filmLength.value.toString()
: filmLength.value.toString()}
</div>
<div
className="absolute block h-full bg-ui-divider"
style={{
width: 1,
left: filmLength.value * sectionWidth + 88,
height: fullHeight.value,
}}
/>
</div>
);
};
@@ -10,63 +10,14 @@ import {
} from "~/pages/PageEnigma/models/track";
export const TrackContext = createContext<{
// timeline characters and it's functions
characters: CharacterGroup[];
updateCharacters: (options: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => void;
toggleLipSyncMute: (characterId: string) => void;
addCharacterAnimation: (options: {
clipId: string;
characterId: string;
animationClips: MediaClip[];
offset: number;
}) => void;
addCharacterAudio: (options: {
clipId: string;
characterId: string;
audioClips: MediaClip[];
offset: number;
}) => void;
selectCharacterClip: (clipId: string) => void;
deleteCharacterClip: (clipId: string) => void;
// timeline camera group
camera: CameraGroup | null;
updateCamera: (options: { id: string; offset: number }) => void;
selectCameraKeyframe: (clipId: string) => void;
// timeline global audio group
audio: AudioGroup | null;
updateAudio: (options: {
id: string;
length: number;
offset: number;
}) => void;
toggleAudioMute: () => void;
addGlobalAudio: (
clipId: string,
animationClips: MediaClip[],
offset: number,
) => void;
selectAudioClip: (clipId: string) => void;
deleteAudioClip: (clipId: string) => void;
// timeline objects group
objects: ObjectGroup;
updateObject: (options: { id: string; offset: number }) => void;
// keyframes
addKeyframe: (keyframe: QueueKeyframe, offset: number) => void;
deleteKeyframe: (keyframe: Keyframe) => void;
// current - only select one item - will be replaced
selectedItem: string | null;
selectItem: (itemId: string) => void;
// keyframes
addKeyframe: (keyframe: QueueKeyframe, offset: number) => void;
deleteKeyframe: (keyframe: Keyframe) => void;
// sidebar clips
animationClips: MediaClip[];
audioClips: MediaClip[];
@@ -79,28 +30,6 @@ export const TrackContext = createContext<{
) => void;
endDrag: () => void;
}>({
characters: [],
updateCharacters: () => {},
toggleLipSyncMute: () => {},
addCharacterAnimation: () => {},
addCharacterAudio: () => {},
selectCharacterClip: () => {},
deleteCharacterClip: () => {},
camera: null,
updateCamera: () => {},
selectCameraKeyframe: () => {},
audio: null,
updateAudio: () => {},
toggleAudioMute: () => {},
selectAudioClip: () => {},
addGlobalAudio: () => {},
deleteAudioClip: () => {},
objects: { id: "", objects: [] },
updateObject: () => {},
addKeyframe: () => {},
deleteKeyframe: () => {},
@@ -1,12 +1,9 @@
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters";
import useUpdateCamera from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera";
import useUpdateAudio from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio";
import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject";
import { ClipType, MediaClip } from "~/pages/PageEnigma/models/track";
import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop";
import {
addCharacterAnimation,
canDrop,
dragId,
dragType,
@@ -21,16 +18,6 @@ interface Props {
}
export const TrackProvider = ({ children }: Props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { addCharacterKeyframe, deleteCharacterKeyframe, ...characters } =
useUpdateCharacters();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { addCameraKeyframe, deleteCameraKeyframe, ...camera } =
useUpdateCamera();
const audio = useUpdateAudio();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { addObjectKeyframe, deleteObjectKeyframe, ...objects } =
useUpdateObject();
const keyframes = useUpdateKeyframe();
const { endDrag, ...dragDrop } = useUpdateDragDrop();
@@ -65,7 +52,7 @@ export const TrackProvider = ({ children }: Props) => {
const dropClip = useCallback(() => {
if (canDrop.value) {
if (dragType.value === "animations") {
characters.addCharacterAnimation({
addCharacterAnimation({
clipId: dragId.value!,
characterId: dropId.value,
animationClips,
@@ -74,7 +61,7 @@ export const TrackProvider = ({ children }: Props) => {
}
}
endDrag();
}, [animationClips, characters, endDrag]);
}, [animationClips, endDrag]);
const fullWidth = useMemo(() => {
return length * 60 * 4 * scale;
@@ -118,11 +105,6 @@ export const TrackProvider = ({ children }: Props) => {
const values = useMemo(() => {
return {
...characters,
...camera,
...audio,
...objects,
...keyframes,
selectItem,
@@ -143,11 +125,6 @@ export const TrackProvider = ({ children }: Props) => {
setTimelineHeight,
};
}, [
characters,
camera,
audio,
objects,
keyframes,
selectItem,
@@ -1,153 +0,0 @@
import { useCallback, useState } from "react";
import {
AudioGroup,
Clip,
ClipGroup,
ClipType,
MediaClip,
} from "~/pages/PageEnigma/models/track";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
export default function useUpdateAudio() {
const [audio, setAudio] = useState<AudioGroup>({
id: "AU1",
muted: false,
clips: [],
});
const updateAudio = useCallback(
({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) => {
setAudio((oldAudio) => {
const newClips = [...oldAudio.clips];
const clipIndex = newClips.findIndex((row) => row.clip_uuid === id);
if (clipIndex === -1) {
return { ...oldAudio };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...oldAudio,
clips: newClips,
};
});
},
[],
);
const addGlobalAudio = useCallback(
(dragId: string, audioClips: MediaClip[], offset: number) => {
const clip = audioClips.find((row) => row.media_id === dragId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
offset,
clip_uuid,
} as Clip;
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [...oldAudio.clips, newClip],
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
},
[],
);
const toggleAudioMute = useCallback(() => {
setAudio((oldAudio) => {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: oldAudio?.muted ? toEngineActions.UNMUTE : toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
},
});
return {
...oldAudio,
muted: !oldAudio.muted,
};
});
}, []);
const selectAudioClip = useCallback((clipId: string) => {
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [
...oldAudio.clips.map((clip) => {
return {
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
};
}),
],
};
});
}, []);
const deleteAudioClip = useCallback((clipId: string) => {
setAudio((oldAudio) => {
return {
...oldAudio,
clips: [
...oldAudio.clips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
],
};
});
}, []);
return {
audio,
updateAudio,
toggleAudioMute,
selectAudioClip,
addGlobalAudio,
deleteAudioClip,
};
}
@@ -1,122 +0,0 @@
import { useCallback, useState } from "react";
import {
CameraGroup,
Keyframe,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
export default function useUpdateCamera() {
const [camera, setCamera] = useState<CameraGroup>({
id: "CA1",
keyframes: [],
});
const updateCamera = useCallback(
({ id, offset }: { id: string; offset: number }) => {
setCamera((oldCamera) => {
const newKeyframes = [...oldCamera.keyframes];
const keyframe = newKeyframes.find((row) => row.keyframe_uuid === id);
if (!keyframe) {
return { ...oldCamera };
}
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: keyframe,
});
return {
...oldCamera,
clips: newKeyframes,
};
});
},
[],
);
const addCameraKeyframe = useCallback(
(keyframe: QueueKeyframe, offset: number) => {
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
setCamera((oldCamera) => {
return {
...oldCamera,
keyframes: [...oldCamera.keyframes, newKeyframe].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
},
[],
);
const selectCameraKeyframe = useCallback((keyframeId: string) => {
setCamera((oldCamera) => {
return {
...oldCamera,
clips: [
...oldCamera.keyframes.map((keyframe) => {
return {
...keyframe,
selected:
keyframe.keyframe_uuid === keyframeId
? !keyframe.selected
: keyframe.selected,
};
}),
],
};
});
}, []);
const deleteCameraKeyframe = useCallback((keyframeId: string) => {
setCamera((oldCamera) => {
return {
...oldCamera,
keyframes: [
...oldCamera.keyframes.filter((keyframe) => {
if (keyframe.keyframe_uuid === keyframeId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: keyframe,
});
return false;
}
return true;
}),
],
};
});
}, []);
return {
camera,
updateCamera,
selectCameraKeyframe,
addCameraKeyframe,
deleteCameraKeyframe,
};
}
@@ -1,383 +0,0 @@
import { useCallback, useState } from "react";
import {
Clip,
CharacterGroup,
ClipGroup,
ClipType,
MediaClip,
QueueKeyframe,
Keyframe,
} from "~/pages/PageEnigma/models/track";
import * as uuid from "uuid";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
export default function useUpdateCharacters() {
const [characters, setCharacters] = useState<CharacterGroup[]>([
{
id: "CH1",
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
},
]);
const updateCharacters = useCallback(
({
type,
id,
offset,
length,
}: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) => {
if (type === "animations") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
animationClips: newAnimationClips,
};
});
});
return;
}
if (type === "positions") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
(row) => row.keyframe_uuid === id,
);
if (keyframeIndex === -1) {
return { ...character };
}
const keyframe = newPositionKeyframes[keyframeIndex];
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: keyframe,
});
return {
...character,
positionClips: newPositionKeyframes,
};
});
});
return;
}
if (type === "lipSync") {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
});
}
},
[],
);
const addCharacterAnimation = useCallback(
({
clipId,
characterId,
animationClips,
offset,
}: {
clipId: string;
characterId: string;
animationClips: MediaClip[];
offset: number;
}) => {
const clip = animationClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
animationClips: [...character.animationClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
},
[],
);
const addCharacterAudio = useCallback(
({
clipId,
characterId,
audioClips,
offset,
}: {
clipId: string;
characterId: string;
audioClips: MediaClip[];
offset: number;
}) => {
const clip = audioClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
},
[],
);
const addCharacterKeyframe = useCallback(
(keyframe: QueueKeyframe, offset: number) => {
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id !== keyframe.object_uuid) {
return { ...character };
}
return {
...character,
positionKeyframes: [
...character.positionKeyframes,
newKeyframe,
].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
});
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
},
[],
);
const toggleLipSyncMute = useCallback((characterId: string) => {
setCharacters((oldCharacters) => {
return oldCharacters.map((character) => {
if (character.id === characterId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: character?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
object_uuid: characterId,
},
});
}
return {
...character,
muted:
character.id === characterId ? !character.muted : character.muted,
};
});
});
}, []);
const selectCharacterClip = useCallback((clipId: string) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionKeyframes.map((keyframe) => ({
...keyframe,
selected:
keyframe.keyframe_uuid === clipId
? !keyframe.selected
: keyframe.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected:
clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
})),
];
});
}, []);
const deleteCharacterClip = useCallback((clipId: string) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
animationClips: character.animationClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
lipSyncClips: character.lipSyncClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
})),
];
});
}, []);
const deleteCharacterKeyframe = useCallback((keyframe: Keyframe) => {
setCharacters((oldCharacters) => {
return [
...oldCharacters.map((character) => ({
...character,
positionClips: character.positionKeyframes.filter((row) => {
if (row.keyframe_uuid === keyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: row,
});
return false;
}
return true;
}),
})),
];
});
}, []);
return {
characters,
updateCharacters,
toggleLipSyncMute,
addCharacterAnimation,
addCharacterAudio,
selectCharacterClip,
deleteCharacterClip,
addCharacterKeyframe,
deleteCharacterKeyframe,
};
}
@@ -4,37 +4,33 @@ import {
QueueKeyframe,
Keyframe,
} from "~/pages/PageEnigma/models/track";
import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject";
import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters";
import {
addCharacterKeyframe,
addObjectKeyframe,
deleteCharacterKeyframe,
deleteObjectKeyframe,
} from "~/pages/PageEnigma/store";
export default function useUpdateKeyframe() {
const { addObjectKeyframe, deleteObjectKeyframe } = useUpdateObject();
const { addCharacterKeyframe, deleteCharacterKeyframe } =
useUpdateCharacters();
const addKeyframe = useCallback((keyframe: QueueKeyframe, offset: number) => {
console.log(keyframe, offset);
if (keyframe.group === ClipGroup.OBJECT) {
console.log(2);
addObjectKeyframe(keyframe, offset);
}
if (keyframe.group === ClipGroup.CHARACTER) {
addCharacterKeyframe(keyframe, offset);
}
}, []);
const addKeyframe = useCallback(
(keyframe: QueueKeyframe, offset: number) => {
if (keyframe.group === ClipGroup.OBJECT) {
addObjectKeyframe(keyframe, offset);
}
if (keyframe.group === ClipGroup.CHARACTER) {
addCharacterKeyframe(keyframe, offset);
}
},
[addCharacterKeyframe, addObjectKeyframe],
);
const deleteKeyframe = useCallback(
(keyframe: Keyframe) => {
if (keyframe.group === ClipGroup.OBJECT) {
deleteObjectKeyframe(keyframe);
}
if (keyframe.group === ClipGroup.CHARACTER) {
deleteCharacterKeyframe(keyframe);
}
},
[deleteCharacterKeyframe, deleteObjectKeyframe],
);
const deleteKeyframe = useCallback((keyframe: Keyframe) => {
if (keyframe.group === ClipGroup.OBJECT) {
deleteObjectKeyframe(keyframe);
}
if (keyframe.group === ClipGroup.CHARACTER) {
deleteCharacterKeyframe(keyframe);
}
}, []);
return {
addKeyframe,
@@ -1,122 +0,0 @@
import { useCallback, useState } from "react";
import {
ObjectGroup,
QueueKeyframe,
Keyframe,
} from "~/pages/PageEnigma/models/track";
import * as uuid from "uuid";
export default function useUpdateObject() {
const [objects, setObjects] = useState<ObjectGroup>({
id: "OB1",
objects: [],
});
const updateObject = useCallback(
({ id, offset }: { id: string; offset: number }) => {
setObjects((oldObject) => {
return {
id: oldObject.id,
objects: oldObject.objects.map((object) => ({
object_uuid: object.object_uuid,
name: object.name,
keyframes: object.keyframes.map((keyFrame) => ({
...keyFrame,
offset: keyFrame.keyframe_uuid === id ? offset : keyFrame.offset,
})),
})),
};
});
},
[],
);
const addObjectKeyframe = useCallback(
(keyframe: QueueKeyframe, offset: number) => {
setObjects((oldObjectGroup) => {
const obj = objects.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
const newObject = obj ?? {
object_uuid: keyframe.object_uuid,
name: keyframe.object_name ?? "unknown",
keyframes: [] as Keyframe[],
};
newObject.keyframes.push({
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe);
newObject.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
return {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
),
newObject,
].sort((objA, objB) =>
objA.object_uuid < objB.object_uuid ? -1 : 1,
),
};
});
},
[objects],
);
const deleteObjectKeyframe = useCallback((keyframe: Keyframe) => {
setObjects((oldObjectGroup) => {
const obj = oldObjectGroup.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
if (!obj) {
return oldObjectGroup;
}
const newKeyframes = [
...obj.keyframes.filter(
(row) => row.keyframe_uuid !== keyframe.keyframe_uuid,
),
];
if (newKeyframes.length) {
return {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.map((object) => ({
...object,
...(object.object_uuid === keyframe.object_uuid
? { keyframes: newKeyframes }
: {}),
})),
],
};
}
return {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
),
],
};
});
}, []);
return {
objects,
updateObject,
addObjectKeyframe,
deleteObjectKeyframe,
};
}
+136
View File
@@ -0,0 +1,136 @@
import {
AudioGroup,
Clip,
ClipGroup,
ClipType,
MediaClip,
} from "~/pages/PageEnigma/models/track";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
import { signal } from "@preact/signals-core";
export const audioGroup = signal<AudioGroup>({
id: "AG-1",
clips: [],
muted: false,
});
export function updateAudio({
id,
offset,
length,
}: {
id: string;
length: number;
offset: number;
}) {
const oldAudioGroup = audioGroup.value;
const newClips = [...oldAudioGroup.clips];
const clipIndex = newClips.findIndex((row) => row.clip_uuid === id);
if (clipIndex === -1) {
return { ...oldAudioGroup };
}
const clip = newClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
audioGroup.value = {
...oldAudioGroup,
clips: newClips,
};
}
export function addGlobalAudio(
dragId: string,
audioClips: MediaClip[],
offset: number,
) {
const clip = audioClips.find((row) => row.media_id === dragId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
offset,
clip_uuid,
} as Clip;
const oldAudioGroup = audioGroup.value;
audioGroup.value = {
...oldAudioGroup,
clips: [...oldAudioGroup.clips, newClip],
};
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function toggleAudioMute() {
const oldAudioGroup = audioGroup.value;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: oldAudioGroup?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.GLOBAL_AUDIO,
},
});
audioGroup.value = {
...oldAudioGroup,
muted: !oldAudioGroup.muted,
};
}
export function selectAudioClip(clipId: string) {
const oldAudioGroup = audioGroup.value;
audioGroup.value = {
...oldAudioGroup,
clips: [
...oldAudioGroup.clips.map((clip) => {
return {
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
};
}),
],
};
}
export function deleteAudioClip(clipId: string) {
const oldAudioGroup = audioGroup.value;
audioGroup.value = {
...oldAudioGroup,
clips: [
...oldAudioGroup.clips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
],
};
}
+99
View File
@@ -0,0 +1,99 @@
import {
CameraGroup,
Keyframe,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
import { signal } from "@preact/signals-core";
export const cameraGroup = signal<CameraGroup>({ id: "CG1", keyframes: [] });
export function updateCamera({ id, offset }: { id: string; offset: number }) {
const oldCameraGroup = cameraGroup.value;
const newKeyframes = [...oldCameraGroup.keyframes];
const keyframe = newKeyframes.find((row) => row.keyframe_uuid === id);
if (!keyframe) {
return;
}
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_KEYFRAME,
data: keyframe,
});
cameraGroup.value = {
...oldCameraGroup,
keyframes: newKeyframes,
};
}
export function addCameraKeyframe(keyframe: QueueKeyframe, offset: number) {
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [...oldCameraGroup.keyframes, newKeyframe].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
}
export function selectCameraKeyframe(keyframeId: string) {
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [
...oldCameraGroup.keyframes.map((keyframe) => {
return {
...keyframe,
selected:
keyframe.keyframe_uuid === keyframeId
? !keyframe.selected
: keyframe.selected,
};
}),
],
};
}
export function deleteCameraKeyframe(keyframeId: string) {
const oldCameraGroup = cameraGroup.value;
cameraGroup.value = {
...oldCameraGroup,
keyframes: [
...oldCameraGroup.keyframes.filter((keyframe) => {
if (keyframe.keyframe_uuid === keyframeId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: keyframe,
});
return false;
}
return true;
}),
],
};
}
@@ -0,0 +1,331 @@
import {
CharacterGroup,
Clip,
ClipGroup,
ClipType,
Keyframe,
MediaClip,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
import { signal } from "@preact/signals-core";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import * as uuid from "uuid";
export const characterGroups = signal<CharacterGroup[]>([
{
id: "CH1",
muted: false,
animationClips: [],
positionKeyframes: [],
lipSyncClips: [],
},
]);
export function updateCharacters({
type,
id,
offset,
length,
}: {
type: "animations" | "positions" | "lipSync";
id: string;
length: number;
offset: number;
}) {
const oldCharacterGroups = characterGroups.value;
if (type === "animations") {
characterGroups.value = oldCharacterGroups.map((character) => {
const newAnimationClips = [...character.animationClips];
const clipIndex = newAnimationClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newAnimationClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
animationClips: newAnimationClips,
};
});
}
if (type === "positions") {
characterGroups.value = oldCharacterGroups.map((character) => {
const newPositionKeyframes = [...character.positionKeyframes];
const keyframeIndex = newPositionKeyframes.findIndex(
(row) => row.keyframe_uuid === id,
);
if (keyframeIndex === -1) {
return { ...character };
}
const keyframe = newPositionKeyframes[keyframeIndex];
keyframe.offset = offset;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: keyframe,
});
return {
...character,
positionClips: newPositionKeyframes,
};
});
}
if (type === "lipSync") {
characterGroups.value = oldCharacterGroups.map((character) => {
const newLipSyncClips = [...character.lipSyncClips];
const clipIndex = newLipSyncClips.findIndex(
(row) => row.clip_uuid === id,
);
if (clipIndex === -1) {
return { ...character };
}
const clip = newLipSyncClips[clipIndex];
clip.offset = offset;
clip.length = length;
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.UPDATE_CLIP,
data: clip,
});
return {
...character,
lipSyncClips: newLipSyncClips,
};
});
}
}
export function addCharacterAnimation({
clipId,
characterId,
animationClips,
offset,
}: {
clipId: string;
characterId: string;
animationClips: MediaClip[];
offset: number;
}) {
const clip = animationClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
animationClips: [...character.animationClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function addCharacterAudio({
clipId,
characterId,
audioClips,
offset,
}: {
clipId: string;
characterId: string;
audioClips: MediaClip[];
offset: number;
}) {
const clip = audioClips.find((row) => row.media_id === clipId);
if (!clip) {
return;
}
const clip_uuid = uuid.v4();
const newClip = {
...clip,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
offset,
clip_uuid,
object_uuid: characterId,
} as Clip;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== characterId) {
return { ...character };
}
return {
...character,
lipSyncClips: [...character.lipSyncClips, newClip].sort(
(clipA, clipB) => clipA.offset - clipB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_CLIP,
data: newClip,
});
}
export function addCharacterKeyframe(keyframe: QueueKeyframe, offset: number) {
const newKeyframe = {
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe;
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id !== keyframe.object_uuid) {
return { ...character };
}
return {
...character,
positionKeyframes: [...character.positionKeyframes, newKeyframe].sort(
(keyFrameA, keyframeB) => keyFrameA.offset - keyframeB.offset,
),
};
});
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.ADD_KEYFRAME,
data: newKeyframe,
});
}
export function toggleLipSyncMute(characterId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => {
if (character.id === characterId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: character?.muted
? toEngineActions.UNMUTE
: toEngineActions.MUTE,
data: {
version: 1,
type: ClipType.AUDIO,
group: ClipGroup.CHARACTER,
object_uuid: characterId,
},
});
}
return {
...character,
muted: character.id === characterId ? !character.muted : character.muted,
};
});
}
export function selectCharacterClip(clipId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
animationClips: character.animationClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
positionClips: character.positionKeyframes.map((keyframe) => ({
...keyframe,
selected:
keyframe.keyframe_uuid === clipId
? !keyframe.selected
: keyframe.selected,
})),
lipSyncClips: character.lipSyncClips.map((clip) => ({
...clip,
selected: clip.clip_uuid === clipId ? !clip.selected : clip.selected,
})),
}));
}
export function deleteCharacterClip(clipId: string) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
animationClips: character.animationClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
lipSyncClips: character.lipSyncClips.filter((clip) => {
if (clip.clip_uuid === clipId) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_CLIP,
data: clip!,
});
return false;
}
return true;
}),
}));
}
export function deleteCharacterKeyframe(keyframe: Keyframe) {
const oldCharacterGroups = characterGroups.value;
characterGroups.value = oldCharacterGroups.map((character) => ({
...character,
positionClips: character.positionKeyframes.filter((row) => {
if (row.keyframe_uuid === keyframe.keyframe_uuid) {
Queue.publish({
queueName: QueueNames.TO_ENGINE,
action: toEngineActions.DELETE_KEYFRAME,
data: row,
});
return false;
}
return true;
}),
}));
}
+4
View File
@@ -1,3 +1,7 @@
export * from "./audioGroup";
export * from "./cameraGroup";
export * from "./characterGroups";
export * from "./dragAndDrop";
export * from "./objectGroup";
export * from "./sizing";
export * from "./timing";
+107
View File
@@ -0,0 +1,107 @@
import { signal } from "@preact/signals-core";
import {
Keyframe,
ObjectGroup,
QueueKeyframe,
} from "~/pages/PageEnigma/models/track";
import { useCallback } from "react";
import * as uuid from "uuid";
export const objectGroup = signal<ObjectGroup>({
id: "OB1",
objects: [],
});
export function updateObject({ id, offset }: { id: string; offset: number }) {
const oldObject = objectGroup.value;
objectGroup.value = {
id: oldObject.id,
objects: oldObject.objects.map((object) => ({
object_uuid: object.object_uuid,
name: object.name,
keyframes: object.keyframes.map((keyFrame) => ({
...keyFrame,
offset: keyFrame.keyframe_uuid === id ? offset : keyFrame.offset,
})),
})),
};
}
export function addObjectKeyframe(keyframe: QueueKeyframe, offset: number) {
console.log(keyframe, offset);
const oldObjectGroup = objectGroup.value;
const obj = oldObjectGroup.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
const newObject = obj ?? {
object_uuid: keyframe.object_uuid,
name: keyframe.object_name ?? "unknown",
keyframes: [] as Keyframe[],
};
newObject.keyframes.push({
version: keyframe.version,
keyframe_uuid: uuid.v4(),
group: keyframe.group,
object_uuid: keyframe.object_uuid,
offset,
position: keyframe.position,
rotation: keyframe.rotation,
scale: keyframe.scale,
selected: false,
} as Keyframe);
console.log(newObject);
newObject.keyframes.sort(
(keyframeA, keyframeB) => keyframeA.offset - keyframeB.offset,
);
console.log(4);
objectGroup.value = {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
),
newObject,
].sort((objA, objB) => (objA.object_uuid < objB.object_uuid ? -1 : 1)),
};
console.log("T", objectGroup.value);
}
export function deleteObjectKeyframe(keyframe: Keyframe) {
const oldObjectGroup = objectGroup.value;
const obj = oldObjectGroup.objects.find(
(row) => row.object_uuid === keyframe.object_uuid,
);
if (!obj) {
return oldObjectGroup;
}
const newKeyframes = [
...obj.keyframes.filter(
(row) => row.keyframe_uuid !== keyframe.keyframe_uuid,
),
];
if (newKeyframes.length) {
return {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.map((object) => ({
...object,
...(object.object_uuid === keyframe.object_uuid
? { keyframes: newKeyframes }
: {}),
})),
],
};
}
objectGroup.value = {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects.filter(
(object) => object.object_uuid !== keyframe.object_uuid,
),
],
};
}
+11
View File
@@ -1,4 +1,6 @@
import { signal, computed } from "@preact/signals-core";
import { characterGroups } from "~/pages/PageEnigma/store/characterGroups";
import { objectGroup } from "~/pages/PageEnigma/store/objectGroup";
export const scale = signal(1);
export const filmLength = signal(12);
@@ -6,3 +8,12 @@ export const timelineHeight = signal(0);
export const fullWidth = computed(() => {
return filmLength.value * 60 * 4 * scale.value;
});
export const fullHeight = computed(() => {
return (
characterGroups.value.length * 268 +
objectGroup.value.objects.length * 60 +
300 +
96
);
});