mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
upgrade timeline to V3
This commit is contained in:
1 parent
2f6b19dbb7
commit
68c472cfd2
32 files changed
+646
-338
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-[228px]" />
|
||||
<div
|
||||
className="h-[30px] overflow-x-auto overflow-y-hidden"
|
||||
onScroll={onScroll}
|
||||
style={{
|
||||
width: pageWidth.value,
|
||||
width: pageWidth.value - 228,
|
||||
}}>
|
||||
<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,7 +1,10 @@
|
||||
import {
|
||||
audioGroup,
|
||||
audioMinimized,
|
||||
cameraMinimized,
|
||||
characterGroup,
|
||||
objectGroup,
|
||||
objectsMinimized,
|
||||
timelineScrollY,
|
||||
toggleAudioMute,
|
||||
toggleLipSyncMute,
|
||||
@@ -16,22 +19,89 @@ export const RowHeaders = () => {
|
||||
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
|
||||
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="flex h-[35px] flex-col justify-center pl-2 text-xs font-medium text-white">
|
||||
{character.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute">
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={character.object_uuid}
|
||||
className="mb-4 h-[199px] w-full rounded-l-lg bg-character-groupBg">
|
||||
<div className="h-[47px] pl-2 pt-2 text-xs font-medium text-white">
|
||||
{character.name}
|
||||
</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">
|
||||
Transition
|
||||
</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="flex h-[35px] flex-col justify-center pl-2 text-xs font-medium text-white">
|
||||
Camera
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 h-[103px] w-full rounded-l-lg bg-camera-groupBg">
|
||||
<div className="h-[47px] pl-2 pt-2 text-xs font-medium text-white">
|
||||
Camera
|
||||
</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="flex h-[35px] flex-col justify-center pl-2 text-xs font-medium text-white">
|
||||
Global Audio
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 h-[103px] w-full rounded-l-lg bg-global_audio-groupBg">
|
||||
<div className="h-[47px] pl-2 pt-2 text-xs font-medium text-white">
|
||||
Global Audio
|
||||
</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 absolute text-white transition-colors duration-100 hover:text-white/80"
|
||||
style={{ top: 174, left: 34 }}
|
||||
onClick={() => toggleLipSyncMute(character.object_uuid)}
|
||||
>
|
||||
{character.muted ? (
|
||||
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"
|
||||
@@ -43,46 +113,33 @@ export const RowHeaders = () => {
|
||||
</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
|
||||
)}
|
||||
{objectGroup.value.objects.length > 0 && (
|
||||
<>
|
||||
{objectsMinimized.value ? (
|
||||
<div className="mb-4 h-[35px] w-full rounded-l-lg bg-object-groupBg">
|
||||
<div className="flex h-[35px] flex-col justify-center pl-2 text-xs font-medium text-white">
|
||||
Objects
|
||||
</div>
|
||||
</div>
|
||||
</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] pl-2 pt-2 text-xs font-medium text-white">
|
||||
Objects
|
||||
</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,28 @@
|
||||
import {
|
||||
timelineScrollX,
|
||||
currentTime,
|
||||
fullHeight,
|
||||
scale,
|
||||
stylizeScrollX,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -23,7 +31,7 @@ export const Scrubber = () => {
|
||||
className="absolute flex cursor-ew-resize flex-col items-center text-brand-primary"
|
||||
style={{
|
||||
top: 8,
|
||||
left: displayTime * 4 * scale.value + 84 - timelineScrollX.value,
|
||||
left: displayTime * 4 * scale.value + 225 - scrollX,
|
||||
}}
|
||||
onPointerDown={onPointerDown}>
|
||||
<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";
|
||||
@@ -25,6 +25,8 @@ import useUpdateKeyframe from "~/pages/PageEnigma/contexts/TrackContext/utils/us
|
||||
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/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,21 +95,20 @@ export const Timeline = () => {
|
||||
return (
|
||||
<>
|
||||
<LowerPanel>
|
||||
<TimerGrid />
|
||||
<TimerGrid page={Pages.EDIT} />
|
||||
<div className="flex">
|
||||
<div
|
||||
className="mt-2 overflow-hidden"
|
||||
className="ml-[82px] mt-2 w-[146px] 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 - 228,
|
||||
height: timelineHeight.value - 54,
|
||||
}}>
|
||||
<div
|
||||
@@ -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-[228px] 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,35 @@ 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 text-left text-xs text-white">{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 - 2,
|
||||
}}
|
||||
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") {
|
||||
|
||||
@@ -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,35 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
currentTime,
|
||||
filmLength,
|
||||
scale,
|
||||
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 - 228) / 4 / scale.value,
|
||||
);
|
||||
if (newTime < 0) {
|
||||
return;
|
||||
}
|
||||
const max = filmLength.value * 60;
|
||||
if (newTime > max) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
Reference in new issue
Block a user