diff options
Diffstat (limited to 'apps/web/src/hooks/timeline/use-timeline-playhead.ts')
| -rw-r--r-- | apps/web/src/hooks/timeline/use-timeline-playhead.ts | 178 |
1 files changed, 104 insertions, 74 deletions
diff --git a/apps/web/src/hooks/timeline/use-timeline-playhead.ts b/apps/web/src/hooks/timeline/use-timeline-playhead.ts index 1426a003..e6204f23 100644 --- a/apps/web/src/hooks/timeline/use-timeline-playhead.ts +++ b/apps/web/src/hooks/timeline/use-timeline-playhead.ts @@ -1,12 +1,13 @@ import { getSnappedSeekTime } from "@/lib/time"; -import { useState, useEffect, useCallback, useRef } from "react"; +import { useEffect, useCallback, useRef } from "react"; import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll"; import { useEditor } from "../use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; +import { findSnapPoints, snapToNearestPoint } from "@/lib/timeline/snap-utils"; import { - findSnapPoints, - snapToNearestPoint, -} from "@/lib/timeline/snap-utils"; + getCenteredLineLeft, + timelineTimeToSnappedPixels, +} from "@/lib/timeline"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; interface UseTimelinePlayheadProps { @@ -25,27 +26,33 @@ export function useTimelinePlayhead({ playheadRef, }: UseTimelinePlayheadProps) { const editor = useEditor(); + const isScrubbing = useEditor((e) => e.playback.getIsScrubbing()); const activeProject = editor.project.getActive(); - const currentTime = editor.playback.getCurrentTime(); const duration = editor.timeline.getTotalDuration(); - const isPlaying = editor.playback.getIsPlaying(); - const isScrubbing = editor.playback.getIsScrubbing(); const isShiftHeldRef = useShiftKey(); + const zoomLevelRef = useRef(zoomLevel); + const durationRef = useRef(duration); + const isScrubbingRef = useRef(isScrubbing); + const isPlayingRef = useRef(false); + + useEffect(() => { + zoomLevelRef.current = zoomLevel; + durationRef.current = duration; + isScrubbingRef.current = isScrubbing; + isPlayingRef.current = editor.playback.getIsPlaying(); + }, [zoomLevel, duration, isScrubbing, editor.playback]); + const seek = useCallback( ({ time }: { time: number }) => editor.playback.seek({ time }), [editor.playback], ); - const [scrubTime, setScrubTime] = useState<number | null>(null); - - const [isDraggingRuler, setIsDraggingRuler] = useState(false); - const [hasDraggedRuler, setHasDraggedRuler] = useState(false); + const scrubTimeRef = useRef<number | null>(null); + const isDraggingRulerRef = useRef(false); + const hasDraggedRulerRef = useRef(false); const lastMouseXRef = useRef<number>(0); - const playheadPosition = - isScrubbing && scrubTime !== null ? scrubTime : currentTime; - const handleScrub = useCallback( ({ event, @@ -86,8 +93,7 @@ export function useTimelinePlayhead({ const time = (() => { if (!shouldSnap) return frameTime; const tracks = editor.timeline.getTracks(); - const bookmarks = - editor.scenes.getActiveScene()?.bookmarks ?? []; + const bookmarks = editor.scenes.getActiveScene()?.bookmarks ?? []; const snapPoints = findSnapPoints({ tracks, playheadTime: frameTime, @@ -102,7 +108,7 @@ export function useTimelinePlayhead({ return snapResult.snapPoint ? snapResult.snappedTime : frameTime; })(); - setScrubTime(time); + scrubTimeRef.current = time; seek({ time }); lastMouseXRef.current = event.clientX; @@ -132,17 +138,16 @@ export function useTimelinePlayhead({ const handleRulerMouseDown = useCallback( ({ event }: { event: React.MouseEvent }) => { if (event.button !== 0) return; - if (playheadRef?.current?.contains(event.target as Node)) return; event.preventDefault(); - setIsDraggingRuler(true); - setHasDraggedRuler(false); + isDraggingRulerRef.current = true; + hasDraggedRulerRef.current = false; - editor.playback.setScrubbing({ isScrubbing: true }); - handleScrub({ event, snappingEnabled: false }); - }, - [handleScrub, playheadRef, editor.playback], + editor.playback.setScrubbing({ isScrubbing: true }); + handleScrub({ event, snappingEnabled: false }); + }, + [handleScrub, playheadRef, editor.playback], ); const handlePlayheadMouseDownEvent = useCallback( @@ -168,31 +173,32 @@ export function useTimelinePlayhead({ const handleMouseMove = ({ event }: { event: MouseEvent }) => { handleScrub({ event }); - if (isDraggingRuler) { - setHasDraggedRuler(true); + if (isDraggingRulerRef.current) { + hasDraggedRulerRef.current = true; } }; const handleMouseUp = ({ event }: { event: MouseEvent }) => { editor.playback.setScrubbing({ isScrubbing: false }); - if (scrubTime !== null) { - seek({ time: scrubTime }); + const finalTime = scrubTimeRef.current; + if (finalTime !== null) { + seek({ time: finalTime }); editor.project.setTimelineViewState({ viewState: { zoomLevel, scrollLeft: tracksScrollRef.current?.scrollLeft ?? 0, - playheadTime: scrubTime, + playheadTime: finalTime, }, }); } - setScrubTime(null); + scrubTimeRef.current = null; - if (isDraggingRuler) { - setIsDraggingRuler(false); - if (!hasDraggedRuler) { + if (isDraggingRulerRef.current) { + isDraggingRulerRef.current = false; + if (!hasDraggedRulerRef.current) { handleScrub({ event, snappingEnabled: false }); } - setHasDraggedRuler(false); + hasDraggedRulerRef.current = false; } }; @@ -206,55 +212,79 @@ export function useTimelinePlayhead({ window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); }; - }, [ - isScrubbing, - scrubTime, - seek, - handleScrub, - isDraggingRuler, - hasDraggedRuler, - editor, - tracksScrollRef, - zoomLevel, - ]); + }, [isScrubbing, seek, handleScrub, editor, tracksScrollRef, zoomLevel]); + + const updatePlayheadLeft = useCallback( + (time: number) => { + const playheadEl = playheadRef?.current; + if (!playheadEl) return; + const centerPosition = timelineTimeToSnappedPixels({ + time, + zoomLevel: zoomLevelRef.current, + }); + const leftPosition = getCenteredLineLeft({ centerPixel: centerPosition }); + const scrollLeft = rulerScrollRef.current?.scrollLeft ?? 0; + playheadEl.style.left = `${leftPosition - scrollLeft}px`; + }, + [playheadRef, rulerScrollRef], + ); useEffect(() => { - if (!isPlaying || isScrubbing) return; + const scrollEl = rulerScrollRef.current; + if (!scrollEl) return; - const rulerViewport = rulerScrollRef.current; - const tracksViewport = tracksScrollRef.current; - if (!rulerViewport || !tracksViewport) return; + const handleScroll = () => { + updatePlayheadLeft(editor.playback.getCurrentTime()); + }; - const playheadPixels = - playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const viewportWidth = rulerViewport.clientWidth; - const scrollMinimum = 0; - const scrollMaximum = rulerViewport.scrollWidth - viewportWidth; + scrollEl.addEventListener("scroll", handleScroll, { passive: true }); + return () => scrollEl.removeEventListener("scroll", handleScroll); + }, [editor.playback, rulerScrollRef, updatePlayheadLeft]); - const needsScroll = - playheadPixels < rulerViewport.scrollLeft || - playheadPixels > rulerViewport.scrollLeft + viewportWidth; + useEffect(() => { + const handlePlaybackUpdate = (e: Event) => { + const time = (e as CustomEvent<{ time: number }>).detail.time; + updatePlayheadLeft(time); - if (needsScroll) { - const desiredScroll = Math.max( - scrollMinimum, - Math.min(scrollMaximum, playheadPixels - viewportWidth / 2), - ); - rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; - } - }, [ - playheadPosition, - zoomLevel, - rulerScrollRef, - tracksScrollRef, - isScrubbing, - isPlaying, - ]); + if (!isPlayingRef.current || isScrubbingRef.current) return; + const rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; + if (!rulerViewport || !tracksViewport) return; + + const playheadPixels = + time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevelRef.current; + const viewportWidth = rulerViewport.clientWidth; + const scrollMinimum = 0; + const scrollMaximum = rulerViewport.scrollWidth - viewportWidth; + + const needsScroll = + playheadPixels < rulerViewport.scrollLeft || + playheadPixels > rulerViewport.scrollLeft + viewportWidth; + + if (needsScroll) { + const desiredScroll = Math.max( + scrollMinimum, + Math.min(scrollMaximum, playheadPixels - viewportWidth / 2), + ); + rulerViewport.scrollLeft = tracksViewport.scrollLeft = desiredScroll; + } + }; + + const initialTime = editor.playback.getCurrentTime(); + handlePlaybackUpdate({ + detail: { time: initialTime }, + } as CustomEvent<{ time: number }>); + + window.addEventListener("playback-update", handlePlaybackUpdate); + window.addEventListener("playback-seek", handlePlaybackUpdate); + return () => { + window.removeEventListener("playback-update", handlePlaybackUpdate); + window.removeEventListener("playback-seek", handlePlaybackUpdate); + }; + }, [editor.playback, rulerScrollRef, tracksScrollRef, updatePlayheadLeft]); return { - playheadPosition, handlePlayheadMouseDown: handlePlayheadMouseDownEvent, handleRulerMouseDown: handleRulerMouseDownEvent, - isDraggingRuler, }; } |
