dragonflight/services/web-ui/public/js/timecode.js
ZGaetano 3c689ccddf fix(timecode): correct framesToTC for all frames beyond position 3
The previous algorithm used `if (rem >= DROP)` (i.e. rem >= 4) to decide
whether to advance to the next minute group.  This fired immediately at
frame 4, still inside minute 0 of the 10-minute non-drop group, producing
00:01:00;00 for what should be 00:00:00;04.  Every timecode display in
the editor was wrong for any position past the first four frames.

Each 10-minute block has one 3600-frame non-drop minute followed by nine
3596-frame drop minutes.  The fix checks `rem < FRAMES_FIRST_MIN` (3600)
to identify the non-drop minute, then subtracts it before dividing into
drop-minute slots.  Frame labels within drop minutes are shifted by DROP
(+4) so the first usable label is :00;04 as per SMPTE 12M.
2026-05-19 00:01:18 -04:00

93 lines
2.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 03 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 (05)
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 19: frame numbers 03 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);