Merge pull request #17 from storytold/feature/drag-and-drop

Feature/drag and drop
This commit is contained in:
ArEnSc authored and GitHub committed 2024-03-27 11:22:10 -04:00
commit 5a1ff6aa95
20 files changed
+697 -147

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import { Dialog, Transition } from "@headlessui/react";
import { Button } from "~/components/Button";
import { Fragment } from "react";
interface Props {
text: string;
title: string;
open: boolean;
onClose: () => void;
onOk?: () => void;
okText?: string;
okColor?: string;
onCancel?: () => void;
cancelText?: string;
}
export const ConfirmationModal = ({
text,
title,
open,
onClose,
onOk,
okText = "OK",
okColor = "bg-brand-success",
onCancel,
cancelText = "Cancel",
}: Props) => {
return (
<Transition appear show={open} as={Fragment}>
<Dialog open={open} onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/40" />
</Transition.Child>
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h4"
className="mb-4 text-xl font-bold text-white"
>
{title}
</Dialog.Title>
<div className="mt-2">{text}</div>
<div className="mt-6 flex justify-end gap-2">
{!!onCancel && (
<Button
type="button"
onClick={onCancel}
className="rounded-lg px-3 py-2"
variant="secondary"
>
{cancelText}
</Button>
)}
{!!onOk && (
<Button
type="button"
onClick={onOk}
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
>
{okText}
</Button>
)}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
};
+23 -5
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import React, { useCallback, useContext } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
interface LowerPanelPropsI {
timelineHeight: number;
@@ -6,10 +7,22 @@ interface LowerPanelPropsI {
}
export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
// const [open, setOpen] = useState(false);
const { setOverTimeline, updateCurrentTime, scale } =
useContext(TrackContext);
const onTimelineClick = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.button === 0) {
updateCurrentTime((Math.round(event.clientX) - 92) / 4 / scale);
}
},
[updateCurrentTime, scale],
);
// const handleOpen = () => {
// setOpen(true);
// };
return (
<div
@@ -20,6 +33,11 @@ export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => {
"bg-ui-panel",
].join(" ")}
style={{ height: timelineHeight }}
onPointerOver={() => {
setOverTimeline(true);
}}
onPointerLeave={() => setOverTimeline(false)}
onPointerDown={onTimelineClick}
>
{children}
</div>
@@ -1,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)}
/>
</>
);
};
+21 -3
View File
@@ -1,11 +1,12 @@
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { BaseClip } from "~/models/track";
import { useContext } from "react";
import { 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 {}