/** * Frame-accurate utilities for VFX review. * All frame calculations use 0-based frame numbers * unless documented otherwise. */ /** Convert frame number to video currentTime in seconds */ export function frameToTime(frame: number, fps: number): number { return frame / fps; } /** Convert video currentTime to frame number (0-based) */ export function timeToFrame(time: number, fps: number): number { return Math.floor(time * fps); } /** Format a frame number as a timecode string: MM:SS:FF */ export function frameToTimecode(frame: number, fps: number): string { const totalSeconds = frame / fps; const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = Math.floor(totalSeconds % 60); const frames = frame % Math.round(fps); if (hours > 0) { return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}:${String(frames).padStart(2, "0")}`; } return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}:${String(frames).padStart(2, "0")}`; } /** Format a raw time (seconds) as MM:SS.mmm */ export function timeToDisplay(time: number): string { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); const ms = Math.floor((time % 1) * 1000); return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(ms).padStart(3, "0")}`; } /** Given duration and fps, compute total frame count */ export function durationToFrameCount(duration: number, fps: number): number { return Math.floor(duration * fps); } /** Clamp a frame number to [0, totalFrames - 1] */ export function clampFrame(frame: number, totalFrames: number): number { return Math.max(0, Math.min(totalFrames - 1, frame)); } /** Step a single frame forward or backward */ export function stepFrame( currentFrame: number, delta: number, totalFrames: number ): number { return clampFrame(currentFrame + delta, totalFrames); } /** Convert a timeline position (0-1) to frame number */ export function positionToFrame(position: number, totalFrames: number): number { return clampFrame(Math.floor(position * totalFrames), totalFrames); } /** Convert a frame number to timeline position (0-1) */ export function frameToPosition(frame: number, totalFrames: number): number { if (totalFrames === 0) return 0; return frame / totalFrames; } /** Common VFX frame rates */ export const COMMON_FPS = [23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60] as const; export type CommonFPS = (typeof COMMON_FPS)[number]; /** Human-readable fps label */ export function fpsLabel(fps: number): string { if (fps === 23.976) return "23.976"; return String(fps); }