Add start/end frame buttons and shift timeline
+ Adjust other UI layout accordingly
This commit is contained in:
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 >
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user