From 8210925f85470b3f09cb454b9586c3577832e237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 6 Oct 2026 04:25:36 -0400 Subject: [PATCH 01/26] perf(studio): coalesce timeline zoom to one write per frame and zoom to a range --- .../src/components/TimelineToolbar.test.tsx | 14 +- .../studio/src/components/TimelineToolbar.tsx | 23 +-- packages/studio/src/index.ts | 1 + .../src/player/components/AudioWaveform.tsx | 93 +++++++--- .../player/components/TimelineClipFades.tsx | 7 +- .../src/player/components/VideoThumbnail.tsx | 8 +- .../src/player/components/timelineMotion.ts | 46 +++++ .../player/components/timelineShellMeta.tsx | 2 + .../components/timelineZoomInput.test.ts | 139 +++++++++++++++ .../player/components/timelineZoomInput.ts | 160 ++++++++++++++++++ .../player/components/useTimelineGeometry.ts | 3 - .../components/useTimelinePlayhead.test.tsx | 89 +++++++--- .../player/components/useTimelinePlayhead.ts | 87 ++++------ .../components/useTimelineProviderState.tsx | 37 ++-- .../src/player/components/useTimelineZoom.ts | 12 +- 15 files changed, 562 insertions(+), 159 deletions(-) create mode 100644 packages/studio/src/player/components/timelineMotion.ts create mode 100644 packages/studio/src/player/components/timelineZoomInput.test.ts create mode 100644 packages/studio/src/player/components/timelineZoomInput.ts diff --git a/packages/studio/src/components/TimelineToolbar.test.tsx b/packages/studio/src/components/TimelineToolbar.test.tsx index a120f34acec..3b85e4382d2 100644 --- a/packages/studio/src/components/TimelineToolbar.test.tsx +++ b/packages/studio/src/components/TimelineToolbar.test.tsx @@ -2,7 +2,7 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import { usePlayerStore } from "../player/store/playerStore"; import { makeSelection } from "../hooks/domSelectionTestHarness"; @@ -239,6 +239,16 @@ describe("TimelineToolbar snap key", () => { }); describe("TimelineToolbar Fit", () => { + // A zoom lands on the next frame; these tests step frames instead of waiting for them. + beforeEach(() => { + vi.useFakeTimers({ + toFake: ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout"], + }); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it("shows Fit as a named icon and says whether fit is on", () => { const { host, root } = renderToolbar(); const fit = () => host.querySelector('button[aria-label="Fit timeline to width"]'); @@ -251,6 +261,7 @@ describe("TimelineToolbar Fit", () => { .querySelector('button[aria-label="Zoom in"]') ?.dispatchEvent(new MouseEvent("click", { bubbles: true })), ); + act(() => vi.advanceTimersToNextFrame()); expect(fit()?.getAttribute("aria-pressed")).toBe("false"); act(() => root.unmount()); }); @@ -263,6 +274,7 @@ describe("TimelineToolbar Fit", () => { .querySelector('button[aria-label="Zoom in"]') ?.dispatchEvent(new MouseEvent("click", { bubbles: true })), ); + act(() => vi.advanceTimersToNextFrame()); expect(usePlayerStore.getState().userZoomCount).toBe(before + 1); act(() => root.unmount()); }); diff --git a/packages/studio/src/components/TimelineToolbar.tsx b/packages/studio/src/components/TimelineToolbar.tsx index f4d0a18d1de..8906a9175e7 100644 --- a/packages/studio/src/components/TimelineToolbar.tsx +++ b/packages/studio/src/components/TimelineToolbar.tsx @@ -14,12 +14,12 @@ import { import { computeElementPercentage, playsNear } from "../hooks/gsapShared"; import { useKeyframeKeyboard } from "../hooks/useKeyframeKeyboard"; import { - getNextTimelineZoomPercent, getTimelineZoomPercent, timelineZoomPercentToSlider, timelineSliderToZoomPercent, } from "../player/components/timelineZoom"; import { useTimelineZoom } from "../player/components/useTimelineZoom"; +import { zoomTimelineStep } from "../player/components/timelineZoomInput"; import { usePlayerStore, type TimelineElement } from "../player"; import { Tooltip } from "./ui"; import { AudioMetersIcon } from "./icons/AudioMetersIcon"; @@ -510,12 +510,7 @@ export const TimelineToolbar = memo(function TimelineToolbar({