diff --git a/trailer.html b/trailer.html
index c45d6b7..db8330b 100644
--- a/trailer.html
+++ b/trailer.html
@@ -432,6 +432,107 @@
return (Date.now() - t) < 15 * 60 * 1000;
}
+ // ---- the storyboard player -------------------------------------------
+ // One call fetches the whole reel; after that nothing touches the network,
+ // which is what makes stop and jump-to instant.
+ var REEL = null, RAT = -1, RTIMER = null;
+ var reelAudio = new Audio();
+
+ function reelHtml() {
+ var st = String(PKG.reel_state || "");
+ var shots = (PKG.shots || []);
+ if (!shots.length) return "";
+ var running = st && st !== "ready" && !/^failed/.test(st);
+
+ var out = '
Watch the storyboard
'
+ + '
The frames in order with the lines spoken over them — '
+ + 'the trailer before it is filmed. Sequence problems are obvious here '
+ + 'and nearly invisible in the script below.
';
+
+ if (running)
+ out += '
Speaking every shot… ' + esc(st) + '
';
+ else if (/^failed/.test(st))
+ out += '
' + esc(st) + '
';
+
+ if (!REEL) {
+ out += '
'
+ + '
';
+ return out;
+ }
+
+ var s = REEL[RAT] || REEL[0] || {};
+ var lines = (s.lines || []).map(function (l) {
+ return '
';
+ return out;
+ }
+
+ function reelPaint() {
+ var host = document.getElementById("reelhost");
+ if (host) host.innerHTML = reelHtml();
+ }
+
+ // The two frames split the shot's running time: the opening frame holds for
+ // the first half, the closing frame for the second. It is a storyboard, not
+ // an animation, but it reads as movement and it marks where the shot lands.
+ function reelShow(i) {
+ if (!REEL || i < 0 || i >= REEL.length) { reelStop(); return; }
+ RAT = i;
+ reelPaint();
+ var s = REEL[i];
+ var dur = Math.max(Number(s.seconds) || 0, Number(s.edit_seconds) || 0, 2.5);
+ if (s.audio_url) { reelAudio.src = s.audio_url; reelAudio.play().catch(function(){}); }
+ else reelAudio.pause();
+
+ var t0 = Date.now(), swapped = false;
+ clearInterval(RTIMER);
+ RTIMER = setInterval(function () {
+ var gone = (Date.now() - t0) / 1000;
+ var bar = document.getElementById("rprog");
+ if (bar) bar.style.width = Math.min(100, 100 * gone / dur) + "%";
+ if (!swapped && gone > dur / 2) {
+ swapped = true;
+ var a = document.getElementById("rfa"), b = document.getElementById("rfb");
+ if (a && b && REEL[RAT].closing_url) { a.classList.remove("on"); b.classList.add("on"); }
+ }
+ if (gone >= dur) reelShow(RAT + 1);
+ }, 120);
+ }
+
+ function reelStop() {
+ clearInterval(RTIMER); RTIMER = null;
+ reelAudio.pause();
+ REEL = null; RAT = -1;
+ openTab(CUR);
+ }
+
// A cut is an ordering, not a render. Playing one is free, so all of them
// can be looked at before anything is joined into a file.
function cutsHtml(meta) {
@@ -573,6 +674,7 @@
shots.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); });
return head + probs + '