From 786fccd303d1003d96bee21e861f637bc8536c71 Mon Sep 17 00:00:00 2001 From: FromTheLOLAshes Date: Sun, 31 Mar 2024 10:26:59 +0700 Subject: [PATCH] Start keyframes --- app/pages/PageEnigma/Queue/toEngineActions.ts | 2 + .../PageEnigma/comps/Timeline/Character.tsx | 7 +- .../PageEnigma/comps/Timeline/Timeline.tsx | 14 +-- .../PageEnigma/comps/Timeline/TrackClip.tsx | 18 ++-- .../comps/Timeline/TrackKeyFrame.tsx | 97 ++++++------------- .../comps/Timeline/TrackKeyFrames.tsx | 81 +--------------- .../Timeline/utils/useMouseEventsKeyframe.ts | 84 ++++++++++++++++ .../comps/Timeline/utils/useQueueHandler.ts | 25 ++++- .../contexts/TrackContext/TrackContext.tsx | 19 ++-- .../contexts/TrackContext/TrackProvider.tsx | 12 +-- .../TrackContext/utils/useUpdateKeyframe.ts | 17 ++++ .../TrackContext/utils/useUpdateObject.ts | 29 +++--- app/pages/PageEnigma/models/track.ts | 21 ++-- 13 files changed, 223 insertions(+), 203 deletions(-) create mode 100644 app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe.ts create mode 100644 app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe.ts diff --git a/app/pages/PageEnigma/Queue/toEngineActions.ts b/app/pages/PageEnigma/Queue/toEngineActions.ts index 867730e006..4f695a239c 100644 --- a/app/pages/PageEnigma/Queue/toEngineActions.ts +++ b/app/pages/PageEnigma/Queue/toEngineActions.ts @@ -15,4 +15,6 @@ export enum toEngineActions { // data=QueueClip UPDATE_TIME = "update_time", // data={currentTime: number} + ADD_KEYFRAME = "add_keyframe", + // data= QueueKeyframe } diff --git a/app/pages/PageEnigma/comps/Timeline/Character.tsx b/app/pages/PageEnigma/comps/Timeline/Character.tsx index a6d6fadd0e..6501221d2f 100644 --- a/app/pages/PageEnigma/comps/Timeline/Character.tsx +++ b/app/pages/PageEnigma/comps/Timeline/Character.tsx @@ -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 (
{ style="character" type="animations" /> - { 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); diff --git a/app/pages/PageEnigma/comps/Timeline/TrackClip.tsx b/app/pages/PageEnigma/comps/Timeline/TrackClip.tsx index e494003304..5747a8509f 100644 --- a/app/pages/PageEnigma/comps/Timeline/TrackClip.tsx +++ b/app/pages/PageEnigma/comps/Timeline/TrackClip.tsx @@ -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)} /> ); }; diff --git a/app/pages/PageEnigma/comps/Timeline/TrackKeyFrames.tsx b/app/pages/PageEnigma/comps/Timeline/TrackKeyFrames.tsx index 950c0daedd..d0befa2ee7 100644 --- a/app/pages/PageEnigma/comps/Timeline/TrackKeyFrames.tsx +++ b/app/pages/PageEnigma/comps/Timeline/TrackKeyFrames.tsx @@ -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) { - 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 (
{keyFrames.map((keyFrame, index) => ( 0 ? keyFrames[index - 1].offset + 1 : 0} max={ index < keyFrames.length - 1 @@ -97,22 +42,6 @@ export const TrackKeyFrames = ({
{title}
- {!!toggleMute && ( - - )}
); diff --git a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe.ts b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe.ts new file mode 100644 index 0000000000..a1f30aee1f --- /dev/null +++ b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsKeyframe.ts @@ -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; +}) => { + 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, + type: string, + ) => { + if (event.button === 0) { + event.stopPropagation(); + event.preventDefault(); + clientX.current = event.clientX; + isActive.current = type; + } + }, + }; +}; diff --git a/app/pages/PageEnigma/comps/Timeline/utils/useQueueHandler.ts b/app/pages/PageEnigma/comps/Timeline/utils/useQueueHandler.ts index 02e96793ca..0190377889 100644 --- a/app/pages/PageEnigma/comps/Timeline/utils/useQueueHandler.ts +++ b/app/pages/PageEnigma/comps/Timeline/utils/useQueueHandler.ts @@ -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]); } diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx index f761df637e..48b2d2429e 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx @@ -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: [], diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx index ee8b41971a..15d7fa7b1c 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx @@ -27,8 +27,8 @@ export const TrackProvider = ({ children }: Props) => { const { endDrag, ...dragDrop } = useUpdateDragDrop(); - const [selectedClip, setSelectedClip] = useState(null); - const selectClip = useCallback((clipId: string | null) => { + const [selectedItem, setSelectedClip] = useState(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, diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe.ts new file mode 100644 index 0000000000..41e5c4279f --- /dev/null +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateKeyframe.ts @@ -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, + }; +} diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts index 55f9a71b0d..06398521be 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts @@ -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({ 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, }; } diff --git a/app/pages/PageEnigma/models/track.ts b/app/pages/PageEnigma/models/track.ts index dd290db8c6..f8ce1194e9 100644 --- a/app/pages/PageEnigma/models/track.ts +++ b/app/pages/PageEnigma/models/track.ts @@ -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 {}