Merge pull request #167 from storytold/feature/timeline-v3

upgrade timeline to V3
This commit is contained in:
ArEnSc authored and GitHub committed 2024-04-16 16:35:23 -04:00
commit 14ba410e2a
38 files changed
+840 -385

No files matched your search

+29 -12
View File
@@ -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" />
&nbsp;&nbsp;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>
)}
+12 -38
View File
@@ -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>
</>
+2 -1
View File
@@ -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) => {
+4 -3
View File
@@ -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>
</>
+13 -40
View File
@@ -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 = [
+47 -6
View File
@@ -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}
/>
+47 -6
View File
@@ -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 },
});
}, []);
}
+1
View File
@@ -0,0 +1 @@
export const DoNotShow = "do not show";
+4
View File
@@ -0,0 +1,4 @@
export enum Pages {
EDIT,
STYLE,
}
+1
View File
@@ -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: [],
+39 -4
View File
@@ -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,
);
});
+1
View File
@@ -4,3 +4,4 @@ export const currentTime = signal(0);
export const timelineScrollX = signal(0);
export const timelineScrollY = signal(0);
export const stylizeScrollX = signal(0);
+4
View File
@@ -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",