Skip to content

Commit 5120e2e

Browse files
committed
feat: timeline zoom anchoring
1 parent 1aec9c5 commit 5120e2e

File tree

2 files changed

+40
-9
lines changed

2 files changed

+40
-9
lines changed

apps/web/src/constants/timeline-constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const TIMELINE_CONSTANTS = {
3939
ZOOM_MIN: 0.1,
4040
ZOOM_MAX: 100,
4141
ZOOM_BUTTON_FACTOR: 1.7,
42+
ZOOM_ANCHOR_PLAYHEAD_THRESHOLD: 0.15,
4243
} as const;
4344

4445
export const DEFAULT_TIMELINE_VIEW_STATE: TTimelineViewState = {

apps/web/src/hooks/timeline/use-timeline-zoom.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "react";
1010
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
1111
import { useEditor } from "@/hooks/use-editor";
12+
import { zoomToSlider } from "@/lib/timeline/zoom-utils";
1213

1314
interface UseTimelineZoomProps {
1415
containerRef: RefObject<HTMLDivElement | null>;
@@ -122,18 +123,47 @@ export function useTimelineZoom({
122123
if (previousZoom === zoomLevel) return;
123124

124125
const scrollElement = tracksScrollRef.current;
125-
if (scrollElement) {
126-
editor.project.setTimelineViewState({
127-
viewState: {
128-
zoomLevel,
129-
scrollLeft: scrollElement.scrollLeft,
130-
playheadTime: editor.playback.getCurrentTime(),
131-
},
132-
});
126+
if (!scrollElement) {
127+
previousZoomRef.current = zoomLevel;
128+
return;
129+
}
130+
131+
const currentScrollLeft = scrollElement.scrollLeft;
132+
const playheadTime = editor.playback.getCurrentTime();
133+
const sliderPercent = zoomToSlider({ zoomLevel, minZoom });
134+
135+
if (sliderPercent >= TIMELINE_CONSTANTS.ZOOM_ANCHOR_PLAYHEAD_THRESHOLD) {
136+
const playheadPixelsBefore =
137+
playheadTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * previousZoom;
138+
const playheadPixelsAfter =
139+
playheadTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
140+
141+
const viewportOffset = playheadPixelsBefore - currentScrollLeft;
142+
const newScrollLeft = playheadPixelsAfter - viewportOffset;
143+
144+
const maxScrollLeft =
145+
scrollElement.scrollWidth - scrollElement.clientWidth;
146+
const clampedScrollLeft = Math.max(
147+
0,
148+
Math.min(maxScrollLeft, newScrollLeft),
149+
);
150+
151+
scrollElement.scrollLeft = clampedScrollLeft;
152+
if (rulerScrollRef.current) {
153+
rulerScrollRef.current.scrollLeft = clampedScrollLeft;
154+
}
133155
}
134156

135157
previousZoomRef.current = zoomLevel;
136-
}, [zoomLevel, editor, tracksScrollRef]);
158+
159+
editor.project.setTimelineViewState({
160+
viewState: {
161+
zoomLevel,
162+
scrollLeft: scrollElement.scrollLeft,
163+
playheadTime,
164+
},
165+
});
166+
}, [zoomLevel, editor, tracksScrollRef, rulerScrollRef, minZoom]);
137167

138168
// biome-ignore lint/correctness/useExhaustiveDependencies: tracksScrollRef is a stable ref
139169
const saveScrollPosition = useCallback(() => {

0 commit comments

Comments
 (0)