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() { -