// services/web-ui/public/js/timecode.js // 59.94 fps drop-frame timecode utilities. // Exposes: window.TC (function (global) { 'use strict'; // 59.94 = 60000/1001 const FPS_EXACT = 60000 / 1001; const NOM = 60; // nominal integer fps const DROP = 4; // frames dropped per minute (except every 10th) const FRAMES_FIRST_MIN = NOM * 60; // 3600 – non-drop minute const FRAMES_PER_MIN = NOM * 60 - DROP; // 3596 – drop minute const FRAMES_PER_10MIN = FRAMES_PER_MIN * 10 + DROP; // 35964 const FRAMES_PER_HOUR = FRAMES_PER_10MIN * 6; // 215784 function pad2(n) { return String(Math.floor(n)).padStart(2, '0'); } /** * Convert a frame count to a 59.94 DF timecode string: HH:MM:SS;FF * * Each 10-minute group contains one non-drop minute (3600 frames) followed * by nine drop minutes (3596 frames each). Frame numbers 0–3 are skipped at * the start of every drop minute; we account for this by shifting the * within-minute frame index by DROP when computing ss/ff. */ function framesToTC(totalFrames) { const fc = Math.max(0, Math.round(totalFrames)); const h = Math.floor(fc / FRAMES_PER_HOUR); let rem = fc % FRAMES_PER_HOUR; const tm = Math.floor(rem / FRAMES_PER_10MIN); // tens-of-minutes group (0–5) rem = rem % FRAMES_PER_10MIN; let m, ss, ff; if (rem < FRAMES_FIRST_MIN) { // First minute of the 10-min group — no frame drop m = 0; ss = Math.floor(rem / NOM); ff = rem % NOM; } else { // Minutes 1–9: frame numbers 0–3 at second :00 are skipped rem -= FRAMES_FIRST_MIN; m = Math.floor(rem / FRAMES_PER_MIN) + 1; const remInMin = rem % FRAMES_PER_MIN; // Shift by DROP so the label starts at :00;04 instead of :00;00 const adj = remInMin + DROP; ss = Math.floor(adj / NOM); ff = adj % NOM; } const M = tm * 10 + m; return `${pad2(h)}:${pad2(M)}:${pad2(ss)};${pad2(ff)}`; } /** * Convert a 59.94 DF timecode string (HH:MM:SS;FF or HH:MM:SS:FF) to frame count. */ function tcToFrames(tc) { if (!tc) return 0; const clean = String(tc).replace(';', ':'); const parts = clean.split(':').map(Number); if (parts.length !== 4) return 0; const [h, m, s, f] = parts; const totalMinutes = h * 60 + m; return (NOM * 3600 * h) + (NOM * 60 * m) + (NOM * s) + f - DROP * (totalMinutes - Math.floor(totalMinutes / 10)); } /** * Convert seconds (float) → frame count at 59.94. */ function secondsToFrames(seconds) { return Math.round(seconds * FPS_EXACT); } /** * Convert frame count → seconds (float) at 59.94. */ function framesToSeconds(frames) { return frames / FPS_EXACT; } global.TC = { framesToTC, tcToFrames, secondsToFrames, framesToSeconds, FPS: FPS_EXACT, }; })(window);