From 7bf0984698e43b9aa2dd879f65951318ed410390 Mon Sep 17 00:00:00 2001 From: Maze <167211895+mazeincoding@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:20:04 +0100 Subject: codebase overhaul (#697) --- apps/web/src/lib/time.ts | 357 +++++++++++++++++++++++++++++------------------ 1 file changed, 220 insertions(+), 137 deletions(-) (limited to 'apps/web/src/lib/time.ts') diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts index 20e2ff6d..b6761b0d 100644 --- a/apps/web/src/lib/time.ts +++ b/apps/web/src/lib/time.ts @@ -1,137 +1,220 @@ -// Time-related utility functions - -import { DEFAULT_FPS } from "@/stores/project-store"; - -export type TimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF"; - -// Helper function to format time in various formats (MM:SS, HH:MM:SS, HH:MM:SS:CS, HH:MM:SS:FF) -export const formatTimeCode = ( - timeInSeconds: number, - format: TimeCode = "HH:MM:SS:CS", - fps = DEFAULT_FPS -): string => { - const hours = Math.floor(timeInSeconds / 3600); - const minutes = Math.floor((timeInSeconds % 3600) / 60); - const seconds = Math.floor(timeInSeconds % 60); - const centiseconds = Math.floor((timeInSeconds % 1) * 100); - const frames = Math.floor((timeInSeconds % 1) * fps); - - switch (format) { - case "MM:SS": - return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; - case "HH:MM:SS": - return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; - case "HH:MM:SS:CS": - return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`; - case "HH:MM:SS:FF": - return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`; - } -}; - -export const parseTimeCode = ( - timeCode: string, - format: TimeCode = "HH:MM:SS:CS", - fps = DEFAULT_FPS -): number | null => { - if (!timeCode || typeof timeCode !== "string") return null; - - // Remove any extra whitespace - const cleanTimeCode = timeCode.trim(); - - try { - switch (format) { - case "MM:SS": { - const parts = cleanTimeCode.split(":"); - if (parts.length !== 2) return null; - const [minutes, seconds] = parts.map((part) => parseInt(part, 10)); - if (isNaN(minutes) || isNaN(seconds)) return null; - if (minutes < 0 || seconds < 0 || seconds >= 60) return null; - return minutes * 60 + seconds; - } - - case "HH:MM:SS": { - const parts = cleanTimeCode.split(":"); - if (parts.length !== 3) return null; - const [hours, minutes, seconds] = parts.map((part) => - parseInt(part, 10) - ); - if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return null; - if ( - hours < 0 || - minutes < 0 || - seconds < 0 || - minutes >= 60 || - seconds >= 60 - ) - return null; - return hours * 3600 + minutes * 60 + seconds; - } - - case "HH:MM:SS:CS": { - const parts = cleanTimeCode.split(":"); - if (parts.length !== 4) return null; - const [hours, minutes, seconds, centiseconds] = parts.map((part) => - parseInt(part, 10) - ); - if ( - isNaN(hours) || - isNaN(minutes) || - isNaN(seconds) || - isNaN(centiseconds) - ) - return null; - if ( - hours < 0 || - minutes < 0 || - seconds < 0 || - centiseconds < 0 || - minutes >= 60 || - seconds >= 60 || - centiseconds >= 100 - ) - return null; - return hours * 3600 + minutes * 60 + seconds + centiseconds / 100; - } - - case "HH:MM:SS:FF": { - const parts = cleanTimeCode.split(":"); - if (parts.length !== 4) return null; - const [hours, minutes, seconds, frames] = parts.map((part) => - parseInt(part, 10) - ); - if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || isNaN(frames)) - return null; - if ( - hours < 0 || - minutes < 0 || - seconds < 0 || - frames < 0 || - minutes >= 60 || - seconds >= 60 || - frames >= fps - ) - return null; - return hours * 3600 + minutes * 60 + seconds + frames / fps; - } - } - } catch { - return null; - } - - return null; -}; - -export const guessTimeCodeFormat = (timeCode: string): TimeCode | null => { - if (!timeCode || typeof timeCode !== "string") return null; - - const numbers = timeCode.split(":"); - - if (!numbers.every((n) => !isNaN(Number(n)))) return null; - - if (numbers.length === 2) return "MM:SS"; - if (numbers.length === 3) return "HH:MM:SS"; - // todo: how to tell frames apart from cs? - if (numbers.length === 4) return "HH:MM:SS:FF"; - - return null; -}; +import type { TTimeCode } from "@/types/time"; + +export function roundToFrame({ + time, + fps, +}: { + time: number; + fps: number; +}): number { + return Math.round(time * fps) / fps; +} + +export function formatTimeCode({ + timeInSeconds, + format = "HH:MM:SS:CS", + fps, +}: { + timeInSeconds: number; + format?: TTimeCode; + fps?: number; +}): string { + const hours = Math.floor(timeInSeconds / 3600); + const minutes = Math.floor((timeInSeconds % 3600) / 60); + const seconds = Math.floor(timeInSeconds % 60); + const centiseconds = Math.floor((timeInSeconds % 1) * 100); + const frames = fps ? Math.floor((timeInSeconds % 1) * fps) : 0; + + switch (format) { + case "MM:SS": + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + case "HH:MM:SS": + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + case "HH:MM:SS:CS": + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`; + case "HH:MM:SS:FF": + if (!fps) throw new Error("FPS is required for HH:MM:SS:FF format"); + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`; + } +} + +export function parseTimeCode({ + timeCode, + format = "HH:MM:SS:CS", + fps, +}: { + timeCode: string; + format?: TTimeCode; + fps: number; +}): number | null { + if (!timeCode || typeof timeCode !== "string") return null; + + const cleanTimeCode = timeCode.trim(); + + try { + switch (format) { + case "MM:SS": { + const parts = cleanTimeCode.split(":"); + if (parts.length !== 2) return null; + const [minutes, seconds] = parts.map((part) => parseInt(part, 10)); + if (Number.isNaN(minutes) || Number.isNaN(seconds)) return null; + if (minutes < 0 || seconds < 0 || seconds >= 60) return null; + return minutes * 60 + seconds; + } + + case "HH:MM:SS": { + const parts = cleanTimeCode.split(":"); + if (parts.length !== 3) return null; + const [hours, minutes, seconds] = parts.map((part) => + parseInt(part, 10), + ); + if ( + Number.isNaN(hours) || + Number.isNaN(minutes) || + Number.isNaN(seconds) + ) + return null; + if ( + hours < 0 || + minutes < 0 || + seconds < 0 || + minutes >= 60 || + seconds >= 60 + ) + return null; + return hours * 3600 + minutes * 60 + seconds; + } + + case "HH:MM:SS:CS": { + const parts = cleanTimeCode.split(":"); + if (parts.length !== 4) return null; + const [hours, minutes, seconds, centiseconds] = parts.map((part) => + parseInt(part, 10), + ); + if ( + Number.isNaN(hours) || + Number.isNaN(minutes) || + Number.isNaN(seconds) || + Number.isNaN(centiseconds) + ) + return null; + if ( + hours < 0 || + minutes < 0 || + seconds < 0 || + centiseconds < 0 || + minutes >= 60 || + seconds >= 60 || + centiseconds >= 100 + ) + return null; + return hours * 3600 + minutes * 60 + seconds + centiseconds / 100; + } + + case "HH:MM:SS:FF": { + const parts = cleanTimeCode.split(":"); + if (parts.length !== 4) return null; + const [hours, minutes, seconds, frames] = parts.map((part) => + parseInt(part, 10), + ); + if ( + Number.isNaN(hours) || + Number.isNaN(minutes) || + Number.isNaN(seconds) || + Number.isNaN(frames) + ) + return null; + if ( + hours < 0 || + minutes < 0 || + seconds < 0 || + frames < 0 || + minutes >= 60 || + seconds >= 60 || + frames >= fps + ) + return null; + return hours * 3600 + minutes * 60 + seconds + frames / fps; + } + } + } catch { + return null; + } +} + +export function guessTimeCodeFormat({ + timeCode, +}: { + timeCode: string; +}): TTimeCode | null { + if (!timeCode || typeof timeCode !== "string") return null; + + const numbers = timeCode.split(":"); + + if (!numbers.every((n) => !Number.isNaN(Number(n)))) return null; + + if (numbers.length === 2) return "MM:SS"; + if (numbers.length === 3) return "HH:MM:SS"; + // todo: how to tell frames apart from cs? + if (numbers.length === 4) return "HH:MM:SS:FF"; + + return null; +} + +export function timeToFrame({ + time, + fps, +}: { + time: number; + fps: number; +}): number { + return Math.round(time * fps); +} + +export function frameToTime({ + frame, + fps, +}: { + frame: number; + fps: number; +}): number { + return frame / fps; +} + +export function snapTimeToFrame({ + time, + fps, +}: { + time: number; + fps: number; +}): number { + if (fps <= 0) return time; + const frame = timeToFrame({ time, fps }); + return frameToTime({ frame, fps }); +} + +export function getSnappedSeekTime({ + rawTime, + duration, + fps, +}: { + rawTime: number; + duration: number; + fps: number; +}): number { + const snappedTime = snapTimeToFrame({ time: rawTime, fps }); + return Math.max(0, Math.min(duration, snappedTime)); +} + +export function getLastFrameTime({ + duration, + fps, +}: { + duration: number; + fps: number; +}): number { + if (duration <= 0) return 0; + if (fps <= 0) return duration; + const frameOffset = 1 / fps; + return Math.max(0, duration - frameOffset); +} -- cgit v1.3.1