From 1aea1415c453ac0584269c006c3fce6269f51997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 22 Apr 2026 01:51:41 +0200 Subject: [PATCH] fix: smooth scrubber end seeking (#386) * fix: smooth scrubber end seeking * fix: stop timeline auto-scroll in fit mode * feat: use percentage-based timeline zoom * fix: sync timeline playhead on zoom changes * fix: reset timeline scroll when returning to fit * fix: keep timeline controls pinned --- packages/core/src/runtime/player.test.ts | 12 +++ packages/studio/src/App.tsx | 18 ++-- .../studio/src/components/nle/NLELayout.tsx | 2 +- .../player/components/PlayerControls.test.ts | 20 +++++ .../src/player/components/PlayerControls.tsx | 13 ++- .../src/player/components/Timeline.test.ts | 45 +++++++++- .../studio/src/player/components/Timeline.tsx | 84 +++++++++++++++++-- .../player/components/timelineZoom.test.ts | 62 ++++++++++++++ .../src/player/components/timelineZoom.ts | 38 +++++++++ .../src/player/store/playerStore.test.ts | 29 ++++--- .../studio/src/player/store/playerStore.ts | 15 ++-- 11 files changed, 302 insertions(+), 36 deletions(-) create mode 100644 packages/studio/src/player/components/PlayerControls.test.ts create mode 100644 packages/studio/src/player/components/timelineZoom.test.ts create mode 100644 packages/studio/src/player/components/timelineZoom.ts diff --git a/packages/core/src/runtime/player.test.ts b/packages/core/src/runtime/player.test.ts index 9a71bf46c..bed968cb5 100644 --- a/packages/core/src/runtime/player.test.ts +++ b/packages/core/src/runtime/player.test.ts @@ -262,6 +262,18 @@ describe("createRuntimePlayer", () => { player.seek(NaN); expect(deps.onDeterministicSeek).toHaveBeenCalledWith(0); }); + + it("seeks to the exact safe duration without snapping back a frame", () => { + const timeline = createMockTimeline({ duration: 8 }); + const deps = createMockDeps(timeline); + deps.getSafeDuration.mockReturnValue(8); + const player = createRuntimePlayer(deps); + player.seek(8); + expect(timeline.pause).toHaveBeenCalled(); + expect(timeline.totalTime).toHaveBeenCalledWith(8, false); + expect(deps.onDeterministicSeek).toHaveBeenCalledWith(8); + expect(deps.onSyncMedia).toHaveBeenCalledWith(8, false); + }); }); describe("renderSeek", () => { diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 3f4b71a55..e3b036b45 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -23,6 +23,10 @@ import { buildTrackZIndexMap, formatTimelineAttributeNumber, } from "./player/components/timelineEditing"; +import { + getNextTimelineZoomPercent, + getTimelineZoomPercent, +} from "./player/components/timelineZoom"; interface EditingFile { path: string; @@ -204,9 +208,9 @@ export function StudioApp() { ? `/api/projects/${projectId}/preview/comp/${activeCompPath}` : null; const zoomMode = usePlayerStore((s) => s.zoomMode); - const pixelsPerSecond = usePlayerStore((s) => s.pixelsPerSecond); + const manualZoomPercent = usePlayerStore((s) => s.manualZoomPercent); const setZoomMode = usePlayerStore((s) => s.setZoomMode); - const setPixelsPerSecond = usePlayerStore((s) => s.setPixelsPerSecond); + const setManualZoomPercent = usePlayerStore((s) => s.setManualZoomPercent); const timelineElements = usePlayerStore((s) => s.elements); const timelineDuration = usePlayerStore((s) => s.duration); const effectiveTimelineDuration = useMemo(() => { @@ -216,6 +220,10 @@ export function StudioApp() { : 0; return Math.max(timelineDuration, maxEnd); }, [timelineDuration, timelineElements]); + const displayedTimelineZoomPercent = useMemo( + () => getTimelineZoomPercent(zoomMode, manualZoomPercent), + [zoomMode, manualZoomPercent], + ); const renderClipContent = useCallback( (el: TimelineElement, style: { clip: string; label: string }): ReactNode => { @@ -336,7 +344,7 @@ export function StudioApp() { type="button" onClick={() => { setZoomMode("manual"); - setPixelsPerSecond(Math.max(20, Math.round(pixelsPerSecond * 0.8))); + setManualZoomPercent(getNextTimelineZoomPercent("out", zoomMode, manualZoomPercent)); }} className="h-7 w-7 rounded-md border border-neutral-800 text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200" title="Zoom out" @@ -344,13 +352,13 @@ export function StudioApp() { -
- {zoomMode === "fit" ? "Auto" : `${Math.round(pixelsPerSecond)} px/s`} + {`${displayedTimelineZoomPercent}%`}