From bd9ec023eefc4b5d26fa8b66c4338e97104f0f96 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 1 Apr 2026 19:27:49 +0200 Subject: refactor: split constants by domain and isolate timeline panel from core --- apps/web/src/lib/fps/__tests__/fps.test.ts | 65 ++++++++++++++++++++++++++++++ apps/web/src/lib/fps/constants.ts | 9 +++++ apps/web/src/lib/fps/utils.ts | 39 ++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 apps/web/src/lib/fps/__tests__/fps.test.ts create mode 100644 apps/web/src/lib/fps/constants.ts create mode 100644 apps/web/src/lib/fps/utils.ts (limited to 'apps/web/src/lib/fps') diff --git a/apps/web/src/lib/fps/__tests__/fps.test.ts b/apps/web/src/lib/fps/__tests__/fps.test.ts new file mode 100644 index 00000000..fd082bb2 --- /dev/null +++ b/apps/web/src/lib/fps/__tests__/fps.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "bun:test"; +import { + getHighestImportedVideoFps, + getRaisedProjectFpsForImportedMedia, +} from "@/lib/fps/utils"; + +describe("getHighestImportedVideoFps", () => { + test("returns the highest valid video fps", () => { + expect( + getHighestImportedVideoFps({ + mediaAssets: [ + { type: "audio" }, + { type: "video", fps: 30 }, + { type: "image", fps: 120 }, + { type: "video", fps: 60 }, + ], + }), + ).toBe(60); + }); + + test("ignores missing and invalid fps values", () => { + expect( + getHighestImportedVideoFps({ + mediaAssets: [ + { type: "video" }, + { type: "video", fps: 0 }, + { type: "video", fps: -10 }, + { type: "audio", fps: 120 }, + ], + }), + ).toBeNull(); + }); +}); + +describe("getRaisedProjectFpsForImportedMedia", () => { + test("raises the project fps to match a higher-fps import", () => { + expect( + getRaisedProjectFpsForImportedMedia({ + currentFps: 30, + importedAssets: [{ type: "video", fps: 60 }], + }), + ).toBe(60); + }); + + test("does not lower the project fps for lower-fps imports", () => { + expect( + getRaisedProjectFpsForImportedMedia({ + currentFps: 60, + importedAssets: [{ type: "video", fps: 10 }], + }), + ).toBeNull(); + }); + + test("ignores non-video imports", () => { + expect( + getRaisedProjectFpsForImportedMedia({ + currentFps: 30, + importedAssets: [ + { type: "image", fps: 60 }, + { type: "audio", fps: 120 }, + ], + }), + ).toBeNull(); + }); +}); diff --git a/apps/web/src/lib/fps/constants.ts b/apps/web/src/lib/fps/constants.ts new file mode 100644 index 00000000..100c043a --- /dev/null +++ b/apps/web/src/lib/fps/constants.ts @@ -0,0 +1,9 @@ +export const FPS_PRESETS = [ + { value: "24", label: "24 fps" }, + { value: "25", label: "25 fps" }, + { value: "30", label: "30 fps" }, + { value: "60", label: "60 fps" }, + { value: "120", label: "120 fps" }, +] as const; + +export const DEFAULT_FPS = 30; diff --git a/apps/web/src/lib/fps/utils.ts b/apps/web/src/lib/fps/utils.ts new file mode 100644 index 00000000..8e3221a1 --- /dev/null +++ b/apps/web/src/lib/fps/utils.ts @@ -0,0 +1,39 @@ +import type { MediaAsset } from "@/lib/media/types"; + +type MediaAssetFpsInput = Pick; + +export function getHighestImportedVideoFps({ + mediaAssets, +}: { + mediaAssets: MediaAssetFpsInput[]; +}): number | null { + let highestFps: number | null = null; + + for (const asset of mediaAssets) { + const fps = asset.fps ?? Number.NaN; + if (asset.type !== "video") continue; + if (!Number.isFinite(fps) || fps <= 0) continue; + + highestFps = highestFps === null ? fps : Math.max(highestFps, fps); + } + + return highestFps; +} + +export function getRaisedProjectFpsForImportedMedia({ + currentFps, + importedAssets, +}: { + currentFps: number; + importedAssets: MediaAssetFpsInput[]; +}): number | null { + const highestImportedVideoFps = getHighestImportedVideoFps({ + mediaAssets: importedAssets, + }); + + if (highestImportedVideoFps === null || highestImportedVideoFps <= currentFps) { + return null; + } + + return highestImportedVideoFps; +} -- cgit v1.3.1