upgrade timeline to V3

This commit is contained in:
Christopher Oliphant committed 2024-04-16 09:29:00 -04:00
1 parent 2f6b19dbb7
commit 68c472cfd2
32 files changed
+646 -338

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-[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 = [
+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,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 },
});
}, []);
}
+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);