mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): restore overscan and publish the zoom viewport (#5151)
* fix(studio): restore the timeline overscan budget * test(studio): align window witnesses with the restored budget * fix(studio): publish the viewport when a zoom anchor lands * fix(studio): publish the viewport when a zoom-out ends at its laid-out scale An eased zoom-out that ends at the scale already laid out only moves the scroll position, so the timeline drew from the old position until the next scroll event. The zoom input now publishes that scroll through the same callback the zoom anchor uses. * fix(studio): a pan at the laid-out scale publishes its scroll before paint The same-scale ending of a zoom ran outside React, so its scroll publish rendered on the next task, after the browser painted the old window. It now flushes like the scale change beside it, and its test pans at the laid-out scale instead of changing it. * test(studio): a same-scale pan must render its scroll before paint Reads the render window outside act, so unwrapping the flush fails it, and records each published scroll at call time instead of reading the live element afterwards.
This commit is contained in:
@@ -85,10 +85,20 @@ jobs:
|
||||
- name: Reject accidental file deletions
|
||||
if: github.event_name == 'pull_request'
|
||||
run: node scripts/check-no-main-deletions.mjs --base origin/main
|
||||
# HEAD is the PR merge ref; its first parent is the current base tip, unlike the payload's
|
||||
# base.sha, which goes stale and would count the base's newer commits as the PR's changes.
|
||||
- name: Find the pull request's current base
|
||||
id: base
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git rev-parse --verify -q HEAD^2 > /dev/null || { echo "::error::HEAD is not the pull request merge commit"; exit 1; }
|
||||
sha="$(git rev-parse --verify HEAD^1)"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
base: ${{ steps.base.outputs.sha }}
|
||||
filters: |
|
||||
catalog_index:
|
||||
- "registry/**"
|
||||
|
||||
@@ -30,10 +30,20 @@ jobs:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# HEAD is the PR merge ref; its first parent is the current base tip, unlike the payload's
|
||||
# base.sha, which goes stale and would count the base's newer commits as the PR's changes.
|
||||
- name: Find the pull request's current base
|
||||
id: base
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git rev-parse --verify -q HEAD^2 > /dev/null || { echo "::error::HEAD is not the pull request merge commit"; exit 1; }
|
||||
sha="$(git rev-parse --verify HEAD^1)"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
base: ${{ steps.base.outputs.sha }}
|
||||
filters: |
|
||||
perf:
|
||||
- "packages/player/**"
|
||||
|
||||
@@ -29,10 +29,20 @@ jobs:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# HEAD is the PR merge ref; its first parent is the current base tip, unlike the payload's
|
||||
# base.sha, which goes stale and would count the base's newer commits as the PR's changes.
|
||||
- name: Find the pull request's current base
|
||||
id: base
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git rev-parse --verify -q HEAD^2 > /dev/null || { echo "::error::HEAD is not the pull request merge commit"; exit 1; }
|
||||
sha="$(git rev-parse --verify HEAD^1)"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
base: ${{ steps.base.outputs.sha }}
|
||||
filters: |
|
||||
preview:
|
||||
- "packages/core/**"
|
||||
|
||||
@@ -50,11 +50,21 @@ jobs:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# HEAD is the PR merge ref; its first parent is the current base tip, unlike the payload's
|
||||
# base.sha, which goes stale and would count the base's newer commits as the PR's changes.
|
||||
- name: Find the pull request's current base
|
||||
id: base
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git rev-parse --verify -q HEAD^2 > /dev/null || { echo "::error::HEAD is not the pull request merge commit"; exit 1; }
|
||||
sha="$(git rev-parse --verify HEAD^1)"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
|
||||
id: filter
|
||||
if: github.event_name != 'schedule'
|
||||
with:
|
||||
token: ""
|
||||
base: ${{ steps.base.outputs.sha }}
|
||||
filters: |
|
||||
code:
|
||||
- "packages/core/**"
|
||||
|
||||
@@ -53,10 +53,20 @@ jobs:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# HEAD is the PR merge ref; its first parent is the current base tip, unlike the payload's
|
||||
# base.sha, which goes stale and would count the base's newer commits as the PR's changes.
|
||||
- name: Find the pull request's current base
|
||||
id: base
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git rev-parse --verify -q HEAD^2 > /dev/null || { echo "::error::HEAD is not the pull request merge commit"; exit 1; }
|
||||
sha="$(git rev-parse --verify HEAD^1)"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
base: ${{ steps.base.outputs.sha }}
|
||||
# A file counts only if it matches every pattern. Player and Studio `src/` run in a browser
|
||||
# and no code reads a package README, so a diff confined to them skips Windows.
|
||||
predicate-quantifier: every
|
||||
|
||||
@@ -271,7 +271,7 @@ describe("TimelineToolbar Fit", () => {
|
||||
timelineFitPps: 10,
|
||||
timelinePps: 10,
|
||||
});
|
||||
return registerTimelineZoomViewport({ scroll, contentOrigin: 32 });
|
||||
return registerTimelineZoomViewport({ scroll, contentOrigin: 32, publishScroll: () => {} });
|
||||
}
|
||||
|
||||
it("moves the slider and its readout with a zoom while it is previewed", () => {
|
||||
|
||||
@@ -27,13 +27,13 @@ import { resolveInsertRow } from "./timelineCollision";
|
||||
import { getTimelineRenderTimeRange } from "./timelineViewportGeometry";
|
||||
|
||||
describe("horizontal timeline window", () => {
|
||||
it("adds the shared half-viewport overscan on each side and clamps to duration", () => {
|
||||
it("adds the shared quarter-viewport overscan on each side and clamps to duration", () => {
|
||||
expect(getTimelineRenderTimeRange({ scrollLeft: 300, clientWidth: 500 }, 100, 200, 20)).toEqual(
|
||||
{ start: 0, end: 8.5 },
|
||||
{ start: 0, end: 7.25 },
|
||||
);
|
||||
expect(
|
||||
getTimelineRenderTimeRange({ scrollLeft: 1_900, clientWidth: 500 }, 100, 200, 20),
|
||||
).toEqual({ start: 14.5, end: 20 });
|
||||
).toEqual({ start: 15.75, end: 20 });
|
||||
});
|
||||
|
||||
it("generates globally aligned ticks directly inside the bounded window", () => {
|
||||
|
||||
@@ -49,6 +49,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
let unregisterViewport = () => {};
|
||||
const publishScroll = vi.fn();
|
||||
|
||||
/** A 1080px timeline viewport with 32px of track headers, holding one scaled row. */
|
||||
function viewport(scrollLeft = 0, scrollWidth = 20_000) {
|
||||
@@ -60,7 +61,7 @@ function viewport(scrollLeft = 0, scrollWidth = 20_000) {
|
||||
});
|
||||
const row = scroll.appendChild(document.createElement("div"));
|
||||
row.setAttribute("data-timeline-zoom-scale", "");
|
||||
unregisterViewport = registerTimelineZoomViewport({ scroll, contentOrigin: 32 });
|
||||
unregisterViewport = registerTimelineZoomViewport({ scroll, contentOrigin: 32, publishScroll });
|
||||
return { scroll, row };
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ describe("requestTimelineZoom", () => {
|
||||
it("lays a zoom-out about the left edge out at once, before it shows unmounted time", () => {
|
||||
usePlayerStore.setState({ duration: 1000 });
|
||||
viewport();
|
||||
// Mounted to (1080 - 32 + 540) / 10 = 158.8 s; at 5 px/s the view reaches 209.6 s.
|
||||
// Mounted to (1080 - 32 + 270) / 10 = 131.8 s; at 5 px/s the view reaches 209.6 s.
|
||||
requestTimelineZoom(50, { time: 0, x: 32 });
|
||||
vi.advanceTimersToNextFrame();
|
||||
expect(usePlayerStore.getState().timelinePps).toBe(5);
|
||||
@@ -134,14 +135,14 @@ describe("requestTimelineZoom", () => {
|
||||
timelinePps: 100,
|
||||
});
|
||||
viewport(5000);
|
||||
// Mounted 44.28..65.88 s; at 60 px/s about 55.24 s the view shows 46.5..63.97 s.
|
||||
requestTimelineZoom(600, { time: 55.24, x: 556 });
|
||||
// Mounted 46.98..63.18 s; at 70 px/s about 55.24 s the view shows 47.75..62.73 s.
|
||||
requestTimelineZoom(700, { time: 55.24, x: 556 });
|
||||
vi.advanceTimersToNextFrame();
|
||||
expect(usePlayerStore.getState().timelinePps).toBe(100);
|
||||
});
|
||||
|
||||
it("lays out a zoom-out before it shows past the window ruler ticks are drawn in", () => {
|
||||
// 50 s of clips in content 1996 s wide: ticks are drawn to 157 s, a view and a half in.
|
||||
// 50 s of clips in content 1996 s wide: ticks are drawn to 131.8 s, a view and a quarter in.
|
||||
usePlayerStore.setState({ duration: 50 });
|
||||
viewport();
|
||||
requestTimelineZoom(60, { time: 0, x: 32 });
|
||||
@@ -230,7 +231,7 @@ describe("requestTimelineZoom", () => {
|
||||
const { scroll } = viewport();
|
||||
requestTimelineZoom(150);
|
||||
unregisterViewport();
|
||||
unregisterViewport = registerTimelineZoomViewport({ scroll, contentOrigin: 32 });
|
||||
unregisterViewport = registerTimelineZoomViewport({ scroll, contentOrigin: 32, publishScroll });
|
||||
await Promise.resolve();
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(usePlayerStore.getState().timelinePps).toBe(15);
|
||||
@@ -240,6 +241,7 @@ describe("requestTimelineZoom", () => {
|
||||
const unregisterOlder = registerTimelineZoomViewport({
|
||||
scroll: document.createElement("div"),
|
||||
contentOrigin: 32,
|
||||
publishScroll,
|
||||
});
|
||||
viewport();
|
||||
unregisterOlder();
|
||||
@@ -258,6 +260,25 @@ describe("requestTimelineZoom", () => {
|
||||
});
|
||||
|
||||
describe("zoomTimelineToRange", () => {
|
||||
it("tells the timeline where a pan at the laid-out scale scrolled to", () => {
|
||||
usePlayerStore.setState({
|
||||
duration: 1000,
|
||||
zoomMode: "manual",
|
||||
manualZoomPercent: 1000,
|
||||
timelineFitPps: 10,
|
||||
timelinePps: 100,
|
||||
});
|
||||
const { scroll } = viewport(0);
|
||||
const published: number[] = [];
|
||||
publishScroll.mockImplementation((el: HTMLDivElement) => published.push(el.scrollLeft));
|
||||
void zoomTimelineToRange(60, 70);
|
||||
for (let i = 0; i < 40; i++) vi.advanceTimersToNextFrame();
|
||||
publishScroll.mockReset();
|
||||
expect(usePlayerStore.getState().timelinePps).toBe(100);
|
||||
expect(scroll.scrollLeft).toBeGreaterThan(5000);
|
||||
expect(published.at(-1)).toBe(scroll.scrollLeft);
|
||||
});
|
||||
|
||||
it("fills the width with the range and puts its start at the left margin", () => {
|
||||
viewport();
|
||||
void zoomTimelineToRange(40, 90, { smooth: false });
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface TimelineZoomAnchor {
|
||||
export interface TimelineZoomViewport {
|
||||
scroll: HTMLDivElement;
|
||||
contentOrigin: number;
|
||||
publishScroll: (scroll: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
/** How an eased zoom ended: it reached its range, or a person's zoom or the caller stopped it. */
|
||||
@@ -209,7 +210,10 @@ function commitPreview() {
|
||||
);
|
||||
}
|
||||
preview = null;
|
||||
if (Math.abs(view.scroll.scrollLeft - left) >= 0.5) view.scroll.scrollLeft = left;
|
||||
if (Math.abs(view.scroll.scrollLeft - left) >= 0.5) {
|
||||
view.scroll.scrollLeft = left;
|
||||
flushSync(() => view.publishScroll(view.scroll));
|
||||
}
|
||||
clearScaled(view.scroll);
|
||||
emitPreview();
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, useRef } from "react";
|
||||
import { act, useLayoutEffect, useRef } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
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";
|
||||
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||
|
||||
const ORIGIN = 32;
|
||||
|
||||
function scrollBox(scrollLeft: number) {
|
||||
function scrollBox(scrollLeft: number, clientWidth = 800) {
|
||||
const el = document.createElement("div");
|
||||
let left = scrollLeft;
|
||||
Object.defineProperties(el, {
|
||||
clientWidth: { value: 800 },
|
||||
clientWidth: { value: clientWidth },
|
||||
scrollWidth: { value: 20_000 },
|
||||
scrollLeft: { get: () => left, set: (v: number) => (left = v) },
|
||||
});
|
||||
@@ -27,9 +30,16 @@ interface HarnessProps {
|
||||
scroll: HTMLDivElement;
|
||||
dragging?: boolean;
|
||||
zoomMode?: ZoomMode;
|
||||
syncScrollViewport?: (scroll: HTMLDivElement) => void;
|
||||
}
|
||||
|
||||
function Harness({ pps: fixedPps, scroll, dragging = false, zoomMode = "manual" }: HarnessProps) {
|
||||
function Harness({
|
||||
pps: fixedPps,
|
||||
scroll,
|
||||
dragging = false,
|
||||
zoomMode = "manual",
|
||||
syncScrollViewport = () => {},
|
||||
}: HarnessProps) {
|
||||
const storePps = usePlayerStore((s) => s.timelinePps);
|
||||
const pps = fixedPps ?? storePps;
|
||||
const scrollRef = useRef(scroll);
|
||||
@@ -37,6 +47,7 @@ function Harness({ pps: fixedPps, scroll, dragging = false, zoomMode = "manual"
|
||||
useTimelinePlayhead({
|
||||
playheadRef: { current: document.createElement("div") },
|
||||
scrollRef,
|
||||
syncScrollViewport,
|
||||
ppsRef: { current: pps },
|
||||
durationRef,
|
||||
isDragging: { current: dragging },
|
||||
@@ -238,6 +249,81 @@ describe("useTimelinePlayhead follow while paused", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTimelinePlayhead committed viewport", () => {
|
||||
function mountViewport(scrollLeft: number) {
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
usePlayerStore.setState({
|
||||
zoomMode: "manual",
|
||||
manualZoomPercent: 1000,
|
||||
timelineFitPps: 10,
|
||||
timelinePps: 100,
|
||||
});
|
||||
const scroll = scrollBox(scrollLeft, 1080);
|
||||
const container = document.createElement("div");
|
||||
function Probe() {
|
||||
const pps = usePlayerStore((s) => s.timelinePps);
|
||||
const { viewport, setScrollRef, syncScrollViewport } = useTimelineScrollViewport(
|
||||
useRef(scroll),
|
||||
[],
|
||||
);
|
||||
useLayoutEffect(() => setScrollRef(scroll), [setScrollRef]);
|
||||
const { renderTimeRange } = useTimelineClipRenderWindow({
|
||||
tracks: [],
|
||||
viewport,
|
||||
pixelsPerSecond: pps,
|
||||
contentOrigin: ORIGIN,
|
||||
duration: 100,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Harness scroll={scroll} syncScrollViewport={syncScrollViewport} />
|
||||
{renderTimeRange.start <= 59 && renderTimeRange.end >= 59 && <span data-clip="59" />}
|
||||
<output>{viewport.scrollLeft}</output>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
act(() => root.render(<Probe />));
|
||||
return { scroll, container };
|
||||
}
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("keeps the visible 59-second clip mounted when a pointer zoom commits", () => {
|
||||
const { scroll, container } = mountViewport(5000);
|
||||
expect(container.querySelector('[data-clip="59"]')).not.toBeNull();
|
||||
act(() => {
|
||||
requestTimelineZoom(1090, { time: 55.24, x: 556 });
|
||||
settleTimelineZoom();
|
||||
});
|
||||
expect(scroll.scrollLeft).toBeCloseTo(5497.16);
|
||||
expect(container.querySelector('[data-clip="59"]')).not.toBeNull();
|
||||
expect(Number(container.querySelector("output")?.textContent)).toBeCloseTo(5497.16);
|
||||
});
|
||||
|
||||
it("renders a pan at the laid-out scale before the browser paints", () => {
|
||||
const { scroll, container } = mountViewport(0);
|
||||
const actEnvironment = globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean };
|
||||
const wasActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
||||
actEnvironment.IS_REACT_ACT_ENVIRONMENT = false;
|
||||
try {
|
||||
requestTimelineZoom(1000, { time: 60, x: ORIGIN });
|
||||
settleTimelineZoom();
|
||||
expect(scroll.scrollLeft).toBe(6000);
|
||||
expect(container.querySelector("output")?.textContent).toBe("6000");
|
||||
} finally {
|
||||
actEnvironment.IS_REACT_ACT_ENVIRONMENT = wasActEnvironment;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTimelinePlayhead wheel zoom", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({
|
||||
|
||||
@@ -42,6 +42,7 @@ function revealPlayheadScrollLeft(
|
||||
interface UseTimelinePlayheadInput {
|
||||
playheadRef: React.RefObject<HTMLDivElement | null>;
|
||||
scrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
syncScrollViewport: (scroll: HTMLDivElement) => void;
|
||||
ppsRef: React.RefObject<number>;
|
||||
durationRef: React.RefObject<number>;
|
||||
isDragging: React.RefObject<boolean>;
|
||||
@@ -61,6 +62,7 @@ interface UseTimelinePlayheadInput {
|
||||
export function useTimelinePlayhead({
|
||||
playheadRef,
|
||||
scrollRef,
|
||||
syncScrollViewport,
|
||||
ppsRef,
|
||||
durationRef,
|
||||
isDragging,
|
||||
@@ -100,6 +102,7 @@ export function useTimelinePlayhead({
|
||||
const maxScrollLeft = Math.max(0, scroll.scrollWidth - scroll.clientWidth);
|
||||
const left = anchor.time * pps + contentOrigin - anchor.x;
|
||||
scroll.scrollLeft = Math.max(0, Math.min(maxScrollLeft, left));
|
||||
syncScrollViewport(scroll);
|
||||
return;
|
||||
}
|
||||
const zoomed = userZoomCount !== prevZoomCount;
|
||||
@@ -121,7 +124,8 @@ export function useTimelinePlayhead({
|
||||
scroll.scrollLeft = zoomed
|
||||
? revealPlayheadScrollLeft(scroll, contentOrigin + time * pps, contentOrigin, anchored)
|
||||
: anchored;
|
||||
}, [pps, userZoomCount, scrollRef, durationRef, contentOrigin]);
|
||||
syncScrollViewport(scroll);
|
||||
}, [pps, userZoomCount, scrollRef, durationRef, contentOrigin, syncScrollViewport]);
|
||||
|
||||
const syncPlayheadPosition = useCallback(
|
||||
(time: number) => {
|
||||
@@ -279,14 +283,25 @@ export function useTimelinePlayhead({
|
||||
// A press meets the zoom it sees, not the one still waiting to be laid out.
|
||||
scroll.addEventListener("pointerdown", settleTimelineZoom, { capture: true });
|
||||
scroll.addEventListener("scroll", redrawTimelineZoomPreview, { passive: true });
|
||||
const unregisterZoomViewport = registerTimelineZoomViewport({ scroll, contentOrigin });
|
||||
const unregisterZoomViewport = registerTimelineZoomViewport({
|
||||
scroll,
|
||||
contentOrigin,
|
||||
publishScroll: syncScrollViewport,
|
||||
});
|
||||
return () => {
|
||||
scroll.removeEventListener("wheel", handlePinchWheel, { capture: true });
|
||||
scroll.removeEventListener("pointerdown", settleTimelineZoom, { capture: true });
|
||||
scroll.removeEventListener("scroll", redrawTimelineZoomPreview);
|
||||
unregisterZoomViewport();
|
||||
};
|
||||
}, [handlePinchWheel, scrollRef, timelineReady, elementsLength, contentOrigin]);
|
||||
}, [
|
||||
handlePinchWheel,
|
||||
scrollRef,
|
||||
timelineReady,
|
||||
elementsLength,
|
||||
contentOrigin,
|
||||
syncScrollViewport,
|
||||
]);
|
||||
|
||||
return { seekFromX, autoScrollDuringDrag, dragScrollRaf };
|
||||
}
|
||||
|
||||
@@ -336,6 +336,7 @@ export function useTimelineProviderState({
|
||||
const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
|
||||
playheadRef,
|
||||
scrollRef,
|
||||
syncScrollViewport,
|
||||
ppsRef,
|
||||
durationRef,
|
||||
isDragging,
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("timeline viewport budgets", () => {
|
||||
expect(TIMELINE_VIEWPORT_BUDGETS).toMatchObject({
|
||||
directScrollSafetyPx: 8_000_000,
|
||||
rowOverscanPerSide: 2,
|
||||
timeOverscanViewportRatio: 0.5,
|
||||
timeOverscanViewportRatio: 0.25,
|
||||
maxMountedRows: 64,
|
||||
maxMountedClipRoots: 512,
|
||||
maxMountedClipRootsPerRow: 128,
|
||||
|
||||
@@ -61,7 +61,7 @@ export const MAX_VISIBLE_THUMBNAIL_FRAMES = Math.ceil(3840 / (66 * (16 / 9))); /
|
||||
export const TIMELINE_VIEWPORT_BUDGETS: Readonly<TimelineViewportBudgets> = Object.freeze({
|
||||
directScrollSafetyPx: 8_000_000,
|
||||
rowOverscanPerSide: 2,
|
||||
timeOverscanViewportRatio: 0.5,
|
||||
timeOverscanViewportRatio: 0.25,
|
||||
maxMountedRows: 64,
|
||||
maxMountedClipRoots: 512,
|
||||
maxMountedClipRootsPerRow: 128,
|
||||
|
||||
Reference in New Issue
Block a user