Add start/end frame buttons and shift timeline

+ Adjust other UI layout accordingly
This commit is contained in:
Tanish Manku committed 2025-01-07 17:27:28 -08:00
1 parent 8698c96cf2
commit 94a102caee
10 files changed
+144 -78

No files matched your search

@@ -43,13 +43,12 @@ export const Camera = () => {
<div
id="track-camera"
className="relative block rounded-r-lg bg-camera-groupBg pb-2 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<div className="flex justify-end">
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
right: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
@@ -1,18 +1,21 @@
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useContext, useMemo } from "react";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import {
fullWidth,
minimizeIconPosition,
toggleCharacterMinimized,
updateCharacters,
} from "~/pages/PageEnigma/signals";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
import { CharacterTrack } from "~/pages/PageEnigma/models";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleDown, faAngleUp } from "@fortawesome/pro-solid-svg-icons";
import {
filmLength,
frameTrackButtonWidthPx,
fullWidth,
minimizeIconPosition,
scale,
toggleCharacterMinimized,
updateCharacters
} from "~/pages/PageEnigma/signals";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
import { ClipGroup, ClipType } from "~/enums";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
function buildUpdaters(
updateCharacters: (options: {
@@ -87,13 +90,13 @@ export const Character = ({ character }: Props) => {
return (
<div
id={`track-character-${character.object_uuid}`}
className="relative flex h-[30px] items-center justify-end rounded-r-lg bg-character-groupBg pr-4"
style={{ width: fullWidth.value + 16 }}
className="relative flex h-[30px] items-center justify-end rounded-r-lg bg-character-groupBg pr-4 box-content"
style={{ width: filmLength.value * 1000 * 4 * scale.value + frameTrackButtonWidthPx * 2 }}
>
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
right: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
@@ -111,13 +114,12 @@ export const Character = ({ character }: Props) => {
<div
id={`track-character-${character.object_uuid}`}
className="relative block rounded-r-lg bg-character-groupBg pb-2 pr-4"
style={{ width: fullWidth.value + 16 }}
>
<div className="flex h-[30px] items-center justify-end">
<button
className="absolute"
style={{
left: minimizeIconPosition.value,
right: minimizeIconPosition.value,
}}
onClick={(event) => {
event.stopPropagation();
@@ -0,0 +1,54 @@
import { faImage } from "@fortawesome/pro-solid-svg-icons";
import { ButtonIconStack } from "~/components/reusable/ButtonIconStack";
import { frameTrackButtonWidthPx } from "../../signals";
import { useCallback, useContext, useState } from "react";
import { EngineContext } from "../../contexts/EngineContext";
import { CHARACTER_FRAME_FILE_TYPE } from "~/enums";
import { UploadModalMedia } from "~/components/reusable/UploadModalMedia";
export enum CharacterFrameTarget {
Start,
End
}
export interface CharacterFrameButtonProps {
target: CharacterFrameTarget;
characterId: string;
className?: string;
}
export const CharacterFrameStrings = {
[CharacterFrameTarget.Start]: "Start Frame",
[CharacterFrameTarget.End]: "End Frame"
}
export default function CharacterFrameButton(
{
target,
characterId,
className,
}: CharacterFrameButtonProps
) {
const editorEngine = useContext(EngineContext);
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const handleFrameClick = useCallback(() => {
})
return (
<>
<div className={className} style={{ minWidth: frameTrackButtonWidthPx, width: frameTrackButtonWidthPx }}>
<ButtonIconStack icon={faImage} additionalStyle="bg-character-frame" text={CharacterFrameStrings[target]} onClick={() => setIsUploadModalOpen(true)} />
</div>
<UploadModalMedia
isOpen={isUploadModalOpen}
onClose={() => setIsUploadModalOpen(false)}
onSuccess={() => setIsUploadModalOpen(false)}
title={"Upload Character Frame Image"}
fileTypes={Object.values(CHARACTER_FRAME_FILE_TYPE)}
/>
</>
)
}
@@ -93,7 +93,7 @@ export const RowHeaders = () => {
<CharacterHeader name={character.name} />
</div>
</div>
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
<div className="mb-1 flex h-[60px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
Animation
</div>
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
@@ -143,7 +143,7 @@ export const RowHeaders = () => {
</div>
</div>
) : (
<div className="mb-1 h-[72px] w-full rounded-l-lg bg-global_audio-groupBg">
<div className="mb-1 h-[102px] w-full rounded-l-lg bg-global_audio-groupBg">
<div className="h-[30px] text-xs font-medium text-white">
<div
className={[
@@ -154,7 +154,7 @@ export const RowHeaders = () => {
<GlobalAudioHeader />
</div>
</div>
<div className="flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
<div className="flex h-[60px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
<div className="flex gap-3">
Track 1
<button
@@ -6,6 +6,7 @@ import { Audio } from "./Audio";
import {
characterGroup,
filmLength,
frameTrackButtonWidthPx,
ignoreKeyDelete,
isHotkeyDisabled,
objectsMinimized,
@@ -139,15 +140,12 @@ export const Timeline = () => {
height: timelineHeight.value - 36,
}}
>
<div
className=""
style={{ width: filmLength.value * 1000 * 4 * scale.value + 72 }}
>
<div className="w-fit">
{currentPage.value === Pages.EDIT ? (
<>
<PremiumLockTimeline locked={false} />
<Characters />
<div className="pb-1 pr-8">
<div className="pb-1 pr-4">
<Camera />
</div>
<div className="pb-1 pr-8">
@@ -5,15 +5,18 @@ import {
canDrop,
dragItem,
filmLength,
frameTrackButtonWidthPx,
scale,
} from "~/pages/PageEnigma/signals";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp } from "@fortawesome/pro-solid-svg-icons";
import { faArrowUp, faImage } from "@fortawesome/pro-solid-svg-icons";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { ClipGroup, ClipType } from "~/enums";
import { currentPage } from "~/signals";
import { Pages } from "~/pages/PageEnigma/constants/page";
import { getCanDrop } from "~/pages/PageEnigma/comps/Timeline/utils/getCanDrop";
import { ButtonIconStack } from "~/components/reusable/ButtonIconStack";
import CharacterFrameButton, { CharacterFrameTarget } from "./CharacterFrameButtons";
interface Props {
id: string;
@@ -92,39 +95,43 @@ export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
<div
id={`track-${trackType}-${id}`}
className={[
"relative mb-1 block h-[30px] w-full rounded-md",
`bg-${group}-unselected`,
clips.length === 0 ? "border border-dashed border-white/30" : "",
"relative mb-1 flex h-[60px] w-fit rounded-md overflow-clip",
].join(" ")}
style={{ marginLeft: (group === ClipGroup.CHARACTER ? 0 : frameTrackButtonWidthPx) }}
>
{(group === ClipGroup.CHARACTER &&
<CharacterFrameButton className={"h-full rounded-l-md"} target={CharacterFrameTarget.Start} characterId={id} />
)}
<div
className={[
"absolute inset-0 rounded-md",
`bg-${group}-unselected inset-0`,
canDropAsset
? "animate-pulse bg-white/30 duration-[750ms]"
: "opacity-0",
: "",
].join(" ")}
style={{ left: frameTrackButtonWidthPx, width: filmLength.value * 1000 * 4 * scale.value }}
onPointerOver={onPointerOver}
onPointerMove={onPointerMove}
/>
{clips.map((clip, index) => (
<TrackClip
key={clip.clip_uuid}
min={
index > 0 ? clips[index - 1].offset + clips[index - 1].length : 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: filmLength.value * 1000
}
group={group}
updateClip={updateClip}
clip={clip}
/>
))}
>
{clips.map((clip, index) => (
<TrackClip
key={clip.clip_uuid}
min={
index > 0 ? clips[index - 1].offset + clips[index - 1].length : 0
}
max={
index < clips.length - 1
? clips[index + 1].offset
: filmLength.value * 1000
}
group={group}
updateClip={updateClip}
clip={clip}
/>
))}
</div>
{clips.length === 0 && currentPage.value === Pages.EDIT && (
<div className="prevent-select absolute flex h-full items-center gap-2 ps-2 text-xs font-medium text-white">
<div className="prevent-select absolute flex h-full items-center gap-2 ps-2 text-xs font-medium text-white" style={{ marginLeft: frameTrackButtonWidthPx + 10 }}>
<div className="animate-bounce">
<FontAwesomeIcon icon={faArrowUp} className="text-white/80" />
</div>
@@ -143,6 +150,9 @@ export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
</div>
</div>
)}
{(group === ClipGroup.CHARACTER &&
<CharacterFrameButton className={"h-full rounded-r-md"} target={CharacterFrameTarget.End} characterId={id} />
)}
</div>
);
};
@@ -1,7 +1,7 @@
import { Keyframe } from "~/pages/PageEnigma/models";
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { dragItem } from "~/pages/PageEnigma/signals";
import { dragItem, filmLength, frameTrackButtonWidthPx, scale } from "~/pages/PageEnigma/signals";
import { AssetType, ClipGroup } from "~/enums";
interface Props {
@@ -64,23 +64,25 @@ export const TrackKeyFrames = ({
setNotDropText({ dragType: dragItem.value?.type, group });
}
return (
<div
id={`track-${group}-${id}`}
className={`relative mb-1 block h-[30px] w-full rounded-md bg-${group}-unselected`}
onPointerMove={onPointerMove}
>
{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 style={{ marginLeft: frameTrackButtonWidthPx, width: filmLength.value * 1000 * 4 * scale.value }}>
<div
id={`track-${group}-${id}`}
className={`relative mb-1 block h-[30px] w-full rounded-md bg-${group}-unselected`}
onPointerMove={onPointerMove}
>
{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>
);
};
@@ -2,6 +2,7 @@ import React, { useCallback } from "react";
import {
currentTime,
filmLength,
frameTrackButtonWidthPx,
scale,
selectedItem,
stylizeScrollX,
@@ -17,7 +18,7 @@ export default function useTimelineClick(page: Pages) {
const scrollX =
page === Pages.EDIT ? timelineScrollX.value : stylizeScrollX.value;
const newTime = Math.round(
(event.clientX + scrollX - 204) / 4 / scale.value,
(event.clientX + scrollX - 204 - frameTrackButtonWidthPx) / 4 / scale.value,
);
if (newTime < 0) {
return;
@@ -33,5 +34,5 @@ export default function useTimelineClick(page: Pages) {
action: toEngineActions.UPDATE_TIME,
data: { currentTime: newTime },
});
}, []);
}, [page]);
}
@@ -1,5 +1,6 @@
import {
filmLength,
frameTrackButtonWidthPx,
fullHeight,
scale,
timelineScrollX,
@@ -15,11 +16,15 @@ export const TimerGrid = () => {
return (
<div
className={[
"prevent-select ml-[254px] mt-2",
"prevent-select mt-2",
"relative flex h-5 overflow-hidden",
"border-t border-t-ui-panel-border",
"text-xs text-white opacity-75",
].join(" ")}
style={{
marginLeft: `${204 + frameTrackButtonWidthPx}px`
}}
>
<div className="absolute" style={{ left: scrollX * -1 }}>
{Array(filmLength.value)
@@ -74,6 +79,6 @@ export const TimerGrid = () => {
}}
/>
</div>
</div>
</div >
);
};
+3 -8
View File
@@ -5,7 +5,7 @@ import { pageWidth } from "~/signals";
import { timelineScrollX } from "~/pages/PageEnigma/signals/timing";
// timeline
export const scale = signal(0.1);
export const scale = signal(0.02);
export const filmLength = signal(7); // seconds
export const timelineHeight = signal(0);
@@ -44,12 +44,7 @@ export const fullHeight = computed(() => {
);
});
export const minimizeIconPosition = computed(() => {
return Math.min(
pageWidth.value - 270 + timelineScrollX.value,
fullWidth.value - 24,
);
});
export const minimizeIconPosition = signal(20);
export const timelinePremiumLockPosition = computed(() => {
return Math.min(
@@ -65,4 +60,4 @@ export const fullWidth = computed(() => {
// side panel
export const sidePanelWidth = signal(0);
export const frameTrackButtonWidthPx = 50;
export const frameTrackButtonWidthPx = 80;