From f1043734438c8518a302f698d9ae6583a75f0c14 Mon Sep 17 00:00:00 2001 From: cashmere Date: Sun, 16 Aug 2026 06:16:56 +0200 Subject: feat: add transitions, track mixing, and voiceover controls --- apps/web/src/timeline/scenes.ts | 229 ++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 114 deletions(-) (limited to 'apps/web/src/timeline/scenes.ts') diff --git a/apps/web/src/timeline/scenes.ts b/apps/web/src/timeline/scenes.ts index 8aa1b6b5..155bda96 100644 --- a/apps/web/src/timeline/scenes.ts +++ b/apps/web/src/timeline/scenes.ts @@ -1,114 +1,115 @@ -import type { TScene } from "@/timeline"; -import { generateUUID } from "@/utils/id"; -import { calculateTotalDuration } from "@/timeline"; -import { MAIN_TRACK_NAME } from "@/timeline/placement/main-track"; -import { type MediaTime, ZERO_MEDIA_TIME } from "@/wasm"; - -export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null { - return scenes.find((scene) => scene.isMain) || null; -} - -export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] { - const hasMain = scenes.some((scene) => scene.isMain); - if (!hasMain) { - const mainScene = buildDefaultScene({ name: "Main scene", isMain: true }); - return [mainScene, ...scenes]; - } - return scenes; -} - -export function buildDefaultScene({ - name, - isMain, -}: { - name: string; - isMain: boolean; -}): TScene { - return { - id: generateUUID(), - name, - isMain, - tracks: { - overlay: [], - main: { - id: generateUUID(), - name: MAIN_TRACK_NAME, - type: "video", - elements: [], - muted: false, - hidden: false, - }, - audio: [], - }, - bookmarks: [], - createdAt: new Date(), - updatedAt: new Date(), - }; -} - -export function canDeleteScene({ scene }: { scene: TScene }): { - canDelete: boolean; - reason?: string; -} { - if (scene.isMain) { - return { canDelete: false, reason: "Cannot delete main scene" }; - } - return { canDelete: true }; -} - -export function getFallbackSceneAfterDelete({ - scenes, - deletedSceneId, - currentSceneId, -}: { - scenes: TScene[]; - deletedSceneId: string; - currentSceneId: string | null; -}): TScene | null { - if (currentSceneId !== deletedSceneId) { - return scenes.find((s) => s.id === currentSceneId) || null; - } - return getMainScene({ scenes }); -} - -export function findCurrentScene({ - scenes, - currentSceneId, -}: { - scenes: TScene[]; - currentSceneId: string; -}): TScene | null { - return ( - scenes.find((s) => s.id === currentSceneId) || - getMainScene({ scenes }) || - scenes[0] || - null - ); -} - -export function getProjectDurationFromScenes({ - scenes, -}: { - scenes: TScene[]; -}): MediaTime { - const mainScene = getMainScene({ scenes }) ?? scenes[0] ?? null; - if (!mainScene?.tracks) { - return ZERO_MEDIA_TIME; - } - - return calculateTotalDuration({ tracks: mainScene.tracks }); -} - -export function updateSceneInArray({ - scenes, - sceneId, - updates, -}: { - scenes: TScene[]; - sceneId: string; - updates: Partial; -}): TScene[] { - return scenes.map((scene) => - scene.id === sceneId ? { ...scene, ...updates } : scene, - ); -} +import type { TScene } from "@/timeline"; +import { generateUUID } from "@/utils/id"; +import { calculateTotalDuration } from "@/timeline"; +import { MAIN_TRACK_NAME } from "@/timeline/placement/main-track"; +import { type MediaTime, ZERO_MEDIA_TIME } from "@/wasm"; + +export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null { + return scenes.find((scene) => scene.isMain) || null; +} + +export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] { + const hasMain = scenes.some((scene) => scene.isMain); + if (!hasMain) { + const mainScene = buildDefaultScene({ name: "Main scene", isMain: true }); + return [mainScene, ...scenes]; + } + return scenes; +} + +export function buildDefaultScene({ + name, + isMain, +}: { + name: string; + isMain: boolean; +}): TScene { + return { + id: generateUUID(), + name, + isMain, + tracks: { + overlay: [], + main: { + id: generateUUID(), + name: MAIN_TRACK_NAME, + type: "video", + elements: [], + muted: false, + volume: 0, + hidden: false, + }, + audio: [], + }, + bookmarks: [], + createdAt: new Date(), + updatedAt: new Date(), + }; +} + +export function canDeleteScene({ scene }: { scene: TScene }): { + canDelete: boolean; + reason?: string; +} { + if (scene.isMain) { + return { canDelete: false, reason: "Cannot delete main scene" }; + } + return { canDelete: true }; +} + +export function getFallbackSceneAfterDelete({ + scenes, + deletedSceneId, + currentSceneId, +}: { + scenes: TScene[]; + deletedSceneId: string; + currentSceneId: string | null; +}): TScene | null { + if (currentSceneId !== deletedSceneId) { + return scenes.find((s) => s.id === currentSceneId) || null; + } + return getMainScene({ scenes }); +} + +export function findCurrentScene({ + scenes, + currentSceneId, +}: { + scenes: TScene[]; + currentSceneId: string; +}): TScene | null { + return ( + scenes.find((s) => s.id === currentSceneId) || + getMainScene({ scenes }) || + scenes[0] || + null + ); +} + +export function getProjectDurationFromScenes({ + scenes, +}: { + scenes: TScene[]; +}): MediaTime { + const mainScene = getMainScene({ scenes }) ?? scenes[0] ?? null; + if (!mainScene?.tracks) { + return ZERO_MEDIA_TIME; + } + + return calculateTotalDuration({ tracks: mainScene.tracks }); +} + +export function updateSceneInArray({ + scenes, + sceneId, + updates, +}: { + scenes: TScene[]; + sceneId: string; + updates: Partial; +}): TScene[] { + return scenes.map((scene) => + scene.id === sceneId ? { ...scene, ...updates } : scene, + ); +} -- cgit v1.3.1