113 lines
3.4 KiB
TypeScript
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;
|
||
|
|
}
|