paytolaugh/trailer.html

1788 lines
95 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trailers — PayToLaugh</title>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="/assets/app.css">
<style>
.tabs{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:0 0 20px}
.tab{border:1px solid var(--line);border-bottom:0;background:none;color:var(--muted);
padding:10px 16px;border-radius:10px 10px 0 0;cursor:pointer;font:inherit;font-size:14px}
.tab.on{color:var(--ink);border-color:var(--line);background:var(--panel)}
.tab.lead{color:var(--accent)}
.scene{color:var(--accent2);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
.tab .sc{color:var(--accent2);margin-left:9px;font-weight:600;font-size:11px;
border:1px solid var(--accent2);border-radius:999px;padding:1px 7px;
font-variant-numeric:tabular-nums}
.tab .st{color:var(--muted);margin-left:7px;font-size:12px}
.head{display:flex;gap:18px;flex-wrap:wrap;align-items:baseline;margin-bottom:6px}
.head h2{margin:0;font-size:22px}
.tag{color:var(--muted);font-style:italic}
.facts{color:var(--muted);font-size:13px;margin:2px 0 18px}
.cast{display:flex;flex-wrap:wrap;gap:14px;margin:0 0 22px}
.ch{border:1px solid var(--line);border-radius:10px;padding:10px;font-size:13px;width:250px}
.ch b{display:block}
.ch .vo{color:var(--muted);font-size:12px}
.ch .ap{color:var(--muted);font-size:12px;margin-top:4px}
.sheets{display:flex;gap:5px;margin-bottom:8px}
.sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block;
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}
.guide{border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:0 0 18px}
.guide h4{margin:0 0 10px;font-size:15px}
.guide ol{list-style:none;margin:0;padding:0;display:grid;gap:7px}
.guide li{display:flex;gap:10px;align-items:baseline;font-size:13px;color:var(--muted)}
.guide li .n{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--line);
display:inline-flex;align-items:center;justify-content:center;font-size:11px}
.guide li.done .n{background:#1f7a3a;border-color:#1f7a3a;color:#fff}
.guide li.now{color:inherit;font-weight:600}
.guide li.now .n{background:#4B8BF5;border-color:#4B8BF5;color:#fff}
.guide li .d{font-weight:400;color:var(--muted);display:block;margin-top:2px}
.guide li .btn{margin-left:4px}
.fchk{font-size:12px;margin:0 0 10px;padding:7px 10px;border-radius:8px;border:1px solid var(--line)}
.fchk.ok{color:#1f7a3a;border-color:transparent;padding-left:0}
.fchk.bad{border-color:#b4531f}
.fchk .w{display:block;margin:3px 0}
.fchk .w.serious{color:#b4531f;font-weight:600}
.fchk .go{font-size:12px;padding:2px 9px;margin-left:6px}
.vars{border:1px solid var(--line);border-radius:10px;padding:10px;margin:0 0 10px}
.vars h5{margin:0 0 3px;font-size:13px}
.vars p{margin:0 0 8px;font-size:12px;color:var(--muted)}
.vars .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}
.vars .v{font-size:12px}
.vars .v img{width:100%;border-radius:7px;border:2px solid var(--line);display:block}
.vars .v.good img{border-color:#1f7a3a}
.vars .v.bad img{border-color:#b4531f}
.vars .v .say{margin:4px 0;color:var(--muted);min-height:2.6em}
.vars .v.bad .say{color:#b4531f}
.vars .v .btn{font-size:12px;padding:3px 10px}
.fnote{margin:0 0 10px;font-size:12px}
.fnote label{display:block;color:var(--muted);margin-bottom:3px}
.fnote input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:5px 8px;
border:1px solid var(--line);border-radius:6px;background:transparent;color:inherit}
.fnote input.saved{border-color:#1f7a3a}
.ffault{display:flex;gap:8px;margin:0 0 10px}
.ffault input{flex:1;min-width:0;font:inherit;font-size:12px;padding:5px 8px;
border:1px solid var(--line);border-radius:6px;background:transparent;color:inherit}
.ffault .go{font-size:12px;padding:3px 10px;white-space:nowrap}
.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 .num{width:40px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}
.shot .body{flex:1;min-width:0}
.shot h3{margin:0 0 3px;font-size:15px}
.shot .where{color:var(--muted);font-size:12px;margin-bottom:8px}
.shot .act{font-size:14px;margin:0 0 8px}
.shot .cam,.shot .cont{color:var(--muted);font-size:12.5px;margin:0 0 5px}
.frames{display:flex;gap:10px;margin:10px 0}
.frames figure{margin:0;flex:1;min-width:0}
.frames img,.frames video{width:100%;border-radius:8px;border:1px solid var(--line);display:block}
.frames figcaption{color:var(--muted);font-size:11px;margin-top:3px}
.line{display:flex;gap:9px;align-items:flex-start;margin:7px 0}
.line .say{flex:1;min-width:0;font-size:14px}
.line .who{color:var(--accent);font-size:12px}
.line .dir{color:var(--muted);font-size:12px}
.play{border:1px solid var(--line);background:none;color:var(--ink);border-radius:999px;
width:30px;height:30px;flex:none;cursor:pointer;font:inherit;line-height:1}
.play:hover{border-color:var(--accent)}
.dur{color:var(--muted);font-size:12px;align-self:center}
.dur.over{color:#e0685f}
.note{width:100%;box-sizing:border-box;margin-top:8px;font:inherit;font-size:13px;
background:transparent;color:var(--ink);border:1px dashed var(--line);
border-radius:8px;padding:8px 10px;resize:vertical}
.note:focus{border-style:solid;border-color:var(--accent);outline:none}
.note.saved{border-color:var(--accent2)}
.probs{border:1px solid #e0685f;border-radius:10px;padding:12px 14px;margin:0 0 18px;font-size:13px}
.probs b{color:#e0685f}
.probs ul{margin:6px 0 0;padding-left:18px}
.bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:0 0 20px}
.soon{color:var(--muted);font-size:12px}
.lip{color:var(--accent2);font-size:11px;border:1px solid var(--accent2);
border-radius:999px;padding:0 6px}
.vobadge{color:var(--muted);font-size:11px;border:1px solid var(--line);
border-radius:999px;padding:0 6px}
.spine{border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 14px}
.spine h4{margin:0 0 8px;font-size:15px}
.spine .eng{font-size:14px;border-left:2px solid var(--accent);padding-left:11px;margin:0 0 12px}
.spine .kv{font-size:13px;margin:3px 0;color:var(--muted)}
.spine .kv b{color:var(--ink);font-weight:600}
.spine h5{margin:12px 0 4px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;
color:var(--muted)}
.spine ul{margin:0;padding-left:18px;font-size:13px}
.spine details{margin-top:10px}
.spine summary{cursor:pointer;color:var(--muted);font-size:13px}
.gaps{border:1px solid var(--accent);border-radius:10px;padding:14px 16px;margin:0 0 14px}
.gaps h4{margin:0 0 4px;font-size:15px;color:var(--accent)}
.gaps .verdict{font-size:14px;margin:0 0 10px}
.gaps ol{margin:0;padding-left:20px;font-size:13px}
.gaps li{margin-bottom:7px}
.gaps .fix{color:var(--muted);display:block}
.gaps .sh{color:var(--muted);font-variant-numeric:tabular-nums}
.rd{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
.rd h4{margin:0 0 6px;font-size:14px}
.rd h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
.rd blockquote{margin:0 0 8px;font-size:14px;border-left:2px solid var(--line);padding-left:10px}
.rd .kv{color:var(--muted);font-size:12.5px;margin:2px 0}
.rd ul{margin:6px 0 0;padding-left:18px;font-size:13px}
.rd .wrong{color:#e0685f}
.tools{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-top:10px}
.tools button{font-size:12px;padding:5px 10px}
.chk{font-size:12px;border-radius:999px;padding:1px 8px;border:1px solid var(--line);color:var(--muted)}
.chk.ok{color:var(--accent2);border-color:var(--accent2)}
.chk.bad{color:#e0685f;border-color:#e0685f}
.eyes{margin:8px 0 0;font-size:12.5px}
.eyes .f{margin:4px 0;padding-left:10px;border-left:2px solid var(--line)}
.eyes .f.serious{border-left-color:#e0685f}
.eyes .f b{color:var(--ink)}
.eyes .fix{color:var(--muted);display:block;margin-top:1px}
.eyes .sum{color:var(--muted);font-style:italic;margin-bottom:5px}
.ins{border-top:1px dashed var(--line);text-align:center;padding:6px 0}
.ins button{font-size:12px;padding:4px 12px;color:var(--muted)}
.rough{width:100%;box-sizing:border-box;margin-top:8px;font:inherit;font-size:13px;
background:transparent;color:var(--ink);border:1px solid var(--accent);
border-radius:8px;padding:9px 11px;resize:vertical}
.busy{color:var(--accent);font-size:12px}
.timing{border:1px solid var(--line);border-radius:10px;padding:11px 14px;margin:0 0 16px;font-size:13px}
.timing.over{border-color:#e0685f}
.timing ul{margin:7px 0 0;padding-left:18px;color:var(--muted);font-size:12.5px}
.cuts{border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0 0 16px}
.cuts h4{margin:0 0 4px;font-size:15px}
.cuts .lede{color:var(--muted);font-size:13px;margin:0 0 12px}
.cut{border-top:1px solid var(--line);padding:11px 0}
.cut:first-of-type{border-top:0}
.cut b{font-size:14px}
.cut .len{color:var(--accent2);font-variant-numeric:tabular-nums;margin-left:8px;font-size:13px}
.cut .strat{font-size:13px;margin:3px 0}
.cut .why{color:var(--muted);font-size:12.5px;margin:3px 0}
.cut .sac{color:#e0a05f;font-size:12.5px;margin:3px 0}
.cut .ids{color:var(--muted);font-size:11.5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
margin:4px 0 0;word-break:break-word}
.cut .stat{color:var(--accent2);font-size:12.5px;margin:3px 0}
.playbox{margin:10px 0 0;background:#000;border-radius:10px;overflow:hidden}
.playbox video{width:100%;display:block;aspect-ratio:16/9;background:#000}
.playbox .bar2{display:flex;gap:10px;align-items:center;padding:7px 10px;font-size:12px;
color:var(--muted);background:var(--panel)}
.reel{border:1px solid var(--accent);border-radius:10px;padding:14px 16px;margin:0 0 18px}
.reel h4{margin:0 0 4px;font-size:15px;color:var(--accent)}
.reel .lede{color:var(--muted);font-size:13px;margin:0 0 12px}
.reelframes{display:flex;gap:12px}
.reelframes figure{margin:0;flex:1;min-width:0}
.reelframes img{width:100%;border-radius:10px;border:1px solid var(--line);display:block;
aspect-ratio:16/9;object-fit:cover;background:#000}
.reelframes img.on{border-color:var(--accent);box-shadow:0 0 0 2px rgba(255,170,80,.25)}
.reelframes figcaption{color:var(--muted);font-size:11px;margin-top:4px;text-align:center}
.reelnow{margin:12px 0 0;text-align:center}
.reelnow .t{font-size:15px;font-weight:600}
.reelnow .w{color:var(--muted);font-size:12.5px;margin-top:2px}
.reelnow .said{font-size:14px;margin-top:8px;max-width:46em;margin-left:auto;margin-right:auto}
.reelnow .said .who{color:var(--accent);font-size:12px;margin-right:6px}
.reelbar{display:flex;gap:8px;align-items:center;justify-content:center;
flex-wrap:wrap;margin-top:12px}
.reelbar .pos{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
.reelprog{height:4px;background:var(--line);border-radius:999px;margin-top:12px;overflow:hidden}
.reelprog i{display:block;height:100%;background:var(--accent);width:0}
.shot.lit{background:rgba(255,170,80,.06);border-radius:10px}
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1 id="title">Trailers</h1>
<p id="sub"></p>
</header>
<div class="msg" id="msg"></div>
<section><div class="card">
<div class="tabs" id="tabs"></div>
<div id="pane"><p class="empty">Loading&hellip;</p></div>
</div></section>
</div>
<script src="/assets/app.js"></script>
<script>
(function () {
if (!requireSession()) return;
navBar("trailer");
var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = [];
var timer = null, EDITING = null;
var PLAYING = null, STATS = {}, QUEUE = [], QAT = 0;
var REELWANT = false; // the author asked to play, not merely to prepare
var runId = new URLSearchParams(location.search).get("run") || "";
var audio = new Audio();
if (!runId) { showMsg("msg", "No run was named in the link."); return; }
function busyDraft(d) {
var s = String(d.draw_state || "");
return d.state === "writing" || (s && !/^(drawn|failed)/.test(s));
}
function busyRun() {
return RUN && /^(writing|reviewing|revising|judging)$/.test(String(RUN.stage || ""));
}
// Work happens in the worker's memory and does not survive a deploy, so a
// flag saying "in progress" is only believed while it is recent.
function stillFilming(s) {
if (!s.filming) return false;
var t = Date.parse(s.filming_since || "");
if (!t) return false; // no stamp: from before this existed
return (Date.now() - t) < 25 * 60 * 1000;
}
var DRAFTNOW = null;
function busyShots() {
var ps = String(PKG.pics_state || "");
if (ps && ps !== "ready"
&& (Date.now() - (Date.parse(PKG.pics_started || "") || 0)) < 15 * 60 * 1000) return true;
if (String(PKG.look_state || "")
&& (Date.now() - (Date.parse(PKG.look_started || "") || 0)) < 15 * 60 * 1000) return true;
if (String(PKG.props_state || "")
&& (Date.now() - (Date.parse(PKG.props_started || "") || 0)) < 15 * 60 * 1000) return true;
return (PKG.shots || []).some(function (s) {
return s.writing || s.drawing || s.fixing || s.varying || stillFilming(s); });
}
// Preparing the storyboard audio is work too. Without this the page stops
// polling and the progress sits at whatever it last happened to see.
function busyReel() {
var st = String(PKG.reel_state || "");
if (!st || st === "ready" || /^failed/.test(st)) return false;
var t = Date.parse(PKG.reel_started || "");
if (!t) return true;
return (Date.now() - t) < 15 * 60 * 1000; // older than that is gone
}
async function load() {
var r = await api("run_status", { run_id: runId });
if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; }
RUN = r.run; DRAFTS = r.drafts || [];
document.getElementById("title").textContent = RUN.book_title || "Trailers";
document.getElementById("sub").textContent = sub();
if (!CUR && DRAFTS.length) CUR = leadDraftId() || DRAFTS[0].id;
paintTabs();
var rv = await api("list_reviews", { run_id: runId });
REVIEWS = (rv && rv.reviews) || [];
await openTab(CUR);
tick();
}
function tick() {
var es = String(PKG.eyes_state || "");
var busyEyes = es && es !== "ready" && !/^failed/.test(es)
&& (Date.now() - (Date.parse(PKG.eyes_started || "") || 0)) < 20 * 60 * 1000;
var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots() || busyReel()
|| busyEyes;
if (busy && !timer) timer = setInterval(refresh, 5000);
if (!busy && timer) { clearInterval(timer); timer = null; }
}
async function refresh() {
var r = await api("run_status", { run_id: runId });
if (!r.ok) return;
RUN = r.run; DRAFTS = r.drafts || [];
document.getElementById("sub").textContent = sub();
paintTabs();
var rv = await api("list_reviews", { run_id: runId });
REVIEWS = (rv && rv.reviews) || [];
await openTab(CUR);
tick();
}
function leadDraftId() {
var best = null;
DRAFTS.forEach(function (d) {
if (d.writer === RUN.lead_writer
&& (!best || Number(d.iteration) > Number(best.iteration))) best = d;
});
return best && best.id;
}
function sub() {
var bits = [];
if (RUN.stage) bits.push(RUN.stage);
bits.push(DRAFTS.length + " script" + (DRAFTS.length === 1 ? "" : "s"));
var spent = DRAFTS.reduce(function (a, d) {
return a + Number(d.write_usd || 0) + Number(d.draw_usd || 0); }, 0);
(PKG.shots || []).forEach(function (s) { spent += Number(s.film_usd || 0); });
if (spent) bits.push("$" + spent.toFixed(2) + " so far");
if (RUN.created_at) bits.push(when(RUN.created_at));
return bits.join(" · ");
}
// THE WRITERS SCORE EACH OTHER. Blind, against the checklist in the guide;
// nobody marks their own script; the winner becomes head writer.
function contestHtml() {
var ready = DRAFTS.filter(function (d) { return /^ready/.test(String(d.state)) && Number(d.iteration || 1) === Number(RUN.iteration || 1); });
if (ready.length < 2) return "";
var c = RUN.contest, label = {};
DRAFTS.forEach(function (d) { label[d.writer] = d.label || d.writer; });
var btn = RUN.stage === "judging"
? '<span class="busy">The writers are reading and scoring each other’s scripts… a few minutes.</span>'
: '<button type="button" class="btn' + (c ? '' : ' primary') + '" data-judge="1">'
+ (c ? 'Score them again' : 'Have the writers score each other’s scripts') + ' &middot; about $' + (0.12 * ready.length).toFixed(2) + '</button>';
var table = "";
if (c && (c.ranked || []).length) {
table = '<table style="border-collapse:collapse;font-size:13px;margin:8px 0"><tr><th style="text-align:left;padding:3px 10px 3px 0">script</th><th style="padding:3px 10px">score</th><th style="text-align:left;padding:3px 0">what the other writers said</th></tr>'
+ c.ranked.map(function (r, i) {
var n = (r.notes || [])[0] || {};
return '<tr style="border-top:1px solid var(--line)"><td style="padding:5px 10px 5px 0;white-space:nowrap">' + (i === 0 ? '★ ' : '') + '<b>' + esc(label[r.writer] || r.writer)
+ '</b></td><td style="padding:5px 10px;text-align:center"><b>' + esc(r.score) + '</b><br><span class="dir">' + esc((r.marks || []).join(", ")) + '</span></td><td style="padding:5px 0"><span style="color:#1f7a3a">+</span> '
+ esc(n.best || "") + '<br><span style="color:#b4531f">−</span> ' + esc(n.worst || "") + '</td></tr>'; }).join("")
+ '</table><p class="cont">The highest scoring script’s writer is now the head writer (★). You can still choose another.</p>';
}
return '<div class="guide" style="margin-bottom:12px"><h4>Which script is best?</h4>' + table + btn + '</div>';
}
// One more writer on this run: same book, same notes, nobody else pays again.
var ALLWRITERS = null;
function addWriterHtml() {
if (!ALLWRITERS) {
ALLWRITERS = [];
api("camera_presets", {}).then(function (r) {
ALLWRITERS = (r && r.writers) || []; if (ALLWRITERS.length) paintTabs(); });
return "";
}
var have = {}; DRAFTS.forEach(function (d) { have[d.writer] = 1; });
var free = ALLWRITERS.filter(function (w) { return !have[w.id]; });
if (!free.length) return "";
return '<span style="margin-left:10px;white-space:nowrap"><select id="addwsel" style="font-size:12px">'
+ free.map(function (w) { return '<option value="' + esc(w.id) + '">' + esc(w.label) + '</option>'; }).join("")
+ '</select> <button type="button" class="btn" data-addwriter="1" style="font-size:12px">+ add this writer &middot; about $1</button></span>';
}
function paintTabs() {
document.getElementById("tabs").innerHTML = DRAFTS.map(function (d) {
var s = String(d.draw_state || "");
var extra = d.state === "writing" ? '<span class="st">writing…</span>'
: d.state === "failed" ? '<span class="st">failed</span>'
: (s && !/^(drawn|failed)/.test(s)) ? '<span class="st">' + esc(s) + '</span>'
: d.score ? '<span class="sc" title="how well readers understood it">'
+ d.score + '</span>'
: '';
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
+ (d.writer === RUN.lead_writer ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
+ esc(d.label || d.writer)
+ (Number(d.iteration) > 1 ? ' v' + d.iteration : '')
+ (d.writer === RUN.lead_writer ? ' ★' : '') + extra + '</button>';
}).join("") + addWriterHtml();
}
document.getElementById("tabs").addEventListener("click", function (e) {
var addb = e.target.closest ? e.target.closest("[data-addwriter]") : null;
if (addb) {
var aw = document.getElementById("addwsel");
addb.disabled = true;
api("add_writer", { run_id: runId, writer: aw ? aw.value : "" }).then(function (r) {
if (r && r.ok) { showMsg("msg", "That writer is reading the book now. Its script appears as a new tab in a few minutes.", "ok"); load(); }
else { showMsg("msg", (r && r.error) || "That writer could not be added."); addb.disabled = false; }
});
return;
}
var id = e.target.closest ? e.target.closest(".tab") : null;
if (!id) return;
CUR = id.getAttribute("data-tab"); EDITING = null;
paintTabs(); openTab(CUR);
});
async function openTab(id) {
var meta = DRAFTS.filter(function (d) { return d.id === id; })[0];
if (!meta) return;
var pane = document.getElementById("pane");
if (meta.state === "failed") {
pane.innerHTML = '<p class="empty"><b>' + esc(meta.label) + ' did not finish.</b><br>'
+ esc(meta.note || "No reason was recorded.") + '</p>'
+ '<div class="bar"><button type="button" class="btn" data-retry="'
+ esc(meta.id) + '">Try this writer again</button></div>';
return;
}
if (meta.state === "writing") {
pane.innerHTML = '<p class="empty">Working. It reads the whole book, works out what the '
+ 'story is, and then writes the trailer from that. Several minutes.</p>';
return;
}
var r = await api("get_draft", { draft_id: id });
if (!r.ok) { pane.innerHTML = '<p class="empty">' + esc(r.error || "") + '</p>'; return; }
PKG = r.draft.package || {};
NOTES = r.draft.notes || {};
STATS = {};
if ((PKG.cuts || []).some(function (c) { return c.project_id; })) {
var cs = await api("cut_stats", { draft_id: id });
((cs && cs.cuts) || []).forEach(function (x) { STATS[x.cut_id] = x; });
}
pane.innerHTML = paneHtml(meta, r.draft);
// "Prepare and play" promised both. Once the audio lands, play it.
if (REELWANT && String(PKG.reel_state || "") === "ready" && !REEL) {
REELWANT = false;
hideMsg("msg");
var rl = await api("reel", { draft_id: id });
if (rl.ok && (rl.shots || []).length) {
REEL = rl.shots; RAT = -1;
pane.innerHTML = paneHtml(meta, r.draft);
reelShow(0);
}
}
}
// The story the writer committed to before writing a shot. This is where a
// misreading of the book is visible and cheap to fix; by the time it is
// twenty-six shots and a storyboard, it is neither.
function spineHtml() {
var sp = PKG.spine;
if (!sp || typeof sp !== "object") return "";
var p = sp.protagonist || {};
function ul(items) {
return (!items || !items.length) ? ""
: '<ul>' + items.map(function (x) {
return '<li>' + esc(typeof x === "string" ? x : JSON.stringify(x)) + '</li>';
}).join("") + '</ul>';
}
var beats = (sp.beats || []).map(function (b) {
return '<li>' + esc(b.what || "")
+ (b.is_ending ? ' <b>(the ending)</b>' : '')
+ (b.why_it_matters ? ' <span class="kv">' + esc(b.why_it_matters) + '</span>' : '')
+ '</li>'; }).join("") + addWriterHtml();
return '<div class="spine"><h4>The story, as the writer worked it out from the book</h4>'
+ (sp.the_engine ? '<p class="eng">' + esc(sp.the_engine) + '</p>' : '')
+ (p.who ? '<div class="kv"><b>who</b> ' + esc(p.who) + '</div>' : '')
+ (p.wants ? '<div class="kv"><b>wants</b> ' + esc(p.wants) + '</div>' : '')
+ (p.obstacle ? '<div class="kv"><b>in the way</b> ' + esc(p.obstacle) + '</div>' : '')
+ (p.cost ? '<div class="kv"><b>cost</b> ' + esc(p.cost) + '</div>' : '')
+ (sp.what_a_viewer_must_understand && sp.what_a_viewer_must_understand.length
? '<h5>A viewer must come away knowing</h5>' + ul(sp.what_a_viewer_must_understand) : '')
+ (sp.moments_to_dramatise && sp.moments_to_dramatise.length
? '<h5>Must be shown, not narrated</h5>' + ul(sp.moments_to_dramatise) : '')
+ (sp.premise ? '<details><summary>the premise in full</summary><p>'
+ esc(sp.premise) + '</p></details>' : '')
+ (beats ? '<details><summary>the beats of the book</summary><ol>' + beats + '</ol></details>' : '')
+ (sp.withhold ? '<details><summary>what the trailer is withholding</summary><p>'
+ esc(sp.withhold) + '</p></details>' : '')
+ '</div>';
}
function barHtml(meta) {
var shots = (PKG.shots || []), cast = (PKG.characters || []);
var undrawn = shots.filter(function (s) { return !s.opening_url; }).length;
var unfilmed = shots.filter(function (s) { return !s.clip_url; }).length;
var filmed = shots.length - unfilmed;
var imgs = boardImages(shots, cast);
var s = String(meta.draw_state || "");
var drawing = s && !/^(drawn|failed)/.test(s);
var isLead = meta.writer === RUN.lead_writer;
var out = [];
if (!isLead) out.push('<button type="button" class="btn primary" data-lead="'
+ esc(meta.id) + '">Make this the head writer</button>');
else out.push('<span class="soon">★ head writer</span>');
if (drawing) out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>');
else if (undrawn) out.push('<button type="button" class="btn" data-draw="' + esc(meta.id)
+ '">Draw ' + (undrawn === shots.length ? 'this storyboard'
: undrawn + ' undrawn shot' + (undrawn === 1 ? '' : 's'))
// The worker counts what it will really draw - looks, back views and
// frames - so the price here is the price. `imgs` knew only about three
// sheets a head and two frames a shot.
+ ' &middot; $' + ((Number(PKG.pictures_to_draw) || imgs) * 0.068).toFixed(2) + '</button>');
else out.push('<span class="soon">storyboard drawn'
+ (meta.draw_usd ? ' &middot; $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>');
if (isLead) {
var others = DRAFTS.filter(function (d) {
return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length;
if (RUN.stage === "reviewing")
out.push('<button type="button" class="btn" disabled>Readers are watching it…</button>');
else if (others)
out.push('<button type="button" class="btn" data-review="1" title="'
+ 'Each other model reads ONLY this script - no book - and says what it understood.">'
+ (REVIEWS.length ? "Test it again" : "Does it make sense? Ask " + others + " readers")
+ '</button>');
if ((PKG.shots || []).length >= 6 && !busyCuts())
out.push('<button type="button" class="btn" data-cuts="' + esc(meta.id)
+ '" title="Several shorter versions of what is already filmed. '
+ 'Costs nothing to propose or to watch.">'
+ ((PKG.cuts || []).length ? "Propose more cuts" : "Propose shorter cuts")
+ '</button>');
out.push('<button type="button" class="btn" data-measure="' + esc(meta.id)
+ '" title="Speaks every line in the cast voices and reports what the '
+ 'trailer really runs.">'
+ ((PKG.timing && PKG.timing.measured_seconds) ? "Time it again" : "Time it for real")
+ '</button>');
if (RUN.stage === "revising")
out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
else {
var over = (PKG.timing && Number(PKG.timing.over_by)) || 0;
// 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' : '')
+ '" data-revise="1">'
+ (over > 5 ? 'Cut ' + Math.round(over) + ' seconds and revise'
: 'Revise the whole script') + '</button>');
}
if (filmed) {
var es = String(PKG.eyes_state || "");
var looking = es && es !== "ready" && !/^failed/.test(es)
&& (Date.now() - (Date.parse(PKG.eyes_started || "") || 0)) < 20 * 60 * 1000;
if (looking)
out.push('<button type="button" class="btn" disabled>Looking at the '
+ 'footage… ' + esc(es) + '</button>');
else
out.push('<button type="button" class="btn" data-eyes="' + esc(meta.id)
+ '" title="Samples frames from every clip and asks what is wrong '
+ 'with the picture, then whether it matches the script.">'
+ (es === "ready" ? "Check the picture again" : "Check the picture")
+ ' &middot; ' + "$" + (filmed * 0.02).toFixed(2)
+ '</button>');
}
if (unfilmed && !undrawn)
out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id)
+ '">Film ' + (filmed ? 'the ' + unfilmed + ' remaining shots' : 'all ' + shots.length + ' shots')
+ ' &middot; $' + (unfilmed * 0.75).toFixed(2) + '</button>');
if (filmed)
out.push('<button type="button" class="btn primary" data-assemble="' + esc(meta.id)
+ '">Make the trailer &middot; ' + filmed + ' of ' + shots.length + ' shots</button>');
}
return out.join("");
}
// A cut may move shots as well as drop them, which makes it a different
// film rather than a shorter one. Worth saying out loud.
function reordered(ids) {
var a = (ids || []).map(String);
var b = a.slice().sort();
return a.join("") !== b.join("");
}
// What the storyboard will actually cost to draw. Three sheets for every
// character who is seen or heard, one more per extra day they are dressed
// differently, and two frames per shot.
function boardImages(shots, cast) {
var seen = {};
shots.forEach(function (s) {
(s.visible_characters || []).forEach(function (c) { seen[String(c)] = 1; });
(s.audio || []).forEach(function (a) {
if (String(a.text || "").trim()) seen[String(a.character_id)] = 1; });
});
var present = cast.filter(function (c) { return seen[String(c.id)]; });
var days = {};
shots.forEach(function (s) {
var d = String(s.day || "").trim();
if (!d) return;
(s.visible_characters || []).forEach(function (c) {
(days[String(c)] = days[String(c)] || {})[d] = 1; });
});
var extra = 0;
present.forEach(function (c) {
var wd = c.wardrobe_by_day || {};
var base = Object.keys(wd).sort()[0] || "";
Object.keys(days[String(c.id)] || {}).forEach(function (d) {
if (d !== base && wd[d]) extra += 1; });
});
return present.length * 3 + extra + shots.length * 2;
}
function mmss(n) {
return Math.floor(n / 60) + ":" + String(Math.round(n % 60)).padStart(2, "0");
}
// The header's runtime is the sum of what the writer typed. This is what the
// lines take when the cast actually says them, which is a different number
// and the only one worth cutting against.
function timingHtml() {
var t = PKG.timing;
if (!t || !t.measured_seconds) return "";
var over = Number(t.over_by || 0);
var line = "Spoken by the cast this script runs <b>" + mmss(t.measured_seconds)
+ "</b>, against a " + mmss(t.target_seconds) + " target";
if (over > 5) line += " — <b>" + Math.round(over) + " seconds over</b>."
+ " Revising the whole script will cut that much.";
else if (over < -15) line += " — " + Math.round(-over) + " seconds short.";
else line += " — that fits.";
var worst = (t.longest_lines || []).slice(0, 3).map(function (l) {
return "<li>" + Number(l.seconds || 0).toFixed(1) + "s &middot; shot "
+ esc(String(l.shot)) + " &middot; “"
+ esc(String(l.text || "").slice(0, 80)) + "”</li>"; }).join("");
return '<div class="timing' + (over > 5 ? " over" : "") + '"><div>' + line + "</div>"
+ (worst && over > 5 ? "<ul>" + worst + "</ul>" : "") + "</div>";
}
// A job lives in the worker's memory and dies with it. Every deploy kills
// one, and the flag it set stays set, so anything older than this is treated
// as gone rather than leaving the page waiting on it for ever.
function busyCuts() {
if (String(PKG.cuts_state || "") !== "thinking") return false;
var t = Date.parse(PKG.cuts_started || "");
if (!t) return false;
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 = '<div class="reel"><h4>Watch the storyboard</h4>'
+ '<p class="lede">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.</p>';
if (running)
out += '<p class="busy">Speaking every shot… ' + esc(st) + '</p>';
else if (st === "ready") {
var spoken = shots.filter(function (s) { return s.reel_url; }).length;
var silent = shots.length - spoken;
out += '<p class="lede">' + spoken + ' shots have dialogue and are spoken'
+ (silent ? '; ' + silent + ' are silent and hold on the frame' : '')
+ '.</p>';
}
else if (/^failed/.test(st))
out += '<p class="cont">' + esc(st) + '</p>';
else if (st)
out += '<p class="cont">The last attempt stopped before it finished — '
+ 'the server restarted under it. Ask again.</p>';
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">'
+ '<button type="button" class="btn primary" data-reelgo="1"'
+ ((running || toDraw > 0) ? ' disabled' : '') + '>'
+ (st === "ready" ? "Play the storyboard" : "Prepare and play")
+ '</button></div></div>';
return out;
}
var s = REEL[RAT] || REEL[0] || {};
var lines = (s.lines || []).map(function (l) {
return '<div><span class="who">' + esc(l.who || "")
+ (l.lip_sync ? "" : " (v.o.)") + '</span>' + esc(l.text || "") + '</div>';
}).join("") || '<div class="w">no dialogue in this shot</div>';
out += '<div class="reelframes">'
+ '<figure><img id="rfa" class="on" src="' + esc(s.opening_url || "")
+ '" alt=""><figcaption>opens on</figcaption></figure>'
+ '<figure><img id="rfb" src="' + esc(s.closing_url || "")
+ '" alt=""><figcaption>ends on</figcaption></figure></div>'
+ '<div class="reelnow"><div class="t">' + esc(s.id) + ' · '
+ esc(s.title || "") + '</div>'
+ '<div class="w">' + esc(s.setting || "")
+ (s.scene ? ' · ' + esc(s.scene) : "")
+ (s.day ? ' · ' + esc(s.day) : "") + '</div>'
+ '<div class="said">' + lines + '</div></div>'
+ '<div class="reelprog"><i id="rprog"></i></div>'
+ '<div class="reelbar">'
+ '<button type="button" class="btn" data-reelstep="-1">◀ back</button>'
+ '<button type="button" class="btn primary" data-reeltoggle="1">'
+ (reelAudio.paused ? "Play" : "Pause") + '</button>'
+ '<button type="button" class="btn" data-reelstep="1">next ▶</button>'
+ '<button type="button" class="btn" data-reelstop="1">Stop</button>'
+ '<button type="button" class="btn" data-reeljump="' + esc(s.id)
+ '">Jump to this shot</button>'
+ '<span class="pos">' + (RAT + 1) + ' of ' + REEL.length + '</span>'
+ '</div></div>';
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) {
if (meta.writer !== RUN.lead_writer) return "";
var shots = PKG.shots || [];
var filmed = shots.filter(function (s) { return s.clip_url; }).length;
var cuts = PKG.cuts || [];
var st = String(PKG.cuts_state || "");
var out = '<div class="cuts"><h4>Shorter cuts of this same shoot</h4>'
+ '<p class="lede">Nothing here is re-filmed. A cut is an order to play '
+ 'the shots in, so making another one costs nothing — which is why '
+ 'the question is not “what is the right length” but “which '
+ 'version do people watch to the end”.</p>';
if (busyCuts()) out += '<p class="busy">Working out what to drop…</p>';
else if (st === "thinking") out += '<p class="cont">The last attempt stopped '
+ 'before it finished — the server restarted under it. Ask again.</p>';
else if (/^failed/.test(st)) out += '<p class="cont">' + esc(st) + '</p>';
out += cuts.map(function (c) {
var stat = STATS[c.id];
var play = (PLAYING === c.id)
? '<div class="playbox"><video id="cutv" controls playsinline></video>'
+ '<div class="bar2"><span id="cutat">loading…</span>'
+ '<button type="button" class="btn" data-stop="1">Close</button></div></div>'
: '';
var tools = '<button type="button" class="btn" data-play="' + esc(c.id) + '">'
+ (PLAYING === c.id ? 'Playing' : 'Watch it · free') + '</button>';
if (c.state === "made" && c.video_url)
tools += '<a class="btn" href="/videos.html">Published · in My videos</a>';
else if (c.state === "making")
tools += '<span class="busy">joining…</span>';
else if (filmed)
tools += '<button type="button" class="btn primary" data-makecut="' + esc(c.id)
+ '">Publish this version</button>';
return '<div class="cut"><b>' + esc(c.name || "") + '</b>'
+ '<span class="len">' + mmss(c.seconds || 0) + ' · '
+ (c.shot_ids || []).length + ' shots</span>'
+ (c.strategy ? '<p class="strat">' + esc(c.strategy) + '</p>' : '')
+ (c.why ? '<p class="why">' + esc(c.why) + '</p>' : '')
+ (c.sacrifices ? '<p class="sac">gives up: ' + esc(c.sacrifices) + '</p>' : '')
+ (stat ? '<p class="stat">' + stat.views + ' viewer'
+ (stat.views === 1 ? '' : 's') + ' · median ' + stat.median_percent
+ '% watched · ' + stat.completion_rate + '% reached the end</p>' : '')
+ '<p class="ids">' + esc((c.shot_ids || []).join(" "))
+ (reordered(c.shot_ids) ? ' &middot; reordered' : '') + '</p>'
+ '<div class="tools">' + tools + '</div>' + play + '</div>';
}).join("");
if (!cuts.length && st !== "thinking")
out += '<p class="cont">No cuts proposed yet.</p>';
return out + '</div>';
}
function reviewsHtml(meta) {
var mine = REVIEWS.filter(function (r) {
return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); });
if (!mine.length) return "";
var gaps = mine.filter(function (r) { return r.reviewer === "__gaps__"; })[0];
var readers = mine.filter(function (r) { return r.reviewer !== "__gaps__"; });
var out = "";
if (gaps) {
var v = gaps.strengths && gaps.strengths.verdict;
var list = gaps.suggestions || [];
out += '<div class="gaps"><h4>What this trailer does not tell a viewer</h4>'
+ (v ? '<p class="verdict">' + esc(v) + '</p>' : '')
+ (list.length ? '<ol>' + list.map(function (g) {
return '<li>' + esc(g.what || "")
+ '<span class="fix"><span class="sh">shot ' + esc(g.shot || "?") + '</span> &rarr; '
+ esc(g.fix || "") + '</span></li>'; }).join("") + '</ol>' : '')
+ '</div>';
}
out += readers.map(function (r) {
var s = r.strengths || {}, e = r.errors || {};
if (typeof s !== "object") s = {};
if (typeof e !== "object") e = {};
function ul(items, cls) {
return (!items || !items.length) ? ""
: '<ul class="' + cls + '">' + items.map(function (x) {
return '<li>' + esc(typeof x === "string" ? x : JSON.stringify(x)) + '</li>';
}).join("") + '</ul>';
}
return '<div class="rd"><h4>' + esc(r.label) + ' watched it without reading the book'
+ (r.score ? '<span class="n" title="how much of the real story they got">'
+ r.score + '/100</span>' : '') + '</h4>'
+ (s.summary ? '<blockquote>' + esc(s.summary) + '</blockquote>' : '')
+ '<div class="kv">main character: ' + esc(s.who || "not clear to them") + '</div>'
+ '<div class="kv">what they want: ' + esc(s.wants || "not clear to them") + '</div>'
+ '<div class="kv">what is in the way: ' + esc(s.obstacle || "not clear to them") + '</div>'
+ (s.would_watch ? '<div class="kv">would they watch the film: '
+ esc(s.would_watch) + '</div>' : '')
+ (s.unanswered && s.unanswered.length
? '<div class="kv">still asking:</div>' + ul(s.unanswered, "") : '')
+ (e.missed && e.missed.length ? '<div class="kv">they missed:</div>' + ul(e.missed, "") : '')
+ (e.got_wrong && e.got_wrong.length
? '<div class="kv wrong">the trailer misled them:</div>' + ul(e.got_wrong, "wrong") : '')
+ '</div>';
}).join("");
return '<div class="revs">' + out + '</div>';
}
function paneHtml(meta, draft) {
var p = PKG, shots = (p.shots || []).slice().sort(function (a, b) {
return String(a.id) < String(b.id) ? -1 : 1; });
var cast = p.characters || [], voice = {};
cast.forEach(function (c) { voice[c.id] = c; });
var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0);
var head = '<div class="head"><h2>' + esc(p.title || meta.label) + '</h2>'
+ (p.tagline ? '<span class="tag">' + esc(p.tagline) + '</span>' : '') + '</div>'
+ (p.logline ? '<p>' + esc(p.logline) + '</p>' : '')
+ '<p class="facts">' + esc(meta.label)
+ (Number(meta.iteration) > 1 ? ' &middot; revision ' + meta.iteration : '')
+ ' &middot; ' + shots.length + ' shots &middot; '
+ Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0")
+ ' &middot; ' + cast.length + ' characters'
+ (PKG.book_words ? ' &middot; spoken lines: <b>' + PKG.book_words.quote + '</b> from the book, <b>' + PKG.book_words.close
+ '</b> close to it, <b>' + PKG.book_words["new"] + '</b> written for the trailer' : '') + '</p>'
+ timingHtml();
var probs = draft.problems
? '<div class="probs"><b>The checker has objections</b><ul>'
+ draft.problems.split("\n").filter(Boolean).map(function (x) {
return '<li>' + esc(x) + '</li>'; }).join("") + '</ul></div>'
: "";
var castHtml = '<div class="cast">' + cast.map(function (c) {
var nm = c.name || c.id;
var sheets = c.head_url
? '<div class="sheets"><img data-zoom="' + esc(nm + " — the face every picture is drawn from")
+ '" 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><div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div>'
+ looksHtml(c) + '</div>';
}).join("") + '</div>';
var body = insertHtml("start");
shots.forEach(function (s) { body += shotHtml(s, voice) + insertHtml(s.id); });
DRAFTNOW = draft;
return head + contestHtml() + guideHtml(meta) + probs + '<div class="bar">' + barHtml(meta) + '</div>'
+ '<div id="reelhost">' + reelHtml() + '</div>'
+ spineHtml() + cutsHtml(meta) + reviewsHtml(meta) + wholeHtml() + castHtml + propsHtml() + body;
}
// THINGS THAT MUST STAY THE SAME. A face is held steady by a reference
// picture; so is an urn, and so is a car. Without one, the object the whole
// story is about was three different objects in three shots.
function propsHtml() {
var props = PKG.props, busy = String(PKG.props_state || "");
var head = '<div class="props"><h3 style="margin:18px 0 4px">Things that must look the same in every shot</h3>';
if (busy) head += '<p class="busy">' + esc(busy) + '\u2026 about a minute.</p>';
if (PKG.props_note) head += '<p class="cont" style="color:#b4531f">' + esc(PKG.props_note) + '</p>';
if (!props || !props.length)
return head + '<p class="cont">An object or a car that appears in more than one shot needs a reference '
+ 'picture, the way each character has one - otherwise it is drawn differently every time. '
+ (busy ? '' : '<button type="button" class="btn" data-findprops="1">Find them in this script &middot; about $0.30</button>')
+ '</p></div>';
return head + '<div class="cast">' + props.map(function (p) {
var pics = [["url", p.kind === "vehicle" ? "outside" : ""], ["interior_url", "inside"]].map(function (f) {
return p[f[0]] ? '<img data-zoom="' + esc(p.name + (f[1] ? " \u2014 " + f[1] : "")) + '" src="' + esc(p[f[0]]) + '" alt="">' : ''; }).join("");
var where = (p.shots || []).map(function (x) { return x.id; }).join(", ");
var drawn = (p.shots || []).filter(function (x) {
return (PKG.shots || []).some(function (q) { return String(q.id) === String(x.id) && q.opening_url; }); }).length;
return '<div class="ch"><div class="sheets">' + (pics || '<i class="lw">not drawn yet</i>') + '</div>'
+ '<b>' + esc(p.name || p.id) + '</b><span class="vo">seen in shots <input type="text" style="width:12em;font-size:12px" data-propshotlist="'
+ esc(p.id) + '" value="' + esc(where) + '"></span>'
+ '<textarea rows="5" style="width:100%;font-size:12px;margin-top:6px" data-propdesc="' + esc(p.id) + '">'
+ esc(p.description || "") + '</textarea>'
+ (busy ? '' : '<div class="tools" style="margin-top:6px">'
+ '<button type="button" class="btn" data-propsave="' + esc(p.id) + '" title="Saves the description and the list of shots. The picture is drawn again only if the description changed.">Save</button>'
+ '<button type="button" class="btn" data-propsave="' + esc(p.id) + '" data-again="1">Draw its picture again &middot; $'
+ (p.kind === "vehicle" ? "0.14" : "0.07") + '</button>'
+ (drawn ? '<button type="button" class="btn" data-propshots="' + esc(p.id) + '">Redraw the ' + drawn
+ ' shot' + (drawn === 1 ? '' : 's') + ' that show it &middot; $' + (drawn * 0.14).toFixed(2) + '</button>' : '')
+ '<button type="button" class="btn" data-propdel="' + esc(p.id) + '">Stop tracking it</button></div>')
+ '</div>';
}).join("") + '</div>'
+ (busy ? '' : '<p class="cont"><button type="button" class="btn" data-findprops="1">Look through the script again &middot; about $0.10</button></p>')
+ '</div>';
}
// 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("");
}
// WHAT TO DO NEXT. The steps of making a trailer, in order, with the one
// button that matters now. Everything else on the page is still there for
// anyone who wants it; nobody has to understand it to get a film made.
function guideHtml(meta) {
var shots = PKG.shots || [];
if (!shots.length || meta.writer !== RUN.lead_writer) return "";
var ds = String(meta.draw_state || "");
var drawing = ds && !/^(drawn|failed)/.test(ds);
var toDraw = Number(PKG.pictures_to_draw || 0);
var drawn = shots.filter(function (x) { return x.opening_url; });
var unchecked = drawn.filter(function (x) {
return !x.clip_url && !((x.frame_check || {}).verdict); }).length;
var faulty = drawn.filter(function (x) {
return !x.clip_url && (x.frame_check || {}).verdict === "broken"; });
var ps = String(PKG.pics_state || "");
var checking = ps && ps !== "ready";
var reelReady = String(PKG.reel_state || "") === "ready";
var unfilmed = shots.filter(function (x) { return !x.clip_url; }).length;
var objections = String((DRAFTNOW || {}).problems || "").split("\n").filter(Boolean).length;
var price = function (n) { return "$" + n.toFixed(2); };
var steps = [];
steps.push({ t: "Read the script", done: true,
d: objections ? "The checker has " + objections + " objection" + (objections === 1 ? "" : "s")
+ " - they are listed just below." : "" });
steps.push({ t: "Tell the writer what to change", done: true, optional: true,
d: "Write in the box \u201cYour notes on the script as a whole\u201d, or under any shot, then press "
+ "\u201cRevise the whole script\u201d. Skip this if you are happy." });
var s3 = { t: "Draw the pictures", done: toDraw === 0 && drawn.length > 0 };
if (drawing) s3.d = "Drawing now\u2026 " + esc(ds) + ". You can leave this page open.";
else if (!s3.done) s3.btn = '<button type="button" class="btn primary" data-draw="' + esc(meta.id)
+ '">Draw ' + toDraw + ' picture' + (toDraw === 1 ? '' : 's') + ' \u00b7 ' + price(toDraw * 0.068) + '</button>';
steps.push(s3);
var s4 = { t: "Check the pictures", done: s3.done && !unchecked && !faulty.length };
if (checking) s4.d = "Looking at every picture for mistakes\u2026 " + esc(ps);
else if (s3.done && unchecked) {
s4.d = "A second model looks at every picture for the mistakes these tools make - the wrong time "
+ "of day, the wrong clothes, extra people, six fingers - and redraws any that are broken.";
s4.btn = '<button type="button" class="btn primary" data-chkpics="1">Check ' + unchecked
+ ' picture' + (unchecked === 1 ? '' : 's') + ' \u00b7 ' + price(unchecked * 0.02) + '</button>';
} else if (s3.done && faulty.length)
s4.d = faulty.length + " picture" + (faulty.length === 1 ? " still has" : "s still have")
+ " a problem: shot" + (faulty.length === 1 ? " " : "s ")
+ faulty.map(function (x) { return x.id; }).join(", ")
+ ". Scroll to " + (faulty.length === 1 ? "it" : "them") + " and press \u201cFix this\u201d, "
+ "or carry on if you think the checker is being fussy.";
steps.push(s4);
var s5 = { t: "Listen to the storyboard", done: reelReady };
if (s3.done && !reelReady)
s5.btn = '<button type="button" class="btn' + (s4.done ? ' primary' : '')
+ '" data-reelgo="1">Play it with the voices</button>';
steps.push(s5);
var s6 = { t: "Film it", done: unfilmed === 0 };
if (s3.done && unfilmed)
s6.btn = '<button type="button" class="btn' + (s4.done && reelReady ? ' primary' : '')
+ '" data-film="' + esc(meta.id) + '">Film ' + unfilmed + ' shot' + (unfilmed === 1 ? '' : 's')
+ ' \u00b7 ' + price(unfilmed * 0.75) + '</button>';
steps.push(s6);
var nowSet = false;
return '<div class="guide"><h4>What to do next <a href="/trailer-guide.html" target="_blank" style="font-weight:400;font-size:12px;margin-left:8px">what makes a good trailer?</a></h4><ol>' + steps.map(function (st, i) {
var isNow = !nowSet && !st.done && !st.optional;
if (isNow) nowSet = true;
return '<li class="' + (st.done ? 'done' : '') + (isNow ? ' now' : '') + '">'
+ '<span class="n">' + (st.done && !st.optional ? '\u2713' : (i + 1)) + '</span>'
+ '<span>' + esc(st.t) + (st.optional ? ' <i>(if you want)</i>' : '')
+ ((isNow || (st.btn && /primary/.test(st.btn))) && st.btn ? ' ' + st.btn : '')
+ (st.d && (isNow || st.optional || i === 0) ? '<span class="d">' + st.d + '</span>' : '')
+ '</span></li>';
}).join("") + '</ol></div>';
}
// What the checker saw in this shot's opening picture, in its own sentence.
function frameCheckHtml(s) {
var fc = s.frame_check || {};
if (!s.opening_url || s.clip_url || !fc.verdict || s.fixing || s.drawing) return "";
var faults = (fc.faults || []).filter(function (f) { return f && f.what; });
var dup = (fc.duplicated || []).filter(Boolean);
var retried = s.frame_retried ? ' <i>(' + esc(s.frame_retried) + ')</i>' : '';
if (!faults.length && !dup.length)
return '<div class="fchk ok">\u2713 picture checked - nothing wrong' + retried + '</div>';
var rows = dup.map(function (d) {
return { what: "The same person appears twice: " + d, serious: true }; })
.concat(faults.map(function (f) { return { what: f.what, serious: f.severity === "serious", zoom: f.zoom_url || "" }; }));
var line = function (r, i) {
// The close-up the checker was looking at. Nobody should have to
// take "six fingers" on trust - and sometimes it is simply wrong.
var zm = r.zoom ? '<img data-zoom="' + esc("what the checker zoomed in on") + '" src="' + esc(r.zoom)
+ '" alt="" style="height:54px;border-radius:5px;vertical-align:middle;margin-right:6px;border:1px solid var(--line)">' : '';
return '<span class="w' + (r.serious ? ' serious' : '') + '">' + zm + esc(r.what)
+ ' <button type="button" class="btn go" data-fixfault="' + esc(s.id) + '|' + i + '">Fix this</button></span>';
};
// What matters is said out loud. What merely differs from the description
// is there for anyone who wants it, and is not an alarm.
var big = "", small = "", nsmall = 0;
rows.forEach(function (r, i) { if (r.serious) big += line(r, i); else { small += line(r, i); nsmall++; } });
return '<div class="fchk ' + (fc.verdict === "broken" ? 'bad' : '') + '">'
+ (big ? '<b>The checker looked at this picture and sees:</b>' + retried + big
: '<span style="color:#1f7a3a">\u2713 picture checked - nothing that needs fixing</span>' + retried)
+ (nsmall ? '<details style="margin-top:4px"><summary style="cursor:pointer;color:var(--muted)">'
+ nsmall + ' small thing' + (nsmall === 1 ? '' : 's') + ' it noticed \u2014 look only if the picture bothers you</summary>'
+ small + '</details>' : '')
+ '</div>';
}
// A note to the CAMERA. It is added to what the video model is told when
// this shot is filmed; it redraws nothing. Useful when the still is right
// and the moving picture goes wrong - a hand that grows a finger, a car door
// that swings from the wrong edge.
function filmNoteHtml(s) {
if (!s.opening_url) return "";
return '<div class="fnote"><label>A note to the camera for this shot (optional) \u2014 it changes how the '
+ 'shot is filmed, not the pictures. Press \u201cFilm\u201d again afterwards.</label>'
+ '<input type="text" maxlength="500" data-filmnote="' + esc(s.id) + '" value="' + esc(s.film_note || "")
+ '" placeholder="For example: his left hand stays in his jacket pocket the whole time."></div>';
}
// Several versions of one picture, best first, each with what the checker
// made of it. Picking the best of four needs no vocabulary at all.
function variantsHtml(s) {
if (s.varying) return '<p class="busy">Drawing several versions of this picture and checking each one\u2026 about a minute.</p>';
var v = s.variants;
if (!v || !(v.items || []).length)
return (v && v.error) ? '<p class="cont" style="color:#b4531f">' + esc(v.error) + '</p>' : "";
return '<div class="vars"><h5>' + v.items.length + ' versions of this picture \u2014 best first</h5>'
+ '<p>' + (v.note ? 'Drawn with your note: \u201c' + esc(v.note) + '\u201d. ' : '')
+ 'Click a picture to see it large. Press \u201cUse this one\u201d under the one you like; '
+ 'the ending picture is then drawn to match it.' + (v.error ? ' ' + esc(v.error) : '') + '</p>'
+ '<div class="grid">' + v.items.map(function (it, i) {
var fc = it.check || {};
var faults = (fc.duplicated || []).filter(Boolean).map(function (d) { return "same person twice: " + d; })
.concat((fc.faults || []).filter(function (f) { return f && f.what && f.severity === "serious"; })
.map(function (f) { return f.what; }));
var minor = (fc.faults || []).filter(function (f) { return f && f.what && f.severity !== "serious"; });
var cls = fc.verdict === "broken" ? "bad" : (fc.verdict === "ok" ? "good" : "");
var say = faults.length ? faults[0]
: (minor.length ? "small things only: " + minor[0].what
: (fc.verdict ? "\u2713 the checker sees nothing wrong" : "not checked"));
return '<div class="v ' + cls + '"><img data-zoom="' + esc("shot " + s.id + " \u2014 version " + (i + 1) + ": " + say)
+ '" src="' + esc(it.url) + '" alt=""><div class="say">' + esc(String(say).slice(0, 150)) + '</div>'
+ '<button type="button" class="btn' + (i === 0 && cls !== "bad" ? ' primary' : '')
+ '" data-pickvar="' + esc(s.id) + '|' + i + '">Use this one</button></div>';
}).join("") + '</div></div>';
}
// "That is not right" - about the PICTURE, in the author's own words. The
// picture is what the shot is filmed from, so a fault here is inherited by
// the film and cannot be fixed by filming again.
function frameFaultHtml(s) {
// Why the last attempt at these pictures failed, in words - an author
// looking at an unchanged frame otherwise concludes the button is broken.
var err = s.draw_error ? '<p class="cont" style="color:#b4531f">' + esc(s.draw_error) + '</p>' : "";
// Available on a filmed shot too: a bad clip is very often a bad still,
// and that is exactly when the author needs to change the picture.
if (!s.opening_url) return err;
if (s.fixing || s.drawing)
return '<p class="busy">Redrawing this scene\u2019s pictures from your note\u2026</p>';
return '<div class="ffault"><input type="text" id="ff-' + esc(s.id) + '" maxlength="400" '
+ 'placeholder="Something wrong with this picture? Say what you see \u2014 \u201cit should be night\u201d, '
+ '\u201cthere should be nobody else in the garden\u201d\u2026">'
+ '<button type="button" class="btn go" data-ffgo="' + esc(s.id) + '">Redraw &middot; $0.16</button>'
+ '<button type="button" class="btn go" data-vargo="' + esc(s.id) + '" title="Draws four versions of this '
+ 'picture, checks each one, and lets you choose. Anything typed in the box is used as a note for all four.">'
+ 'Try 4 versions &middot; $0.35</button></div>' + err;
}
// 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) {
if (EDITING === "new:" + after) {
return '<div class="ins" style="text-align:left">'
+ '<textarea class="rough" id="newrough" rows="4" placeholder="'
+ 'What happens in this scene, who is in it and what they say. A sentence or two is enough '
+ '— the writer turns it into a proper shot."></textarea>'
+ '<div class="tools"><button type="button" class="btn primary" data-addgo="'
+ esc(after) + '">Write this scene</button>'
+ '<button type="button" class="btn" data-cancel="1">Cancel</button></div></div>';
}
return '<div class="ins"><button type="button" class="btn" data-add="' + esc(after)
+ '">+ Add a scene ' + (after === "start" ? "at the start" : "after " + esc(after)) + '</button></div>';
}
// The faults themselves, with the prompt change that would prevent each -
// which is the half that makes the check actionable rather than a verdict.
function faultsHtml(s) {
var vc = s.vision_check;
if (!vc || !(vc.faults || []).length) return "";
return '<div class="eyes">'
+ (vc.summary ? '<div class="sum">' + esc(vc.summary) + '</div>' : '')
+ vc.faults.map(function (f) {
return '<div class="f ' + (f.severity === "serious" ? "serious" : "")
+ '"><b>' + esc(f.kind || "") + '</b>'
+ (f.at != null ? ' at ' + Number(f.at).toFixed(1) + 's' : '')
+ ' &middot; ' + esc(f.what || "")
+ (f.prompt_fix ? '<span class="fix">fix: ' + esc(f.prompt_fix)
+ '</span>' : '') + '</div>';
}).join("") + '</div>';
}
function shotHtml(s, voice) {
if (s.writing) {
return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
+ '<h3>' + esc(s.title || "New scene") + '</h3>'
+ '<p class="busy">The writer is turning your note into a shot…</p>'
+ '<p class="cont">' + esc(String(s.rough || "").slice(0, 300)) + '</p></div></div>';
}
var lines = (s.audio || []).map(function (a, ai) {
var who = voice[a.character_id] || {};
return '<div class="line">'
+ '<button type="button" class="play" data-shot="' + esc(s.id) + '">&#9654;</button>'
+ '<div class="say"><span class="who">' + esc(who.name || a.character_id) + '</span> '
+ (a.lip_sync ? '<span class="lip">on camera</span>' : '<span class="vobadge">voice over</span>')
+ '<br>' + esc(a.text || "")
+ (a.from_book ? ' <span title="' + esc(a.from_book.near ? "The book says: " + a.from_book.near : "") + '" style="font-size:11px;border-radius:9px;padding:1px 7px;'
+ (a.from_book.kind === "quote" ? 'background:#e3f3e6;color:#1f7a3a">your words, from the book'
: a.from_book.kind === "close" ? 'background:#fdf1dc;color:#8a5a00">close to the book'
: 'background:#f1e7e2;color:#b4531f">written for the trailer') + '</span>'
+ (a.from_book.near ? '<br><span class="dir">the book: “' + esc(a.from_book.near) + '”</span>' : '') : '')
+ ' <details style="display:inline"><summary style="cursor:pointer;color:var(--muted);font-size:12px;display:inline">change the words or when they are said</summary>'
+ '<input type="text" maxlength="400" style="width:100%;margin:4px 0" data-linetext="' + esc(s.id) + '|' + ai + '" value="' + esc(a.text || "") + '">'
+ (a.lip_sync ? '<label style="display:block;font-size:12px;color:var(--muted);margin:2px 0 4px">Said '
+ '<select data-linewhen="' + esc(s.id) + '|' + ai + '">'
+ [["start", "at the start of the shot"], ["middle", "in the middle of the shot"], ["end", "at the end of the shot"]].map(function (o) {
return '<option value="' + o[0] + '"' + ((a.say_at || "middle") === o[0] ? ' selected' : '') + '>' + o[1] + '</option>'; }).join("")
+ '</select> — the rest of the shot plays in quiet around it.</label>' : '')
+ '<button type="button" class="btn" data-lineset="' + esc(s.id) + '|' + ai + '">Save</button></details>'
+ (a.delivery ? '<br><span class="dir">' + esc(a.delivery) + '</span>' : '')
+ '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>';
}).join("");
var media = "";
if (s.clip_url) {
media = '<div class="frames"><figure><video src="' + esc(s.clip_url) + '" controls '
+ 'preload="metadata"></video><figcaption>the filmed shot'
+ (s.clip_seconds ? ' &middot; ' + Number(s.clip_seconds).toFixed(1) + 's' : '')
+ (s.clip_stale ? ' &middot; <b style="color:#b4531f">filmed from an older picture — film it again</b>' : '')
+ '</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>';
} else if (s.opening_url || s.closing_url) {
media = '<div class="frames">'
+ (s.opening_url ? '<figure><img data-zoom="' + esc("shot " + s.id + " opens on")
+ '" 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>';
}
var chk = s.audio_check || {};
var chkHtml = "";
if (chk.verdict && chk.verdict !== "no dialogue") {
var good = chk.verdict === "ok" || chk.verdict === "drifted";
chkHtml = '<span class="chk ' + (good ? "ok" : "bad") + '" title="'
+ esc("heard: " + String(chk.heard || "")) + '">audio ' + esc(chk.verdict)
+ (chk.coverage != null ? ' ' + Math.round(chk.coverage * 100) + '%' : '')
+ (chk.missing ? ' &mdash; lost “' + esc(String(chk.missing).slice(0, 40)) + '”' : '')
+ '</span>';
}
// What the eyes found, if they have looked. Kinds: render (the picture is
// broken), continuity (the frames disagree), script (wrong shot, well made).
var vc = s.vision_check || null;
var eyeHtml = "";
if (vc && vc.verdict) {
var bad = vc.verdict === "broken";
var faults = vc.faults || [];
eyeHtml = '<span class="chk ' + (bad ? "bad" : (faults.length ? "" : "ok"))
+ '" title="' + esc(vc.summary || "") + '">picture ' + esc(vc.verdict)
+ (faults.length ? ' &middot; ' + faults.length + ' fault'
+ (faults.length === 1 ? '' : 's') : '')
+ (vc.people_on_screen != null ? ' &middot; ' + vc.people_on_screen
+ ' on screen' : '') + '</span>';
}
var tools;
if (s.drawing) tools = '<span class="busy">drawing…</span>';
else if (stillFilming(s)) tools = '<span class="busy">filming…</span>';
else if (EDITING === s.id) {
tools = '<div style="width:100%"><textarea class="rough" id="rough" rows="3" placeholder="'
+ 'What should change in this shot?">' + esc(s.rough || "") + '</textarea>'
+ '<div class="tools"><button type="button" class="btn primary" data-rewrite="' + esc(s.id)
+ '">Rewrite this shot</button>'
+ '<button type="button" class="btn" data-cancel="1">Cancel</button></div></div>';
} else {
tools = '<button type="button" class="btn" data-edit="' + esc(s.id) + '">Change this shot</button>'
+ '<button type="button" class="btn" data-drawone="' + esc(s.id) + '">'
+ (s.opening_url ? 'Redraw' : 'Draw') + ' &middot; $0.14</button>'
+ (s.opening_url ? '<button type="button" class="btn" data-filmone="' + esc(s.id) + '">'
+ (s.clip_url ? 'Film it again' : 'Film this shot') + ' &middot; $0.75</button>' : '')
+ (s.clip_url && s.take_url ? '<button type="button" class="btn" data-recut="' + esc(s.id)
+ '" title="Cuts and mixes the shot again from the footage already paid for">Cut it again from the same footage &middot; free</button>' : '')
+ chkHtml + eyeHtml
+ '<button type="button" class="btn" data-delete="' + esc(s.id) + '">Remove</button>';
}
return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
+ '<h3>' + esc(s.title || "") + '</h3>'
+ '<div class="where">' + esc(s.setting || "") + ' &middot; '
+ Number(s.edit_seconds || 0).toFixed(1) + 's'
+ (s.scene ? ' &middot; <span class="scene">' + esc(s.scene) + '</span>' : '')
+ (s.day ? ' &middot; <span class="scene">' + esc(s.day) + '</span>' : '')
+ '</div>'
+ media
+ variantsHtml(s)
+ frameCheckHtml(s)
+ frameFaultHtml(s)
+ whoInHtml(s)
+ '<p class="act">' + esc(s.action || "") + '</p>'
+ (s.camera ? '<p class="cam">camera: ' + esc(s.camera) + '</p>' : '')
+ (s.continuity ? '<p class="cont">continuity: ' + esc(s.continuity) + '</p>' : '')
+ lines
+ faultsHtml(s)
+ filmNoteHtml(s)
+ '<div class="tools">' + tools + '</div>'
+ (s.draw_unchanged && !s.drawing ? '<p class="cont">Nothing about these pictures has changed since they '
+ 'were drawn, so they were not drawn again and nothing was charged. '
+ '<button type="button" class="btn" data-drawanyway="' + esc(s.id)
+ '">Draw another attempt anyway &middot; $0.14</button></p>' : '')
+ (s.film_unchanged && !s.filming ? '<p class="cont">Nothing that reaches the camera has changed since '
+ 'the last take - same pictures, same words, same voices - so it was not filmed again and nothing '
+ 'was charged. <button type="button" class="btn" data-filmanyway="' + esc(s.id)
+ '">Film another take anyway &middot; $0.75</button></p>' : '')
+ '<textarea class="note" rows="2" data-note="' + esc(s.id) + '" '
+ 'placeholder="Your note on this shot, for the next full revision.">'
+ esc(NOTES[s.id] || "") + '</textarea>'
+ '</div></div>';
}
async function act(name, payload) {
var r = await api(name, payload);
if (!r.ok) { showMsg("msg", r.error || "That did not work."); return null; }
hideMsg("msg");
return r;
}
document.getElementById("pane").addEventListener("click", async function (e) {
var hit = function (attr) { return e.target.closest("[" + attr + "]"); };
var el, r;
if ((el = hit("data-cancel"))) { EDITING = null; openTab(CUR); return; }
if ((el = hit("data-add"))) { EDITING = "new:" + el.getAttribute("data-add"); openTab(CUR); return; }
if ((el = hit("data-edit"))) { EDITING = el.getAttribute("data-edit"); openTab(CUR); return; }
if ((el = hit("data-addgo"))) {
var rough = document.getElementById("newrough").value.trim();
if (rough.length < 20) { showMsg("msg", "Say what happens in the scene."); return; }
el.disabled = true;
r = await act("add_shot", { draft_id: CUR, after: el.getAttribute("data-addgo"), rough: rough });
EDITING = null; if (r) await refresh(); return;
}
if ((el = hit("data-rewrite"))) {
var rw = document.getElementById("rough").value.trim();
if (rw.length < 20) { showMsg("msg", "Say what should change."); return; }
el.disabled = true;
r = await act("write_shot", { draft_id: CUR, shot_id: el.getAttribute("data-rewrite"), rough: rw });
EDITING = null; if (r) await refresh(); return;
}
if ((el = hit("data-drawone"))) {
el.disabled = true;
r = await act("draw_one_shot", { draft_id: CUR, shot_id: el.getAttribute("data-drawone") });
if (r) await refresh(); return;
}
if ((el = hit("data-filmone"))) {
el.disabled = true;
// NEVER force from here. If nothing that reaches the video model has
// changed, the worker films nothing, charges nothing and says so; the
// author can then ask for another take with the button that says what
// it costs. Forcing on every press made a slip of the finger cost $0.75.
var fsid1 = el.getAttribute("data-filmone");
r = await act("film_one_shot_now", { draft_id: CUR, shot_id: fsid1 });
if (r) { showMsg("msg", "Shot " + fsid1 + " sent to be filmed, about $" + r.usd
+ " if anything has changed since the last take.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-recut"))) {
el.disabled = true;
r = await act("recut_shot", { draft_id: CUR, shot_id: el.getAttribute("data-recut") });
if (r) { showMsg("msg", "Cutting shot " + el.getAttribute("data-recut") + " again from the footage you already have. No charge.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-filmanyway"))) {
el.disabled = true;
var fsid2 = el.getAttribute("data-filmanyway");
r = await act("film_one_shot_now", { draft_id: CUR, shot_id: fsid2, force: true });
if (r) { showMsg("msg", "Filming another take of shot " + fsid2 + ", about $" + r.usd
+ ". It takes a minute or two.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-drawanyway"))) {
el.disabled = true;
r = await act("draw_one_shot", { draft_id: CUR, shot_id: el.getAttribute("data-drawanyway"), force: true });
if (r) await refresh(); return;
}
if ((el = hit("data-delete"))) {
if (!confirm("Remove shot " + el.getAttribute("data-delete") + " from this trailer?")) return;
el.disabled = true;
r = await act("delete_shot", { draft_id: CUR, shot_id: el.getAttribute("data-delete") });
if (r) await refresh(); return;
}
if ((el = hit("data-assemble"))) {
el.disabled = true;
r = await act("assemble_trailer", { draft_id: CUR });
if (r) { showMsg("msg", "Joining " + r.shots + " shots. It appears in My videos.", "ok");
setTimeout(function () { location.href = "/videos.html"; }, 2200); }
return;
}
if ((el = hit("data-lead"))) {
el.disabled = true;
r = await act("set_lead", { run_id: runId, draft_id: el.getAttribute("data-lead") });
if (r) await refresh(); return;
}
if ((el = hit("data-draw"))) {
el.disabled = true;
r = await act("draw_storyboard", { draft_id: el.getAttribute("data-draw") });
if (r) await refresh(); return;
}
if ((el = hit("data-reelgo"))) {
el.disabled = true;
var rl = await api("reel", { draft_id: CUR });
if (!rl.ok) { showMsg("msg", rl.error || "Could not load the storyboard."); return; }
if (String(PKG.reel_state || "") !== "ready") {
el.textContent = "Speaking every shot…";
REELWANT = true;
r = await act("prepare_reel", { draft_id: CUR });
if (r) showMsg("msg", "Speaking " + r.shots + " shots. This is the same "
+ "audio the film will use, so it is only paid for once. It starts "
+ "playing on its own when it is ready.", "ok");
else REELWANT = false;
await refresh();
return;
}
hideMsg("msg");
REEL = rl.shots || []; RAT = -1;
reelShow(0);
return;
}
if ((el = hit("data-reeltoggle"))) {
if (reelAudio.paused) { reelAudio.play().catch(function(){}); reelShow(RAT); }
else { reelAudio.pause(); clearInterval(RTIMER); RTIMER = null; reelPaint(); }
return;
}
if ((el = hit("data-reelstep"))) {
var d = Number(el.getAttribute("data-reelstep")) || 1;
reelShow(Math.max(0, Math.min((REEL || []).length - 1, RAT + d)));
return;
}
if ((el = hit("data-reelstop"))) { reelStop(); return; }
if ((el = hit("data-reeljump"))) {
var jid = el.getAttribute("data-reeljump");
clearInterval(RTIMER); RTIMER = null; reelAudio.pause();
var target = [...document.querySelectorAll(".shot")].find(function (x) {
var nu = x.querySelector(".num");
return nu && nu.textContent.trim() === jid; });
if (target) {
document.querySelectorAll(".shot.lit").forEach(function (x) {
x.classList.remove("lit"); });
target.classList.add("lit");
target.scrollIntoView({ behavior: "smooth", block: "center" });
var ta = target.querySelector("textarea.note");
if (ta) setTimeout(function () { ta.focus(); }, 500);
}
return;
}
if ((el = hit("data-eyes"))) {
el.disabled = true; el.textContent = "Looking at the footage…";
r = await act("check_clips", { draft_id: el.getAttribute("data-eyes") });
if (r) showMsg("msg", "Sampling frames from " + r.shots + " clips and "
+ "checking them against the script.", "ok");
await refresh();
return;
}
if ((el = hit("data-cuts"))) {
el.disabled = true; el.textContent = "Working out what to drop…";
r = await act("propose_cuts", { draft_id: el.getAttribute("data-cuts") });
if (r) { showMsg("msg", "Asking for " + r.count + " different cuts.", "ok");
setTimeout(refresh, 25000); }
return;
}
if ((el = hit("data-play"))) {
var cid = el.getAttribute("data-play");
el.disabled = true;
var cc = await api("cut_clips", { draft_id: CUR, cut_id: cid });
el.disabled = false;
if (!cc.ok) { showMsg("msg", cc.error || "That would not play."); return; }
QUEUE = (cc.shots || []).filter(function (s) { return s.clip_url; });
if (!QUEUE.length) {
showMsg("msg", "None of the shots in this cut are filmed yet, so there "
+ "is nothing to play. Film them first.");
return;
}
if (cc.filmed < cc.of)
showMsg("msg", "Playing the " + cc.filmed + " of " + cc.of
+ " shots that are filmed. The rest are skipped.");
PLAYING = cid; QAT = 0;
await openTab(CUR); // the video element does not exist until this lands
playCut();
return;
}
if ((el = hit("data-stop"))) { PLAYING = null; QUEUE = []; openTab(CUR); return; }
if ((el = hit("data-makecut"))) {
el.disabled = true;
r = await act("make_cut", { draft_id: CUR, cut_id: el.getAttribute("data-makecut") });
if (r) { showMsg("msg", "Joining " + r.shots + " shots into this version. "
+ "It appears in My videos with its own share page.", "ok");
await refresh(); }
return;
}
if ((el = hit("data-measure"))) {
el.disabled = true; el.textContent = "Speaking every line…";
r = await act("measure_script", { draft_id: el.getAttribute("data-measure") });
if (r) {
showMsg("msg", "Speaking every line in the cast voices. A minute or two.", "ok");
setTimeout(refresh, 20000);
}
return;
}
if ((el = hit("data-judge"))) {
el.disabled = true;
r = await act("judge_scripts", { run_id: runId });
if (r) { showMsg("msg", "Every writer is reading all " + r.scripts + " scripts, without knowing whose is whose. About $" + r.usd + ".", "ok"); await load(); }
return;
}
if ((el = hit("data-review"))) {
el.disabled = true;
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-vargo"))) {
var vsid = el.getAttribute("data-vargo");
var vbox = document.getElementById("ff-" + vsid);
el.disabled = true; el.textContent = "starting\u2026";
r = await act("draw_variants", { draft_id: CUR, shot_id: vsid, n: 4,
note: vbox ? vbox.value.trim() : "" });
if (r) { showMsg("msg", "Drawing " + r.n + " versions of shot " + vsid + ", about $" + r.usd
+ ". They appear here in about a minute.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-pickvar"))) {
var pv = el.getAttribute("data-pickvar").split("|");
el.disabled = true; el.textContent = "working\u2026";
r = await act("pick_variant", { draft_id: CUR, shot_id: pv[0], index: Number(pv[1]) });
if (r) { showMsg("msg", "Using that version for shot " + pv[0] + " and drawing its ending picture to match.", "ok");
await refresh(); }
return;
}
if ((el = hit("data-chkpics"))) {
el.disabled = true; el.textContent = "starting\u2026";
r = await act("check_pictures", { draft_id: CUR });
if (r) { showMsg("msg", r.shots ? "Checking " + r.shots + " pictures. Broken ones are redrawn "
+ "automatically. This takes a few minutes." : (r.note || "Already checked."), "ok"); await refresh(); }
return;
}
if ((el = hit("data-addwriter"))) {
var aw = document.getElementById("addwsel");
el.disabled = true;
r = await act("add_writer", { run_id: runId, writer: aw ? aw.value : "" });
if (r) { showMsg("msg", "That writer is reading the book now. Its script appears as a new tab in a few minutes.", "ok"); await load(); }
return;
}
if ((el = hit("data-lineset"))) {
var lk = el.getAttribute("data-lineset"), lp = lk.split("|");
var lin = document.querySelector('[data-linetext="' + lk + '"]');
el.disabled = true;
var lwh = document.querySelector('[data-linewhen="' + lk + '"]');
r = await act("set_line", { draft_id: CUR, shot_id: lp[0], index: Number(lp[1]), text: lin ? lin.value : "",
say_at: lwh ? lwh.value : "" });
if (r) { showMsg("msg", "Saved. The voice will be recorded again with the new words the next time it is played or filmed.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-findprops"))) {
el.disabled = true;
r = await act("find_props", { draft_id: CUR });
if (r) { showMsg("msg", "Reading the script for objects that appear more than once, then drawing a reference for each. About two minutes.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-propsave"))) {
var ppid = el.getAttribute("data-propsave");
var pta = document.querySelector('[data-propdesc="' + ppid + '"]');
el.disabled = true;
var psl = document.querySelector('[data-propshotlist="' + ppid + '"]');
r = await act("set_prop", { draft_id: CUR, prop_id: ppid, description: pta ? pta.value : "",
shots: psl ? psl.value : null, force: !!el.getAttribute("data-again") });
if (r) { showMsg("msg", el.getAttribute("data-again")
? "Drawing the reference picture again, about $" + r.usd + ". When it looks right, redraw the shots that show it."
: "Saved. If you changed the description its picture is being drawn again.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-propshots"))) {
el.disabled = true;
r = await act("redraw_prop_shots", { draft_id: CUR, prop_id: el.getAttribute("data-propshots") });
if (r) { showMsg("msg", "Redrawing those shots one after another, about $" + r.usd + ". A few minutes.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-propdel"))) {
if (!confirm("Stop keeping this object the same from shot to shot?")) return;
el.disabled = true;
r = await act("remove_prop", { draft_id: CUR, prop_id: el.getAttribute("data-propdel") });
if (r) await refresh(); return;
}
if ((el = hit("data-fixfault"))) {
var ff = el.getAttribute("data-fixfault").split("|");
var fs = (PKG.shots || []).filter(function (x) { return String(x.id) === ff[0]; })[0] || {};
var fcx = fs.frame_check || {};
var all = (fcx.duplicated || []).filter(Boolean).map(function (d) {
return "The same person appears twice: " + d + ". Each person appears once only."; })
.concat((fcx.faults || []).filter(function (f) { return f && f.what; })
.map(function (f) { return f.what; }));
var say = all[Number(ff[1])] || "";
if (!say) return;
el.disabled = true; el.textContent = "working\u2026";
r = await act("report_frame_fault", { draft_id: CUR, shot_id: ff[0], note: say });
if (r) { showMsg("msg", "Redrawing shot " + ff[0] + " to fix that. About a minute.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-ffgo"))) {
var fsid = el.getAttribute("data-ffgo");
var fbox = document.getElementById("ff-" + fsid);
var fnote = fbox ? fbox.value.trim() : "";
if (fnote.length < 4) { if (fbox) fbox.focus(); return; }
el.disabled = true; el.textContent = "working\u2026";
r = await act("report_frame_fault", { draft_id: CUR, shot_id: fsid, note: fnote });
if (r) { showMsg("msg", "Redrawing shot " + fsid + " from your note. It takes about a minute.", "ok");
await refresh(); }
else { el.disabled = false; el.innerHTML = "Redraw &middot; $0.16"; }
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"))) {
el.disabled = true;
r = await act("start_revision", { run_id: runId });
// Say so. Pressing this used to change nothing on screen for several
// seconds, which reads as a button that does not work.
if (r) { showMsg("msg", "The head writer is rewriting the script with your notes. "
+ "It takes a few minutes and appears as a new tab.", "ok"); await refresh(); }
return;
}
if ((el = hit("data-film"))) {
el.disabled = true;
r = await act("film_draft", { draft_id: el.getAttribute("data-film") });
if (r) { showMsg("msg", "Filming " + r.shots + " shots, about $" + r.usd
+ ". It appears in My videos as it goes.", "ok");
setTimeout(function () { location.href = "/videos.html"; }, 2500); }
return;
}
if ((el = hit("data-retry"))) {
el.disabled = true;
r = await act("retry_writer", { draft_id: el.getAttribute("data-retry") });
if (r) await refresh(); return;
}
var btn = e.target.closest(".play");
if (!btn) return;
var sid = btn.getAttribute("data-shot");
btn.disabled = true; btn.innerHTML = "&hellip;";
var a = await api("shot_audio", { draft_id: CUR, shot_id: sid });
btn.disabled = false; btn.innerHTML = "&#9654;";
if (!a.ok) { showMsg("msg", a.error || "That would not play."); return; }
hideMsg("msg");
if (!a.url) return;
document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (x) {
x.textContent = a.seconds.toFixed(1) + "s / " + Number(a.planned).toFixed(1) + "s";
x.className = "dur" + (a.seconds > Number(a.planned) ? " over" : "");
});
audio.pause(); audio.src = a.url; audio.play();
});
// Play the cut's clips one after another in a single element. This is the
// whole preview: no joining, no encoding, no upload, nothing to pay for.
function playCut() {
var v = document.getElementById("cutv");
if (!v || !QUEUE.length) return;
var at = document.getElementById("cutat");
function step() {
if (QAT >= QUEUE.length) {
if (at) at.textContent = "end of this cut";
return;
}
var s = QUEUE[QAT];
if (at) at.textContent = "shot " + s.id + " · " + (QAT + 1)
+ " of " + QUEUE.length + (s.title ? " · " + s.title : "");
v.src = s.clip_url;
v.play().catch(function () {});
}
v.onended = function () { QAT += 1; step(); };
v.onerror = function () { QAT += 1; step(); };
step();
}
var filmNoteTimers = {};
document.getElementById("pane").addEventListener("input", function (e) {
var el = e.target;
var fsid = el.getAttribute && el.getAttribute("data-filmnote");
if (!fsid) return;
el.classList.remove("saved");
clearTimeout(filmNoteTimers[fsid]);
filmNoteTimers[fsid] = setTimeout(async function () {
var r = await api("set_film_note", { draft_id: CUR, shot_id: fsid, note: el.value });
if (r.ok) {
el.classList.add("saved");
(PKG.shots || []).forEach(function (x) { if (String(x.id) === fsid) x.film_note = r.note; });
}
}, 700);
});
var noteTimers = {};
document.getElementById("pane").addEventListener("input", function (e) {
var ta = e.target;
var sid = ta.getAttribute && ta.getAttribute("data-note");
if (!sid) return;
ta.classList.remove("saved");
clearTimeout(noteTimers[sid]);
noteTimers[sid] = setTimeout(async function () {
var r = await api("add_shot_note", { draft_id: CUR, shot_id: sid, note: ta.value });
if (r.ok) { NOTES[sid] = ta.value; ta.classList.add("saved"); }
}, 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();
})();
</script>
</body>
</html>