From fc858729f3efaee65541a6b5d6bc47db8205d22c Mon Sep 17 00:00:00 2001 From: gator Date: Thu, 17 Sep 2026 14:07:50 +0000 Subject: [PATCH] Play the storyboard: frames and narration, in order, at the top Reviewing meant scrolling to a shot, pressing play, listening, scrolling to the next - which destroys exactly the flow a trailer is made of, so faults of sequence were the hardest to notice and the ones we kept shipping. This plays the whole thing: opening frame, then closing frame, while the shot's lines are spoken, then on to the next. Stop, or jump to any shot to leave a note. It is the trailer before it is filmed, for cents of speech rather than dollars of video. --- trailer.html | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) 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 '
' + esc(l.who || "") + + (l.lip_sync ? "" : " (v.o.)") + '' + esc(l.text || "") + '
'; + }).join("") || '
no dialogue in this shot
'; + + out += '
' + + '
opens on
' + + '
ends on
' + + '
' + esc(s.id) + ' · ' + + esc(s.title || "") + '
' + + '
' + esc(s.setting || "") + + (s.scene ? ' · ' + esc(s.scene) : "") + + (s.day ? ' · ' + esc(s.day) : "") + '
' + + '
' + lines + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + (RAT + 1) + ' of ' + REEL.length + '' + + '
'; + 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 + '
' + barHtml(meta) + '
' + + '
' + reelHtml() + '
' + spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + castHtml + body; }