mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-22 01:29:31 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9ac685cfc |
@@ -258,6 +258,7 @@ export const PromptBox = () => {
|
||||
maybe_number_of_samples: 1,
|
||||
},
|
||||
});
|
||||
console.log("Generate response:", generateResponse);
|
||||
toast.success("Please wait while we process your image.");
|
||||
};
|
||||
|
||||
@@ -358,7 +359,6 @@ export const PromptBox = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Button, TopBar } from "~/components";
|
||||
import { PreviewImages } from "~/pages/PageEnigma/comps/PreviewImages";
|
||||
import { useContext, useState } from "react";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { LowerPanel } from "~/pages/PageEnigma/comps/LowerPanel";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import { currentPage, pageWidth } from "~/signals";
|
||||
import { Timeline } from "~/pages/PageEnigma/comps/Timeline";
|
||||
import { ControlsVideo } from "~/pages/PageEnigma/comps/ControlsVideo";
|
||||
import { StylizeTabPages } from "~/pages/PageEnigma/enums";
|
||||
import { generateProgress, selectedArtStyle } from "~/pages/PageEnigma/signals";
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from "react";
|
||||
import { useMouseEventsTimeline } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsTimeline";
|
||||
import { dndTimelineHeight, timelineHeight } from "~/pages/PageEnigma/signals";
|
||||
import useTimelineClick from "~/pages/PageEnigma/comps/Timeline/utils/useTimelineClick";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import { currentPage } from "~/signals";
|
||||
|
||||
interface LowerPanelPropsI {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const LowerPanel = ({ children }: LowerPanelPropsI) => {
|
||||
const { onPointerDown } = useMouseEventsTimeline();
|
||||
|
||||
const displayHeight =
|
||||
dndTimelineHeight.value > -1
|
||||
? dndTimelineHeight.value
|
||||
: timelineHeight.value;
|
||||
|
||||
const onTimelineClick = useTimelineClick(Pages.EDIT);
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPage.value === Pages.EDIT && (
|
||||
<div
|
||||
className="absolute z-10 h-[3px] w-full cursor-ns-resize bg-ui-panel-border"
|
||||
style={{ bottom: displayHeight }}
|
||||
onPointerDown={onPointerDown}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={["absolute bottom-0", "w-screen", "bg-ui-panel"].join(" ")}
|
||||
style={{ height: displayHeight }}
|
||||
onClick={onTimelineClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { LowerPanel } from './LowerPanel';
|
||||
@@ -534,7 +534,7 @@ export const PromptBox = () => {
|
||||
},
|
||||
});
|
||||
|
||||
//console.log("response", generateResponse);
|
||||
console.log("response", generateResponse);
|
||||
|
||||
//if (generateResponse.errorMessage) {
|
||||
// handleError(response.errorMessage);
|
||||
@@ -625,8 +625,6 @@ export const PromptBox = () => {
|
||||
src={image.url}
|
||||
alt="Reference"
|
||||
className="h-full w-full rounded-lg object-cover"
|
||||
// crossOrigin="anonymous"
|
||||
// referrerPolicy="no-referrer"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveReference(image.id)}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ChangeEvent, useLayoutEffect, useState } from "react";
|
||||
import { Button, Textarea, TransitionDialogue } from "~/components";
|
||||
import {
|
||||
addPromptTravel,
|
||||
updatePromptTravel,
|
||||
} from "~/pages/PageEnigma/signals/promptTravelGroup";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Clip } from "~/pages/PageEnigma/models";
|
||||
|
||||
interface Props {
|
||||
clip?: Clip;
|
||||
openPrompt: boolean;
|
||||
setOpenPrompt: (open: boolean) => void;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export const AddEditPrompt = ({
|
||||
clip,
|
||||
openPrompt,
|
||||
setOpenPrompt,
|
||||
offset,
|
||||
}: Props) => {
|
||||
useSignals();
|
||||
const [error, setError] = useState("");
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (clip) {
|
||||
setPrompt(clip.name);
|
||||
}
|
||||
}, [clip]);
|
||||
|
||||
const onChangeHandlerPositive = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPrompt(event.target.value);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (!prompt) {
|
||||
setError("Enter a prompt before saving");
|
||||
return;
|
||||
}
|
||||
setOpenPrompt(false);
|
||||
if (!clip) {
|
||||
addPromptTravel({
|
||||
text: prompt,
|
||||
length: 60,
|
||||
offset: offset,
|
||||
});
|
||||
return;
|
||||
}
|
||||
updatePromptTravel({
|
||||
id: clip.clip_uuid,
|
||||
offset: clip.offset,
|
||||
length: clip.length,
|
||||
name: prompt,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionDialogue
|
||||
isOpen={openPrompt}
|
||||
onClose={() => setOpenPrompt(false)}
|
||||
title="Add Text Prompt"
|
||||
>
|
||||
<div>
|
||||
<Textarea
|
||||
label="Enter a Prompt"
|
||||
className="w-full"
|
||||
rows={3}
|
||||
name="positive-prompt"
|
||||
placeholder="Type here to describe your scene"
|
||||
onChange={onChangeHandlerPositive}
|
||||
required
|
||||
value={prompt}
|
||||
/>
|
||||
{!!error && (
|
||||
<div className="mt-2 text-sm text-brand-primary">{error}</div>
|
||||
)}
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="action" onClick={() => setOpenPrompt(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
|
||||
import {
|
||||
audioGroup,
|
||||
audioMinimized,
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
updateAudio,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { ClipGroup } from "~/enums";
|
||||
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
|
||||
id="track-global-audio"
|
||||
className="relative flex h-[30px] 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
|
||||
id="track-global-audio"
|
||||
className="relative block rounded-r-lg bg-global_audio-groupBg pb-2 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-[30px]">
|
||||
<TrackClips
|
||||
id={audioGroup.value.id}
|
||||
clips={clips}
|
||||
updateClip={updateAudio}
|
||||
group={ClipGroup.GLOBAL_AUDIO}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import {
|
||||
cameraGroup,
|
||||
cameraMinimized,
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
updateCamera,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { ClipGroup } from "~/enums";
|
||||
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
|
||||
id="track-camera"
|
||||
className="relative flex h-[30px] 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
|
||||
id="track-camera"
|
||||
className="relative block rounded-r-lg bg-camera-groupBg pb-2 pr-4"
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
right: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
cameraMinimized.value = !cameraMinimized.value;
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleUp} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="pt-[30px]">
|
||||
<TrackKeyFrames
|
||||
id={cameraGroup.value.id}
|
||||
keyframes={keyframes}
|
||||
group={ClipGroup.CAMERA}
|
||||
updateKeyframe={updateCamera}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
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 { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
|
||||
import { CharacterTrack } from "~/pages/PageEnigma/models";
|
||||
import {
|
||||
filmLength,
|
||||
frameTrackButtonWidthPx,
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
scale,
|
||||
toggleCharacterMinimized,
|
||||
updateCharacters
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
|
||||
function buildUpdaters(
|
||||
updateCharacters: (options: {
|
||||
type: ClipType;
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) => void,
|
||||
) {
|
||||
function updateClipAnimations(options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) {
|
||||
updateCharacters({ ...options, type: ClipType.ANIMATION });
|
||||
}
|
||||
|
||||
function updateClipPosition(options: { id: string; offset: number }) {
|
||||
updateCharacters({
|
||||
...options,
|
||||
length: 0,
|
||||
type: ClipType.TRANSFORM,
|
||||
});
|
||||
}
|
||||
|
||||
function updateClipEmotions(options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) {
|
||||
updateCharacters({ ...options, type: ClipType.EXPRESSION });
|
||||
}
|
||||
|
||||
function updateClipLipSync(options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
}) {
|
||||
updateCharacters({ ...options, type: ClipType.AUDIO });
|
||||
}
|
||||
return {
|
||||
updateClipLipSync,
|
||||
updateClipPosition,
|
||||
updateClipAnimations,
|
||||
updateClipEmotions,
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
character: CharacterTrack;
|
||||
}
|
||||
|
||||
export const Character = ({ character }: Props) => {
|
||||
const editorEngine = useContext(EngineContext);
|
||||
|
||||
const {
|
||||
updateClipLipSync,
|
||||
updateClipPosition,
|
||||
updateClipAnimations,
|
||||
updateClipEmotions,
|
||||
} = useMemo(() => buildUpdaters(updateCharacters), []);
|
||||
|
||||
const {
|
||||
animationClips,
|
||||
positionKeyframes,
|
||||
lipSyncClips,
|
||||
expressionClips,
|
||||
minimized,
|
||||
} = character;
|
||||
|
||||
if (minimized) {
|
||||
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 box-content"
|
||||
style={{ width: filmLength.value * 1000 * 4 * scale.value + frameTrackButtonWidthPx * 2 }}
|
||||
>
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
right: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
toggleCharacterMinimized(character.object_uuid);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`track-character-${character.object_uuid}`}
|
||||
className="relative block rounded-r-lg bg-character-groupBg pb-2 pr-4"
|
||||
>
|
||||
<div className="flex h-[30px] items-center justify-end">
|
||||
<button
|
||||
className="absolute"
|
||||
style={{
|
||||
right: minimizeIconPosition.value,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
toggleCharacterMinimized(character.object_uuid);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleUp} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<TrackClips
|
||||
id={character.object_uuid}
|
||||
clips={animationClips}
|
||||
updateClip={updateClipAnimations}
|
||||
group={ClipGroup.CHARACTER}
|
||||
type={ClipType.ANIMATION}
|
||||
/>
|
||||
<TrackKeyFrames
|
||||
id={character.object_uuid}
|
||||
keyframes={positionKeyframes}
|
||||
updateKeyframe={updateClipPosition}
|
||||
group={ClipGroup.CHARACTER}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
import { faImage, faTrashCan } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ButtonIconStack } from "~/components/reusable/ButtonIconStack";
|
||||
import { frameTrackButtonWidthPx } from "../../signals";
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { EngineContext } from "../../contexts/EngineContext";
|
||||
import { CHARACTER_FRAME_FILE_TYPE } from "~/enums";
|
||||
import { UploadModalMedia } from "~/components/reusable/UploadModalMedia";
|
||||
import { UploadImageMediaModal } from "~/components/reusable/UploadModalMedia/UploadImageMediaModal";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { MediaFile } from "../../models";
|
||||
import { ApiResponse } from "~/Classes/ApiManager/ApiManager";
|
||||
import { get_media_url } from "~/Classes/ApiHelpers";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ButtonIcon } from "~/components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
// TODO(bt,2025-01-28): This is just a temporary flag
|
||||
const TODO_REMOVE_RUN_POSE_CALCULATION = false;
|
||||
|
||||
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 clickable = useRef(true);
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const [mediaFile, setMediaFile] = useState<string | undefined>(undefined);
|
||||
|
||||
const handleFrameClick = useCallback(() => {
|
||||
if (!clickable.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make button unresponsive to clicks until modal is closed
|
||||
clickable.current = false;
|
||||
setIsUploadModalOpen(true);
|
||||
}, [])
|
||||
|
||||
|
||||
const unlockButton = useCallback(() => {
|
||||
clickable.current = true;
|
||||
setIsUploadModalOpen(false);
|
||||
}, [setIsUploadModalOpen]);
|
||||
|
||||
const handleFrameSet = useCallback((token?: string) => {
|
||||
if (!token) {
|
||||
console.error("No image media file token to set as first frame");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(brandon,2024-01-27): Please forgive me for this ugly hack. It's just
|
||||
// temporary to move to integration testing quickly. Setting the media token
|
||||
// to a global space so we can read from it when calling the inference API.
|
||||
console.log(`Setting image media file token as first frame token: ${token}`);
|
||||
(window as any).firstFrameMediaToken = token;
|
||||
|
||||
// TODO: Fetch the image and pass the token to the character engine
|
||||
|
||||
// Fetch the image and set as button bg
|
||||
get_media_url(token)
|
||||
.then(async (url) => {
|
||||
console.debug("Frame url: ", url)
|
||||
|
||||
setMediaFile(url);
|
||||
|
||||
// TODO(brandon,2024-01-27): Please forgive me for this ugly hack. It's just
|
||||
// temporary to move to integration testing quickly. Setting the media token
|
||||
// to a global space so we can read from it when calling the inference API.
|
||||
(window as any).firstFrameMediaUrl = url;
|
||||
|
||||
// TODO(brandon,2024-01-27): TEMPORARY EXPERIMENT. REMOVE ME.
|
||||
// TODO(brandon,2024-01-27): TEMPORARY EXPERIMENT. REMOVE ME.
|
||||
// TODO(brandon,2024-01-27): TEMPORARY EXPERIMENT. REMOVE ME.
|
||||
// await testGlobalExperiment();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching media file", error);
|
||||
})
|
||||
.finally(() => {
|
||||
unlockButton();
|
||||
})
|
||||
|
||||
}, [unlockButton]);
|
||||
|
||||
const handleDeleteFrame = useCallback(() => {
|
||||
const character = editorEngine!.timeline.scene.get_object_by_uuid(characterId)!;
|
||||
editorEngine!.animation_engine.clearStartFrame(
|
||||
character,
|
||||
editorEngine!.timeline.current_time,
|
||||
editorEngine!.timeline.timeline_limit
|
||||
);
|
||||
|
||||
setMediaFile(undefined);
|
||||
unlockButton();
|
||||
}, [unlockButton, setMediaFile, characterId, editorEngine]);
|
||||
|
||||
|
||||
if (!mediaFile) {
|
||||
return (
|
||||
<>
|
||||
<div className={className} style={{ minWidth: frameTrackButtonWidthPx, width: frameTrackButtonWidthPx }}>
|
||||
<ButtonIconStack icon={faImage} additionalStyle="bg-character-frame" text={CharacterFrameStrings[target]} onClick={handleFrameClick} />
|
||||
</div>
|
||||
<UploadImageMediaModal
|
||||
isOpen={isUploadModalOpen}
|
||||
onClose={unlockButton}
|
||||
onSuccess={handleFrameSet}
|
||||
title={"Upload Character Frame Image"}
|
||||
fileTypes={Object.values(CHARACTER_FRAME_FILE_TYPE)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
const classes = twMerge([
|
||||
className,
|
||||
"bg-character-unselected overflow-hidden"
|
||||
]);
|
||||
|
||||
const buttonClasses = twMerge([
|
||||
"absolute flex box-content top-1 right-1 w-6 h-6 rounded-md invisible group-hover:visible items-center justify-center",
|
||||
"bg-gray-800 hover:bg-red text-gray-500 hover:text-gray-100 border-2 border-transparent hover:border-white",
|
||||
"transition-all duration-150",
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes} style={{ minWidth: frameTrackButtonWidthPx, width: frameTrackButtonWidthPx }}>
|
||||
<div className={"rounded-md w-full h-full relative group border-2 border-gray-400"}>
|
||||
<img src={mediaFile} alt={CharacterFrameStrings[target]} className={"rounded-md object-cover w-full h-full"} />
|
||||
<button className={buttonClasses} onClick={handleDeleteFrame}>
|
||||
<FontAwesomeIcon icon={faTrashCan} size={"xs"} className={""} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { characterGroup } from "~/pages/PageEnigma/signals";
|
||||
import { Character } from "~/pages/PageEnigma/comps/Timeline/Character";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const Characters = () => {
|
||||
useSignals();
|
||||
return (
|
||||
<>
|
||||
{characterGroup.value.characters.map((character) => (
|
||||
<div key={character.object_uuid} className="mb-1 pr-4">
|
||||
<Character character={character} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
fullWidth,
|
||||
minimizeIconPosition,
|
||||
objectGroup,
|
||||
objectsMinimized,
|
||||
selectedObject,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
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
|
||||
id="track-objects"
|
||||
className="relative mb-4 flex h-[30px] 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 (
|
||||
<div
|
||||
id="track-objects"
|
||||
className="relative mb-5 block rounded-r-lg bg-object-groupBg pb-2 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-[30px]">
|
||||
{objectGroup.value.objects.map((object) => (
|
||||
<div
|
||||
key={object.object_uuid}
|
||||
className="mb-1"
|
||||
id={`track-object-${selectedObject.value?.id}`}
|
||||
>
|
||||
<ObjectTrackComponent object={object} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { updateObject } from "~/pages/PageEnigma/signals";
|
||||
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
|
||||
import { ClipGroup } from "~/enums";
|
||||
import { ObjectTrack } from "~/pages/PageEnigma/models";
|
||||
|
||||
interface Props {
|
||||
object: ObjectTrack;
|
||||
}
|
||||
|
||||
export const ObjectTrackComponent = ({ object }: Props) => {
|
||||
return (
|
||||
<TrackKeyFrames
|
||||
id={object.object_uuid}
|
||||
keyframes={object.keyframes}
|
||||
updateKeyframe={updateObject}
|
||||
group={ClipGroup.OBJECT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { fullWidth, timelineHeight } from "../../signals";
|
||||
|
||||
interface PremiumLockTimelineProps {
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
export default function PremiumLockTimeline({
|
||||
locked,
|
||||
}: PremiumLockTimelineProps) {
|
||||
return (
|
||||
<>
|
||||
{locked ? (
|
||||
<div className="sticky left-0 top-1/2 z-20 h-full w-full -translate-y-1/2 transform">
|
||||
<div
|
||||
style={{
|
||||
height: timelineHeight.value,
|
||||
width: fullWidth.value - 704,
|
||||
}}
|
||||
className="absolute top-1/2 ml-[720px] -translate-y-1/2 transform border-l border-[#43435C] bg-ui-panel/70 backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex h-full w-full flex-col justify-center pl-12">
|
||||
<div className="flex w-[400px] flex-col gap-1 rounded-lg border border-white/40 bg-ui-controls p-4 text-center shadow-lg">
|
||||
<h6 className="text-md font-medium">
|
||||
Your timeline is currently limited to 3 seconds.
|
||||
</h6>
|
||||
<p className="text-sm font-normal">
|
||||
Please{" "}
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="font-medium text-brand-primary brightness-125"
|
||||
>
|
||||
upgrade your account
|
||||
</Link>{" "}
|
||||
to increase the movie render length up to 7 seconds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { fullWidth } from "~/pages/PageEnigma/signals";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
promptTravelGroup,
|
||||
updatePromptTravel,
|
||||
} from "~/pages/PageEnigma/signals/promptTravelGroup";
|
||||
import { TrackClipsPrompt } from "~/pages/PageEnigma/comps/Timeline/TrackClipsPrompt";
|
||||
|
||||
export const PromptTravel = () => {
|
||||
useSignals();
|
||||
const { clips } = promptTravelGroup.value;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="track-prompt-travel"
|
||||
className="bg-prompt-groupBg relative block rounded-r-lg pb-2 pr-4"
|
||||
style={{ width: fullWidth.value + 16 }}
|
||||
>
|
||||
<div className="pt-[30px]">
|
||||
<TrackClipsPrompt
|
||||
id={promptTravelGroup.value.id}
|
||||
clips={clips}
|
||||
updateClip={updatePromptTravel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
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
|
||||
</>
|
||||
);
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
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>
|
||||
Music
|
||||
</>
|
||||
);
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { toggleLipSyncMute } from "~/pages/PageEnigma/signals";
|
||||
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { CharacterTrack } from "~/pages/PageEnigma/models";
|
||||
|
||||
export const LipSyncSubHeader = ({
|
||||
character,
|
||||
}: {
|
||||
character: CharacterTrack;
|
||||
}) => {
|
||||
const editorEngine = useContext(EngineContext);
|
||||
if (editorEngine && editorEngine.isObjectLipsync(character.object_uuid)) {
|
||||
return (
|
||||
<div className="mb-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
<div className="flex gap-3">
|
||||
Vocal / Speech
|
||||
<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>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
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
|
||||
</>
|
||||
);
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
export function PromptTravelHeader() {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.714 2.57098V1.71399H5.142V5.14196H4.7135C4.23947 5.14196 3.8565 5.52493 3.8565 5.99896C3.8565 6.47298 4.23947 6.85595 4.7135 6.85595H7.28449C7.75852 6.85595 8.14149 6.47298 8.14149 5.99896C8.14149 5.52493 7.75852 5.14196 7.28449 5.14196H6.85599V1.71399H10.284V2.57098C10.284 3.04501 10.667 3.42798 11.141 3.42798C11.615 3.42798 11.998 3.04501 11.998 2.57098V1.28549C11.998 0.575793 11.4222 0 10.7125 0H5.999H1.2855C0.575796 0 0 0.575793 0 1.28549V2.57098C0 3.04501 0.382972 3.42798 0.856999 3.42798C1.33103 3.42798 1.714 3.04501 1.714 2.57098ZM0.251744 8.82168C-0.0830218 9.15645 -0.0830218 9.7001 0.251744 10.0349L1.96574 11.7489C2.21213 11.9952 2.57903 12.0675 2.90041 11.9336C3.22178 11.7997 3.43068 11.4891 3.43068 11.1409V10.2839H8.56999V11.1409C8.56999 11.4864 8.77889 11.7997 9.10026 11.9336C9.42164 12.0675 9.78854 11.9926 10.0349 11.7489L11.7489 10.0349C12.0837 9.7001 12.0837 9.15645 11.7489 8.82168L10.0349 7.10769C9.78854 6.86131 9.42164 6.789 9.10026 6.9229C8.77889 7.05681 8.56999 7.36747 8.56999 7.71562V8.57262H3.428V7.71295C3.428 7.36747 3.2191 7.05413 2.89773 6.92023C2.57635 6.78632 2.20945 6.86131 1.96306 7.10502L0.249065 8.819L0.251744 8.82168Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Prompt Travel
|
||||
</>
|
||||
);
|
||||
}
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
import {
|
||||
audioGroup,
|
||||
audioMinimized,
|
||||
cameraMinimized,
|
||||
characterGroup,
|
||||
objectGroup,
|
||||
objectsMinimized,
|
||||
timelineScrollY,
|
||||
toggleAudioMute,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
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";
|
||||
import { LipSyncSubHeader } from "./LipSyncSubHeader";
|
||||
import { MediaFileAnimationType } from "~/enums";
|
||||
import { MediaFileType } from "~/pages/PageEnigma/enums";
|
||||
import { currentPage } from "~/signals";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import { PromptTravelHeader } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/PromptTravelHeader";
|
||||
|
||||
export const RowHeaders = () => {
|
||||
useSignals();
|
||||
|
||||
const compressedHeaderClasses =
|
||||
"flex h-[30px] items-center gap-2 pl-2 text-xs rounded-tl-lg font-medium text-white w-32";
|
||||
const uncompressedHeaderClasses =
|
||||
"flex h-[30px] items-center gap-2 rounded-br-lg rounded-tl-lg pl-2 text-xs font-medium text-white w-32";
|
||||
|
||||
if (currentPage.value === Pages.STYLE) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute mt-2 w-[146px]" style={{ top: -8 }}>
|
||||
<div className="mb-1 h-[72px] w-full rounded-l-lg bg-prompt-groupBg">
|
||||
<div className="h-[30px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-prompt-titleBg",
|
||||
].join(" ")}
|
||||
>
|
||||
<PromptTravelHeader />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Text Prompt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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-1 h-[30px] 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-1 w-full rounded-l-lg bg-character-groupBg pb-2"
|
||||
>
|
||||
<div className="h-[30px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-character-titleBg",
|
||||
].join(" ")}
|
||||
>
|
||||
<CharacterHeader name={character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
Position
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{cameraMinimized.value ? (
|
||||
<div className="mb-1 h-[30px] w-full rounded-l-lg bg-camera-groupBg">
|
||||
<div
|
||||
className={[compressedHeaderClasses, "bg-camera-titleBg"].join(
|
||||
" ",
|
||||
)}
|
||||
>
|
||||
<CameraHeader />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-1 h-[72px] w-full rounded-l-lg bg-camera-groupBg">
|
||||
<div className="h-[30px] text-xs font-medium text-white">
|
||||
<div
|
||||
className={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-camera-titleBg",
|
||||
].join(" ")}
|
||||
>
|
||||
<CameraHeader />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80">
|
||||
Position
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{audioMinimized.value ? (
|
||||
<div className="mb-1 h-[30px] w-full rounded-l-lg bg-global_audio-groupBg">
|
||||
<div
|
||||
className={[
|
||||
compressedHeaderClasses,
|
||||
"bg-global_audio-titleBg",
|
||||
].join(" ")}
|
||||
>
|
||||
<GlobalAudioHeader />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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={[
|
||||
uncompressedHeaderClasses,
|
||||
"bg-global_audio-titleBg",
|
||||
].join(" ")}
|
||||
>
|
||||
<GlobalAudioHeader />
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
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="h-[30px] w-full rounded-l-lg bg-object-groupBg">
|
||||
<div
|
||||
className={[
|
||||
compressedHeaderClasses,
|
||||
"bg-object-titleBg",
|
||||
].join(" ")}
|
||||
>
|
||||
<ObjectsHeader />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="mb-1 h-[72px] w-full rounded-l-lg bg-object-groupBg"
|
||||
style={{
|
||||
height: `${38 + objectGroup.value.objects.length * 34}px`,
|
||||
}}
|
||||
>
|
||||
<div className="h-[30px] 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-1 flex h-[30px] flex-col justify-center pl-[22px] text-xs font-medium text-white opacity-80"
|
||||
>
|
||||
{obj.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import {
|
||||
currentTime,
|
||||
scale,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
pointerScrubber,
|
||||
secondaryScrubber,
|
||||
frameTrackButtonWidthPx,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { useMouseEventsScrubber } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsScrubber";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { currentPage } from "~/signals";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
|
||||
export const Scrubber = () => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const { onPointerDown, time } = useMouseEventsScrubber();
|
||||
|
||||
const isPlaying = editorEngine?.timeline?.is_playing ?? false;
|
||||
const displayTime = time === -1 ? currentTime.value : time;
|
||||
|
||||
useEffect(() => {
|
||||
secondaryScrubber.value = displayTime;
|
||||
if (currentPage.value === Pages.STYLE && isPlaying) {
|
||||
return;
|
||||
}
|
||||
pointerScrubber.value = displayTime;
|
||||
}, [displayTime, isPlaying]);
|
||||
|
||||
const scrollX = timelineScrollX.value;
|
||||
|
||||
return (
|
||||
<>
|
||||
{pointerScrubber.value * 4 * scale.value - scrollX + frameTrackButtonWidthPx >= 0 && (
|
||||
<div
|
||||
className="absolute z-[20] flex cursor-ew-resize flex-col items-center"
|
||||
style={{
|
||||
top: 8,
|
||||
left: pointerScrubber.value * 4 * scale.value + 199 - scrollX + frameTrackButtonWidthPx,
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<div>
|
||||
<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-white"
|
||||
style={{
|
||||
width: 2,
|
||||
marginTop: -5,
|
||||
height: timelineHeight.value - 36,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{secondaryScrubber.value * 4 * scale.value - scrollX >= 0 &&
|
||||
currentPage.value === Pages.STYLE && (
|
||||
<div
|
||||
className="absolute z-[20] flex cursor-ew-resize flex-col items-center"
|
||||
style={{
|
||||
top: 28,
|
||||
left: secondaryScrubber.value * 4 * scale.value + 204 - scrollX,
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<div
|
||||
className="block bg-white/80"
|
||||
style={{
|
||||
width: 1,
|
||||
marginTop: -5,
|
||||
height: timelineHeight.value - 36,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,171 +0,0 @@
|
||||
import { UIEvent, useCallback, useEffect, useRef } from "react";
|
||||
import { LowerPanel } from "~/pages/PageEnigma/comps/LowerPanel";
|
||||
|
||||
import { Camera } from "./Camera";
|
||||
import { Audio } from "./Audio";
|
||||
import {
|
||||
characterGroup,
|
||||
filmLength,
|
||||
frameTrackButtonWidthPx,
|
||||
ignoreKeyDelete,
|
||||
isHotkeyDisabled,
|
||||
objectsMinimized,
|
||||
scale,
|
||||
selectedItem,
|
||||
selectedObject,
|
||||
sidePanelVisible,
|
||||
sidePanelWidth,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
timelineScrollY,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
|
||||
import { TimerGrid } from "~/pages/PageEnigma/comps/TimerGrid/TimerGrid";
|
||||
import { Scrubber } from "~/pages/PageEnigma/comps/Timeline/Scrubber";
|
||||
import { Characters } from "~/pages/PageEnigma/comps/Timeline/Characters";
|
||||
import { ObjectGroups } from "~/pages/PageEnigma/comps/Timeline/ObjectGroups";
|
||||
import { RowHeaders } from "~/pages/PageEnigma/comps/Timeline/RowHeaders/RowHeaders";
|
||||
import { currentPage, pageWidth } from "~/signals";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import PremiumLockTimeline from "./PremiumLockTimeline";
|
||||
import { AssetType } from "~/enums";
|
||||
import { PromptTravel } from "~/pages/PageEnigma/comps/Timeline/PromptTravel";
|
||||
import { useOnDelete } from "~/pages/PageEnigma/comps/Timeline/utils/useOnDelete";
|
||||
|
||||
function scrollItem(itemId: string) {
|
||||
const element = document.getElementById(itemId);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
}
|
||||
|
||||
export const Timeline = () => {
|
||||
useSignals();
|
||||
const lastSelectedObject = useRef(selectedObject.value);
|
||||
|
||||
if (selectedObject.value !== lastSelectedObject.current) {
|
||||
lastSelectedObject.current = selectedObject.value;
|
||||
switch (selectedObject.value?.type) {
|
||||
case AssetType.CHARACTER:
|
||||
if (
|
||||
characterGroup.value.characters.some(
|
||||
(character) => character.object_uuid === selectedObject.value?.id,
|
||||
)
|
||||
) {
|
||||
scrollItem(`track-character-${selectedObject.value?.id}`);
|
||||
break;
|
||||
}
|
||||
if (objectsMinimized.value) {
|
||||
scrollItem("track-objects");
|
||||
break;
|
||||
}
|
||||
scrollItem(`track-object-${selectedObject.value?.id}`);
|
||||
break;
|
||||
case AssetType.CAMERA:
|
||||
scrollItem("track-camera");
|
||||
break;
|
||||
case AssetType.OBJECT:
|
||||
if (objectsMinimized.value) {
|
||||
scrollItem("track-objects");
|
||||
break;
|
||||
}
|
||||
scrollItem(`track-object-${selectedObject.value?.id}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const onScroll = useCallback((event: UIEvent<HTMLDivElement>) => {
|
||||
timelineScrollX.value = event.currentTarget.scrollLeft;
|
||||
timelineScrollY.value = event.currentTarget.scrollTop;
|
||||
}, []);
|
||||
|
||||
useSignalEffect(() => {
|
||||
timelineHeight.value = currentPage.value === Pages.EDIT ? 208 : 120;
|
||||
});
|
||||
|
||||
const { onDeleteAsk, confirmationModal } = useOnDelete();
|
||||
|
||||
const onDeleteKey = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (ignoreKeyDelete.value || isHotkeyDisabled()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
["Backspace", "Delete"].indexOf(event.key) > -1 &&
|
||||
selectedItem.value !== null
|
||||
) {
|
||||
onDeleteAsk(event, selectedItem.value);
|
||||
}
|
||||
},
|
||||
[onDeleteAsk],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onDeleteKey);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onDeleteKey);
|
||||
};
|
||||
}, [onDeleteKey]);
|
||||
|
||||
const sidebarAdjustment = sidePanelVisible.value
|
||||
? sidePanelWidth.value + 84
|
||||
: 84;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LowerPanel>
|
||||
<div
|
||||
style={{
|
||||
marginRight:
|
||||
currentPage.value === Pages.EDIT ? sidebarAdjustment : 16,
|
||||
}}
|
||||
>
|
||||
<TimerGrid />
|
||||
<div className="flex">
|
||||
<div
|
||||
className="ml-[60px] mt-2 w-[144px] min-w-[144px] overflow-hidden"
|
||||
style={{
|
||||
height: timelineHeight.value - 36,
|
||||
}}
|
||||
>
|
||||
<RowHeaders />
|
||||
</div>
|
||||
<div
|
||||
className="relative mt-2 overflow-auto"
|
||||
onScroll={onScroll}
|
||||
style={{
|
||||
width: pageWidth.value - 204,
|
||||
height: timelineHeight.value - 36,
|
||||
}}
|
||||
>
|
||||
<div className="w-fit">
|
||||
{currentPage.value === Pages.EDIT ? (
|
||||
<>
|
||||
<PremiumLockTimeline locked={false} />
|
||||
<Characters />
|
||||
<div className="pb-1 pr-4">
|
||||
<Camera />
|
||||
</div>
|
||||
<div className="pb-1 pr-8">
|
||||
<Audio />
|
||||
</div>
|
||||
<ObjectGroups />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PremiumLockTimeline locked={false} />
|
||||
<PromptTravel />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Scrubber />
|
||||
</div>
|
||||
</div>
|
||||
</LowerPanel>
|
||||
{confirmationModal()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { ClipGroup } from "~/enums";
|
||||
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
|
||||
import { Clip } from "~/pages/PageEnigma/models";
|
||||
import {
|
||||
canDrop,
|
||||
dragHandleWidth,
|
||||
dragItem,
|
||||
frameTrackButtonWidthPx,
|
||||
scale,
|
||||
selectedItem,
|
||||
unitPx,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
|
||||
|
||||
interface Props {
|
||||
min: number;
|
||||
max: number;
|
||||
group: ClipGroup;
|
||||
clip: Clip;
|
||||
updateClip: (options: { id: string; offset: number; length: number }) => void;
|
||||
}
|
||||
|
||||
export const TrackClip = ({ clip, min, max, group, updateClip }: Props) => {
|
||||
const [state, setState] = useState({
|
||||
length: clip.length,
|
||||
offset: clip.offset,
|
||||
});
|
||||
const { onPointerDown } = useMouseEventsClip(
|
||||
clip,
|
||||
max,
|
||||
min,
|
||||
updateClip,
|
||||
setState,
|
||||
);
|
||||
|
||||
const selectedClipId = (selectedItem.value as Clip | null)?.clip_uuid ?? "";
|
||||
|
||||
const { length, offset } = state;
|
||||
|
||||
const classes = [
|
||||
"absolute",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? `bg-${group}-selected border-white focus-visible:outline-0`
|
||||
: `bg-${group}-clip`,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{ /* Left drag handle button */}
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"rounded-l-md border border-transparent",
|
||||
"block h-full cursor-ew-resize",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: dragHandleWidth,
|
||||
left: offset * unitPx * scale.value + frameTrackButtonWidthPx,
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "left")}
|
||||
onPointerMove={() => {
|
||||
if (dragItem.value) {
|
||||
canDrop.value = false;
|
||||
DndAsset.notDropText = "Cannot drop onto another clip";
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full items-center justify-start">
|
||||
<div className="ml-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{ /* Label/Move button */}
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"block h-full",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: length * unitPx * scale.value - 2 * dragHandleWidth,
|
||||
left: offset * unitPx * scale.value + dragHandleWidth + frameTrackButtonWidthPx,
|
||||
cursor: "move",
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "drag")}
|
||||
onPointerMove={() => {
|
||||
if (dragItem.value) {
|
||||
canDrop.value = false;
|
||||
DndAsset.notDropText = "Cannot drop onto another 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>
|
||||
|
||||
{ /* Right drag handle button */}
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"rounded-r-md border border-transparent",
|
||||
"block h-full cursor-ew-resize",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: dragHandleWidth,
|
||||
left: offset * unitPx * scale.value + length * unitPx * scale.value - dragHandleWidth + frameTrackButtonWidthPx,
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "right")}
|
||||
onPointerMove={() => {
|
||||
if (dragItem.value) {
|
||||
canDrop.value = false;
|
||||
DndAsset.notDropText = "Cannot drop onto another clip";
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full items-center justify-end">
|
||||
<div className="mr-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,192 +0,0 @@
|
||||
import { useMouseEventsClip } from "./utils/useMouseEventsClip";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
canDrop,
|
||||
dragItem,
|
||||
scale,
|
||||
selectedItem,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { ClipGroup } from "~/enums";
|
||||
import { Clip } from "~/pages/PageEnigma/models";
|
||||
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
|
||||
import { faPencil, faTrashCan } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useOnDelete } from "~/pages/PageEnigma/comps/Timeline/utils/useOnDelete";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AddEditPrompt } from "~/pages/PageEnigma/comps/Timeline/AddEditPrompt";
|
||||
|
||||
interface Props {
|
||||
min: number;
|
||||
max: number;
|
||||
group: ClipGroup;
|
||||
clip: Clip;
|
||||
updateClip: (options: {
|
||||
id: string;
|
||||
offset: number;
|
||||
length: number;
|
||||
name: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const TrackClipPrompt = ({
|
||||
clip,
|
||||
min,
|
||||
max,
|
||||
group,
|
||||
updateClip,
|
||||
}: Props) => {
|
||||
const [state, setState] = useState({
|
||||
length: clip.length,
|
||||
offset: clip.offset,
|
||||
});
|
||||
const [hover, setHover] = useState<boolean>(false);
|
||||
const [openPrompt, setOpenPrompt] = useState(false);
|
||||
const { onPointerDown } = useMouseEventsClip(
|
||||
clip,
|
||||
max,
|
||||
min,
|
||||
(options) => updateClip({ ...options, name: clip.name }),
|
||||
setState,
|
||||
);
|
||||
|
||||
const { confirmationModal, onDeleteAsk } = useOnDelete();
|
||||
|
||||
const selectedClipId = (selectedItem.value as Clip | null)?.clip_uuid ?? "";
|
||||
|
||||
const { length, offset } = state;
|
||||
|
||||
const classes = [
|
||||
"absolute",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? `bg-${group}-selected border-white focus-visible:outline-0`
|
||||
: `bg-${group}-clip`,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"z-40 w-[15px] rounded-l-md border border-transparent",
|
||||
"block h-full cursor-ew-resize",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-2 border-r-0 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
left: offset * 4 * scale.value,
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "left")}
|
||||
onPointerMove={() => {
|
||||
if (dragItem.value) {
|
||||
canDrop.value = false;
|
||||
DndAsset.notDropText = "Cannot drop onto another clip";
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full items-center justify-start">
|
||||
<div className="ml-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"z-50 block h-full",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-0 border-r-0 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
width: length * 4 * scale.value - 30,
|
||||
left: offset * 4 * scale.value + 15,
|
||||
cursor: "move",
|
||||
}}
|
||||
onPointerOver={() => setHover(true)}
|
||||
onPointerLeave={() => setHover(false)}
|
||||
onPointerDown={(event) => onPointerDown(event, "drag")}
|
||||
onPointerMove={() => {
|
||||
if (dragItem.value) {
|
||||
canDrop.value = false;
|
||||
DndAsset.notDropText = "Cannot drop onto another clip";
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="ml-2 w-full truncate text-left text-xs text-white"
|
||||
style={{ width: length * 4 * scale.value - 46 }}
|
||||
>
|
||||
{clip.name}
|
||||
</div>
|
||||
{hover && (
|
||||
<>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={(event) => onDeleteAsk(event, clip)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashCan}
|
||||
className="text-sm text-white/80 hover:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => setOpenPrompt(true)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPencil}
|
||||
className="me-1 ms-2 text-sm text-white/80 hover:text-white"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={[
|
||||
...classes,
|
||||
"z-50 w-[15px] rounded-r-md border border-transparent",
|
||||
"block h-full cursor-ew-resize",
|
||||
clip.clip_uuid === selectedClipId
|
||||
? "border border-b-2 border-l-0 border-r-2 border-t-2"
|
||||
: "",
|
||||
].join(" ")}
|
||||
style={{
|
||||
left: offset * 4 * scale.value + length * 4 * scale.value - 15,
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "right")}
|
||||
onPointerMove={() => {
|
||||
if (dragItem.value) {
|
||||
canDrop.value = false;
|
||||
DndAsset.notDropText = "Cannot drop onto another clip";
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = clip;
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full items-center justify-end">
|
||||
<div className="mr-1.5 block h-[10px] w-[2px] bg-white opacity-60" />
|
||||
</div>
|
||||
</button>
|
||||
{confirmationModal()}
|
||||
<AddEditPrompt
|
||||
clip={clip}
|
||||
setOpenPrompt={setOpenPrompt}
|
||||
openPrompt={openPrompt}
|
||||
offset={offset}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
import { TrackClip } from "~/pages/PageEnigma/comps/Timeline/TrackClip";
|
||||
import { Clip } from "~/pages/PageEnigma/models";
|
||||
import { PointerEvent } from "react";
|
||||
import {
|
||||
canDrop,
|
||||
dragItem,
|
||||
filmLength,
|
||||
frameTrackButtonWidthPx,
|
||||
scale,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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";
|
||||
import { MIN_ANIM_DURATION } from "~/constants";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
clips: Clip[];
|
||||
group: ClipGroup;
|
||||
type?: ClipType;
|
||||
updateClip: (options: { id: string; length: number; offset: number }) => void;
|
||||
}
|
||||
|
||||
export const TrackClips = ({ id, clips, updateClip, group, type }: Props) => {
|
||||
const trackType = (type ?? group) as ClipType;
|
||||
const canDropAsset = getCanDrop({
|
||||
dragType: dragItem.value?.type,
|
||||
type,
|
||||
group,
|
||||
});
|
||||
|
||||
function onPointerOver() {
|
||||
if (getCanDrop({ dragType: dragItem.value?.type, type, group })) {
|
||||
DndAsset.dropId = id;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerMove(
|
||||
event: PointerEvent<HTMLDivElement | HTMLButtonElement>,
|
||||
) {
|
||||
if (!getCanDrop({ dragType: dragItem.value?.type, type, group })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = document.getElementById(`track-${trackType}-${id}`);
|
||||
DndAsset.overElement = element!.getBoundingClientRect();
|
||||
|
||||
const track = document.getElementById(`track-${trackType}-${id}`);
|
||||
|
||||
if (!track) {
|
||||
canDrop.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Now check if the clip fits
|
||||
const position = track.getBoundingClientRect();
|
||||
const clipOffset = Math.round(
|
||||
(event.clientX - position.x - frameTrackButtonWidthPx) / 4 / scale.value,
|
||||
);
|
||||
|
||||
if (clipOffset + MIN_ANIM_DURATION > filmLength.value * 1000) {
|
||||
DndAsset.notDropText = "Not enough room to hold item";
|
||||
canDrop.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const overlap = clips.some((clip) => {
|
||||
if (clipOffset === clip.offset) {
|
||||
return true;
|
||||
}
|
||||
if (clipOffset > clip.offset && clipOffset <= clip.offset + clip.length) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
clipOffset < clip.offset &&
|
||||
clipOffset + (dragItem.value!.length ?? 0) >= clip.offset
|
||||
);
|
||||
});
|
||||
|
||||
canDrop.value = !overlap;
|
||||
if (!overlap) {
|
||||
DndAsset.dropOffset = clipOffset;
|
||||
}
|
||||
if (overlap) {
|
||||
DndAsset.notDropText = "Not enough space to drop here";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`track-${trackType}-${id}`}
|
||||
className={[
|
||||
"relative mb-1 flex h-[60px] w-fit rounded-md overflow-hidden",
|
||||
].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={[
|
||||
`bg-${group}-unselected inset-0`,
|
||||
canDropAsset
|
||||
? "animate-pulse bg-white/30 duration-[750ms]"
|
||||
: "",
|
||||
].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}
|
||||
/>
|
||||
))}
|
||||
</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" style={{ marginLeft: frameTrackButtonWidthPx + 10 }}>
|
||||
<div className="animate-bounce">
|
||||
<FontAwesomeIcon icon={faArrowUp} className="text-white/80" />
|
||||
</div>
|
||||
<div className="text-xs text-white/80">
|
||||
{type === ClipType.TRANSFORM
|
||||
? "Drag and Drop TRANSFORM clip here" // This should not show
|
||||
: type === ClipType.AUDIO
|
||||
? "Drag and drop character speech or vocal audio clips here"
|
||||
: type === ClipType.ANIMATION
|
||||
? "Drag and drop character animation clips here"
|
||||
: type === ClipType.EXPRESSION
|
||||
? "Drag and drop facial expression animation clips here"
|
||||
: type === ClipType.FAKE
|
||||
? "Drag and Drop FAKE clip here" // this should not show.
|
||||
: "Drag and drop uploaded music clips or sound effects from the audio tab here"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(group === ClipGroup.CHARACTER &&
|
||||
<CharacterFrameButton className={"h-full rounded-r-md"} target={CharacterFrameTarget.End} characterId={id} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
import { TrackClipPrompt } from "~/pages/PageEnigma/comps/Timeline/TrackClipPrompt";
|
||||
import { Clip } from "~/pages/PageEnigma/models";
|
||||
import { PointerEvent, useRef, useState } from "react";
|
||||
import { filmLength, scale } from "~/pages/PageEnigma/signals";
|
||||
import { ClipGroup } from "~/enums";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { AddEditPrompt } from "~/pages/PageEnigma/comps/Timeline/AddEditPrompt";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
clips: Clip[];
|
||||
updateClip: (options: {
|
||||
id: string;
|
||||
length: number;
|
||||
offset: number;
|
||||
name: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const TrackClipsPrompt = ({ id, clips, updateClip }: Props) => {
|
||||
useSignals();
|
||||
const [hover, setHover] = useState(-1);
|
||||
const [openPrompt, setOpenPrompt] = useState(false);
|
||||
const [hasOverlap, setHasOverlap] = useState(false);
|
||||
const clientX = useRef(0);
|
||||
|
||||
function onPointerOver(event: PointerEvent) {
|
||||
const max = filmLength.value * 1000 * 4 * scale.value;
|
||||
if (event.pageX < 320) {
|
||||
setHover(320);
|
||||
} else {
|
||||
setHover(Math.min(event.pageX, max + 80));
|
||||
clientX.current = event.pageX - 320;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerMove(
|
||||
event: PointerEvent<HTMLDivElement | HTMLButtonElement>,
|
||||
) {
|
||||
if (hover === -1) {
|
||||
return;
|
||||
}
|
||||
const max = filmLength.value * 1000 * 4 * scale.value;
|
||||
const offset =
|
||||
(event.pageX < 320 ? 0 : Math.min(event.pageX - 320, max + 80)) /
|
||||
4 /
|
||||
scale.value;
|
||||
if (event.pageX < 320) {
|
||||
setHover(320);
|
||||
clientX.current = 0;
|
||||
} else {
|
||||
const offset = Math.min(event.pageX, max + 80);
|
||||
setHover(offset);
|
||||
clientX.current = offset - 320;
|
||||
}
|
||||
const overlap = clips.some((clip) => {
|
||||
if (offset + 60 < clip.offset) {
|
||||
return false;
|
||||
}
|
||||
return clip.offset + clip.length >= offset;
|
||||
});
|
||||
setHasOverlap(overlap);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`track-prompt-travel-${id}`}
|
||||
className={twMerge(
|
||||
"relative mb-1 block h-[30px] w-full rounded-md",
|
||||
`bg-prompt-unselected`,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge("absolute inset-0 rounded-md", "opacity-0")}
|
||||
onPointerOver={onPointerOver}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerLeave={() => setHover(-1)}
|
||||
/>
|
||||
{clips.map((clip, index) => (
|
||||
<TrackClipPrompt
|
||||
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={ClipGroup.PROMPT_TRAVEL}
|
||||
updateClip={updateClip}
|
||||
clip={clip}
|
||||
/>
|
||||
))}
|
||||
{hover > -1 && (
|
||||
<button
|
||||
className={twMerge(
|
||||
"text- absolute h-full rounded-md",
|
||||
"border border-dashed border-white/60",
|
||||
"text-sm text-white/60",
|
||||
hasOverlap ? "bg-action" : "bg-prompt-selected/30",
|
||||
)}
|
||||
style={{ width: 60 * scale.value * 4, left: hover - 320 }}
|
||||
onPointerOver={onPointerOver}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerLeave={() => setHover(-1)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
!hasOverlap && setOpenPrompt(true);
|
||||
}}
|
||||
>
|
||||
{hasOverlap ? "Overlap" : "Click to add prompt"}
|
||||
</button>
|
||||
)}
|
||||
{openPrompt && (
|
||||
<AddEditPrompt
|
||||
setOpenPrompt={setOpenPrompt}
|
||||
openPrompt={openPrompt}
|
||||
offset={clientX.current / 4 / scale.value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { filmLength, scale, selectedItem } from "~/pages/PageEnigma/signals";
|
||||
import { useMouseEventsKeyframe } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
interface Props {
|
||||
keyframe: Keyframe;
|
||||
updateKeyframe: (options: { id: string; offset: number }) => void;
|
||||
}
|
||||
|
||||
export const TrackKeyFrame = ({ keyframe, updateKeyframe }: Props) => {
|
||||
useSignals();
|
||||
|
||||
const { onPointerDown, offset } = useMouseEventsKeyframe({
|
||||
keyframe,
|
||||
max: filmLength.value * 1000,
|
||||
min: 0,
|
||||
updateKeyframe,
|
||||
});
|
||||
|
||||
const displayOffset = offset > -1 ? offset : keyframe.offset;
|
||||
|
||||
const selectedKeyframeId =
|
||||
(selectedItem.value as Keyframe | null)?.keyframe_uuid ?? "";
|
||||
|
||||
return (
|
||||
<button
|
||||
className={[
|
||||
"top-[9px] block h-[12px] w-[12px] rotate-45 cursor-ew-resize",
|
||||
"absolute",
|
||||
keyframe.keyframe_uuid === selectedKeyframeId
|
||||
? "bg-keyframe-selected"
|
||||
: "bg-keyframe-unselected",
|
||||
].join(" ")}
|
||||
style={{
|
||||
left: displayOffset * 4 * scale.value - 5,
|
||||
}}
|
||||
onPointerDown={(event) => onPointerDown(event, "drag")}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
selectedItem.value = keyframe;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
|
||||
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
|
||||
import { dragItem, filmLength, frameTrackButtonWidthPx, scale } from "~/pages/PageEnigma/signals";
|
||||
import { AssetType, ClipGroup } from "~/enums";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
keyframes: Keyframe[];
|
||||
title?: string;
|
||||
group: ClipGroup;
|
||||
toggleMute?: () => void;
|
||||
muted?: boolean;
|
||||
updateKeyframe: (options: {
|
||||
id: string;
|
||||
offset: number;
|
||||
force?: boolean;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
function setNotDropText({
|
||||
dragType,
|
||||
group,
|
||||
}: {
|
||||
dragType?: AssetType;
|
||||
group: ClipGroup;
|
||||
}) {
|
||||
if (dragType === AssetType.ANIMATION) {
|
||||
if (group === ClipGroup.CHARACTER) {
|
||||
DndAsset.notDropText = "Cannot drag animation onto movement track";
|
||||
}
|
||||
if (group === ClipGroup.CAMERA) {
|
||||
DndAsset.notDropText = "Cannot drag animation onto camera track";
|
||||
}
|
||||
if (group === ClipGroup.OBJECT) {
|
||||
DndAsset.notDropText = "Cannot drag animation onto object track";
|
||||
}
|
||||
}
|
||||
if (dragType === AssetType.AUDIO) {
|
||||
if (group === ClipGroup.CHARACTER) {
|
||||
DndAsset.notDropText = "Cannot drag audio onto movement track";
|
||||
}
|
||||
if (group === ClipGroup.CAMERA) {
|
||||
DndAsset.notDropText = "Cannot drag audio onto camera track";
|
||||
}
|
||||
if (group === ClipGroup.OBJECT) {
|
||||
DndAsset.notDropText = "Cannot drag audio onto object track";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TrackKeyFrames = ({
|
||||
id,
|
||||
keyframes,
|
||||
updateKeyframe,
|
||||
title,
|
||||
group,
|
||||
}: Props) => {
|
||||
function onPointerMove() {
|
||||
if (!DndAsset.overElement) {
|
||||
const element = document.getElementById(`track-${group}-${id}`);
|
||||
DndAsset.overElement = element!.getBoundingClientRect();
|
||||
}
|
||||
setNotDropText({ dragType: dragItem.value?.type, group });
|
||||
}
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { Timeline } from './Timeline';
|
||||
@@ -1,61 +0,0 @@
|
||||
import { ClipGroup, ClipType, AssetType } from "~/enums";
|
||||
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
|
||||
|
||||
export function getCanDrop({
|
||||
dragType,
|
||||
type,
|
||||
group,
|
||||
}: {
|
||||
dragType?: AssetType;
|
||||
type?: ClipType;
|
||||
group: ClipGroup;
|
||||
}) {
|
||||
if (dragType === AssetType.ANIMATION) {
|
||||
if (type === ClipType.ANIMATION) {
|
||||
return true;
|
||||
}
|
||||
if (type === ClipType.AUDIO) {
|
||||
DndAsset.notDropText =
|
||||
"Cannot drag character animation onto vocals and speech track";
|
||||
}
|
||||
if (type === ClipType.EXPRESSION) {
|
||||
DndAsset.notDropText =
|
||||
"Cannot drag character animation onto facial expression track";
|
||||
}
|
||||
if (group === ClipGroup.GLOBAL_AUDIO) {
|
||||
DndAsset.notDropText = "Cannot drag character animation onto music track";
|
||||
}
|
||||
if (group === ClipGroup.CAMERA) {
|
||||
DndAsset.notDropText =
|
||||
"Cannot drag character animation onto camera track";
|
||||
}
|
||||
}
|
||||
if (dragType === AssetType.EXPRESSION) {
|
||||
if (type === ClipType.EXPRESSION) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (dragType === AssetType.EXPRESSION) {
|
||||
if (type === ClipType.EXPRESSION) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (dragType === AssetType.AUDIO) {
|
||||
if (group === ClipGroup.CHARACTER && type === ClipType.AUDIO) {
|
||||
return true;
|
||||
}
|
||||
if (group === ClipGroup.GLOBAL_AUDIO) {
|
||||
return true;
|
||||
}
|
||||
if (group === ClipGroup.CAMERA) {
|
||||
DndAsset.notDropText = "Cannot drag audio onto camera track";
|
||||
}
|
||||
if (type === ClipType.ANIMATION) {
|
||||
DndAsset.notDropText = "Cannot drag audio onto character animation track";
|
||||
}
|
||||
if (type === ClipType.EXPRESSION) {
|
||||
DndAsset.notDropText = "Cannot drag audio onto facial expression track";
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
|
||||
import { Clip } from "~/pages/PageEnigma/models";
|
||||
import { canDrop, scale } from "~/pages/PageEnigma/signals";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const useMouseEventsClip = (
|
||||
clip: Clip,
|
||||
max: number,
|
||||
min: number,
|
||||
updateClip: (args: { id: string; offset: number; length: number }) => void,
|
||||
setState: Dispatch<{ length: number; offset: number }>,
|
||||
) => {
|
||||
useSignals();
|
||||
const currLength = useRef(clip.length);
|
||||
const currOffset = useRef(clip.offset);
|
||||
const initLength = useRef(clip.length);
|
||||
const initOffset = useRef(clip.offset);
|
||||
const isActive = useRef("");
|
||||
const clientX = useRef(0);
|
||||
|
||||
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") {
|
||||
if (deltaOffset < min) {
|
||||
currOffset.current = min;
|
||||
} else if (deltaOffset + currLength.current > max) {
|
||||
currOffset.current = max - currLength.current;
|
||||
} else {
|
||||
currOffset.current = deltaOffset;
|
||||
}
|
||||
}
|
||||
if (isActive.current === "left") {
|
||||
if (
|
||||
initLength.current - delta < 90 / 4 / scale.value ||
|
||||
deltaOffset < min
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currOffset.current = deltaOffset;
|
||||
currLength.current = initLength.current - delta;
|
||||
}
|
||||
if (isActive.current === "right") {
|
||||
if (
|
||||
initLength.current + delta < 90 / 4 / scale.value ||
|
||||
currOffset.current + initLength.current + delta > max
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currLength.current = initLength.current + delta;
|
||||
}
|
||||
setState({ length: currLength.current, offset: currOffset.current });
|
||||
},
|
||||
[max, min, setState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
currLength.current = clip.length;
|
||||
currOffset.current = clip.offset;
|
||||
initLength.current = clip.length;
|
||||
initOffset.current = clip.offset;
|
||||
}, [clip.length, clip.offset]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [onPointerUp, onMouseMove]);
|
||||
|
||||
return {
|
||||
onPointerDown: (
|
||||
event: React.PointerEvent<HTMLButtonElement>,
|
||||
type: string,
|
||||
) => {
|
||||
if (event.button === 0) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
clientX.current = event.clientX;
|
||||
isActive.current = type;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { canDrop, scale } from "~/pages/PageEnigma/signals";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
export const useMouseEventsKeyframe = ({
|
||||
keyframe,
|
||||
max,
|
||||
min,
|
||||
updateKeyframe,
|
||||
}: {
|
||||
keyframe: Keyframe;
|
||||
max: number;
|
||||
min: number;
|
||||
updateKeyframe: (args: { id: string; offset: number }) => void;
|
||||
}) => {
|
||||
useSignals();
|
||||
const [offset, setOffset] = useState(-1);
|
||||
const currOffset = useRef(keyframe.offset);
|
||||
const initOffset = useRef(keyframe.offset);
|
||||
const isActive = useRef("");
|
||||
const clientX = useRef(0);
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (isActive.current) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
updateKeyframe({
|
||||
id: keyframe.keyframe_uuid,
|
||||
offset: Math.round(currOffset.current),
|
||||
});
|
||||
isActive.current = "";
|
||||
canDrop.value = false;
|
||||
setOffset(-1);
|
||||
}
|
||||
},
|
||||
[updateKeyframe, keyframe.keyframe_uuid, setOffset],
|
||||
);
|
||||
|
||||
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) {
|
||||
currOffset.current = max;
|
||||
} else {
|
||||
currOffset.current = deltaOffset;
|
||||
}
|
||||
setOffset(currOffset.current);
|
||||
}
|
||||
},
|
||||
[max, min, setOffset],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
currOffset.current = keyframe.offset;
|
||||
initOffset.current = keyframe.offset;
|
||||
}, [keyframe.offset]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [onPointerUp, onMouseMove]);
|
||||
|
||||
return {
|
||||
onPointerDown: (
|
||||
event: React.PointerEvent<HTMLButtonElement>,
|
||||
type: string,
|
||||
) => {
|
||||
if (event.button === 0) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
clientX.current = event.clientX;
|
||||
isActive.current = type;
|
||||
setOffset(keyframe.offset);
|
||||
}
|
||||
},
|
||||
offset,
|
||||
};
|
||||
};
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
currentTime,
|
||||
filmLength,
|
||||
scale,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
|
||||
export const useMouseEventsScrubber = () => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const clientX = useRef(0);
|
||||
|
||||
const [time, setTime] = useState(-1);
|
||||
|
||||
const getDelta = useCallback((event: MouseEvent) => {
|
||||
const max = filmLength.value * 1000;
|
||||
|
||||
const delta = Math.round(
|
||||
(event.clientX - clientX.current) / 4 / scale.value + currentTime.value,
|
||||
);
|
||||
if (delta < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (delta > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return delta;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerUp = () => {
|
||||
if (isActive) {
|
||||
currentTime.value = Math.round(time);
|
||||
setIsActive(false);
|
||||
setTime(-1);
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: Math.round(time) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (isActive) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const delta = getDelta(event);
|
||||
setTime((oldTime) => {
|
||||
if (oldTime !== delta) {
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: {
|
||||
currentTime: delta,
|
||||
},
|
||||
});
|
||||
}
|
||||
return delta;
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [clientX, isActive, time, getDelta]);
|
||||
|
||||
return {
|
||||
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
clientX.current = event.clientX;
|
||||
setIsActive(true);
|
||||
setTime(currentTime.value);
|
||||
}
|
||||
}, []),
|
||||
time,
|
||||
};
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/signals";
|
||||
|
||||
export const useMouseEventsSidePanel = () => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [clientX, setClientX] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerUp = () => {
|
||||
if (isActive) {
|
||||
sidePanelWidth.value = Math.round(dndSidePanelWidth.value);
|
||||
setIsActive(false);
|
||||
dndSidePanelWidth.value = -1;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (isActive) {
|
||||
const delta = event.clientX - clientX;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (sidePanelWidth.value - delta < 240) {
|
||||
return;
|
||||
}
|
||||
if (sidePanelWidth.value - delta > 443) {
|
||||
return;
|
||||
}
|
||||
dndSidePanelWidth.value = sidePanelWidth.value - delta;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [clientX, isActive]);
|
||||
|
||||
return {
|
||||
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
event.stopPropagation();
|
||||
setClientX(event.clientX);
|
||||
setIsActive(true);
|
||||
dndSidePanelWidth.value = sidePanelWidth.value;
|
||||
}
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
dndTimelineHeight,
|
||||
filmLength,
|
||||
scale,
|
||||
secondaryScrubber,
|
||||
timelineHeight,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { currentPage, pageHeight } from "~/signals";
|
||||
import { Pages } from "~/pages/PageEnigma/constants/page";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
|
||||
export const useMouseEventsTimeline = () => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [clientY, setClientY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerUp = (event: PointerEvent) => {
|
||||
if (isActive) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
timelineHeight.value = Math.round(dndTimelineHeight.value);
|
||||
setIsActive(false);
|
||||
dndTimelineHeight.value = -1;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (isActive) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const delta = event.clientY - clientY;
|
||||
if (timelineHeight.value - delta < 30) {
|
||||
return;
|
||||
}
|
||||
if (timelineHeight.value - delta > pageHeight.value * 0.5) {
|
||||
return;
|
||||
}
|
||||
dndTimelineHeight.value = timelineHeight.value - delta;
|
||||
return;
|
||||
}
|
||||
if (currentPage.value === Pages.STYLE) {
|
||||
let newPosition =
|
||||
(event.clientX - 200 + timelineScrollX.value) / 4 / scale.value;
|
||||
if (newPosition < 0) {
|
||||
newPosition = 0;
|
||||
}
|
||||
const max = filmLength.value * 1000;
|
||||
if (newPosition > max) {
|
||||
newPosition = max;
|
||||
}
|
||||
if (newPosition !== secondaryScrubber.value) {
|
||||
secondaryScrubber.value = newPosition;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_TIMELINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: newPosition },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onMouseMove);
|
||||
};
|
||||
}, [clientY, isActive]);
|
||||
|
||||
return {
|
||||
onPointerDown: useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button === 0) {
|
||||
event.stopPropagation();
|
||||
dndTimelineHeight.value = timelineHeight.value;
|
||||
setClientY(event.clientY);
|
||||
setIsActive(true);
|
||||
}
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import { MouseEvent, useCallback, useState } from "react";
|
||||
import {
|
||||
deleteAudioClip,
|
||||
deleteCharacterClip,
|
||||
deleteKeyframe,
|
||||
selectedItem,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
import { Clip, Keyframe } from "~/pages/PageEnigma/models";
|
||||
import { deletePromptTravelClip } from "~/pages/PageEnigma/signals/promptTravelGroup";
|
||||
import { DoNotShow } from "~/constants";
|
||||
import { ConfirmationModal } from "~/components";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
function getItemType(item: Clip | Keyframe | null) {
|
||||
if (!item) {
|
||||
return "";
|
||||
}
|
||||
return (item as Clip).clip_uuid ? "clip" : "keyframe";
|
||||
}
|
||||
|
||||
export const useOnDelete = () => {
|
||||
useSignals();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [deletingItem, setDeletingItem] = useState<Clip | Keyframe | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const onDelete = useCallback((item: Clip | Keyframe) => {
|
||||
if ((item as Clip).clip_uuid) {
|
||||
deleteCharacterClip(item as Clip);
|
||||
deleteAudioClip(item as Clip);
|
||||
deletePromptTravelClip(item as Clip);
|
||||
} else {
|
||||
deleteKeyframe(item as Keyframe);
|
||||
}
|
||||
selectedItem.value = null;
|
||||
}, []);
|
||||
|
||||
const onDeleteAsk = useCallback(
|
||||
(event: KeyboardEvent | MouseEvent, item: Clip | Keyframe) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const show = localStorage.getItem(`Delete-${getItemType(item)}`);
|
||||
if (show === DoNotShow) {
|
||||
onDelete(item);
|
||||
return;
|
||||
}
|
||||
setDeletingItem(item);
|
||||
setDialogOpen(true);
|
||||
},
|
||||
[onDelete],
|
||||
);
|
||||
|
||||
const confirmationModal = () => {
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={`Delete ${getItemType(deletingItem)}`}
|
||||
text={`Are you sure you want to delete the selected ${getItemType(deletingItem)}?`}
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onOk={() => {
|
||||
onDelete(deletingItem!);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
okText="Delete"
|
||||
okColor="bg-brand-primary"
|
||||
onCancel={() => setDialogOpen(false)}
|
||||
canHide
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
onDelete,
|
||||
onDeleteAsk,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
confirmationModal,
|
||||
};
|
||||
};
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
currentTime,
|
||||
filmLength,
|
||||
frameTrackButtonWidthPx,
|
||||
scale,
|
||||
selectedItem,
|
||||
stylizeScrollX,
|
||||
timelineScrollX,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
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 - frameTrackButtonWidthPx) / 4 / scale.value,
|
||||
);
|
||||
if (newTime < 0) {
|
||||
return;
|
||||
}
|
||||
const max = filmLength.value * 1000;
|
||||
if (newTime > max) {
|
||||
return;
|
||||
}
|
||||
selectedItem.value = null;
|
||||
currentTime.value = newTime;
|
||||
Queue.publish({
|
||||
queueName: QueueNames.TO_ENGINE,
|
||||
action: toEngineActions.UPDATE_TIME,
|
||||
data: { currentTime: newTime },
|
||||
});
|
||||
}, [page]);
|
||||
}
|
||||
Reference in New Issue
Block a user