diff --git a/app/modules/LowerPanel/LowerPanel.tsx b/app/modules/LowerPanel/LowerPanel.tsx index 680a097953..2fb21e12b1 100644 --- a/app/modules/LowerPanel/LowerPanel.tsx +++ b/app/modules/LowerPanel/LowerPanel.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useContext, useState } from "react"; +import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; interface LowerPanelPropsI { timelineHeight: number; @@ -7,6 +8,7 @@ interface LowerPanelPropsI { export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => { const [open, setOpen] = useState(false); + const { setOverTimeline } = useContext(TrackContext); const handleOpen = () => { setOpen(true); }; @@ -20,6 +22,11 @@ export const LowerPanel = ({ children, timelineHeight }: LowerPanelPropsI) => { "bg-ui-panel", ].join(" ")} style={{ height: timelineHeight }} + onPointerOver={() => { + console.log("over"); + setOverTimeline(true); + }} + onPointerLeave={() => setOverTimeline(false)} > {children} diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx index 747d64434d..c8f6e6fba5 100644 --- a/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx +++ b/app/pages/PageEnigma/comps/SidePanelTabs/ClipElement.tsx @@ -1,5 +1,5 @@ import { useCallback, useContext, useEffect, useState } from "react"; -import { BaseClip } from "~/models/track"; +import { BaseClip } from "~/pages/PageEnigma/models/track"; import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; interface Props { @@ -8,8 +8,16 @@ interface Props { } export const ClipElement = ({ clip, type }: Props) => { - const { startDrag, dragId, endDrag, scale, canDrop, setCanDrop } = - useContext(TrackContext); + const { + startDrag, + dragId, + endDrag, + scale, + canDrop, + setCanDrop, + overTimeline, + dropId, + } = useContext(TrackContext); const [initPosition, setInitPosition] = useState<{ initX: number; initY: number; @@ -36,6 +44,8 @@ export const ClipElement = ({ clip, type }: Props) => { const onMouseMove = (event: MouseEvent) => { if (dragId) { + event.stopPropagation(); + event.preventDefault(); const deltaX = event.clientX - initX; const deltaY = event.clientY - initY; setCurrPosition({ x: deltaX, y: deltaY }); @@ -55,20 +65,24 @@ export const ClipElement = ({ clip, type }: Props) => { const onPointerDown = useCallback( (event: React.PointerEvent) => { if (event.button === 0) { + const box = document.getElementById(`ani-clip-${clip.id}`); + const position = box!.getBoundingClientRect(); + startDrag(type, clip.id); setCurrPosition({ - x: 0, - y: 0, + x: position.x, + y: position.y + position.height + 1, + }); + setInitPosition({ + initX: position.x, + initY: position.y + position.height + 1, }); - setInitPosition({ initY: event.clientY, initX: event.clientX }); setCanDrop(false); } }, [type, clip.id, startDrag, setCanDrop], ); - // console.log("clip", canDrop); - return (
{
diff --git a/app/pages/PageEnigma/comps/Timeline/Audio.tsx b/app/pages/PageEnigma/comps/Timeline/Audio.tsx index 2b7550edd6..e0ceaf066a 100644 --- a/app/pages/PageEnigma/comps/Timeline/Audio.tsx +++ b/app/pages/PageEnigma/comps/Timeline/Audio.tsx @@ -17,11 +17,12 @@ export const Audio = () => {
diff --git a/app/pages/PageEnigma/comps/Timeline/Camera.tsx b/app/pages/PageEnigma/comps/Timeline/Camera.tsx index 65f7fc59f9..ab0a4fb1bb 100644 --- a/app/pages/PageEnigma/comps/Timeline/Camera.tsx +++ b/app/pages/PageEnigma/comps/Timeline/Camera.tsx @@ -14,6 +14,7 @@ export const Camera = () => {
Camera
{
{ type="animations" /> { type="positions" /> { {objects.objects.map((object) => (
{ - const { length, setCanDrop, dragType } = useContext(TrackContext); + const { length, scale, setCanDrop, dragType, setDropId, setDropOffset } = + useContext(TrackContext); const trackType = type ?? style; function onPointerOver() { @@ -32,6 +35,19 @@ export const Track = ({ return; } setCanDrop(true); + setDropId(id); + } + + function onPointerMove(event: PointerEvent) { + if (dragType !== trackType) { + return; + } + const track = document.getElementById(`track-${trackType}-${id}`); + if (!track) { + return; + } + const position = track.getBoundingClientRect(); + setDropOffset((event.clientX - position.x) / 4 / scale); } function onPointerLeave() { if (dragType !== trackType) { @@ -43,9 +59,11 @@ export const Track = ({ return (
{clips.map((clip, index) => ( { const onMouseMove = (event: MouseEvent) => { const delta = (event.clientX - clientX) / 4 / scale + currentTime; if (isActive === "drag") { + event.stopPropagation(); + event.preventDefault(); if (delta < 0 || delta > max) { return; } diff --git a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts index cad387f83b..b92f68bfb0 100644 --- a/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts +++ b/app/pages/PageEnigma/comps/Timeline/utils/useMouseEventsClip.ts @@ -5,7 +5,7 @@ import React, { useEffect, useRef, } from "react"; -import { BaseClip } from "~/models/track"; +import { BaseClip } from "~/pages/PageEnigma/models/track"; import { TrackContext } from "~/pages/PageEnigma/contexts/TrackContext/TrackContext"; export const useMouseEventsClip = ( diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx index aa744a0f7b..5ce6ff89c9 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackContext.tsx @@ -3,78 +3,143 @@ import { AnimationClip, AudioClip, AudioGroup, + BaseClip, CameraGroup, CharacterGroup, ObjectGroup, -} from "~/models/track"; +} from "~/pages/PageEnigma/models/track"; export const TrackContext = createContext<{ + // timeline characters and it's functions characters: CharacterGroup[]; - camera: CameraGroup | null; - audio: AudioGroup | null; - objects: ObjectGroup; - selectedClip: string | null; updateCharacters: (options: { type: "animations" | "positions" | "lipSync"; id: string; length: number; offset: number; }) => void; + toggleLipSyncMute: (characterId: string) => void; + addCharacterAnimation: (options: { + clipId: string; + characterId: string; + animationClips: BaseClip[]; + offset: number; + }) => void; + addCharacterAudio: (options: { + clipId: string; + characterId: string; + audioClips: BaseClip[]; + offset: number; + }) => void; + selectCharacterClip: (clipId: string) => void; + + // timeline camera group + camera: CameraGroup | null; updateCamera: (options: { id: string; length: number; offset: number; }) => void; + selectCameraClip: (clipId: string) => void; + + // timeline global audio group + audio: AudioGroup | null; updateAudio: (options: { id: string; length: number; offset: number; }) => void; + toggleAudioMute: () => void; + addGlobalAudio: (clipId: string, animationClips: BaseClip[]) => void; + selectAudioClip: (clipId: string) => void; + + // timeline objects group + objects: ObjectGroup; updateObject: (options: { id: string; length: number; offset: number; }) => void; + selectObjectClip: (clipId: string) => void; + + // current - only select one item - will be replaced + selectedClip: string | null; selectClip: (clipId: string) => void; - toggleLipSyncMute: (characterId: string) => void; - toggleAudioMute: () => void; + + // sidebar clips animationClips: AnimationClip[]; audioClips: AudioClip[]; + + // drag and drop dragType: "animations" | "lipSync" | null; dragId: string | null; startDrag: (type: "animations" | "lipSync", id: string) => void; endDrag: () => void; - scale: number; - currentTime: number; - length: number; - updateCurrentTime: (time: number) => void; canDrop: boolean; setCanDrop: (can: boolean) => void; + overTimeline: boolean; + setOverTimeline: (over: boolean) => void; + dropId: string; + setDropId: (id: string) => void; + dropOffset: number; + setDropOffset: (offset: number) => void; + + // scale of timeline displa + scale: number; + + // current time position + currentTime: number; + updateCurrentTime: (time: number) => void; + + // total length of the film clip + length: number; + + // computed width length * 60 * 4 * scale fullWidth: number; }>({ characters: [], - camera: null, - audio: null, - objects: { id: "", objects: [] }, - selectedClip: null, updateCharacters: () => {}, - updateCamera: () => {}, - updateAudio: () => {}, - updateObject: () => {}, - selectClip: () => {}, toggleLipSyncMute: () => {}, + addCharacterAnimation: () => {}, + addCharacterAudio: () => {}, + selectCharacterClip: () => {}, + + camera: null, + updateCamera: () => {}, + selectCameraClip: () => {}, + + audio: null, + updateAudio: () => {}, toggleAudioMute: () => {}, + selectAudioClip: () => {}, + addGlobalAudio: () => {}, + + objects: { id: "", objects: [] }, + updateObject: () => {}, + selectObjectClip: () => {}, + + selectedClip: null, + selectClip: () => {}, + animationClips: [], audioClips: [], + dragType: null, dragId: null, startDrag: () => {}, endDrag: () => {}, - scale: 1, - currentTime: 0, - length: 12, - updateCurrentTime: () => {}, + dropId: "", + setDropId: () => {}, canDrop: false, setCanDrop: () => {}, + overTimeline: false, + setOverTimeline: () => {}, + dropOffset: 0, + setDropOffset: () => {}, + + scale: 1, + currentTime: 0, + updateCurrentTime: () => {}, + length: 12, fullWidth: 0, }); diff --git a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx index ecdcd15063..a6a9acadd7 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx +++ b/app/pages/PageEnigma/contexts/TrackContext/TrackProvider.tsx @@ -4,50 +4,51 @@ import useUpdateCharacters from "~/pages/PageEnigma/contexts/TrackContext/utils/ import useUpdateCamera from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera"; import useUpdateAudio from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio"; import useUpdateObject from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject"; -import { AnimationClip, AudioClip } from "~/models/track"; +import { AnimationClip, AudioClip } from "~/pages/PageEnigma/models/track"; +import useUpdateDragDrop from "~/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop"; interface Props { children: ReactNode; } export const TrackProvider = ({ children }: Props) => { - const { characters, updateCharacters, toggleLipSyncMute } = - useUpdateCharacters(); - const { camera, updateCamera } = useUpdateCamera(); - const { audio, updateAudio, toggleAudioMute } = useUpdateAudio(); - const { objects, updateObject } = useUpdateObject(); - const [selectedClip, setSelectedClip] = useState(null); + const characters = useUpdateCharacters(); + const camera = useUpdateCamera(); + const audio = useUpdateAudio(); + const objects = useUpdateObject(); + const { endDrag, ...dragDrop } = useUpdateDragDrop(); + + const [selectedClip, setSelectedClip] = useState(null); const selectClip = useCallback((clipId: string | null) => { setSelectedClip(clipId); }, []); - const [state, setState] = useState<{ - dragType: "animations" | "lipSync" | null; - dragId: string | null; - }>({ dragType: null, dragId: null }); const [animationClips, setAnimationClips] = useState([]); const [audioClips, setAudioClips] = useState([]); - const startDrag = useCallback( - (type: "animations" | "lipSync", id: string) => { - setState({ dragId: id, dragType: type }); - }, - [], - ); - - const endDrag = useCallback(() => { - setState({ dragId: null, dragType: null }); - }, []); - + const [scale, setScale] = useState(1); + const [length, setLength] = useState(1); const [time, setTime] = useState(0); const updateCurrentTime = useCallback((newTime: number) => { setTime(newTime); }, []); - const [canDrop, setCanDrop] = useState(false); - const [scale, setScale] = useState(1); - const [length, setLength] = useState(1); + // cross group functions + const dropClip = useCallback(() => { + const { canDrop, dragType, dropId, dragId, dropOffset } = dragDrop; + if (canDrop) { + if (dragType === "animations") { + characters.addCharacterAnimation({ + clipId: dragId!, + characterId: dropId, + animationClips, + offset: dropOffset, + }); + } + } + endDrag(); + }, [dragDrop, animationClips, characters, endDrag]); const fullWidth = useMemo(() => { return length * 60 * 4 * scale; @@ -69,54 +70,42 @@ export const TrackProvider = ({ children }: Props) => { const values = useMemo(() => { return { - characters, - camera, - audio, - objects, - updateCharacters, - updateCamera, - updateAudio, - updateObject, + ...characters, + ...camera, + ...audio, + ...objects, + selectClip, selectedClip, - toggleLipSyncMute, - toggleAudioMute, - dragType: state.dragType, - dragId: state.dragId, + + ...dragDrop, + endDrag: dropClip, + animationClips, audioClips, - startDrag, - endDrag, + scale, currentTime: time, length, updateCurrentTime, - canDrop, - setCanDrop, fullWidth, }; }, [ characters, - updateCharacters, + camera, + audio, + objects, + selectClip, selectedClip, - camera, - updateCamera, - audio, - updateAudio, - objects, - updateObject, - toggleLipSyncMute, - toggleAudioMute, - state.dragId, - state.dragType, + + dragDrop, + dropClip, + animationClips, audioClips, - startDrag, - endDrag, - canDrop, + updateCurrentTime, - setCanDrop, time, fullWidth, length, diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts index e366ba419a..8b7e95300b 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateAudio.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { AudioGroup } from "~/models/track"; +import { AudioGroup, BaseClip } from "~/pages/PageEnigma/models/track"; export default function useUpdateAudio() { const [audio, setAudio] = useState({ @@ -49,6 +49,23 @@ export default function useUpdateAudio() { [], ); + const addGlobalAudio = useCallback( + (dragId: string, audioClips: BaseClip[]) => { + const clip = audioClips.find((row) => row.id === dragId); + if (!clip) { + return; + } + + setAudio((oldAudio) => { + return { + ...oldAudio, + clips: [...oldAudio.clips, clip], + }; + }); + }, + [], + ); + const toggleAudioMute = useCallback(() => { setAudio((oldAudio) => { return { @@ -58,9 +75,27 @@ export default function useUpdateAudio() { }); }, []); + const selectAudioClip = useCallback((clipId: string) => { + setAudio((oldAudio) => { + return { + ...oldAudio, + clips: [ + ...oldAudio.clips.map((clip) => { + return { + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + }; + }), + ], + }; + }); + }, []); + return { audio, updateAudio, toggleAudioMute, + selectAudioClip, + addGlobalAudio, }; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts index f0d3f59c9f..b0a0912157 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCamera.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { CameraGroup } from "~/models/track"; +import { CameraGroup } from "~/pages/PageEnigma/models/track"; export default function useUpdateCamera() { const [camera, setCamera] = useState({ @@ -48,8 +48,25 @@ export default function useUpdateCamera() { [], ); + const selectCameraClip = useCallback((clipId: string) => { + setCamera((oldCamera) => { + return { + ...oldCamera, + clips: [ + ...oldCamera.clips.map((clip) => { + return { + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + }; + }), + ], + }; + }); + }, []); + return { camera, updateCamera, + selectCameraClip, }; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts index 60f013df55..ddbbab21da 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateCharacters.ts @@ -1,25 +1,12 @@ import { useCallback, useRef, useState } from "react"; -import { CharacterGroup } from "~/models/track"; +import { BaseClip, CharacterGroup } from "~/pages/PageEnigma/models/track"; export default function useUpdateCharacters() { const [characters, setCharacters] = useState([ { id: "CH1", muted: false, - animationClips: [ - { - id: "CH1-A1", - length: 200, - offset: 0, - name: "ani 1", - }, - { - id: "CH1-A2", - length: 180, - offset: 300, - name: "ani 2", - }, - ], + animationClips: [], positionClips: [ { id: "CH1-P1", @@ -120,6 +107,70 @@ export default function useUpdateCharacters() { [], ); + const addCharacterAnimation = useCallback( + ({ + clipId, + characterId, + animationClips, + offset, + }: { + clipId: string; + characterId: string; + animationClips: BaseClip[]; + offset: number; + }) => { + const clip = animationClips.find((row) => row.id === clipId); + if (!clip) { + return; + } + + setCharacters((oldCharacters) => { + return oldCharacters.map((character) => { + if (character.id !== characterId) { + return { ...character }; + } + return { + ...character, + animationClips: [...character.animationClips, { ...clip, offset }], + }; + }); + }); + }, + [], + ); + + const addCharacterAudio = useCallback( + ({ + clipId, + characterId, + audioClips, + offset, + }: { + clipId: string; + characterId: string; + audioClips: BaseClip[]; + offset: number; + }) => { + const clip = audioClips.find((row) => row.id === clipId); + if (!clip) { + return; + } + + setCharacters((oldCharacters) => { + return oldCharacters.map((character) => { + if (character.id !== characterId) { + return { ...character }; + } + return { + ...character, + lipSyncClips: [...character.lipSyncClips, { ...clip, offset }], + }; + }); + }); + }, + [], + ); + const toggleLipSyncMute = useCallback((characterId: string) => { setCharacters((oldCharacters) => { return oldCharacters.map((character) => ({ @@ -130,9 +181,34 @@ export default function useUpdateCharacters() { }); }, []); + const selectCharacterClip = useCallback((clipId: string) => { + setCharacters((oldCharacters) => { + return [ + ...oldCharacters.map((character) => ({ + ...character, + animationClips: character.animationClips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + positionClips: character.positionClips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + lipSyncClips: character.lipSyncClips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + })), + ]; + }); + }, []); + return { characters, updateCharacters, toggleLipSyncMute, + addCharacterAnimation, + addCharacterAudio, + selectCharacterClip, }; } diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop.ts new file mode 100644 index 0000000000..35bf92ccb8 --- /dev/null +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateDragDrop.ts @@ -0,0 +1,38 @@ +import { useCallback, useState } from "react"; + +export default function useUpdateDragDrop() { + const [state, setState] = useState<{ + dragType: "animations" | "lipSync" | null; + dragId: string | null; + }>({ dragType: null, dragId: null }); + const startDrag = useCallback( + (type: "animations" | "lipSync", id: string) => { + setState({ dragId: id, dragType: type }); + }, + [], + ); + const [canDrop, setCanDrop] = useState(false); + const [overTimeline, setOverTimeline] = useState(false); + + const endDrag = useCallback(() => { + setState({ dragId: null, dragType: null }); + }, []); + + const [dropId, setDropId] = useState(""); + const [dropOffset, setDropOffset] = useState(0); + + return { + dragId: state.dragId, + dragType: state.dragType, + startDrag, + endDrag, + canDrop, + setCanDrop, + overTimeline, + setOverTimeline, + dropId, + setDropId, + dropOffset, + setDropOffset, + }; +} diff --git a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts index 5fbad191e7..2866aa9faa 100644 --- a/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts +++ b/app/pages/PageEnigma/contexts/TrackContext/utils/useUpdateObject.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { ObjectGroup } from "~/models/track"; +import { ObjectGroup } from "~/pages/PageEnigma/models/track"; export default function useUpdateObject() { const [objects, setObjects] = useState({ @@ -46,8 +46,24 @@ export default function useUpdateObject() { [], ); + const selectObjectClip = useCallback((clipId: string) => { + setObjects((oldObjectGroup) => { + return { + ...oldObjectGroup, + objects: oldObjectGroup.objects.map((object) => ({ + ...object, + clips: object.clips.map((clip) => ({ + ...clip, + selected: clip.id === clipId ? !clip.selected : clip.selected, + })), + })), + }; + }); + }, []); + return { objects, updateObject, + selectObjectClip, }; } diff --git a/app/models/track.ts b/app/pages/PageEnigma/models/track.ts similarity index 97% rename from app/models/track.ts rename to app/pages/PageEnigma/models/track.ts index 29d570891d..50c0723e17 100644 --- a/app/models/track.ts +++ b/app/pages/PageEnigma/models/track.ts @@ -3,6 +3,7 @@ export interface BaseClip { name: string; offset: number; length: number; + selected?: boolean; } export interface AnimationClip extends BaseClip {}