Start keyframes

This commit is contained in:
FromTheLOLAshes committed 2024-03-31 10:26:59 +07:00
1 parent 902fdd5adc
commit 786fccd303
13 files changed
+223 -203

No files matched your search

@@ -15,4 +15,6 @@ export enum toEngineActions {
// data=QueueClip
UPDATE_TIME = "update_time",
// data={currentTime: number}
ADD_KEYFRAME = "add_keyframe",
// data= QueueKeyframe
}
@@ -2,6 +2,7 @@ import { useCallback, useContext, useMemo } from "react";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { TrackClips } from "~/pages/PageEnigma/comps/Timeline/TrackClips";
import { fullWidth } from "~/pages/PageEnigma/store";
import { TrackKeyFrames } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrames";
function buildUpdaters(
updateCharacters: (options: {
@@ -52,7 +53,7 @@ export const Character = ({ characterId }: Props) => {
if (!character) {
return false;
}
const { animationClips, positionClips, lipSyncClips } = character;
const { animationClips, positionKeyframes, lipSyncClips } = character;
return (
<div
@@ -71,9 +72,9 @@ export const Character = ({ characterId }: Props) => {
style="character"
type="animations"
/>
<TrackClips
<TrackKeyFrames
id={character.id}
clips={positionClips}
keyFrames={positionKeyframes}
title="Character Position/Rotation"
updateClip={updateClipPosition}
style="character"
@@ -22,7 +22,7 @@ export const Timeline = () => {
const {
characters,
objects,
selectedClip,
selectedItem,
deleteCharacterClip,
deleteAudioClip,
deleteCameraClip,
@@ -43,20 +43,20 @@ export const Timeline = () => {
const onDeleteAsk = useCallback(
(event: KeyboardEvent) => {
if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedClip) {
if (["Backspace", "Delete"].indexOf(event.key) > -1 && selectedItem) {
setDialogOpen(true);
}
},
[selectedClip],
[selectedItem],
);
const displayTime = time === -1 ? currentTime.value : time;
const onDelete = useCallback(() => {
deleteCharacterClip(selectedClip!);
deleteCameraClip(selectedClip!);
deleteAudioClip(selectedClip!);
}, [selectedClip, deleteAudioClip, deleteCharacterClip, deleteCameraClip]);
deleteCharacterClip(selectedItem!);
deleteCameraClip(selectedItem!);
deleteAudioClip(selectedItem!);
}, [selectedItem, deleteAudioClip, deleteCharacterClip, deleteCameraClip]);
useEffect(() => {
document.addEventListener("keydown", onDeleteAsk);
@@ -18,7 +18,7 @@ interface Props {
}
export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
const { selectClip, selectedClip } = useContext(TrackContext);
const { selectItem, selectedItem } = useContext(TrackContext);
const [state, setState] = useState({
length: clip.length,
offset: clip.offset,
@@ -48,7 +48,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
const classes = [
"absolute",
clip.clip_uuid === selectedClip
clip.clip_uuid === selectedItem
? `bg-${style}-selected`
: `bg-${style}-clip`,
];
@@ -71,7 +71,7 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
...classes,
"rounded-l-lg",
"block h-full",
clip.clip_uuid === selectedClip
clip.clip_uuid === selectedItem
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -81,13 +81,13 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
cursor: "w-resize",
}}
onPointerDown={(event) => onPointerDown(event, "left")}
onClick={() => selectClip(clip.clip_uuid)}
onClick={() => selectItem(clip.clip_uuid)}
/>
<button
className={[
...classes,
"block h-full",
clip.clip_uuid === selectedClip
clip.clip_uuid === selectedItem
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -97,14 +97,14 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
cursor: "move",
}}
onPointerDown={(event) => onPointerDown(event, "drag")}
onClick={() => selectClip(clip.clip_uuid)}
onClick={() => selectItem(clip.clip_uuid)}
/>
<button
className={[
...classes,
"rounded-r-lg",
"block h-full",
clip.clip_uuid === selectedClip
clip.clip_uuid === selectedItem
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -114,9 +114,9 @@ export const TrackClip = ({ clip, min, max, style, updateClip }: Props) => {
cursor: "e-resize",
}}
onPointerDown={(event) => onPointerDown(event, "right")}
onClick={() => selectClip(clip.clip_uuid)}
onClick={() => selectItem(clip.clip_uuid)}
/>
{selectedClip === clip.clip_uuid && (
{selectedItem === clip.clip_uuid && (
<button
className="absolute flex h-full items-center justify-center"
style={{
@@ -1,62 +1,52 @@
import { BaseKeyFrame } from "~/pages/PageEnigma/models/track";
import { KeyFrame } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
import { useContext, useState } from "react";
import { scale } from "~/pages/PageEnigma/store";
import { useMouseEventsKeyframe } from "~/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSortUpDown } from "@fortawesome/pro-solid-svg-icons";
interface Props {
min: number;
max: number;
style: "character" | "camera" | "audio" | "objects";
keyFrame: BaseKeyFrame;
updateClip: (options: { id: string; offset: number; length: number }) => void;
keyframe: KeyFrame;
updateKeyframe: (options: { id: string; offset: number }) => void;
}
export const TrackKeyFrame = ({
keyFrame,
// min,
// max,
keyframe,
min,
max,
style,
// updateClip,
updateKeyframe,
}: Props) => {
const { selectedClip } = useContext(TrackContext);
// const { selectClip, selectedClip } = useContext(TrackContext);
const [state, setState] = useState({
offset: keyFrame.offset,
const { selectedItem, selectItem } = useContext(TrackContext);
// const { selectItem, selectedItem } = useContext(TrackContext);
const [offset, setOffset] = useState(keyframe.offset);
const { onPointerDown } = useMouseEventsKeyframe({
keyframe,
max,
min,
updateKeyframe,
setOffset,
});
// const { onPointerDown } = useMouseEventsClip(
// keyFrame,
// max,
// min,
// updateClip,
// setState,
// );
const { offset } = state;
const classes = [
"absolute",
keyFrame.id === selectedClip ? `bg-${style}-selected` : `bg-${style}-clip`,
keyframe.keyframe_uuid === selectedItem
? `bg-${style}-selected`
: `bg-${style}-clip`,
];
return (
<>
<div
className={[
...classes,
"px-[6px] py-[3px]",
"rounded",
"prevent-select text-xs font-medium text-white",
].join(" ")}
style={{ top: -28, left: offset * 4 * scale.value + 2 }}
>
{keyFrame.name}
</div>
<button
className={[
...classes,
"rounded-l-lg",
"block h-full",
keyFrame.id === selectedClip
keyframe.keyframe_uuid === selectedItem
? "border border-b-2 border-l-2 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
@@ -65,42 +55,11 @@ export const TrackKeyFrame = ({
left: offset * 4 * scale.value,
cursor: "w-resize",
}}
// onPointerDown={(event) => onPointerDown(event, "left")}
// onClick={() => selectClip(clip.id)}
/>
<button
className={[
...classes,
"block h-full",
keyFrame.id === selectedClip
? "border border-b-2 border-l-0 border-r-0 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
width: length * 4 * scale.value - 30,
left: offset * 4 * scale.value + 15,
cursor: "move",
}}
// onPointerDown={(event) => onPointerDown(event, "drag")}
// onClick={() => selectClip(clip.id)}
/>
<button
className={[
...classes,
"rounded-r-lg",
"block h-full",
keyFrame.id === selectedClip
? "border border-b-2 border-l-0 border-r-2 border-t-2 border-white focus-visible:outline-0"
: "",
].join(" ")}
style={{
width: 15,
left: offset * 4 * scale.value + length * 4 * scale.value - 15,
cursor: "e-resize",
}}
// onPointerDown={(event) => onPointerDown(event, "right")}
// onClick={() => selectClip(clip.id)}
/>
onPointerDown={(event) => onPointerDown(event, "left")}
onClick={() => selectItem(keyframe.keyframe_uuid)}
>
<FontAwesomeIcon icon={faSortUpDown} />
</button>
</>
);
};
@@ -1,24 +1,12 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolume, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
import { BaseKeyFrame } from "~/pages/PageEnigma/models/track";
import { PointerEvent } from "react";
import {
canDrop,
clipLength,
dragType,
dropId,
dropOffset,
filmLength,
scale,
} from "~/pages/PageEnigma/store";
import { KeyFrame } from "~/pages/PageEnigma/models/track";
import { filmLength } from "~/pages/PageEnigma/store";
import { TrackKeyFrame } from "~/pages/PageEnigma/comps/Timeline/TrackKeyFrame";
interface Props {
id: string;
keyFrames: BaseKeyFrame[];
keyFrames: KeyFrame[];
title: string;
style: "character" | "audio" | "camera" | "objects";
type?: "animations" | "positions" | "lipSync";
toggleMute?: () => void;
muted?: boolean;
updateClip: (options: { id: string; length: number; offset: number }) => void;
@@ -27,62 +15,19 @@ interface Props {
export const TrackKeyFrames = ({
id,
keyFrames,
toggleMute,
updateClip,
muted,
title,
style,
type,
}: Props) => {
const trackType = type ?? style;
function onPointerOver() {
if (dragType.value !== trackType) {
return;
}
dropId.value = id;
}
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
if (dragType.value !== trackType) {
return;
}
const track = document.getElementById(`track-${trackType}-${id}`);
if (!track) {
return;
}
// Now check the the clip fits
const position = track.getBoundingClientRect();
const clipOffset = (event.clientX - position.x) / 4 / scale.value;
if (clipOffset + clipLength.value > filmLength.value * 60) {
canDrop.value = false;
return;
}
canDrop.value = true;
dropOffset.value = clipOffset;
}
function onPointerLeave() {
if (dragType.value !== trackType) {
return;
}
canDrop.value = false;
}
return (
<div className="pl-16">
<div
id={`track-${trackType}-${id}`}
id={`track-${style}-${id}`}
className={`relative mt-4 block h-9 w-full rounded-lg bg-${style}-unselected`}
onPointerOver={onPointerOver}
onPointerLeave={onPointerLeave}
onPointerMove={onPointerMove}
>
{keyFrames.map((keyFrame, index) => (
<TrackKeyFrame
key={keyFrame.id}
key={keyFrame.keyframe_uuid}
min={index > 0 ? keyFrames[index - 1].offset + 1 : 0}
max={
index < keyFrames.length - 1
@@ -97,22 +42,6 @@ export const TrackKeyFrames = ({
<div className="prevent-select absolute ps-2 pt-1 text-xs font-medium text-white">
{title}
</div>
{!!toggleMute && (
<button
className="text-md absolute text-white transition-colors duration-100 hover:text-white/80"
style={{ top: 6, left: -28 }}
onClick={toggleMute}
>
{muted ? (
<FontAwesomeIcon
icon={faVolumeSlash}
className="text-brand-primary transition-colors duration-100 hover:text-brand-primary/80"
/>
) : (
<FontAwesomeIcon icon={faVolume} />
)}
</button>
)}
</div>
</div>
);
@@ -0,0 +1,84 @@
import React, { Dispatch, useCallback, useEffect, useRef } from "react";
import { KeyFrame } from "~/pages/PageEnigma/models/track";
import { canDrop, scale } from "~/pages/PageEnigma/store";
import { useSignals } from "@preact/signals-react/runtime";
export const useMouseEventsKeyframe = ({
keyframe,
max,
min,
updateKeyframe,
setOffset,
}: {
keyframe: KeyFrame;
max: number;
min: number;
updateKeyframe: (args: { id: string; offset: number }) => void;
setOffset: Dispatch<number>;
}) => {
useSignals();
const currOffset = useRef(keyframe.offset);
const initOffset = useRef(keyframe.offset);
const isActive = useRef("");
const clientX = useRef(0);
const onPointerUp = useCallback(() => {
if (isActive.current) {
updateKeyframe({
id: keyframe.keyframe_uuid,
offset: Math.round(currOffset.current),
});
isActive.current = "";
canDrop.value = false;
}
}, [updateKeyframe, keyframe.keyframe_uuid]);
const onMouseMove = useCallback(
(event: MouseEvent) => {
const delta = (event.clientX - clientX.current) / 4 / scale.value;
const deltaOffset = delta + initOffset.current;
if (isActive.current === "drag") {
if (deltaOffset < min) {
currOffset.current = min;
return;
}
if (deltaOffset > max) {
currOffset.current = max;
return;
}
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;
}
},
};
};
@@ -1,11 +1,12 @@
import { fromEngineActions } from "~/pages/PageEnigma/Queue/fromEngineActions";
import { useSignals } from "@preact/signals-react/runtime";
import { useCallback, useEffect } from "react";
import { useCallback, useContext, useEffect } from "react";
import { currentTime } from "~/pages/PageEnigma/store";
import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { QueueClip } from "~/pages/PageEnigma/models/track";
import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext";
interface UpdateTime {
currentTime: number;
@@ -18,8 +19,10 @@ interface Arguments {
export function useQueueHandler() {
useSignals();
const handleActions = useCallback(({ action, data }: Arguments) => {
console.log(`FROM ENGINE:${action} ${data}`)
const { addKeyframe } = useContext(TrackContext);
const handleFromEngineActions = useCallback(({ action, data }: Arguments) => {
console.log(`FROM ENGINE:${action} ${data}`);
switch (action) {
case fromEngineActions.UPDATE_TIME:
currentTime.value = (data as UpdateTime).currentTime;
@@ -29,7 +32,19 @@ export function useQueueHandler() {
}
}, []);
const handleToTimelineActions = useCallback(({ action, data }: Arguments) => {
console.log(`FROM ENGINE:${action} ${data}`);
switch (action) {
case toTimelineActions.ADD_KEYFRAME:
addKeyframe(data);
break;
default:
throw new Error(`Unknown action ${action}`);
}
}, []);
useEffect(() => {
Queue.subscribe(QueueNames.FROM_ENGINE, handleActions);
}, [handleActions]);
Queue.subscribe(QueueNames.FROM_ENGINE, handleFromEngineActions);
Queue.subscribe(QueueNames.TO_TIMELINE, handleToTimelineActions);
}, [handleFromEngineActions, handleToTimelineActions]);
}
@@ -4,8 +4,8 @@ import {
CameraGroup,
CharacterGroup,
ObjectGroup,
ObjectTrack,
MediaClip,
KeyFrame,
} from "~/pages/PageEnigma/models/track";
export const TrackContext = createContext<{
@@ -62,11 +62,14 @@ export const TrackContext = createContext<{
// timeline objects group
objects: ObjectGroup;
updateObject: (options: { id: string; offset: number }) => void;
addObject: (options: ObjectTrack) => void;
// current - only select one item - will be replaced
selectedClip: string | null;
selectClip: (clipId: string) => void;
selectedItem: string | null;
selectItem: (itemId: string) => void;
// keyframes
addKeyframe: (options: { object_uuid: string }) => void;
deleteKeyframe: (options: KeyFrame) => void;
// sidebar clips
animationClips: MediaClip[];
@@ -102,10 +105,12 @@ export const TrackContext = createContext<{
objects: { id: "", objects: [] },
updateObject: () => {},
addObject: () => {},
selectedClip: null,
selectClip: () => {},
addKeyframe: () => {},
deleteKeyframe: () => {},
selectedItem: null,
selectItem: () => {},
animationClips: [],
audioClips: [],
@@ -27,8 +27,8 @@ export const TrackProvider = ({ children }: Props) => {
const { endDrag, ...dragDrop } = useUpdateDragDrop();
const [selectedClip, setSelectedClip] = useState<string | null>(null);
const selectClip = useCallback((clipId: string | null) => {
const [selectedItem, setSelectedClip] = useState<string | null>(null);
const selectItem = useCallback((clipId: string | null) => {
setSelectedClip(clipId);
}, []);
@@ -115,8 +115,8 @@ export const TrackProvider = ({ children }: Props) => {
...audio,
...objects,
selectClip,
selectedClip,
selectItem,
selectedItem,
...dragDrop,
endDrag: dropClip,
@@ -138,8 +138,8 @@ export const TrackProvider = ({ children }: Props) => {
audio,
objects,
selectClip,
selectedClip,
selectItem,
selectedItem,
dragDrop,
dropClip,
@@ -0,0 +1,17 @@
import { useCallback } from "react";
import { ObjectTrack } from "~/pages/PageEnigma/models/track";
export default function useUpdateKeyframe() {
const addKeyframe = useCallback((obj: ObjectTrack) => {
setObjects((oldObjectGroup) => {
return {
...oldObjectGroup,
objects: [...oldObjectGroup.objects, obj],
};
});
}, []);
return {
addKeyframe,
};
}
@@ -1,21 +1,11 @@
import { useCallback, useState } from "react";
import { ObjectGroup, ObjectTrack } from "~/pages/PageEnigma/models/track";
import * as uuid from "uuid";
export default function useUpdateObject() {
const [objects, setObjects] = useState<ObjectGroup>({
id: "OB1",
objects: [
// {
// id: "OB1-O1",
// keyFrames: [
// {
// id: "OB1-O1-1",
// offset: 0,
// name: "obj 1",
// },
// ],
// },
],
objects: [],
});
const updateObject = useCallback(
@@ -27,7 +17,7 @@ export default function useUpdateObject() {
id: object.id,
keyFrames: object.keyFrames.map((keyFrame) => ({
...keyFrame,
offset: keyFrame.id === id ? offset : keyFrame.offset,
offset: keyFrame.keyframe_uuid === id ? offset : keyFrame.offset,
})),
})),
};
@@ -36,6 +26,18 @@ export default function useUpdateObject() {
[],
);
const addObject = useCallback((name: string) => {
setObjects((oldObjectGroup) => {
return {
...oldObjectGroup,
objects: [
...oldObjectGroup.objects,
{ object_uuid: uuid.v4(), name: name, keyframes: [] },
],
};
});
}, []);
const addObject = useCallback((obj: ObjectTrack) => {
setObjects((oldObjectGroup) => {
return {
@@ -49,5 +51,6 @@ export default function useUpdateObject() {
objects,
updateObject,
addObject,
addKeyframe,
};
}
+13 -8
View File
@@ -1,3 +1,5 @@
import { XYZ } from "../datastructures/common";
export interface QueueClip {
version: number;
type: ClipType;
@@ -45,10 +47,15 @@ export enum ClipGroup {
OBJECT = "object",
}
export interface BaseKeyFrame {
id: string;
name: string;
export interface KeyFrame {
version: number;
keyframe_uuid: string;
group: ClipGroup;
object_uuid?: string;
offset: number;
position: XYZ;
rotation: XYZ;
scale: XYZ;
selected?: boolean;
}
@@ -56,13 +63,13 @@ export interface CharacterGroup {
id: string;
muted: boolean;
animationClips: Clip[];
positionClips: Clip[];
positionKeyframes: KeyFrame[];
lipSyncClips: Clip[];
}
export interface CameraGroup {
id: string;
clips: Clip[];
clips: KeyFrame[];
}
export interface AudioGroup {
@@ -78,7 +85,5 @@ export interface ObjectGroup {
export interface ObjectTrack {
id: string;
keyFrames: ObjectKeyFrame[];
keyFrames: KeyFrame[];
}
export interface ObjectKeyFrame extends BaseKeyFrame {}