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:
parent
a5ee10e99b
commit
c02eb5d511
1 changed files with 165 additions and 12 deletions
177
trailer.html
177
trailer.html
|
|
@ -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 ? ' · ' + Number(s.clip_seconds).toFixed(1) + 's' : '')
|
+ (s.clip_seconds ? ' · ' + 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 ? ' · <span class="scene">' + esc(s.day) + '</span>' : '')
|
+ (s.day ? ' · <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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue