Files
flatrender/src/lib/studio-timeline.ts
T

113 lines
3.4 KiB
TypeScript

import type { Scene } from "@/lib/studio-types";
export const TIMELINE_ZOOM_LEVELS = [30, 60, 90, 120] as const;
export type TimelineZoomLevel = (typeof TIMELINE_ZOOM_LEVELS)[number];
export const DEFAULT_PX_PER_SECOND: TimelineZoomLevel = 60;
/** Compact scale for scene thumbnail strip (Renderforest-style) */
export const STRIP_PX_PER_SECOND = 24;
export const MIN_SCENE_DURATION = 1;
export const MAX_SCENE_DURATION = 30;
export const SCENE_BLOCK_COLORS = [
{ base: "bg-blue-600", active: "bg-blue-500" },
{ base: "bg-purple-600", active: "bg-purple-500" },
{ base: "bg-green-600", active: "bg-green-500" },
{ base: "bg-orange-600", active: "bg-orange-500" },
] as const;
/** Inline gradient styles for scene thumbnails — avoids Tailwind purging dynamic class names */
export const SCENE_THUMB_GRADIENTS = [
{ backgroundImage: "linear-gradient(135deg,#60a5fa,#8b5cf6)" },
{ backgroundImage: "linear-gradient(135deg,#a78bfa,#ec4899)" },
{ backgroundImage: "linear-gradient(135deg,#22d3ee,#3b82f6)" },
{ backgroundImage: "linear-gradient(135deg,#34d399,#14b8a6)" },
{ backgroundImage: "linear-gradient(135deg,#fbbf24,#f97316)" },
{ backgroundImage: "linear-gradient(135deg,#fb7185,#ef4444)" },
] as const;
export function formatTimelineTime(seconds: number): string {
const safe = Math.max(0, seconds);
const mins = Math.floor(safe / 60);
const secs = Math.floor(safe % 60);
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}
export function getProjectDuration(scenes: Scene[]): number {
return scenes.reduce((total, scene) => total + scene.duration, 0);
}
export interface SceneTimelineSegment {
scene: Scene;
startTime: number;
index: number;
}
export function getSceneTimelineSegments(
scenes: Scene[]
): SceneTimelineSegment[] {
let startTime = 0;
return scenes.map((scene, index) => {
const segment = { scene, startTime, index };
startTime += scene.duration;
return segment;
});
}
export function getSceneAtTime(scenes: Scene[], time: number): Scene | undefined {
const segments = getSceneTimelineSegments(scenes);
if (segments.length === 0) return undefined;
const total = getProjectDuration(scenes);
if (time >= total) {
return segments[segments.length - 1]?.scene;
}
return segments.find(
(segment) =>
time >= segment.startTime &&
time < segment.startTime + segment.scene.duration
)?.scene;
}
export function getSceneStartTime(scenes: Scene[], sceneId: string): number {
let start = 0;
for (const scene of scenes) {
if (scene.id === sceneId) return start;
start += scene.duration;
}
return 0;
}
export function clampSceneDuration(duration: number): number {
return Math.min(
MAX_SCENE_DURATION,
Math.max(MIN_SCENE_DURATION, Math.round(duration * 10) / 10)
);
}
export function getNextZoomLevel(
current: number,
direction: "in" | "out"
): TimelineZoomLevel {
const index = TIMELINE_ZOOM_LEVELS.findIndex((level) => level === current);
const resolvedIndex = index === -1 ? 1 : index;
if (direction === "in") {
return TIMELINE_ZOOM_LEVELS[
Math.min(resolvedIndex + 1, TIMELINE_ZOOM_LEVELS.length - 1)
];
}
return TIMELINE_ZOOM_LEVELS[Math.max(resolvedIndex - 1, 0)];
}
export function snapZoomLevel(value: number): TimelineZoomLevel {
const snapped = TIMELINE_ZOOM_LEVELS.reduce((prev, level) =>
Math.abs(level - value) < Math.abs(prev - value) ? level : prev
);
return snapped;
}