mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
Merge pull request #17 from storytold/feature/drag-and-drop
Feature/drag and drop
This commit is contained in:
20 files changed
+697
-147
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Fragment } from "react";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onOk?: () => void;
|
||||
okText?: string;
|
||||
okColor?: string;
|
||||
onCancel?: () => void;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
text,
|
||||
title,
|
||||
open,
|
||||
onClose,
|
||||
onOk,
|
||||
okText = "OK",
|
||||
okColor = "bg-brand-success",
|
||||
onCancel,
|
||||
cancelText = "Cancel",
|
||||
}: Props) => {
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/40" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h4"
|
||||
className="mb-4 text-xl font-bold text-white"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="mt-2">{text}</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{!!onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg px-3 py-2"
|
||||
variant="secondary"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
{!!onOk && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onOk}
|
||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
|
||||
>
|
||||
{okText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
interface LowerPanelPropsI {
|
||||
timelineHeight: number;
|
||||
@@ -6,10 +7,22 @@ interface LowerPanelPropsI {
|
||||
}
|
||||
|
||||
export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
// const [open, setOpen] = useState(false);
|
||||
const { setOverTimeline, updateCurrentTime, scale } =
|
||||
useContext(TrackContext);
|
||||
|
||||
const onTimelineClick = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
updateCurrentTime((Math.round(event.clientX) - 92) / 4 / scale);
|
||||
}
|
||||
},
|
||||
[updateCurrentTime, scale],
|
||||
);
|
||||
|
||||
// const handleOpen = () => {
|
||||
// setOpen(true);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -20,6 +33,11 @@ export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
|
||||
"bg-ui-panel",
|
||||
].join(" ")}
|
||||
style={{ height: timelineHeight }}
|
||||
onPointerOver={() => {
|
||||
setOverTimeline(true);
|
||||
}}
|
||||
onPointerLeave={() => setOverTimeline(false)}
|
||||
onPointerDown={onTimelineClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
interface Props {
|
||||
@@ -8,8 +8,16 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ClipElement = ({ clip, type }: Props) => {
|
||||
const { startDrag, dragId, endDrag, scale, canDrop, setCanDrop } =
|
||||
useContext(TrackContext);
|
||||
const {
|
||||
startDrag,
|
||||
dragId,
|
||||
endDrag,
|
||||
scale,
|
||||
canDrop,
|
||||
setCanDrop,
|
||||
overTimeline,
|
||||
dropId,
|
||||
} = useContext(TrackContext);
|
||||
const [initPosition, setInitPosition] = useState<{
|
||||
initX: number;
|
||||
initY: number;
|
||||
@@ -36,6 +44,8 @@ export const ClipElement = ({ clip, type }: Props) => {
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (dragId) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const deltaX = event.clientX - initX;
|
||||
const deltaY = event.clientY - initY;
|
||||
setCurrPosition({ x: deltaX, y: deltaY });
|
||||
@@ -55,20 +65,24 @@ export const ClipElement = ({ clip, type }: Props) => {
|
||||
const onPointerDown = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
const box = document.getElementById(`ani-clip-${clip.id}`);
|
||||
const position = box!.getBoundingClientRect();
|
||||
|
||||
startDrag(type, clip.id);
|
||||
setCurrPosition({
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: position.x,
|
||||
y: position.y + position.height + 1,
|
||||
});
|
||||
setInitPosition({
|
||||
initX: position.x,
|
||||
initY: position.y + position.height + 1,
|
||||
});
|
||||
setInitPosition({ initY: event.clientY, initX: event.clientX });
|
||||
setCanDrop(false);
|
||||
}
|
||||
},
|
||||
[type, clip.id, startDrag, setCanDrop],
|
||||
);
|
||||
|
||||
// console.log("clip", canDrop);
|
||||
|
||||
return (
|
||||
<div key={clip.id} className="relative h-16 w-16">
|
||||
<div
|
||||
@@ -82,16 +96,17 @@ export const ClipElement = ({ clip, type }: Props) => {
|
||||
<div
|
||||
id={`ani-dnd-${clip.id}`}
|
||||
className={[
|
||||
"absolute h-8 p-2",
|
||||
"absolute p-2",
|
||||
"rounded-lg",
|
||||
!canDrop ? "bg-brand-primary" : "bg-brand-secondary-700",
|
||||
dragId ? "block" : "hidden",
|
||||
].join(" ")}
|
||||
style={{
|
||||
top: y,
|
||||
left: x,
|
||||
top: overTimeline ? y + 24 : y,
|
||||
left: overTimeline ? x : x,
|
||||
zIndex: 5000,
|
||||
width: clip.length * 4 * scale,
|
||||
width: overTimeline ? clip.length * 4 * scale : 64,
|
||||
height: overTimeline ? 32 : 64,
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
|
||||
@@ -17,11 +17,12 @@ export const Audio = () => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={audio!.id}
|
||||
clips={clips}
|
||||
title="Global Audio Track"
|
||||
updateClip={updateAudio}
|
||||
style="audio"
|
||||
muted={audio?.muted}
|
||||
muted={audio!.muted}
|
||||
toggleMute={toggleAudioMute}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,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">
|
||||
<Track
|
||||
id={camera!.id}
|
||||
clips={clips}
|
||||
title="Camera Position/Rotation"
|
||||
style="camera"
|
||||
|
||||
@@ -63,6 +63,7 @@ export const Character = ({ characterId }: Props) => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={character.id}
|
||||
clips={animationClips}
|
||||
title="Animation"
|
||||
updateClip={updateClipAnimations}
|
||||
@@ -70,6 +71,7 @@ export const Character = ({ characterId }: Props) => {
|
||||
type="animations"
|
||||
/>
|
||||
<Track
|
||||
id={character.id}
|
||||
clips={positionClips}
|
||||
title="Character Position/Rotation"
|
||||
updateClip={updateClipPosition}
|
||||
@@ -77,6 +79,7 @@ export const Character = ({ characterId }: Props) => {
|
||||
type="positions"
|
||||
/>
|
||||
<Track
|
||||
id={character.id}
|
||||
clips={lipSyncClips}
|
||||
title="Lipsync Audio Track"
|
||||
updateClip={updateClipLipSync}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const Objects = () => {
|
||||
{objects.objects.map((object) => (
|
||||
<div key={object.id} className="flex flex-col gap-4">
|
||||
<Track
|
||||
id={object.id}
|
||||
clips={object.clips}
|
||||
title="Mask Position/Rotation"
|
||||
updateClip={updateObject}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useContext } from "react";
|
||||
import { Fragment, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { LowerPanel } from "~/modules/LowerPanel";
|
||||
import { Character } from "./Character";
|
||||
@@ -9,18 +9,63 @@ import { Objects } from "./Objects";
|
||||
import { useMouseEventsAnimation } from "./utils/useMouseEventsAnimation";
|
||||
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ConfirmationModal } from "~/components/ConfirmationModal";
|
||||
|
||||
interface Props {
|
||||
timelineHeight: number;
|
||||
}
|
||||
|
||||
export const Timeline = ({ timelineHeight }: Props) => {
|
||||
const { characters, objects, scale, length } = useContext(TrackContext);
|
||||
const {
|
||||
characters,
|
||||
objects,
|
||||
scale,
|
||||
length,
|
||||
selectedClip,
|
||||
currentTime,
|
||||
deleteCharacterClip,
|
||||
deleteAudioClip,
|
||||
deleteCameraClip,
|
||||
deleteObjectClip,
|
||||
} = useContext(TrackContext);
|
||||
const { onPointerDown, time } = useMouseEventsAnimation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const sectionWidth = 60 * 4 * scale;
|
||||
const fullHeight =
|
||||
characters.length * 200 + objects.objects.length * 80 + 248 + 96;
|
||||
characters.length * 268 + objects.objects.length * 60 + 300 + 96;
|
||||
|
||||
const onDeleteAsk = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedClip) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
},
|
||||
[selectedClip],
|
||||
);
|
||||
|
||||
const displayTime = time === -1 ? currentTime : time;
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
deleteCharacterClip(selectedClip!);
|
||||
deleteCameraClip(selectedClip!);
|
||||
deleteAudioClip(selectedClip!);
|
||||
deleteObjectClip(selectedClip!);
|
||||
}, [
|
||||
selectedClip,
|
||||
deleteAudioClip,
|
||||
deleteCharacterClip,
|
||||
deleteCameraClip,
|
||||
deleteObjectClip,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onDeleteAsk);
|
||||
|
||||
return () => {
|
||||
document.addEventListener("keydown", onDeleteAsk);
|
||||
};
|
||||
}, [onDeleteAsk]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -84,7 +129,7 @@ export const Timeline = ({ timelineHeight }: Props) => {
|
||||
</div>
|
||||
<div
|
||||
className="absolute text-brand-primary"
|
||||
style={{ top: 8, left: time * 4 * scale + 88 }}
|
||||
style={{ top: 8, left: displayTime * 4 * scale + 88 }}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
@@ -102,6 +147,19 @@ export const Timeline = ({ timelineHeight }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</LowerPanel>
|
||||
<ConfirmationModal
|
||||
title="Delete Clip"
|
||||
text="Are you sure you want to delete the selected clip?"
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onOk={() => {
|
||||
onDelete();
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
okText="Delete"
|
||||
okColor="bg-brand-primary"
|
||||
onCancel={() => setDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { useContext } from "react";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { PointerEvent, useContext } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
clips: BaseClip[];
|
||||
title: string;
|
||||
style: "character" | "audio" | "camera" | "objects";
|
||||
@@ -16,6 +17,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const Track = ({
|
||||
id,
|
||||
clips,
|
||||
toggleMute,
|
||||
updateClip,
|
||||
@@ -24,7 +26,8 @@ export const Track = ({
|
||||
style,
|
||||
type,
|
||||
}: Props) => {
|
||||
const { length, setCanDrop, dragType } = useContext(TrackContext);
|
||||
const { length, scale, setCanDrop, dragType, setDropId, setDropOffset } =
|
||||
useContext(TrackContext);
|
||||
const trackType = type ?? style;
|
||||
|
||||
function onPointerOver() {
|
||||
@@ -32,6 +35,19 @@ export const Track = ({
|
||||
return;
|
||||
}
|
||||
setCanDrop(true);
|
||||
setDropId(id);
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
|
||||
if (dragType !== trackType) {
|
||||
return;
|
||||
}
|
||||
const track = document.getElementById(`track-${trackType}-${id}`);
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
const position = track.getBoundingClientRect();
|
||||
setDropOffset((event.clientX - position.x) / 4 / scale);
|
||||
}
|
||||
function onPointerLeave() {
|
||||
if (dragType !== trackType) {
|
||||
@@ -43,9 +59,11 @@ export const Track = ({
|
||||
return (
|
||||
<div className="pl-16">
|
||||
<div
|
||||
id={`track-${trackType}-${id}`}
|
||||
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
|
||||
onPointerOver={onPointerOver}
|
||||
onPointerLeave={onPointerLeave}
|
||||
onPointerMove={onPointerMove}
|
||||
>
|
||||
{clips.map((clip, index) => (
|
||||
<TrackClip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
@@ -51,7 +51,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
|
||||
"rounded-l-lg",
|
||||
"block h-full",
|
||||
clip.id === selectedClip
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white"
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{ width: 15, left: offset * 4 * scale, cursor: "w-resize" }}
|
||||
@@ -63,7 +63,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
|
||||
...classes,
|
||||
"block h-full",
|
||||
clip.id === selectedClip
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white"
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
@@ -80,7 +80,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
|
||||
"rounded-r-lg",
|
||||
"block h-full",
|
||||
clip.id === selectedClip
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white"
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
export const useMouseEventsAnimation = () => {
|
||||
@@ -7,7 +7,7 @@ export const useMouseEventsAnimation = () => {
|
||||
const [isActive, setIsActive] = useState("");
|
||||
const [clientX, setClientX] = useState(0);
|
||||
|
||||
const [time, setTime] = useState(currentTime);
|
||||
const [time, setTime] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
const max = length * 60 * 4 * scale;
|
||||
@@ -16,12 +16,15 @@ export const useMouseEventsAnimation = () => {
|
||||
if (isActive) {
|
||||
updateCurrentTime(Math.round(time));
|
||||
setIsActive("");
|
||||
setTime(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
const delta = (event.clientX - clientX) / 4 / scale + currentTime;
|
||||
if (isActive === "drag") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (delta < 0 || delta > max) {
|
||||
return;
|
||||
}
|
||||
@@ -37,15 +40,16 @@ export const useMouseEventsAnimation = () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [clientX, isActive, length, updateCurrentTime, scale, time]);
|
||||
}, [clientX, isActive, length, updateCurrentTime, scale, time, currentTime]);
|
||||
|
||||
return {
|
||||
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
setClientX(event.clientX);
|
||||
setIsActive("drag");
|
||||
setTime(currentTime);
|
||||
}
|
||||
},
|
||||
}, []),
|
||||
time,
|
||||
};
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { BaseClip } from "~/models/track";
|
||||
import { BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
|
||||
|
||||
export const useMouseEventsClip = (
|
||||
@@ -89,6 +89,8 @@ export const useMouseEventsClip = (
|
||||
type: string,
|
||||
) => {
|
||||
if (event.button === 0) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
clientX.current = event.clientX;
|
||||
isActive.current = type;
|
||||
}
|
||||
|
||||
@@ -3,78 +3,155 @@ import {
|
||||
AnimationClip,
|
||||
AudioClip,
|
||||
AudioGroup,
|
||||
BaseClip,
|
||||
CameraGroup,
|
||||
CharacterGroup,
|
||||
ObjectGroup,
|
||||
} from "~/models/track";
|
||||
} from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export const TrackContext = createContext<{
|
||||
// timeline characters and it's functions
|
||||
characters: CharacterGroup[];
|
||||
camera: CameraGroup | null;
|
||||
audio: AudioGroup | null;
|
||||
objects: ObjectGroup;
|
||||
selectedClip: string | null;
|
||||
updateCharacters: (options: {
|
||||
type: "animations" | "positions" | "lipSync";
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
toggleLipSyncMute: (characterId: string) => void;
|
||||
addCharacterAnimation: (options: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
animationClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => void;
|
||||
addCharacterAudio: (options: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
audioClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => void;
|
||||
selectCharacterClip: (clipId: string) => void;
|
||||
deleteCharacterClip: (clipId: string) => void;
|
||||
|
||||
// timeline camera group
|
||||
camera: CameraGroup | null;
|
||||
updateCamera: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
selectCameraClip: (clipId: string) => void;
|
||||
deleteCameraClip: (clipId: string) => void;
|
||||
|
||||
// timeline global audio group
|
||||
audio: AudioGroup | null;
|
||||
updateAudio: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
toggleAudioMute: () => void;
|
||||
addGlobalAudio: (
|
||||
clipId: string,
|
||||
animationClips: BaseClip[],
|
||||
offset: number,
|
||||
) => void;
|
||||
selectAudioClip: (clipId: string) => void;
|
||||
deleteAudioClip: (clipId: string) => void;
|
||||
|
||||
// timeline objects group
|
||||
objects: ObjectGroup;
|
||||
updateObject: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void;
|
||||
selectObjectClip: (clipId: string) => void;
|
||||
deleteObjectClip: (clipId: string) => void;
|
||||
|
||||
// current - only select one item - will be replaced
|
||||
selectedClip: string | null;
|
||||
selectClip: (clipId: string) => void;
|
||||
toggleLipSyncMute: (characterId: string) => void;
|
||||
toggleAudioMute: () => void;
|
||||
|
||||
// sidebar clips
|
||||
animationClips: AnimationClip[];
|
||||
audioClips: AudioClip[];
|
||||
|
||||
// drag and drop
|
||||
dragType: "animations" | "lipSync" | null;
|
||||
dragId: string | null;
|
||||
startDrag: (type: "animations" | "lipSync", id: string) => void;
|
||||
endDrag: () => void;
|
||||
scale: number;
|
||||
currentTime: number;
|
||||
length: number;
|
||||
updateCurrentTime: (time: number) => void;
|
||||
canDrop: boolean;
|
||||
setCanDrop: (can: boolean) => void;
|
||||
overTimeline: boolean;
|
||||
setOverTimeline: (over: boolean) => void;
|
||||
dropId: string;
|
||||
setDropId: (id: string) => void;
|
||||
dropOffset: number;
|
||||
setDropOffset: (offset: number) => void;
|
||||
|
||||
// scale of timeline displa
|
||||
scale: number;
|
||||
|
||||
// current time position
|
||||
currentTime: number;
|
||||
updateCurrentTime: (time: number) => void;
|
||||
|
||||
// total length of the film clip
|
||||
length: number;
|
||||
|
||||
// computed width length * 60 * 4 * scale
|
||||
fullWidth: number;
|
||||
}>({
|
||||
characters: [],
|
||||
camera: null,
|
||||
audio: null,
|
||||
objects: { id: "", objects: [] },
|
||||
selectedClip: null,
|
||||
updateCharacters: () => {},
|
||||
updateCamera: () => {},
|
||||
updateAudio: () => {},
|
||||
updateObject: () => {},
|
||||
selectClip: () => {},
|
||||
toggleLipSyncMute: () => {},
|
||||
addCharacterAnimation: () => {},
|
||||
addCharacterAudio: () => {},
|
||||
selectCharacterClip: () => {},
|
||||
deleteCharacterClip: () => {},
|
||||
|
||||
camera: null,
|
||||
updateCamera: () => {},
|
||||
selectCameraClip: () => {},
|
||||
deleteCameraClip: () => {},
|
||||
|
||||
audio: null,
|
||||
updateAudio: () => {},
|
||||
toggleAudioMute: () => {},
|
||||
selectAudioClip: () => {},
|
||||
addGlobalAudio: () => {},
|
||||
deleteAudioClip: () => {},
|
||||
|
||||
objects: { id: "", objects: [] },
|
||||
updateObject: () => {},
|
||||
selectObjectClip: () => {},
|
||||
deleteObjectClip: () => {},
|
||||
|
||||
selectedClip: null,
|
||||
selectClip: () => {},
|
||||
|
||||
animationClips: [],
|
||||
audioClips: [],
|
||||
|
||||
dragType: null,
|
||||
dragId: null,
|
||||
startDrag: () => {},
|
||||
endDrag: () => {},
|
||||
scale: 1,
|
||||
currentTime: 0,
|
||||
length: 12,
|
||||
updateCurrentTime: () => {},
|
||||
dropId: "",
|
||||
setDropId: () => {},
|
||||
canDrop: false,
|
||||
setCanDrop: () => {},
|
||||
overTimeline: false,
|
||||
setOverTimeline: () => {},
|
||||
dropOffset: 0,
|
||||
setDropOffset: () => {},
|
||||
|
||||
scale: 1,
|
||||
currentTime: 0,
|
||||
updateCurrentTime: () => {},
|
||||
length: 12,
|
||||
fullWidth: 0,
|
||||
});
|
||||
@@ -4,50 +4,56 @@ import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/
|
||||
import useUpdateCamera from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera";
|
||||
import useUpdateAudio from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio";
|
||||
import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject";
|
||||
import { AnimationClip, AudioClip } from "~/models/track";
|
||||
import { AnimationClip, AudioClip } from "~/pages/PageEnigma/models/track";
|
||||
import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const TrackProvider = ({ children }: Props) => {
|
||||
const { characters, updateCharacters, toggleLipSyncMute } =
|
||||
useUpdateCharacters();
|
||||
const { camera, updateCamera } = useUpdateCamera();
|
||||
const { audio, updateAudio, toggleAudioMute } = useUpdateAudio();
|
||||
const { objects, updateObject } = useUpdateObject();
|
||||
const [selectedClip, setSelectedClip] = useState<string | null>(null);
|
||||
const characters = useUpdateCharacters();
|
||||
const camera = useUpdateCamera();
|
||||
const audio = useUpdateAudio();
|
||||
const objects = useUpdateObject();
|
||||
|
||||
const { endDrag, ...dragDrop } = useUpdateDragDrop();
|
||||
|
||||
const [selectedClip, setSelectedClip] = useState<string | null>(null);
|
||||
const selectClip = useCallback((clipId: string | null) => {
|
||||
setSelectedClip(clipId);
|
||||
}, []);
|
||||
|
||||
const [state, setState] = useState<{
|
||||
dragType: "animations" | "lipSync" | null;
|
||||
dragId: string | null;
|
||||
}>({ dragType: null, dragId: null });
|
||||
const [animationClips, setAnimationClips] = useState<AnimationClip[]>([]);
|
||||
const [audioClips, setAudioClips] = useState<AudioClip[]>([]);
|
||||
|
||||
const startDrag = useCallback(
|
||||
(type: "animations" | "lipSync", id: string) => {
|
||||
setState({ dragId: id, dragType: type });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
setState({ dragId: null, dragType: null });
|
||||
}, []);
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
const [length, setLength] = useState(1);
|
||||
const [time, setTime] = useState(0);
|
||||
const updateCurrentTime = useCallback((newTime: number) => {
|
||||
setTime(newTime);
|
||||
console.log("message", {
|
||||
action: "UpdateCurrentTime",
|
||||
id: "",
|
||||
data: { currentTime: newTime },
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [canDrop, setCanDrop] = useState(false);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [length, setLength] = useState(1);
|
||||
// cross group functions
|
||||
const dropClip = useCallback(() => {
|
||||
const { canDrop, dragType, dropId, dragId, dropOffset } = dragDrop;
|
||||
if (canDrop) {
|
||||
if (dragType === "animations") {
|
||||
characters.addCharacterAnimation({
|
||||
clipId: dragId!,
|
||||
characterId: dropId,
|
||||
animationClips,
|
||||
offset: dropOffset,
|
||||
});
|
||||
}
|
||||
}
|
||||
endDrag();
|
||||
}, [dragDrop, animationClips, characters, endDrag]);
|
||||
|
||||
const fullWidth = useMemo(() => {
|
||||
return length * 60 * 4 * scale;
|
||||
@@ -69,54 +75,42 @@ export const TrackProvider = ({ children }: Props) => {
|
||||
|
||||
const values = useMemo(() => {
|
||||
return {
|
||||
characters,
|
||||
camera,
|
||||
audio,
|
||||
objects,
|
||||
updateCharacters,
|
||||
updateCamera,
|
||||
updateAudio,
|
||||
updateObject,
|
||||
...characters,
|
||||
...camera,
|
||||
...audio,
|
||||
...objects,
|
||||
|
||||
selectClip,
|
||||
selectedClip,
|
||||
toggleLipSyncMute,
|
||||
toggleAudioMute,
|
||||
dragType: state.dragType,
|
||||
dragId: state.dragId,
|
||||
|
||||
...dragDrop,
|
||||
endDrag: dropClip,
|
||||
|
||||
animationClips,
|
||||
audioClips,
|
||||
startDrag,
|
||||
endDrag,
|
||||
|
||||
scale,
|
||||
currentTime: time,
|
||||
length,
|
||||
updateCurrentTime,
|
||||
canDrop,
|
||||
setCanDrop,
|
||||
length,
|
||||
fullWidth,
|
||||
};
|
||||
}, [
|
||||
characters,
|
||||
updateCharacters,
|
||||
camera,
|
||||
audio,
|
||||
objects,
|
||||
|
||||
selectClip,
|
||||
selectedClip,
|
||||
camera,
|
||||
updateCamera,
|
||||
audio,
|
||||
updateAudio,
|
||||
objects,
|
||||
updateObject,
|
||||
toggleLipSyncMute,
|
||||
toggleAudioMute,
|
||||
state.dragId,
|
||||
state.dragType,
|
||||
|
||||
dragDrop,
|
||||
dropClip,
|
||||
|
||||
animationClips,
|
||||
audioClips,
|
||||
startDrag,
|
||||
endDrag,
|
||||
canDrop,
|
||||
|
||||
updateCurrentTime,
|
||||
setCanDrop,
|
||||
time,
|
||||
fullWidth,
|
||||
length,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { AudioGroup } from "~/models/track";
|
||||
import { AudioGroup, BaseClip } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateAudio() {
|
||||
const [audio, setAudio] = useState<AudioGroup>({
|
||||
@@ -45,6 +45,33 @@ export default function useUpdateAudio() {
|
||||
clips: newClips,
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateGlobalAudio",
|
||||
id,
|
||||
data: { offset, length },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addGlobalAudio = useCallback(
|
||||
(dragId: string, audioClips: BaseClip[], offset: number) => {
|
||||
const clip = audioClips.find((row) => row.id === dragId);
|
||||
if (!clip) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: [...oldAudio.clips, { ...clip, offset }],
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateGlobalAudio",
|
||||
id: dragId,
|
||||
data: { offset },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -56,11 +83,47 @@ export default function useUpdateAudio() {
|
||||
muted: !oldAudio.muted,
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "ToggleGlobalAudioMute",
|
||||
id: "",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectAudioClip = useCallback((clipId: string) => {
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: [
|
||||
...oldAudio.clips.map((clip) => {
|
||||
return {
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteAudioClip = useCallback((clipId: string) => {
|
||||
setAudio((oldAudio) => {
|
||||
return {
|
||||
...oldAudio,
|
||||
clips: [...oldAudio.clips.filter((clip) => clip.id !== clipId)],
|
||||
};
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateGlobalAudio",
|
||||
id: clipId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audio,
|
||||
updateAudio,
|
||||
toggleAudioMute,
|
||||
selectAudioClip,
|
||||
addGlobalAudio,
|
||||
deleteAudioClip,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { CameraGroup } from "~/models/track";
|
||||
import { CameraGroup } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateCamera() {
|
||||
const [camera, setCamera] = useState<CameraGroup>({
|
||||
@@ -48,8 +48,35 @@ export default function useUpdateCamera() {
|
||||
[],
|
||||
);
|
||||
|
||||
const selectCameraClip = useCallback((clipId: string) => {
|
||||
setCamera((oldCamera) => {
|
||||
return {
|
||||
...oldCamera,
|
||||
clips: [
|
||||
...oldCamera.clips.map((clip) => {
|
||||
return {
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteCameraClip = useCallback((clipId: string) => {
|
||||
setCamera((oldCamera) => {
|
||||
return {
|
||||
...oldCamera,
|
||||
clips: [...oldCamera.clips.filter((clip) => clip.id !== clipId)],
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
camera,
|
||||
updateCamera,
|
||||
selectCameraClip,
|
||||
deleteCameraClip,
|
||||
};
|
||||
}
|
||||
@@ -1,39 +1,13 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { CharacterGroup } from "~/models/track";
|
||||
import { BaseClip, CharacterGroup } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateCharacters() {
|
||||
const [characters, setCharacters] = useState<CharacterGroup[]>([
|
||||
{
|
||||
id: "CH1",
|
||||
muted: false,
|
||||
animationClips: [
|
||||
{
|
||||
id: "CH1-A1",
|
||||
length: 200,
|
||||
offset: 0,
|
||||
name: "ani 1",
|
||||
},
|
||||
{
|
||||
id: "CH1-A2",
|
||||
length: 180,
|
||||
offset: 300,
|
||||
name: "ani 2",
|
||||
},
|
||||
],
|
||||
positionClips: [
|
||||
{
|
||||
id: "CH1-P1",
|
||||
length: 200,
|
||||
offset: 100,
|
||||
name: "pos 1",
|
||||
},
|
||||
{
|
||||
id: "CH1-P2",
|
||||
length: 180,
|
||||
offset: 500,
|
||||
name: "pos 2",
|
||||
},
|
||||
],
|
||||
animationClips: [],
|
||||
positionClips: [],
|
||||
lipSyncClips: [
|
||||
{
|
||||
id: "CH1-L1",
|
||||
@@ -75,6 +49,11 @@ export default function useUpdateCharacters() {
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateAnimation",
|
||||
id,
|
||||
data: { offset, length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === "positions") {
|
||||
@@ -115,11 +94,90 @@ export default function useUpdateCharacters() {
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "UpdateLipSync",
|
||||
id,
|
||||
data: { offset, length },
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addCharacterAnimation = useCallback(
|
||||
({
|
||||
clipId,
|
||||
characterId,
|
||||
animationClips,
|
||||
offset,
|
||||
}: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
animationClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => {
|
||||
const clip = animationClips.find((row) => row.id === clipId);
|
||||
if (!clip) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
if (character.id !== characterId) {
|
||||
return { ...character };
|
||||
}
|
||||
return {
|
||||
...character,
|
||||
animationClips: [...character.animationClips, { ...clip, offset }],
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "AddAnimation",
|
||||
id: clipId,
|
||||
data: { offset },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const addCharacterAudio = useCallback(
|
||||
({
|
||||
clipId,
|
||||
characterId,
|
||||
audioClips,
|
||||
offset,
|
||||
}: {
|
||||
clipId: string;
|
||||
characterId: string;
|
||||
audioClips: BaseClip[];
|
||||
offset: number;
|
||||
}) => {
|
||||
const clip = audioClips.find((row) => row.id === clipId);
|
||||
if (!clip) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => {
|
||||
if (character.id !== characterId) {
|
||||
return { ...character };
|
||||
}
|
||||
return {
|
||||
...character,
|
||||
lipSyncClips: [...character.lipSyncClips, { ...clip, offset }],
|
||||
};
|
||||
});
|
||||
});
|
||||
console.log("message", {
|
||||
action: "AddLipSync",
|
||||
id: clipId,
|
||||
data: { offset },
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const toggleLipSyncMute = useCallback((characterId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return oldCharacters.map((character) => ({
|
||||
@@ -128,11 +186,64 @@ export default function useUpdateCharacters() {
|
||||
character.id === characterId ? !character.muted : character.muted,
|
||||
}));
|
||||
});
|
||||
console.log("message", {
|
||||
action: "ToggleLipSync",
|
||||
id: characterId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectCharacterClip = useCallback((clipId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return [
|
||||
...oldCharacters.map((character) => ({
|
||||
...character,
|
||||
animationClips: character.animationClips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
positionClips: character.positionClips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
lipSyncClips: character.lipSyncClips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
})),
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteCharacterClip = useCallback((clipId: string) => {
|
||||
setCharacters((oldCharacters) => {
|
||||
return [
|
||||
...oldCharacters.map((character) => ({
|
||||
...character,
|
||||
animationClips: character.animationClips.filter(
|
||||
(clip) => clip.id !== clipId,
|
||||
),
|
||||
positionClips: character.positionClips.filter(
|
||||
(clip) => clip.id !== clipId,
|
||||
),
|
||||
lipSyncClips: character.lipSyncClips.filter(
|
||||
(clip) => clip.id !== clipId,
|
||||
),
|
||||
})),
|
||||
];
|
||||
});
|
||||
console.log("message", {
|
||||
action: "DeleteCharacterClip",
|
||||
id: clipId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
characters,
|
||||
updateCharacters,
|
||||
toggleLipSyncMute,
|
||||
addCharacterAnimation,
|
||||
addCharacterAudio,
|
||||
selectCharacterClip,
|
||||
deleteCharacterClip,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function useUpdateDragDrop() {
|
||||
const [state, setState] = useState<{
|
||||
dragType: "animations" | "lipSync" | null;
|
||||
dragId: string | null;
|
||||
}>({ dragType: null, dragId: null });
|
||||
const startDrag = useCallback(
|
||||
(type: "animations" | "lipSync", id: string) => {
|
||||
setState({ dragId: id, dragType: type });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const [canDrop, setCanDrop] = useState(false);
|
||||
const [overTimeline, setOverTimeline] = useState(false);
|
||||
|
||||
const endDrag = useCallback(() => {
|
||||
setState({ dragId: null, dragType: null });
|
||||
}, []);
|
||||
|
||||
const [dropId, setDropId] = useState("");
|
||||
const [dropOffset, setDropOffset] = useState(0);
|
||||
|
||||
return {
|
||||
dragId: state.dragId,
|
||||
dragType: state.dragType,
|
||||
startDrag,
|
||||
endDrag,
|
||||
canDrop,
|
||||
setCanDrop,
|
||||
overTimeline,
|
||||
setOverTimeline,
|
||||
dropId,
|
||||
setDropId,
|
||||
dropOffset,
|
||||
setDropOffset,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { ObjectGroup } from "~/models/track";
|
||||
import { ObjectGroup } from "~/pages/PageEnigma/models/track";
|
||||
|
||||
export default function useUpdateObject() {
|
||||
const [objects, setObjects] = useState<ObjectGroup>({
|
||||
@@ -46,8 +46,37 @@ export default function useUpdateObject() {
|
||||
[],
|
||||
);
|
||||
|
||||
const selectObjectClip = useCallback((clipId: string) => {
|
||||
setObjects((oldObjectGroup) => {
|
||||
return {
|
||||
...oldObjectGroup,
|
||||
objects: oldObjectGroup.objects.map((object) => ({
|
||||
...object,
|
||||
clips: object.clips.map((clip) => ({
|
||||
...clip,
|
||||
selected: clip.id === clipId ? !clip.selected : clip.selected,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteObjectClip = useCallback((clipId: string) => {
|
||||
setObjects((oldObjectGroup) => {
|
||||
return {
|
||||
...oldObjectGroup,
|
||||
objects: oldObjectGroup.objects.map((object) => ({
|
||||
...object,
|
||||
clips: object.clips.filter((clip) => clip.id !== clipId),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
objects,
|
||||
updateObject,
|
||||
selectObjectClip,
|
||||
deleteObjectClip,
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export interface BaseClip {
|
||||
name: string;
|
||||
offset: number;
|
||||
length: number;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface AnimationClip extends BaseClip {}
|
||||
Reference in new issue
Block a user