Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/studio/src/player/components/timelineZoomInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,20 @@ describe("zoomTimelineStep", () => {
expect(usePlayerStore.getState().userZoomCount).toBeGreaterThan(0);
});

it("lands at once when settled mid-glide, and keeps a scroll made after", () => {
const { scroll } = viewport();
zoomTimelineStep("in");
vi.advanceTimersToNextFrame();
vi.advanceTimersToNextFrame();
settleTimelineZoom();
expect(usePlayerStore.getState().timelinePps).toBeCloseTo(20);
const landed = scroll.scrollLeft;
scroll.scrollLeft = landed + 500;
run();
expect(usePlayerStore.getState().timelinePps).toBeCloseTo(20);
expect(scroll.scrollLeft).toBe(landed + 500);
});

it("doubles the scale and keeps an on-screen playhead where it is", () => {
usePlayerStore.setState({ currentTime: 40 });
viewport();
Expand Down
12 changes: 12 additions & 0 deletions packages/studio/src/player/components/timelineZoomInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ let anchorForCommit: TimelineZoomAnchor | null = null;
let animation = 0;
let easingTo: number | null = null;
let resolveEase: ((result: TimelineZoomResult) => void) | null = null;
let landEase: (() => void) | null = null;
const previewListeners = new Set<() => void>();

export function currentTimelineZoomPercent(): number {
Expand Down Expand Up @@ -273,9 +274,15 @@ export function requestTimelineZoom(percent: number, anchor: TimelineZoomAnchor

/** Lays out a pending zoom now, so a press meets the real layout. */
export function settleTimelineZoom(): void {
landTimelineZoomEase();
if (preview) commitPreview();
}

/** Lands a zoom that is easing at its target now: each eased frame would undo a scroll made meanwhile. */
export function landTimelineZoomEase(): void {
landEase?.();
}

/** After a scroll: scales what it mounted, or lays the zoom out if it shows past what is drawn. */
export function redrawTimelineZoomPreview(): void {
if (preview && !frame) frame = requestAnimationFrame(drawPreview);
Expand All @@ -299,6 +306,7 @@ function stopEase(result: TimelineZoomResult = "cancelled") {
cancelAnimationFrame(animation);
animation = 0;
easingTo = null;
landEase = null;
const done = resolveEase;
resolveEase = null;
done?.(result);
Expand Down Expand Up @@ -378,6 +386,10 @@ function easeToRange(
commitPreview();
stopEase("done");
};
landEase = () => {
request(toPercent, { time: start, x }, byPerson);
finish();
};
if (!smooth || reducedMotion()) {
request(toPercent, { time: start, x }, byPerson);
// Laid out next frame, never inside a caller's render or effect.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { liveTime, usePlayerStore, type ZoomMode } from "../store/playerStore";
import { useTimelinePlayhead } from "./useTimelinePlayhead";
import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
import { useTimelineClipRenderWindow } from "./useTimelineClipRenderWindow";
import { requestTimelineZoom, settleTimelineZoom } from "./timelineZoomInput";
import { requestTimelineZoom, settleTimelineZoom, zoomTimelineStep } from "./timelineZoomInput";

Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });

Expand Down Expand Up @@ -369,6 +369,28 @@ describe("useTimelinePlayhead wheel zoom", () => {
expect(usePlayerStore.getState().manualZoomPercent).toBeGreaterThan(100);
});

it("lands a Zoom button's glide on a plain wheel, so the wheel scrolls the landed zoom", () => {
// The glide times itself by performance.now().
vi.useFakeTimers({
toFake: [
"requestAnimationFrame",
"cancelAnimationFrame",
"setTimeout",
"clearTimeout",
"performance",
],
});
const scroll = scrollBox(0);
mount({ scroll });
act(() => {
zoomTimelineStep("in");
vi.advanceTimersToNextFrame();
});
expect(usePlayerStore.getState().timelinePps).toBe(100);
previewWheel(scroll, {});
expect(usePlayerStore.getState().timelinePps).toBeCloseTo(200);
});

it("keeps previewing a pinch through a scroll, laying it out only at rest", () => {
const scroll = scrollBox(0);
mount({ scroll });
Expand Down
6 changes: 5 additions & 1 deletion packages/studio/src/player/components/useTimelinePlayhead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
currentTimelineZoomPercent,
requestTimelineZoom,
registerTimelineZoomViewport,
landTimelineZoomEase,
redrawTimelineZoomPreview,
settleTimelineZoom,
subscribeTimelineZoomPreview,
Expand Down Expand Up @@ -256,7 +257,10 @@ export function useTimelinePlayhead({
// Trackpad pinch arrives as ctrl+wheel; Cmd+wheel zooms too, as Mac editors do.
const handlePinchWheel = useCallback(
(e: WheelEvent) => {
if (!e.ctrlKey && !e.metaKey) return;
if (!e.ctrlKey && !e.metaKey) {
landTimelineZoomEase();
return;
}
const scroll = scrollRef.current;
if (!scroll || durationRef.current <= 0 || fitPpsRef.current <= 0 || ppsRef.current <= 0)
return;
Expand Down
Loading