summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/time.ts
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/lib/time.ts
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/lib/time.ts')
-rw-r--r--apps/web/src/lib/time.ts325
1 files changed, 204 insertions, 121 deletions
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 type { TTimeCode } from "@/types/time";
-import { DEFAULT_FPS } from "@/stores/project-store";
+export function roundToFrame({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ return Math.round(time * fps) / fps;
+}
-export type TimeCode = "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF";
+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;
-// 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":
+ 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")}`;
+ }
+}
- 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 function parseTimeCode({
+ timeCode,
+ format = "HH:MM:SS:CS",
+ fps,
+}: {
+ timeCode: string;
+ format?: TTimeCode;
+ fps: number;
+}): number | null {
+ if (!timeCode || typeof timeCode !== "string") return null;
-export const parseTimeCode = (
- timeCode: string,
- format: TimeCode = "HH:MM:SS:CS",
- fps = DEFAULT_FPS
-): number | null => {
- if (!timeCode || typeof timeCode !== "string") return null;
+ const cleanTimeCode = timeCode.trim();
- // 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 (Number.isNaN(minutes) || Number.isNaN(seconds)) return null;
+ if (minutes < 0 || seconds < 0 || seconds >= 60) return null;
+ return minutes * 60 + seconds;
+ }
- 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 (
+ 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": {
- 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 (
+ 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: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 (
+ 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;
+ }
+}
- 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;
- }
+export function guessTimeCodeFormat({
+ timeCode,
+}: {
+ timeCode: string;
+}): TTimeCode | null {
+ if (!timeCode || typeof timeCode !== "string") return null;
- return null;
-};
+ const numbers = timeCode.split(":");
-export const guessTimeCodeFormat = (timeCode: string): TimeCode | null => {
- if (!timeCode || typeof timeCode !== "string") return null;
+ if (!numbers.every((n) => !Number.isNaN(Number(n)))) return null;
- const numbers = timeCode.split(":");
+ 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";
- if (!numbers.every((n) => !isNaN(Number(n)))) return null;
+ 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";
+export function timeToFrame({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ return Math.round(time * fps);
+}
- return null;
-};
+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);
+}