One stage at a time instead of the whole pipeline at once

The page showed the script, every shot's directing controls, your pictures,
filming, the joins and the cut simultaneously. That is four different jobs on
one screen, and the first of them - reading the words and getting them right -
is the one that matters most and was the easiest to skip past.

It is now four stages with a stepper: the words, the layout, filming, the cut.
Each shows only what belongs to it, and you can step back to look at an earlier
one at any time.

This also fixes something that was simply wrong: approving the script used to
hide every shot control, so the setting, camera and cutaways became uneditable
at exactly the moment you were meant to be arranging them. Directing now lives
in its own stage, after the words are settled and before anything is filmed.
This commit is contained in:
gator 2026-09-07 23:23:12 +00:00
parent b523474cbf
commit 29a054f8c8

View file

@ -49,6 +49,12 @@
.tlhead{position:absolute;top:0;bottom:0;width:2px;background:var(--accent2);pointer-events:none} .tlhead{position:absolute;top:0;bottom:0;width:2px;background:var(--accent2);pointer-events:none}
.tlruler{position:absolute;left:0;right:0;bottom:0;height:14px;font:9px/14px var(--mono);color:var(--muted)} .tlruler{position:absolute;left:0;right:0;bottom:0;height:14px;font:9px/14px var(--mono);color:var(--muted)}
.tlruler span{position:absolute;transform:translateX(-50%)} .tlruler span{position:absolute;transform:translateX(-50%)}
.steps{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}
.steps button{background:#0f1117;border:1px solid var(--line);color:var(--muted);
border-radius:999px;padding:8px 15px;font:12px/1 var(--mono);cursor:pointer}
.steps button.done{color:var(--ink)}
.steps button.on{background:var(--accent);border-color:var(--accent);color:#fff;cursor:default}
.steps button[disabled]{opacity:.35;cursor:not-allowed}
.stage{position:relative;width:100%;max-width:300px} .stage{position:relative;width:100%;max-width:300px}
.stage>video{width:100%;aspect-ratio:9/16;border-radius:10px;background:#000;display:block} .stage>video{width:100%;aspect-ratio:9/16;border-radius:10px;background:#000;display:block}
.ovl{position:absolute;inset:0;border-radius:10px;overflow:hidden;background:#000;display:none} .ovl{position:absolute;inset:0;border-radius:10px;overflow:hidden;background:#000;display:none}
@ -161,6 +167,38 @@ function shotHtml(s, locked) {
+ '</div></div>'; + '</div></div>';
} }
// Four jobs, one at a time. Which one you are on follows from the project's
// status; VIEW lets you step back to look at an earlier one without pretending
// the work has been undone.
var STAGES = ["The words", "The layout", "Filming", "The cut"];
var VIEW = null;
function stageOf(st) {
if (st === "ready" || st === "film_failed") return 4;
if (st === "queued_to_film" || st === "filming") return 3;
if (st === "approved") return 2;
return 1;
}
function stepper(stage, view) {
return '<div class="steps">' + STAGES.map(function (n, k) {
var i = k + 1;
var cls = i === view ? "on" : (i <= stage ? "done" : "");
return '<button type="button" class="' + cls + '" data-stage="' + i + '"'
+ (i <= stage ? '' : ' disabled') + '>' + i + ' &middot; ' + esc(n) + '</button>';
}).join("") + '</div>';
}
// Stage one shows the writing and nothing else: no frames, no camera, no
// settings. Just what the pet actually says.
function linesHtml(s) {
return '<div class="shot" style="grid-template-columns:1fr;padding:16px 20px"><div>'
+ '<span class="n">Shot ' + (s.index || "") + '</span>'
+ '<p class="line" style="margin:8px 0 6px">' + esc(s.line || "") + '</p>'
+ '<p class="foot">' + (s.word_count || 0) + ' words &middot; about '
+ (s.est_seconds || 0) + 's</p></div></div>';
}
function render(r) { function render(r) {
DATA = r; DATA = r;
var p = r.project, s = r.script, out = document.getElementById("out"); var p = r.project, s = r.script, out = document.getElementById("out");
@ -190,7 +228,12 @@ function render(r) {
document.getElementById("title").textContent = s.title || "The script"; document.getElementById("title").textContent = s.title || "The script";
document.getElementById("sub").textContent = s.premise || ""; document.getElementById("sub").textContent = s.premise || "";
var locked = p.status === "approved"; var stage = stageOf(p.status);
if (VIEW === null || VIEW > stage) VIEW = stage;
// Shot controls belong to the layout stage. Elsewhere the shots are shown
// but not touchable, which is what "locked" means to shotHtml.
var locked = VIEW !== 2;
var canEditScript = (stage === 1);
var warn = (s._warnings && s._warnings.length) var warn = (s._warnings && s._warnings.length)
? '<div class="warn"><strong>Worth a look:</strong> ' + s._warnings.map(esc).join(" · ") + '</div>' : ""; ? '<div class="warn"><strong>Worth a look:</strong> ' + s._warnings.map(esc).join(" · ") + '</div>' : "";
@ -299,8 +342,9 @@ function render(r) {
+ '</div>'; + '</div>';
} }
var actions = locked var actions = !canEditScript
? '<div class="card"><p style="margin:0"><strong>Approved.</strong> Filming is switched on once payment is live. Nothing has been charged.</p></div>' ? '<div class="card"><p style="margin:0"><strong>These words are settled.</strong> '
+ 'Everything after this arranges how they are shot, so the writing is left alone.</p></div>'
: '<div class="card">' : '<div class="card">'
+ '<h2 class="sec">Change the words</h2>' + '<h2 class="sec">Change the words</h2>'
+ '<div class="field"><label for="fb">What would you change</label>' + '<div class="field"><label for="fb">What would you change</label>'
@ -312,13 +356,30 @@ function render(r) {
+ '<button class="btn" id="approve" style="margin-left:10px">Approve and lock it</button>' + '<button class="btn" id="approve" style="margin-left:10px">Approve and lock it</button>'
+ '</div>'; + '</div>';
out.innerHTML = cams + head + warn + full var body;
+ '<h2 class="sec">The shots</h2>' if (VIEW === 1) {
+ s.segments.map(function (x) { return shotHtml(x, locked); }).join("") body = head + warn + full
+ own + '<h2 class="sec">The words, shot by shot</h2>'
+ film + s.segments.map(linesHtml).join("")
+ post
+ actions; + actions;
} else if (VIEW === 2) {
body = head
+ '<p class="note" style="margin:0 0 22px"><strong>How it will be laid out.</strong> '
+ 'Each shot has a setting, a camera move and an opening frame that carries your '
+ 'pet&rsquo;s likeness. Cutaways sit across the joins so the cut is never on screen. '
+ 'All of it has been chosen for you &mdash; change anything you disagree with, or '
+ 'go straight to filming.</p>'
+ '<h2 class="sec">The shots</h2>'
+ s.segments.map(function (x) { return shotHtml(x, false); }).join("")
+ own
+ film;
} else if (VIEW === 3) {
body = head + film;
} else {
body = head + post + film;
}
out.innerHTML = cams + stepper(stage, VIEW) + body;
wire(locked); wire(locked);
} }
@ -447,6 +508,16 @@ function bindPics() {
} }
function wire(locked) { function wire(locked) {
[].forEach.call(document.querySelectorAll("[data-stage]"), function (b) {
b.onclick = function () {
var want = parseInt(b.getAttribute("data-stage"), 10);
if (want === VIEW) return;
VIEW = want;
hideMsg("msg");
render(DATA);
window.scrollTo({ top: 0, behavior: "smooth" });
};
});
bindPics(); bindPics();
bindFilm((DATA && DATA.project) || {}); bindFilm((DATA && DATA.project) || {});
bindPost(); bindPost();