Looks per scene, a change prompt on every character picture, click to enlarge

Each scene now shows who is in it as they look IN THAT SCENE, with one box:
'Change how Stewart looks from this scene on'. Cast cards show every look
front, side and back. Every picture opens large. The storyboard waits until
all pictures exist. A box for the author's notes on the script as a whole,
and Revise is always on offer.
This commit is contained in:
gator 2026-09-19 15:16:26 +00:00
parent a5ee10e99b
commit c02eb5d511

View file

@ -28,7 +28,37 @@
.ch .ap{color:var(--muted);font-size:12px;margin-top:4px} .ch .ap{color:var(--muted);font-size:12px;margin-top:4px}
.sheets{display:flex;gap:5px;margin-bottom:8px} .sheets{display:flex;gap:5px;margin-bottom:8px}
.sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block; .sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block;
aspect-ratio:16/9;object-fit:cover} aspect-ratio:3/4;object-fit:cover;object-position:top}
img[data-zoom]{cursor:zoom-in}
.lookset{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px}
.lookset .lw{color:var(--muted);font-size:12px;margin-bottom:5px}
.lookset .lw b{display:inline;color:inherit}
.lookset.idle{opacity:.55}
.whoin{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0}
.whoin .p{display:flex;gap:8px;border:1px solid var(--line);border-radius:10px;
padding:8px;width:310px;font-size:12px;align-items:flex-start}
.whoin .p img{width:64px;aspect-ratio:3/4;object-fit:cover;object-position:top;
border-radius:6px;border:1px solid var(--line);flex:none}
.whoin .p .none{width:64px;aspect-ratio:3/4;border-radius:6px;flex:none;
border:1px dashed var(--line);display:flex;align-items:center;
justify-content:center;color:var(--muted);font-size:10px;text-align:center}
.whoin .p .t{flex:1;min-width:0}
.whoin .p .t b{display:block;font-size:13px}
.whoin .p .t .w{color:var(--muted);margin:2px 0 6px}
.whoin .p .t .st{color:#b4531f}
.whoin .p .t .new{color:#1f7a3a;font-weight:600}
.whoin .p input{width:100%;box-sizing:border-box;font:inherit;padding:5px 7px;
border:1px solid var(--line);border-radius:6px;background:transparent;color:inherit}
.whoin .p .go{margin-top:5px;font-size:12px;padding:3px 9px}
.whole{border:1px solid var(--line);border-radius:10px;padding:12px;margin:0 0 18px}
.whole h4{margin:0 0 4px;font-size:14px}
.whole p{margin:0 0 8px;color:var(--muted);font-size:13px}
.whole textarea{width:100%;box-sizing:border-box}
#zoom{position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:99;display:none;
align-items:center;justify-content:center;cursor:zoom-out;padding:18px}
#zoom.on{display:flex}
#zoom img{max-width:100%;max-height:100%;border-radius:6px}
#zoom .cap{position:fixed;left:0;right:0;bottom:10px;text-align:center;color:#ddd;font-size:13px}
.shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px} .shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px}
.shot .num{width:40px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums} .shot .num{width:40px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}
.shot .body{flex:1;min-width:0} .shot .body{flex:1;min-width:0}
@ -191,6 +221,8 @@
} }
function busyShots() { function busyShots() {
if (String(PKG.look_state || "")
&& (Date.now() - (Date.parse(PKG.look_started || "") || 0)) < 15 * 60 * 1000) return true;
return (PKG.shots || []).some(function (s) { return (PKG.shots || []).some(function (s) {
return s.writing || s.drawing || stillFilming(s); }); return s.writing || s.drawing || stillFilming(s); });
} }
@ -413,7 +445,10 @@
out.push('<button type="button" class="btn" disabled>Rewriting…</button>'); out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
else { else {
var over = (PKG.timing && Number(PKG.timing.over_by)) || 0; var over = (PKG.timing && Number(PKG.timing.over_by)) || 0;
if (REVIEWS.length || Object.keys(NOTES).length || over > 5) // Always on offer. It used to appear only once a note existed, and
// a note typed a moment ago does not repaint the bar - so an author
// wrote their notes and found nothing to press.
if (true)
out.push('<button type="button" class="btn' + (over > 5 ? ' primary' : '') out.push('<button type="button" class="btn' + (over > 5 ? ' primary' : '')
+ '" data-revise="1">' + '" data-revise="1">'
+ (over > 5 ? 'Cut ' + Math.round(over) + ' seconds and revise' + (over > 5 ? 'Cut ' + Math.round(over) + ' seconds and revise'
@ -552,9 +587,16 @@
+ 'the server restarted under it. Ask again.</p>'; + 'the server restarted under it. Ask again.</p>';
if (!REEL) { if (!REEL) {
// A storyboard is its pictures. With some missing it plays as a slide
// show with holes, which tells the author nothing about the sequence.
var toDraw = Number(PKG.pictures_to_draw || 0);
if (toDraw > 0)
out += '<p class="cont" style="text-align:center">' + toDraw + ' picture'
+ (toDraw === 1 ? ' is' : 's are') + ' still to be drawn. Use <b>Draw this storyboard</b> '
+ 'above, then come back here.</p>';
out += '<div class="tools" style="justify-content:center">' out += '<div class="tools" style="justify-content:center">'
+ '<button type="button" class="btn primary" data-reelgo="1"' + '<button type="button" class="btn primary" data-reelgo="1"'
+ (running ? ' disabled' : '') + '>' + ((running || toDraw > 0) ? ' disabled' : '') + '>'
+ (st === "ready" ? "Play the storyboard" : "Prepare and play") + (st === "ready" ? "Play the storyboard" : "Prepare and play")
+ '</button></div></div>'; + '</button></div></div>';
return out; return out;
@ -759,12 +801,14 @@
: ""; : "";
var castHtml = '<div class="cast">' + cast.map(function (c) { var castHtml = '<div class="cast">' + cast.map(function (c) {
var sheets = (c.head_url || c.front_url || c.side_url) var nm = c.name || c.id;
? '<div class="sheets">' + [c.head_url, c.front_url, c.side_url].map(function (u) { var sheets = c.head_url
return u ? '<img src="' + esc(u) + '" alt="">' : ''; }).join("") + '</div>' : ''; ? '<div class="sheets"><img data-zoom="' + esc(nm + " — the face every picture is drawn from")
return '<div class="ch">' + sheets + '<b>' + esc(c.name || c.id) + '</b>' + '" src="' + esc(c.head_url) + '" alt=""></div>' : '';
return '<div class="ch">' + sheets + '<b>' + esc(nm) + '</b>'
+ '<span class="vo">' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent") + '<span class="vo">' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent")
+ '</span><div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>'; + '</span><div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div>'
+ looksHtml(c) + '</div>';
}).join("") + '</div>'; }).join("") + '</div>';
var body = insertHtml("start"); var body = insertHtml("start");
@ -772,7 +816,81 @@
return head + probs + '<div class="bar">' + barHtml(meta) + '</div>' return head + probs + '<div class="bar">' + barHtml(meta) + '</div>'
+ '<div id="reelhost">' + reelHtml() + '</div>' + '<div id="reelhost">' + reelHtml() + '</div>'
+ spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + castHtml + body; + spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + wholeHtml() + castHtml + body;
}
// Every look a character has, in the order the story reaches them: the
// clothes in words, then front, side and back. A look nobody wears any more
// is kept (the author may change their mind back) but shown faded.
function looksHtml(c) {
var looks = c.looks || {};
var keys = Object.keys(looks).sort(function (a, b) {
return String(looks[a].first_shot || "").localeCompare(String(looks[b].first_shot || "")); });
if (!keys.length) return "";
var nm = c.name || c.id;
return keys.map(function (k) {
var L = looks[k], pics = [["front_url", "front"], ["side_url", "side"], ["back_url", "back"]];
var imgs = pics.map(function (p) {
return L[p[0]] ? '<img data-zoom="' + esc(nm + " — " + p[1] + " — " + (L.wear || ""))
+ '" src="' + esc(L[p[0]]) + '" alt="">' : ''; }).join("");
return '<div class="lookset' + (L.in_use === false ? ' idle' : '') + '">'
+ '<div class="lw">from shot <b>' + esc(L.first_shot || "?") + '</b>: ' + esc(L.wear || "")
+ (L.state ? ' <span style="color:#b4531f">· ' + esc(L.state) + '</span>' : '')
+ '</div>' + (imgs ? '<div class="sheets">' + imgs + '</div>'
: '<div class="lw"><i>not drawn yet</i></div>')
+ '</div>';
}).join("");
}
// WHO IS IN THIS SCENE, as they look in it. This is where an author changes
// a hat, a dress, a cut lip - in words, about one person, in one scene - and
// it holds from here on until they change it again.
function whoInHtml(s) {
var ids = s.visible_characters || [];
if (!ids.length) return "";
var byId = {}; (PKG.characters || []).forEach(function (c) { byId[String(c.id)] = c; });
var busy = !!String(PKG.look_state || "");
var prevLooks = {};
(PKG.shots || []).slice().sort(function (a, b) { return String(a.id).localeCompare(String(b.id)); })
.some(function (x) {
if (String(x.id) === String(s.id)) return true;
Object.keys(x.looks || {}).forEach(function (cid) { prevLooks[cid] = x.looks[cid]; });
return false; });
return '<div class="whoin">' + ids.map(function (cid) {
cid = String(cid);
var c = byId[cid] || {}, key = (s.looks || {})[cid], L = (c.looks || {})[key] || {};
var nm = c.name || cid;
var pic = L.front_url || c.front_url || c.head_url || "";
var isNew = prevLooks[cid] && prevLooks[cid] !== key;
var asked = (s.look_asks || {})[cid];
return '<div class="p">'
+ (pic ? '<img data-zoom="' + esc(nm + " in shot " + s.id + " — " + (L.wear || ""))
+ '" src="' + esc(pic) + '" alt="">'
: '<div class="none">not drawn yet</div>')
+ '<div class="t"><b>' + esc(nm) + '</b>'
+ '<div class="w">' + (isNew ? '<span class="new">changes here: </span>' : '')
+ esc(L.wear || "as described") + (L.state ? ' <span class="st">· ' + esc(L.state) + '</span>' : '')
+ (asked ? '<br><i>you asked: ' + esc(asked) + '</i>' : '') + '</div>'
+ '<input type="text" id="look-' + esc(s.id) + '-' + esc(cid) + '" maxlength="300" '
+ (busy ? 'disabled ' : '') + 'placeholder="Change how ' + esc(nm.split(" ")[0])
+ ' looks from this scene on…">'
+ '<button type="button" class="btn go" data-lookgo="' + esc(s.id) + '|' + esc(cid) + '"'
+ (busy ? ' disabled' : '') + '>' + (busy ? 'redrawing…' : 'Change') + '</button>'
+ '</div></div>';
}).join("") + '</div>';
}
// The author's note on the script as a whole. It is an ordinary note filed
// under ALL, so it saves itself like the others and the head writer is shown
// it before anything else.
function wholeHtml() {
return '<div class="whole"><h4>Your notes on the script as a whole</h4>'
+ '<p>What is missing, what should go, what order the story should be told in, '
+ 'what you want people to feel. The head writer reads this first when you press '
+ '<b>Revise the whole script</b>. Notes on a single shot go in the box under that shot.</p>'
+ '<textarea class="note" rows="7" data-note="ALL" placeholder="For example: open on Stewart, '
+ 'not on the men in suits. The traffic stop is the moment people will talk about and it is not here…">'
+ esc(NOTES.ALL || "") + '</textarea></div>';
} }
function insertHtml(after) { function insertHtml(after) {
@ -831,12 +949,15 @@
+ 'preload="metadata"></video><figcaption>the filmed shot' + 'preload="metadata"></video><figcaption>the filmed shot'
+ (s.clip_seconds ? ' &middot; ' + Number(s.clip_seconds).toFixed(1) + 's' : '') + (s.clip_seconds ? ' &middot; ' + Number(s.clip_seconds).toFixed(1) + 's' : '')
+ '</figcaption></figure>' + '</figcaption></figure>'
+ (s.opening_url ? '<figure><img src="' + esc(s.opening_url) + '" alt=""><figcaption>filmed from this frame</figcaption></figure>' : '') + (s.opening_url ? '<figure><img data-zoom="' + esc("shot " + s.id + " — the frame it was filmed from")
+ '" src="' + esc(s.opening_url) + '" alt=""><figcaption>filmed from this frame</figcaption></figure>' : '')
+ '</div>'; + '</div>';
} else if (s.opening_url || s.closing_url) { } else if (s.opening_url || s.closing_url) {
media = '<div class="frames">' media = '<div class="frames">'
+ (s.opening_url ? '<figure><img src="' + esc(s.opening_url) + '" alt=""><figcaption>opens on</figcaption></figure>' : '') + (s.opening_url ? '<figure><img data-zoom="' + esc("shot " + s.id + " opens on")
+ (s.closing_url ? '<figure><img src="' + esc(s.closing_url) + '" alt=""><figcaption>ends on</figcaption></figure>' : '') + '" src="' + esc(s.opening_url) + '" alt=""><figcaption>opens on</figcaption></figure>' : '')
+ (s.closing_url ? '<figure><img data-zoom="' + esc("shot " + s.id + " ends on")
+ '" src="' + esc(s.closing_url) + '" alt=""><figcaption>ends on</figcaption></figure>' : '')
+ '</div>'; + '</div>';
} }
@ -893,6 +1014,7 @@
+ (s.day ? ' &middot; <span class="scene">' + esc(s.day) + '</span>' : '') + (s.day ? ' &middot; <span class="scene">' + esc(s.day) + '</span>' : '')
+ '</div>' + '</div>'
+ media + media
+ whoInHtml(s)
+ '<p class="act">' + esc(s.action || "") + '</p>' + '<p class="act">' + esc(s.action || "") + '</p>'
+ (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '') + (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '')
+ (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '') + (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '')
@ -1071,6 +1193,28 @@
el.disabled = true; el.disabled = true;
r = await act("start_review", { run_id: runId }); if (r) await refresh(); return; r = await act("start_review", { run_id: runId }); if (r) await refresh(); return;
} }
var zi = e.target.closest && e.target.closest("img[data-zoom]");
if (zi) {
var z = document.getElementById("zoom");
z.querySelector("img").src = zi.getAttribute("src");
z.querySelector(".cap").textContent = zi.getAttribute("data-zoom") || "";
z.classList.add("on");
return;
}
if ((el = hit("data-lookgo"))) {
var pr = el.getAttribute("data-lookgo").split("|");
var box = document.getElementById("look-" + pr[0] + "-" + pr[1]);
var ask = box ? box.value.trim() : "";
if (ask.length < 3) { if (box) box.focus(); return; }
el.disabled = true; el.textContent = "working…";
r = await act("set_look", { draft_id: CUR, shot_id: pr[0], character_id: pr[1], prompt: ask });
if (r) {
showMsg("msg", "Redrawing that look" + (r.frames ? " and " + r.frames + " scene"
+ (r.frames === 1 ? "" : "s") + " it appears in" : "") + ", about $" + r.usd + ".", "ok");
await refresh();
} else { el.disabled = false; el.textContent = "Change"; }
return;
}
if ((el = hit("data-revise"))) { if ((el = hit("data-revise"))) {
el.disabled = true; el.disabled = true;
r = await act("start_revision", { run_id: runId }); if (r) await refresh(); return; r = await act("start_revision", { run_id: runId }); if (r) await refresh(); return;
@ -1140,6 +1284,15 @@
}, 700); }, 700);
}); });
(function () {
var z = document.createElement("div");
z.id = "zoom"; z.innerHTML = '<img alt=""><div class="cap"></div>';
z.addEventListener("click", function () { z.classList.remove("on"); });
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") z.classList.remove("on"); });
document.body.appendChild(z);
})();
load(); load();
})(); })();
</script> </script>