mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-08 02:52:12 +00:00
Merge pull request #167 from storytold/feature/timeline-v3
upgrade timeline to V3
This commit is contained in:
38 files changed
+840
-385
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import { DoNotShow } from "~/pages/PageEnigma/constants/misc";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
okColor?: string;
|
||||
onCancel?: () => void;
|
||||
cancelText?: string;
|
||||
canHide?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
@@ -24,7 +26,19 @@ export const ConfirmationModal = ({
|
||||
okColor = "bg-brand-success",
|
||||
onCancel,
|
||||
cancelText = "Cancel",
|
||||
canHide,
|
||||
}: Props) => {
|
||||
const handleOk = useCallback(() => {
|
||||
const element = document.getElementById("can-delete");
|
||||
if (element) {
|
||||
if ((element as HTMLInputElement).checked) {
|
||||
localStorage.setItem(title.replace(" ", "-"), DoNotShow);
|
||||
}
|
||||
}
|
||||
|
||||
onOk!();
|
||||
}, [onOk, title]);
|
||||
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
@@ -35,8 +49,7 @@ export const ConfirmationModal = ({
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
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">
|
||||
@@ -47,17 +60,23 @@ export const ConfirmationModal = ({
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
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"
|
||||
>
|
||||
className="mb-4 text-xl font-bold text-white">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="mt-2">{text}</div>
|
||||
{canHide && (
|
||||
<div className="mt-2">
|
||||
<label>
|
||||
<input id="can-delete" type="checkbox" />
|
||||
Do not show this again
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
{!!onCancel && (
|
||||
@@ -65,17 +84,15 @@ export const ConfirmationModal = ({
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg px-3 py-2"
|
||||
variant="secondary"
|
||||
>
|
||||
variant="secondary">
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
{!!onOk && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onOk}
|
||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
|
||||
>
|
||||
onClick={handleOk}
|
||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}>
|
||||
{okText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { useMouseEventsTimeline } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsTimeline";
|
||||
import {
|
||||
timelineScrollX,
|
||||
currentTime,
|
||||
dndTimelineHeight,
|
||||
overTimeline,
|
||||
scale,
|
||||
timelineHeight,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import useTimelineClick from "~/pages/PageEnigma/comps/Timeline/utils/useTimelineClick";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
interface LowerPanelPropsI {
|
||||
children: React.ReactNode;
|
||||
onStyle?: boolean;
|
||||
}
|
||||
|
||||
export const LowerPanel = ({ children, onStyle }: LowerPanelPropsI) => {
|
||||
export const LowerPanel = ({ children }: LowerPanelPropsI) => {
|
||||
const { onPointerDown } = useMouseEventsTimeline();
|
||||
|
||||
const displayHeight =
|
||||
@@ -25,44 +20,23 @@ export const LowerPanel = ({ children, onStyle }: LowerPanelPropsI) => {
|
||||
? dndTimelineHeight.value
|
||||
: timelineHeight.value;
|
||||
|
||||
const onTimelineClick = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
const newTime = Math.round(
|
||||
(event.clientX + timelineScrollX.value - 92) / 4 / scale.value,
|
||||
);
|
||||
if (newTime < 0) {
|
||||
return;
|
||||
}
|
||||
currentTime.value = newTime;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: Math.round(newTime) },
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const onTimelineClick = useTimelineClick(Pages.EDIT);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!onStyle && (
|
||||
<div
|
||||
className="absolute w-full cursor-ns-resize bg-ui-panel-border"
|
||||
style={{ height: 3, zIndex: 1000, bottom: displayHeight }}
|
||||
onPointerDown={onPointerDown}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="absolute w-full cursor-ns-resize bg-ui-panel-border"
|
||||
style={{ height: 3, zIndex: 1000, bottom: displayHeight }}
|
||||
onPointerDown={onPointerDown}
|
||||
/>
|
||||
<div
|
||||
className={["absolute bottom-0", "w-screen", "bg-ui-panel"].join(" ")}
|
||||
style={{ height: onStyle ? 80 : displayHeight }}
|
||||
style={{ height: displayHeight }}
|
||||
onPointerOver={() => {
|
||||
overTimeline.value = true;
|
||||
}}
|
||||
onPointerLeave={() => (overTimeline.value = false)}
|
||||
onPointerDown={onTimelineClick}
|
||||
>
|
||||
onClick={onTimelineClick}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -24,9 +24,10 @@ import { AppUiContext } from "~/contexts/AppUiContext";
|
||||
import { pageHeight, pageWidth } from "~/store";
|
||||
import { TopBar } from "~/modules/TopBar";
|
||||
import { DialogueTTS } from "./comps/DialogueTTS/DialogueTTS";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
interface Props {
|
||||
setPage: (page: string) => void;
|
||||
setPage: (page: Pages) => void;
|
||||
}
|
||||
|
||||
export const PageEditor = ({ setPage }: Props) => {
|
||||
|
||||
@@ -5,11 +5,12 @@ import { EngineContext } from "~/contexts/EngineContext";
|
||||
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
|
||||
import { PageStyling } from "~/pages/PageEnigma/PageStyling";
|
||||
import { useParams } from "@remix-run/react";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
export const PageEnigmaComponent = () => {
|
||||
const [, dispatchAppUiState] = useContext(AppUiContext);
|
||||
const editor = useContext(EngineContext);
|
||||
const [page, setPage] = useState("edit");
|
||||
const [page, setPage] = useState<Pages>(Pages.EDIT);
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,10 +28,10 @@ export const PageEnigmaComponent = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={page === "edit" ? "visible" : "hidden"}>
|
||||
<div className={page === Pages.EDIT ? "visible" : "hidden"}>
|
||||
<PageEditor setPage={setPage} />
|
||||
</div>
|
||||
<div className={page === "style" ? "visible" : "hidden"}>
|
||||
<div className={page === Pages.STYLE ? "visible" : "hidden"}>
|
||||
<PageStyling setPage={setPage} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,23 +11,15 @@ import { TimerGrid } from "~/pages/PageEnigma/comps/Timeline/TimerGrid";
|
||||
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
|
||||
import { PreviewImages } from "~/pages/PageEnigma/comps/PreviewImages";
|
||||
import { TopBar } from "~/modules/TopBar";
|
||||
import React, { UIEvent, useCallback, useContext } from "react";
|
||||
import { UIEvent, useCallback, useContext } from "react";
|
||||
import { EngineContext } from "~/contexts/EngineContext";
|
||||
import {
|
||||
currentTime,
|
||||
filmLength,
|
||||
scale,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { filmLength, scale, stylizeScrollX } from "~/pages/PageEnigma/store";
|
||||
import { pageWidth } from "~/store";
|
||||
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders";
|
||||
import useTimelineClick from "~/pages/PageEnigma/comps/Timeline/utils/useTimelineClick";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
interface Props {
|
||||
setPage: (page: string) => void;
|
||||
setPage: (page: Pages) => void;
|
||||
}
|
||||
|
||||
export const PageStyling = ({ setPage }: Props) => {
|
||||
@@ -44,33 +36,14 @@ export const PageStyling = ({ setPage }: Props) => {
|
||||
|
||||
const switchEdit = () => {
|
||||
editorEngine?.switchEdit();
|
||||
setPage("edit");
|
||||
setPage(Pages.EDIT);
|
||||
};
|
||||
|
||||
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
||||
timelineScrollX.value = event.currentTarget.scrollLeft;
|
||||
stylizeScrollX.value = event.currentTarget.scrollLeft;
|
||||
}, []);
|
||||
|
||||
const onTimelineClick = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
console.log("click", event.clientX, event.pageX, timelineScrollX.value);
|
||||
const newTime = Math.round(
|
||||
(event.clientX + timelineScrollX.value - 92) / 4 / scale.value,
|
||||
);
|
||||
if (newTime < 0) {
|
||||
return;
|
||||
}
|
||||
currentTime.value = newTime;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: newTime },
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const onTimelineClick = useTimelineClick(Pages.STYLE);
|
||||
|
||||
return (
|
||||
<div className="w-screen">
|
||||
@@ -103,21 +76,21 @@ export const PageStyling = ({ setPage }: Props) => {
|
||||
<div
|
||||
className="fixed bottom-0 left-0 w-full border-t border-t-action-600 bg-ui-panel"
|
||||
onClick={onTimelineClick}>
|
||||
<TimerGrid />
|
||||
<TimerGrid page={Pages.STYLE} />
|
||||
<div className="flex w-full ">
|
||||
<div className="block h-[30px] w-[88px]" />
|
||||
<div className="block h-[30px] w-[204px]" />
|
||||
<div
|
||||
className="h-[30px] overflow-x-auto overflow-y-hidden"
|
||||
onScroll={onScroll}
|
||||
style={{
|
||||
width: pageWidth.value,
|
||||
width: pageWidth.value - 264,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: filmLength.value * 60 * 4 * scale.value + 90,
|
||||
width: filmLength.value * 60 * 4 * scale.value + 42,
|
||||
height: 60,
|
||||
}}>
|
||||
<Scrubber />
|
||||
<Scrubber page={Pages.STYLE} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,22 +2,16 @@ import {
|
||||
faAngleRight,
|
||||
faArrowsRotate,
|
||||
faArrowsUpDownLeftRight,
|
||||
faCube,
|
||||
faPlus,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
fa3dCylinder,
|
||||
fa3dTorus,
|
||||
fa3dSphere,
|
||||
} from "@awesome.me/kit-fde2be5eb0/icons/kit/custom";
|
||||
import { Button, ButtonIcon, ButtonIconSelect, Link } from "~/components";
|
||||
import { EngineContext } from "../../../../contexts/EngineContext";
|
||||
import { Button, ButtonIconSelect } from "~/components";
|
||||
import { EngineContext } from "~/contexts/EngineContext";
|
||||
import { useContext } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
interface Props {
|
||||
setPage: (page: string) => void;
|
||||
setPage: (page: Pages) => void;
|
||||
}
|
||||
|
||||
export const Controls3D = ({ setPage }: Props) => {
|
||||
@@ -60,7 +54,7 @@ export const Controls3D = ({ setPage }: Props) => {
|
||||
|
||||
const changeStylize = () => {
|
||||
editorEngine?.switchPreview();
|
||||
setPage("style");
|
||||
setPage(Pages.STYLE);
|
||||
};
|
||||
|
||||
const modes = [
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
|
||||
import { audioGroup, fullWidth, updateAudio } from "~/pages/PageEnigma/store";
|
||||
import {
|
||||
audioGroup,
|
||||
audioMinimized,
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
updateAudio,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { ClipGroup } from "~/pages/PageEnigma/models";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const Audio = () => {
|
||||
useSignals();
|
||||
const { clips } = audioGroup.value;
|
||||
|
||||
if (audioMinimized.value) {
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-[35px] items-center justify-end rounded-r-lg bg-global_audio-groupBg pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
audioMinimized.value = !audioMinimized.value;
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="block rounded-r-lg bg-global_audio-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}
|
||||
>
|
||||
<div className="flex flex-col gap-4 pt-4">
|
||||
className="relative block rounded-r-lg bg-global_audio-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
audioMinimized.value = !audioMinimized.value;
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleUp} />
|
||||
</button>
|
||||
<div className="pt-[47px]">
|
||||
<TrackClips
|
||||
id={audioGroup.value.id}
|
||||
clips={clips}
|
||||
title="Global Audio TrackClips"
|
||||
updateClip={updateAudio}
|
||||
group={ClipGroup.GLOBAL_AUDIO}
|
||||
/>
|
||||
|
||||
@@ -1,22 +1,63 @@
|
||||
import { cameraGroup, fullWidth, updateCamera } from "~/pages/PageEnigma/store";
|
||||
import {
|
||||
cameraGroup,
|
||||
cameraMinimized,
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
updateCamera,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { ClipGroup } from "~/pages/PageEnigma/models";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export const Camera = () => {
|
||||
useSignals();
|
||||
const { keyframes } = cameraGroup.value!;
|
||||
|
||||
if (cameraMinimized.value) {
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-[35px] items-center justify-end rounded-r-lg bg-camera-groupBg pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
cameraMinimized.value = !cameraMinimized.value;
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="block rounded-r-lg bg-camera-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}
|
||||
>
|
||||
<div className="pt-4">
|
||||
className="relative block rounded-r-lg bg-camera-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
cameraMinimized.value = !cameraMinimized.value;
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleUp} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="pt-[47px]">
|
||||
<TrackKeyFrames
|
||||
id={cameraGroup.value.id}
|
||||
keyframes={keyframes}
|
||||
title="Camera Position/Rotation"
|
||||
group={ClipGroup.CAMERA}
|
||||
updateKeyframe={updateCamera}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,16 @@ import { useCallback, useMemo } from "react";
|
||||
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
|
||||
import {
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
toggleCharacterMinimized,
|
||||
toggleLipSyncMute,
|
||||
updateCharacters,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
|
||||
import { CharacterTrack, ClipGroup, ClipType } from "~/pages/PageEnigma/models";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
function buildUpdaters(
|
||||
updateCharacters: (options: {
|
||||
@@ -43,18 +48,52 @@ export const Character = ({ character }: Props) => {
|
||||
const { updateClipLipSync, updateClipPosition, updateClipAnimations } =
|
||||
useMemo(() => buildUpdaters(updateCharacters), []);
|
||||
|
||||
const { animationClips, positionKeyframes, lipSyncClips } = character;
|
||||
const { animationClips, positionKeyframes, lipSyncClips, minimized } =
|
||||
character;
|
||||
|
||||
if (minimized) {
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-[35px] items-center justify-end rounded-r-lg bg-character-groupBg pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
toggleCharacterMinimized(character.object_uuid);
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="block rounded-r-lg bg-character-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}
|
||||
>
|
||||
<div className="flex flex-col gap-4 pt-4">
|
||||
className="relative block rounded-r-lg bg-character-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<div className="flex h-[35px] items-center justify-end">
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
toggleCharacterMinimized(character.object_uuid);
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleUp} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-[12px]">
|
||||
<TrackClips
|
||||
id={character.object_uuid}
|
||||
clips={animationClips}
|
||||
title="Animation"
|
||||
updateClip={updateClipAnimations}
|
||||
group={ClipGroup.CHARACTER}
|
||||
type={ClipType.ANIMATION}
|
||||
@@ -62,14 +101,12 @@ export const Character = ({ character }: Props) => {
|
||||
<TrackKeyFrames
|
||||
id={character.object_uuid}
|
||||
keyframes={positionKeyframes}
|
||||
title="Character Position/Rotation"
|
||||
updateKeyframe={updateClipPosition}
|
||||
group={ClipGroup.CHARACTER}
|
||||
/>
|
||||
<TrackClips
|
||||
id={character.object_uuid}
|
||||
clips={lipSyncClips}
|
||||
title="Lipsync Audio TrackClips"
|
||||
updateClip={updateClipLipSync}
|
||||
group={ClipGroup.CHARACTER}
|
||||
type={ClipType.AUDIO}
|
||||
|
||||
@@ -1,17 +1,67 @@
|
||||
import { objectGroup } from "~/pages/PageEnigma/store";
|
||||
import {
|
||||
audioMinimized,
|
||||
cameraMinimized,
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
objectGroup,
|
||||
objectsMinimized,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { ObjectTrackComponent } from "~/pages/PageEnigma/comps/Timeline/ObjectTrack";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export const ObjectGroups = () => {
|
||||
useSignals();
|
||||
|
||||
if (!objectGroup.value.objects.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (objectsMinimized.value) {
|
||||
return (
|
||||
<div
|
||||
className="relative mb-4 flex h-[35px] items-center justify-end rounded-r-lg bg-object-groupBg pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
objectsMinimized.value = !objectsMinimized.value;
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{objectGroup.value.objects.map((object) => (
|
||||
<div key={object.object_uuid} className="pb-4 pr-4">
|
||||
<ObjectTrackComponent object={object} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
<div
|
||||
className="relative mb-5 block rounded-r-lg bg-object-groupBg pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
left: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
objectsMinimized.value = !objectsMinimized.value;
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faAngleUp} />
|
||||
</button>
|
||||
<div className="pt-[47px]">
|
||||
{objectGroup.value.objects.map((object) => (
|
||||
<div key={object.object_uuid} className="pb-4 pr-4">
|
||||
<ObjectTrackComponent object={object} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,19 +8,11 @@ interface Props {
|
||||
|
||||
export const ObjectTrackComponent = ({ object }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className="mr-4 block rounded-r-lg bg-object-groupBg pb-5 pr-4"
|
||||
style={{ width: fullWidth.value + 32 }}
|
||||
>
|
||||
<div key={object.object_uuid} className="pt-4">
|
||||
<TrackKeyFrames
|
||||
id={object.object_uuid}
|
||||
keyframes={object.keyframes}
|
||||
title={`${object.name} Position/Rotation`}
|
||||
updateKeyframe={updateObject}
|
||||
group={ClipGroup.OBJECT}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TrackKeyFrames
|
||||
id={object.object_uuid}
|
||||
keyframes={object.keyframes}
|
||||
updateKeyframe={updateObject}
|
||||
group={ClipGroup.OBJECT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import {
|
||||
audioGroup,
|
||||
characterGroup,
|
||||
objectGroup,
|
||||
timelineScrollY,
|
||||
toggleAudioMute,
|
||||
toggleLipSyncMute,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const RowHeaders = () => {
|
||||
useSignals();
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute mt-2"
|
||||
style={{ top: timelineScrollY.value * -1 - 8 }}
|
||||
>
|
||||
{characterGroup.value.characters.map((character) => (
|
||||
<div className="mb-4 ml-6" key={character.object_uuid}>
|
||||
<div className="relative block h-[224px] w-[64px] overflow-x-hidden rounded-l-lg bg-character-groupBg">
|
||||
<div className="absolute left-2 top-2 text-xs font-medium text-white">
|
||||
Character
|
||||
</div>
|
||||
<div className="absolute">
|
||||
<button
|
||||
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
|
||||
style={{ top: 174, left: 34 }}
|
||||
onClick={() => toggleLipSyncMute(character.object_uuid)}
|
||||
>
|
||||
{character.muted ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faVolumeSlash}
|
||||
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faVolume} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-4 ml-6">
|
||||
<div className="relative block h-[88px] w-[64px] rounded-l-lg bg-camera-groupBg">
|
||||
<div className="absolute left-2 top-2 text-xs font-medium text-white">
|
||||
Camera
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 ml-6">
|
||||
<div className="relative block h-[88px] w-[64px] rounded-l-lg bg-global_audio-groupBg">
|
||||
<div className="absolute left-2 top-2 text-xs font-medium text-white">
|
||||
Global Audio
|
||||
</div>
|
||||
<div className="absolute">
|
||||
<button
|
||||
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
|
||||
style={{ top: 40, left: 34 }}
|
||||
onClick={() => toggleAudioMute()}
|
||||
>
|
||||
{audioGroup.value.muted ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faVolumeSlash}
|
||||
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faVolume} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{objectGroup.value.objects.map((obj) => (
|
||||
<div key={obj.object_uuid} className="mb-4 ml-6">
|
||||
<div className="relative block h-[88px] w-[64px] rounded-l-lg bg-object-groupBg">
|
||||
<div className="absolute left-2 top-2 text-xs font-medium text-white">
|
||||
Object
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export function CameraHeader() {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none">
|
||||
<path
|
||||
d="M3.11111 1.5C2.69855 1.5 2.30289 1.65804 2.01117 1.93934C1.71944 2.22064 1.55556 2.60218 1.55556 3C1.55556 3.39782 1.71944 3.77936 2.01117 4.06066C2.30289 4.34197 2.69855 4.5 3.11111 4.5C3.52367 4.5 3.91933 4.34197 4.21106 4.06066C4.50278 3.77936 4.66667 3.39782 4.66667 3C4.66667 2.60218 4.50278 2.22064 4.21106 1.93934C3.91933 1.65804 3.52367 1.5 3.11111 1.5ZM8.55556 6H3.11111C1.39271 6 0 4.65703 0 3C0 1.34297 1.39271 0 3.11111 0C4.28264 0 5.30347 0.623437 5.83333 1.54688C6.36319 0.623437 7.38403 0 8.55556 0C10.274 0 11.6667 1.34297 11.6667 3C11.6667 4.65703 10.274 6 8.55556 6ZM8.55556 1.5C8.143 1.5 7.74734 1.65804 7.45561 1.93934C7.16389 2.22064 7 2.60218 7 3C7 3.39782 7.16389 3.77936 7.45561 4.06066C7.74734 4.34197 8.143 4.5 8.55556 4.5C8.96811 4.5 9.36378 4.34197 9.6555 4.06066C9.94722 3.77936 10.1111 3.39782 10.1111 3C10.1111 2.60218 9.94722 2.22064 9.6555 1.93934C9.36378 1.65804 8.96811 1.5 8.55556 1.5ZM13.5698 6.07969C13.8323 6.20625 14 6.46641 14 6.75V11.25C14 11.5336 13.8347 11.7938 13.5698 11.9203C13.3049 12.0469 12.9913 12.0211 12.7556 11.85L11.2 10.725L10.8889 10.5V10.125V7.875V7.5L11.2 7.275L12.7556 6.15C12.9913 5.97891 13.3073 5.95312 13.5698 6.07969ZM1.55556 6.75H3.11111H8.55556C9.41354 6.75 10.1111 7.42266 10.1111 8.25V10.5C10.1111 11.3273 9.41354 12 8.55556 12H3.11111C2.25312 12 1.55556 11.3273 1.55556 10.5V8.25H0.777778C0.347569 8.25 0 7.91484 0 7.5C0 7.08516 0.347569 6.75 0.777778 6.75H1.55556Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Camera
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
interface Props {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function CharacterHeader({ name }: Props) {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="11"
|
||||
height="12"
|
||||
viewBox="0 0 11 12"
|
||||
fill="none">
|
||||
<path
|
||||
d="M5.5 6C6.33354 6 7.13294 5.68393 7.72234 5.12132C8.31174 4.55871 8.64286 3.79565 8.64286 3C8.64286 2.20435 8.31174 1.44129 7.72234 0.87868C7.13294 0.316071 6.33354 0 5.5 0C4.66646 0 3.86706 0.316071 3.27766 0.87868C2.68826 1.44129 2.35714 2.20435 2.35714 3C2.35714 3.79565 2.68826 4.55871 3.27766 5.12132C3.86706 5.68393 4.66646 6 5.5 6ZM4.3779 7.125C1.95938 7.125 0 8.99531 0 11.3039C0 11.6883 0.326563 12 0.729241 12H10.2708C10.6734 12 11 11.6883 11 11.3039C11 8.99531 9.04063 7.125 6.6221 7.125H4.3779Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
{name}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function GlobalAudioHeader() {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15"
|
||||
height="12"
|
||||
viewBox="0 0 15 12"
|
||||
fill="none">
|
||||
<path
|
||||
d="M8.25 11.25C8.25 11.6648 7.91484 12 7.5 12C7.08516 12 6.75 11.6648 6.75 11.25V0.75C6.75 0.335156 7.08516 0 7.5 0C7.91484 0 8.25 0.335156 8.25 0.75V11.25ZM12.75 9.75C12.75 10.1648 12.4148 10.5 12 10.5C11.5852 10.5 11.25 10.1648 11.25 9.75V2.25C11.25 1.83516 11.5852 1.5 12 1.5C12.4148 1.5 12.75 1.83516 12.75 2.25V9.75ZM6 9C6 9.41484 5.66484 9.75 5.25 9.75C4.83516 9.75 4.5 9.41484 4.5 9V3C4.5 2.58516 4.83516 2.25 5.25 2.25C5.66484 2.25 6 2.58516 6 3V9ZM10.5 8.25C10.5 8.66484 10.1648 9 9.75 9C9.33516 9 9 8.66484 9 8.25V3.75C9 3.33516 9.33516 3 9.75 3C10.1648 3 10.5 3.33516 10.5 3.75V8.25ZM3.75 6.75C3.75 7.16484 3.41484 7.5 3 7.5C2.58516 7.5 2.25 7.16484 2.25 6.75V5.25C2.25 4.83516 2.58516 4.5 3 4.5C3.41484 4.5 3.75 4.83516 3.75 5.25V6.75ZM0 6C0 5.80109 0.0790176 5.61032 0.21967 5.46967C0.360322 5.32902 0.551088 5.25 0.75 5.25C0.948912 5.25 1.13968 5.32902 1.28033 5.46967C1.42098 5.61032 1.5 5.80109 1.5 6C1.5 6.19891 1.42098 6.38968 1.28033 6.53033C1.13968 6.67098 0.948912 6.75 0.75 6.75C0.551088 6.75 0.360322 6.67098 0.21967 6.53033C0.0790176 6.38968 0 6.19891 0 6ZM13.5 6C13.5 5.80109 13.579 5.61032 13.7197 5.46967C13.8603 5.32902 14.0511 5.25 14.25 5.25C14.4489 5.25 14.6397 5.32902 14.7803 5.46967C14.921 5.61032 15 5.80109 15 6C15 6.19891 14.921 6.38968 14.7803 6.53033C14.6397 6.67098 14.4489 6.75 14.25 6.75C14.0511 6.75 13.8603 6.67098 13.7197 6.53033C13.579 6.38968 13.5 6.19891 13.5 6Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Global Audio
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function ObjectsHeader() {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 13 13"
|
||||
fill="none">
|
||||
<path
|
||||
d="M5.9541 0.0959457C6.30703 -0.0319819 6.69297 -0.0319819 7.04844 0.0959457L11.9234 1.85111C12.5684 2.08394 13 2.70055 13 3.39392V9.60608C13 10.2969 12.5684 10.9161 11.9209 11.1489L7.0459 12.9041C6.69297 13.032 6.30703 13.032 5.95156 12.9041L1.07656 11.1489C0.431641 10.9161 0 10.2994 0 9.60608V3.39392C0 2.70311 0.431641 2.08394 1.0791 1.85111L5.9541 0.0959457ZM6.5 1.63875L2.08965 3.22505L6.5 4.81136L10.9104 3.22505L6.5 1.63875ZM1.625 9.60608L5.6875 11.067V6.25694L1.625 4.796V9.60608ZM7.3125 11.067L11.375 9.60608V4.796L7.3125 6.2595V11.0696V11.067Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Objects
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
audioGroup,
|
||||
audioMinimized,
|
||||
cameraMinimized,
|
||||
characterGroup,
|
||||
objectGroup,
|
||||
objectsMinimized,
|
||||
timelineScrollY,
|
||||
toggleAudioMute,
|
||||
toggleLipSyncMute,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { GlobalAudioHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/GlobalAudioHeader";
|
||||
import { CameraHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/CameraHeader";
|
||||
import { ObjectsHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/ObjectsHeader";
|
||||
import { CharacterHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/CharacterHeader";
|
||||
|
||||
export const RowHeaders = () => {
|
||||
useSignals();
|
||||
|
||||
const compressedHeaderClasses =
|
||||
"flex h-[35px] items-center gap-2 pl-2 text-xs rounded-tl-lg font-medium text-white";
|
||||
const uncompressedHeaderClasses =
|
||||
"flex h-[35px] items-center gap-2 rounded-br-lg rounded-tl-lg pl-2 text-xs font-medium text-white";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute mt-2 w-[146px]"
|
||||
style={{ top: timelineScrollY.value * -1 - 8 }}>
|
||||
{characterGroup.value.characters.map((character) => {
|
||||
if (character.minimized) {
|
||||
return (
|
||||
<div
|
||||
key={character.object_uuid}
|
||||
className="mb-4 h-[35px] w-full rounded-l-lg bg-character-groupBg">
|
||||
<div
|
||||
className={[
|
||||
compressedHeaderClasses,
|
||||
"bg-character-titleBg",
|
||||
].join(" ")}>
|
||||
<CharacterHeader name={character.name} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={character.object_uuid}
|
||||
className="mb-4 h-[199px] w-full rounded-l-lg bg-character-groupBg">
|
||||
<div className="h-[47px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-character-titleBg",
|
||||
].join(" ")}>
|
||||
<CharacterHeader name={character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex h-[36px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Animation
|
||||
</div>
|
||||
<div className="mb-3 flex h-[36px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Movement
|
||||
</div>
|
||||
<div className="flex h-[36px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
<div className="flex gap-3">
|
||||
Lip Sync
|
||||
<button
|
||||
className="text-md text-white transition-colors duration-100 hover:text-white/80"
|
||||
onClick={() => toggleLipSyncMute(character.object_uuid)}>
|
||||
{character.muted ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faVolumeSlash}
|
||||
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faVolume} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{cameraMinimized.value ? (
|
||||
<div className="mb-4 h-[35px] w-full rounded-l-lg bg-camera-groupBg">
|
||||
<div
|
||||
className={[compressedHeaderClasses, "bg-camera-titleBg"].join(
|
||||
" ",
|
||||
)}>
|
||||
<CameraHeader />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 h-[103px] w-full rounded-l-lg bg-camera-groupBg">
|
||||
<div className="h-[47px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-camera-titleBg",
|
||||
].join(" ")}>
|
||||
<CameraHeader />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex h-[36px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Movement
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{audioMinimized.value ? (
|
||||
<div className="mb-4 h-[35px] w-full rounded-l-lg bg-global_audio-groupBg">
|
||||
<div
|
||||
className={[
|
||||
compressedHeaderClasses,
|
||||
"bg-global_audio-titleBg",
|
||||
].join(" ")}>
|
||||
<GlobalAudioHeader />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 h-[103px] w-full rounded-l-lg bg-global_audio-groupBg">
|
||||
<div className="h-[47px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-global_audio-titleBg",
|
||||
].join(" ")}>
|
||||
<GlobalAudioHeader />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex h-[36px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
<div className="flex gap-3">
|
||||
Track 1
|
||||
<button
|
||||
className="text-md text-white transition-colors duration-100 hover:text-white/80"
|
||||
onClick={() => toggleAudioMute()}>
|
||||
{audioGroup.value.muted ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faVolumeSlash}
|
||||
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faVolume} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{objectGroup.value.objects.length > 0 && (
|
||||
<>
|
||||
{objectsMinimized.value ? (
|
||||
<div className="mb-4 h-[35px] w-full rounded-l-lg bg-object-groupBg">
|
||||
<div
|
||||
className={[
|
||||
compressedHeaderClasses,
|
||||
"bg-object-titleBg",
|
||||
].join(" ")}>
|
||||
<ObjectsHeader />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="mb-4 h-[103px] w-full rounded-l-lg bg-object-groupBg"
|
||||
style={{ height: 55 + objectGroup.value.objects.length * 48 }}>
|
||||
<div className="h-[47px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-object-titleBg",
|
||||
].join(" ")}>
|
||||
<ObjectsHeader />
|
||||
</div>
|
||||
</div>
|
||||
{objectGroup.value.objects.map((obj) => (
|
||||
<div
|
||||
key={obj.object_uuid}
|
||||
className="mb-3 flex h-[36px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
{obj.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,26 @@
|
||||
import {
|
||||
timelineScrollX,
|
||||
currentTime,
|
||||
fullHeight,
|
||||
scale,
|
||||
stylizeScrollX,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSortDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useMouseEventsScrubber } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsScrubber";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
export const Scrubber = () => {
|
||||
interface Props {
|
||||
page: Pages;
|
||||
}
|
||||
|
||||
export const Scrubber = ({ page }: Props) => {
|
||||
useSignals();
|
||||
const { onPointerDown, time } = useMouseEventsScrubber();
|
||||
const displayTime = time === -1 ? currentTime.value : time;
|
||||
const scrollX =
|
||||
page === Pages.EDIT ? timelineScrollX.value : stylizeScrollX.value;
|
||||
|
||||
if (displayTime * 4 * scale.value - timelineScrollX.value < 0) {
|
||||
if (displayTime * 4 * scale.value - scrollX < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,19 +28,31 @@ export const Scrubber = () => {
|
||||
<div
|
||||
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
|
||||
style={{
|
||||
top: 8,
|
||||
left: displayTime * 4 * scale.value + 84 - timelineScrollX.value,
|
||||
top: 16,
|
||||
left: displayTime * 4 * scale.value + 199 - scrollX,
|
||||
}}
|
||||
onPointerDown={onPointerDown}>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSortDown} className="h-5 text-brand-primary" />
|
||||
<svg
|
||||
width="14"
|
||||
height="21"
|
||||
viewBox="0 0 14 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 19.5858L1.58578 14.1715C1.21071 13.7965 0.999999 13.2878 0.999999 12.7573L1 2C1 1.44772 1.44771 1 2 1L12 1C12.5523 1 13 1.44772 13 2L13 12.7573C13 13.2878 12.7893 13.7965 12.4142 14.1715L7 19.5858Z"
|
||||
fill="white"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="block bg-brand-primary"
|
||||
className="block bg-white"
|
||||
style={{
|
||||
width: 2,
|
||||
marginTop: -5,
|
||||
height: fullHeight.value,
|
||||
height: timelineHeight.value - 48,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,15 +5,15 @@ import { Camera } from "./Camera";
|
||||
import { Audio } from "./Audio";
|
||||
import { ConfirmationModal } from "~/components/ConfirmationModal";
|
||||
import {
|
||||
timelineHeight,
|
||||
deleteAudioClip,
|
||||
deleteCharacterClip,
|
||||
selectedItem,
|
||||
filmLength,
|
||||
ignoreDelete,
|
||||
scale,
|
||||
selectedItem,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
timelineScrollY,
|
||||
filmLength,
|
||||
scale,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { useQueueHandler } from "~/pages/PageEnigma/comps/Timeline/utils/useQueueHandler";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
@@ -23,8 +23,10 @@ import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
|
||||
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
|
||||
import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe";
|
||||
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders";
|
||||
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/RowHeaders";
|
||||
import { pageWidth } from "~/store";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import { DoNotShow } from "~/pages/PageEnigma/constants/misc";
|
||||
|
||||
function getItemType(item: Clip | Keyframe | null) {
|
||||
if (!item) {
|
||||
@@ -50,20 +52,6 @@ export const Timeline = () => {
|
||||
timelineHeight.value = window.outerHeight * 0.25;
|
||||
}, []);
|
||||
|
||||
const onDeleteAsk = useCallback((event: KeyboardEvent) => {
|
||||
if (ignoreDelete.value) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
["Backspace", "Delete"].indexOf(event.key) > -1 &&
|
||||
selectedItem.value !== null
|
||||
) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
if ((selectedItem.value as Clip).clip_uuid) {
|
||||
deleteCharacterClip(selectedItem.value as Clip);
|
||||
@@ -74,6 +62,28 @@ export const Timeline = () => {
|
||||
selectedItem.value = null;
|
||||
}, [deleteKeyframe]);
|
||||
|
||||
const onDeleteAsk = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (ignoreDelete.value) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
["Backspace", "Delete"].indexOf(event.key) > -1 &&
|
||||
selectedItem.value !== null
|
||||
) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const show = localStorage.getItem("Delete-Clip");
|
||||
if (show === DoNotShow) {
|
||||
onDelete();
|
||||
return;
|
||||
}
|
||||
setDialogOpen(true);
|
||||
}
|
||||
},
|
||||
[onDelete],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onDeleteAsk);
|
||||
|
||||
@@ -85,25 +95,24 @@ export const Timeline = () => {
|
||||
return (
|
||||
<>
|
||||
<LowerPanel>
|
||||
<TimerGrid />
|
||||
<TimerGrid page={Pages.EDIT} />
|
||||
<div className="flex">
|
||||
<div
|
||||
className="mt-2 overflow-hidden"
|
||||
className="ml-[60px] mt-2 w-[144px] overflow-hidden"
|
||||
style={{
|
||||
width: 88,
|
||||
height: timelineHeight.value - 54,
|
||||
}}>
|
||||
<RowHeaders />
|
||||
</div>
|
||||
<div
|
||||
className="mt-2 overflow-auto"
|
||||
className="mb-20 mt-2 overflow-auto"
|
||||
onScroll={onScroll}
|
||||
style={{
|
||||
width: pageWidth.value - 90,
|
||||
width: pageWidth.value - 204,
|
||||
height: timelineHeight.value - 54,
|
||||
}}>
|
||||
<div
|
||||
style={{ width: filmLength.value * 60 * 4 * scale.value + 32 }}>
|
||||
style={{ width: filmLength.value * 60 * 4 * scale.value + 72 }}>
|
||||
<Characters />
|
||||
<div className="pb-4 pr-8">
|
||||
<Camera />
|
||||
@@ -114,7 +123,7 @@ export const Timeline = () => {
|
||||
<ObjectGroups />
|
||||
</div>
|
||||
</div>
|
||||
<Scrubber />
|
||||
<Scrubber page={Pages.EDIT} />
|
||||
</div>
|
||||
</LowerPanel>
|
||||
<ConfirmationModal
|
||||
@@ -129,6 +138,7 @@ export const Timeline = () => {
|
||||
okText="Delete"
|
||||
okColor="bg-brand-primary"
|
||||
onCancel={() => setDialogOpen(false)}
|
||||
canHide
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import {
|
||||
timelineScrollX,
|
||||
filmLength,
|
||||
fullHeight,
|
||||
scale,
|
||||
stylizeScrollX,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { Fragment } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
export const TimerGrid = () => {
|
||||
interface Props {
|
||||
page: Pages;
|
||||
}
|
||||
export const TimerGrid = ({ page }: Props) => {
|
||||
useSignals();
|
||||
const sectionWidth = 60 * 4 * scale.value;
|
||||
const scrollX =
|
||||
page === Pages.EDIT ? timelineScrollX.value : stylizeScrollX.value;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"prevent-select ml-[88px] mt-4",
|
||||
"prevent-select ml-[204px] mt-4",
|
||||
"relative flex h-7 overflow-hidden",
|
||||
"border-t border-t-ui-panel-border",
|
||||
"text-xs text-white opacity-75",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="absolute" style={{ left: timelineScrollX.value * -1 }}>
|
||||
].join(" ")}>
|
||||
<div className="absolute" style={{ left: scrollX * -1 }}>
|
||||
{Array(filmLength.value)
|
||||
.fill(0)
|
||||
.map((_, index) => (
|
||||
<Fragment key={index}>
|
||||
<div
|
||||
className="absolute ps-1 pt-2"
|
||||
style={{ left: index * sectionWidth + 4 }}
|
||||
>
|
||||
style={{ left: index * sectionWidth + 4 }}>
|
||||
00:{index < 10 ? "0" + index.toString() : index.toString()}
|
||||
</div>
|
||||
<div
|
||||
@@ -57,8 +62,7 @@ export const TimerGrid = () => {
|
||||
))}
|
||||
<div
|
||||
className="absolute pt-2"
|
||||
style={{ left: filmLength.value * sectionWidth + 4 }}
|
||||
>
|
||||
style={{ left: filmLength.value * sectionWidth + 4 }}>
|
||||
00:
|
||||
{filmLength.value < 10
|
||||
? "0" + filmLength.value.toString()
|
||||
|
||||
@@ -31,46 +31,40 @@ export const TrackClip = ({ clip, min, max, group, updateClip }: Props) => {
|
||||
const classes = [
|
||||
"absolute",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? `bg-${group}-selected`
|
||||
? `bg-${group}-selected border-white focus-visible:outline-0`
|
||||
: `bg-${group}-clip`,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={[
|
||||
...classes,
|
||||
"px-[6px] py-[3px]",
|
||||
"rounded",
|
||||
"prevent-select text-xs font-medium text-white",
|
||||
].join(" ")}
|
||||
style={{ top: -28, left: offset * 4 * scale.value + 2 }}
|
||||
>
|
||||
{clip.name}
|
||||
</div>
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"rounded-l-lg",
|
||||
"block h-full",
|
||||
"w-[15px] rounded-l-lg",
|
||||
"block h-full cursor-ew-resize",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: 15,
|
||||
left: offset * 4 * scale.value,
|
||||
cursor: "w-resize",
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "left")}
|
||||
onClick={() => (selectedItem.value = clip)}
|
||||
/>
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}>
|
||||
<div className="flex h-full items-center justify-end">
|
||||
<div className="block h-[10px] w-[2px] bg-white opacity-60" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"block h-full",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
@@ -79,25 +73,39 @@ export const TrackClip = ({ clip, min, max, group, updateClip }: Props) => {
|
||||
cursor: "move",
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "drag")}
|
||||
onClick={() => (selectedItem.value = clip)}
|
||||
/>
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}>
|
||||
<div
|
||||
className="ml-2 w-full overflow-hidden text-ellipsis whitespace-nowrap text-left text-xs text-white"
|
||||
style={{ width: length * 4 * scale.value - 46 }}>
|
||||
{clip.name}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"rounded-r-lg",
|
||||
"block h-full",
|
||||
"w-[15px] rounded-r-lg",
|
||||
"block h-full cursor-ew-resize",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: 15,
|
||||
left: offset * 4 * scale.value + length * 4 * scale.value - 15,
|
||||
cursor: "e-resize",
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "right")}
|
||||
onClick={() => (selectedItem.value = clip)}
|
||||
/>
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}>
|
||||
<div className="flex h-full items-center">
|
||||
<div className="block h-[10px] w-[2px] bg-white opacity-60" />
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
AssetType,
|
||||
Clip,
|
||||
@@ -20,7 +18,7 @@ import {
|
||||
interface Props {
|
||||
id: string;
|
||||
clips: Clip[];
|
||||
title: string;
|
||||
title?: string;
|
||||
group: ClipGroup;
|
||||
type?: ClipType;
|
||||
updateClip: (options: { id: string; length: number; offset: number }) => void;
|
||||
@@ -79,7 +77,9 @@ export const TrackClips = ({
|
||||
|
||||
// Now check if the clip fits
|
||||
const position = track.getBoundingClientRect();
|
||||
const clipOffset = (event.clientX - position.x) / 4 / scale.value;
|
||||
const clipOffset = Math.round(
|
||||
(event.clientX - position.x) / 4 / scale.value,
|
||||
);
|
||||
|
||||
if (clipOffset + (dragItem.value!.length ?? 0) > filmLength.value * 60) {
|
||||
canDrop.value = false;
|
||||
@@ -112,11 +112,10 @@ export const TrackClips = ({
|
||||
return (
|
||||
<div
|
||||
id={`track-${trackType}-${id}`}
|
||||
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
|
||||
className={`relative block h-9 w-full rounded-lg bg-${group}-unselected`}
|
||||
onPointerOver={onPointerOver}
|
||||
onPointerLeave={onPointerLeave}
|
||||
onPointerMove={onPointerMove}
|
||||
>
|
||||
onPointerMove={onPointerMove}>
|
||||
{clips.map((clip, index) => (
|
||||
<TrackClip
|
||||
key={clip.clip_uuid}
|
||||
@@ -133,9 +132,11 @@ export const TrackClips = ({
|
||||
clip={clip}
|
||||
/>
|
||||
))}
|
||||
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
|
||||
{title}
|
||||
</div>
|
||||
{!!title && (
|
||||
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -33,20 +33,21 @@ export const TrackKeyFrame = ({ keyframe, updateKeyframe }: Props) => {
|
||||
return (
|
||||
<button
|
||||
className={[
|
||||
"block rotate-45 cursor-ew-resize",
|
||||
"top-[11px] block h-[14px] w-[14px] rotate-45 cursor-ew-resize",
|
||||
"absolute",
|
||||
keyframe.keyframe_uuid === selectedKeyframeId
|
||||
? "bg-keyframe-selected"
|
||||
: "bg-keyframe-unselected",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
left: displayOffset * 4 * scale.value - 6,
|
||||
top: 11,
|
||||
left: displayOffset * 4 * scale.value - 5,
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "drag")}
|
||||
onClick={() => (selectedItem.value = keyframe)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = keyframe;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
|
||||
interface Props {
|
||||
id: string;
|
||||
keyframes: Keyframe[];
|
||||
title: string;
|
||||
title?: string;
|
||||
group: ClipGroup;
|
||||
toggleMute?: () => void;
|
||||
muted?: boolean;
|
||||
@@ -24,22 +24,21 @@ export const TrackKeyFrames = ({
|
||||
group,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id={`track-${group}-${id}`}
|
||||
className={`relative mt-4 block h-9 w-full rounded-lg bg-${group}-unselected`}
|
||||
>
|
||||
{keyframes.map((keyframe) => (
|
||||
<TrackKeyFrame
|
||||
key={keyframe.keyframe_uuid}
|
||||
updateKeyframe={updateKeyframe}
|
||||
keyframe={keyframe}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
id={`track-${group}-${id}`}
|
||||
className={`relative block h-9 w-full rounded-lg bg-${group}-unselected`}>
|
||||
{keyframes.map((keyframe) => (
|
||||
<TrackKeyFrame
|
||||
key={keyframe.keyframe_uuid}
|
||||
updateKeyframe={updateKeyframe}
|
||||
keyframe={keyframe}
|
||||
/>
|
||||
))}
|
||||
{!!title && (
|
||||
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -18,20 +18,29 @@ export const useMouseEventsClip = (
|
||||
const isActive = useRef("");
|
||||
const clientX = useRef(0);
|
||||
|
||||
const onPointerUp = useCallback(() => {
|
||||
if (isActive.current) {
|
||||
updateClip({
|
||||
id: clip.clip_uuid,
|
||||
offset: Math.round(currOffset.current),
|
||||
length: Math.round(currLength.current),
|
||||
});
|
||||
isActive.current = "";
|
||||
canDrop.value = false;
|
||||
}
|
||||
}, [updateClip, clip.clip_uuid]);
|
||||
const onPointerUp = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (isActive.current) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
updateClip({
|
||||
id: clip.clip_uuid,
|
||||
offset: Math.round(currOffset.current),
|
||||
length: Math.round(currLength.current),
|
||||
});
|
||||
isActive.current = "";
|
||||
canDrop.value = false;
|
||||
}
|
||||
},
|
||||
[updateClip, clip.clip_uuid],
|
||||
);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (isActive.current) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
const delta = (event.clientX - clientX.current) / 4 / scale.value;
|
||||
const deltaOffset = delta + initOffset.current;
|
||||
if (isActive.current === "drag") {
|
||||
@@ -44,7 +53,10 @@ export const useMouseEventsClip = (
|
||||
}
|
||||
}
|
||||
if (isActive.current === "left") {
|
||||
if (initLength.current - delta < 30 || deltaOffset < min) {
|
||||
if (
|
||||
initLength.current - delta < 30 / 4 / scale.value ||
|
||||
deltaOffset < min
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currOffset.current = deltaOffset;
|
||||
@@ -52,7 +64,7 @@ export const useMouseEventsClip = (
|
||||
}
|
||||
if (isActive.current === "right") {
|
||||
if (
|
||||
initLength.current + delta < 30 ||
|
||||
initLength.current + delta < 30 / 4 / scale.value ||
|
||||
currOffset.current + initLength.current + delta > max
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -27,24 +27,31 @@ export const useMouseEventsKeyframe = ({
|
||||
const isActive = useRef("");
|
||||
const clientX = useRef(0);
|
||||
|
||||
const onPointerUp = useCallback(() => {
|
||||
if (isActive.current) {
|
||||
updateKeyframe({
|
||||
id: keyframe.keyframe_uuid,
|
||||
offset: Math.round(currOffset.current),
|
||||
addToast,
|
||||
});
|
||||
isActive.current = "";
|
||||
canDrop.value = false;
|
||||
setOffset(-1);
|
||||
}
|
||||
}, [updateKeyframe, keyframe.keyframe_uuid, setOffset]);
|
||||
const onPointerUp = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (isActive.current) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
updateKeyframe({
|
||||
id: keyframe.keyframe_uuid,
|
||||
offset: Math.round(currOffset.current),
|
||||
addToast,
|
||||
});
|
||||
isActive.current = "";
|
||||
canDrop.value = false;
|
||||
setOffset(-1);
|
||||
}
|
||||
},
|
||||
[updateKeyframe, keyframe.keyframe_uuid, setOffset, addToast],
|
||||
);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const delta = (event.clientX - clientX.current) / 4 / scale.value;
|
||||
const deltaOffset = delta + initOffset.current;
|
||||
if (isActive.current === "drag") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (deltaOffset < min) {
|
||||
currOffset.current = min;
|
||||
} else if (deltaOffset > max) {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
timelineScrollX,
|
||||
currentTime,
|
||||
filmLength,
|
||||
scale,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import { currentTime, filmLength, scale } from "~/pages/PageEnigma/store";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
@@ -16,7 +11,7 @@ export const useMouseEventsScrubber = () => {
|
||||
const [time, setTime] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
const max = filmLength.value * 60 * 4 * scale.value;
|
||||
const max = filmLength.value * 60;
|
||||
|
||||
const onPointerUp = () => {
|
||||
if (isActive) {
|
||||
@@ -38,16 +33,11 @@ export const useMouseEventsScrubber = () => {
|
||||
if (isActive) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
console.log(delta, max);
|
||||
if (delta < 0 || delta > max) {
|
||||
return;
|
||||
}
|
||||
setTime((oldTime) => {
|
||||
console.log(
|
||||
oldTime,
|
||||
delta,
|
||||
delta + timelineScrollX.value,
|
||||
delta + timelineScrollX.value / 4 / scale.value,
|
||||
);
|
||||
if (oldTime !== delta) {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
|
||||
@@ -7,8 +7,10 @@ export const useMouseEventsTimeline = () => {
|
||||
const [clientY, setClientY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerUp = () => {
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (isActive) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
timelineHeight.value = Math.round(dndTimelineHeight.value);
|
||||
setIsActive(false);
|
||||
dndTimelineHeight.value = -1;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
currentTime,
|
||||
filmLength,
|
||||
scale,
|
||||
selectedItem,
|
||||
stylizeScrollX,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/store";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
|
||||
export default function useTimelineClick(page: Pages) {
|
||||
return useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
const scrollX =
|
||||
page === Pages.EDIT ? timelineScrollX.value : stylizeScrollX.value;
|
||||
const newTime = Math.round(
|
||||
(event.clientX + scrollX - 204) / 4 / scale.value,
|
||||
);
|
||||
if (newTime < 0) {
|
||||
return;
|
||||
}
|
||||
const max = filmLength.value * 60;
|
||||
if (newTime > max) {
|
||||
return;
|
||||
}
|
||||
selectedItem.value = null;
|
||||
currentTime.value = newTime;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: newTime },
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const DoNotShow = "do not show";
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum Pages {
|
||||
EDIT,
|
||||
STYLE,
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export interface CharacterTrack {
|
||||
media_id: string;
|
||||
name: string;
|
||||
muted: boolean;
|
||||
minimized: boolean;
|
||||
animationClips: Clip[];
|
||||
positionKeyframes: Keyframe[];
|
||||
lipSyncClips: Clip[];
|
||||
|
||||
@@ -23,6 +23,7 @@ export function addNewCharacter(data: MediaItem) {
|
||||
name: data.name,
|
||||
media_id: data.media_id,
|
||||
muted: false,
|
||||
minimized: false,
|
||||
animationClips: [],
|
||||
positionKeyframes: [],
|
||||
lipSyncClips: [],
|
||||
|
||||
@@ -197,6 +197,22 @@ export function toggleLipSyncMute(characterId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleCharacterMinimized(characterId: string) {
|
||||
const oldCharacterGroup = characterGroup.value;
|
||||
characterGroup.value = {
|
||||
...oldCharacterGroup,
|
||||
characters: oldCharacterGroup.characters.map((character) => {
|
||||
return {
|
||||
...character,
|
||||
minimized:
|
||||
character.object_uuid === characterId
|
||||
? !character.minimized
|
||||
: character.minimized,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function selectCharacterClip(clipId: string) {
|
||||
const oldCharacterGroup = characterGroup.value;
|
||||
characterGroup.value = {
|
||||
|
||||
@@ -21,6 +21,7 @@ function getAddCharacter(item: ClipUI) {
|
||||
name: item.object_name,
|
||||
media_id: item.media_id,
|
||||
muted: false,
|
||||
minimized: false,
|
||||
animationClips: [],
|
||||
positionKeyframes: [],
|
||||
lipSyncClips: [],
|
||||
|
||||
@@ -1,18 +1,53 @@
|
||||
import { signal, computed } from "@preact/signals-core";
|
||||
import { characterGroup } from "~/pages/PageEnigma/store/characterGroups";
|
||||
import { objectGroup } from "~/pages/PageEnigma/store/objectGroup";
|
||||
import { pageWidth } from "~/store";
|
||||
import { timelineScrollX } from "~/pages/PageEnigma/store/timing";
|
||||
|
||||
// timeline
|
||||
export const scale = signal(1);
|
||||
export const filmLength = signal(12);
|
||||
export const timelineHeight = signal(0);
|
||||
|
||||
export const cameraMinimized = signal(false);
|
||||
export const audioMinimized = signal(false);
|
||||
export const objectsMinimized = signal(false);
|
||||
|
||||
export const characterHeight = computed(() => {
|
||||
if (!characterGroup.value.characters.length) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
characterGroup.value.characters.reduce((totalHeight, character) => {
|
||||
return totalHeight + (character.minimized ? 47 : 211);
|
||||
}, 0) + 8
|
||||
);
|
||||
});
|
||||
|
||||
export const objectHeight = computed(() => {
|
||||
if (!objectGroup.value.objects.length) {
|
||||
return 0;
|
||||
}
|
||||
if (objectsMinimized.value) {
|
||||
return 55;
|
||||
}
|
||||
return 47 + objectGroup.value.objects.length * 47 + 8;
|
||||
});
|
||||
|
||||
export const fullHeight = computed(() => {
|
||||
return (
|
||||
characterGroup.value.characters.length * 268 +
|
||||
objectGroup.value.objects.length * 60 +
|
||||
300 +
|
||||
96
|
||||
characterHeight.value +
|
||||
objectHeight.value +
|
||||
(cameraMinimized.value ? 47 : 103) +
|
||||
(audioMinimized.value ? 47 : 103) +
|
||||
24
|
||||
);
|
||||
});
|
||||
|
||||
export const minimizeIconPosition = computed(() => {
|
||||
return Math.min(
|
||||
pageWidth.value - 270 + timelineScrollX.value,
|
||||
fullWidth.value - 24,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ export const currentTime = signal(0);
|
||||
|
||||
export const timelineScrollX = signal(0);
|
||||
export const timelineScrollY = signal(0);
|
||||
export const stylizeScrollX = signal(0);
|
||||
@@ -115,24 +115,28 @@ export default {
|
||||
selected: "#6384F4",
|
||||
clip: "#7E92DA",
|
||||
groupBg: "#2B3448",
|
||||
titleBg: "#384763",
|
||||
},
|
||||
camera: {
|
||||
unselected: "#466C7C",
|
||||
selected: "#56BBC1",
|
||||
clip: "#5F949F",
|
||||
groupBg: "#2B393E",
|
||||
titleBg: "#395259",
|
||||
},
|
||||
global_audio: {
|
||||
unselected: "#864C68",
|
||||
selected: "#D460A6",
|
||||
clip: "#E37BAD",
|
||||
groupBg: "#382940",
|
||||
titleBg: "#5A3D65",
|
||||
},
|
||||
object: {
|
||||
unselected: "#7C5646",
|
||||
selected: "#EA8E5A",
|
||||
clip: "#D49D75",
|
||||
groupBg: "#372E32",
|
||||
titleBg: "#514248",
|
||||
},
|
||||
keyframe: {
|
||||
unselected: "#EEEEEE",
|
||||
|
||||
Reference in new issue
Block a user