trailer.html: edit, draw, film and reassemble one shot at a time
The trailer was all-or-nothing. If one shot was wrong - a line cut short, a character duplicated, a scene missing from the front - the only lever was to film all twenty-four again. Every shot now carries its own controls: describe a change and have the writer turn it into a proper shot, draw its two frames, film just that one, watch the clip that came back, and read the machine check that compared what it says with what the script says. "Add a scene here" sits between shots, because the note that prompted this was that the trailer needed an opening the writer never thought to include. Assemble is separate and cheap - it joins whatever has been filmed - so re-cutting after changing one shot costs nothing but the one shot. Prices are on the buttons because they differ by two orders of magnitude: rewriting a shot is cents, drawing it is fourteen, filming it is about a dollar.
This commit is contained in:
parent
a20ed2d57b
commit
d21360ea2b
1 changed files with 250 additions and 174 deletions
418
trailer.html
418
trailer.html
|
|
@ -14,7 +14,6 @@
|
|||
.tab.lead{color:var(--accent)}
|
||||
.tab .sc{color:var(--accent2);margin-left:7px;font-weight:600}
|
||||
.tab .st{color:var(--muted);margin-left:7px;font-size:12px}
|
||||
.tab .crown{margin-left:6px}
|
||||
.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}
|
||||
|
|
@ -28,7 +27,7 @@
|
|||
.sheets img{width:33%;border-radius:6px;border:1px solid var(--line);display:block;
|
||||
aspect-ratio:16/9;object-fit:cover}
|
||||
.shot{border-top:1px solid var(--line);padding:16px 0;display:flex;gap:16px}
|
||||
.shot .num{width:34px;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 h3{margin:0 0 3px;font-size:15px}
|
||||
.shot .where{color:var(--muted);font-size:12px;margin-bottom:8px}
|
||||
|
|
@ -36,7 +35,7 @@
|
|||
.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{width:100%;border-radius:8px;border:1px solid var(--line);display:block}
|
||||
.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}
|
||||
|
|
@ -65,11 +64,21 @@
|
|||
.rev{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
|
||||
.rev h4{margin:0 0 6px;font-size:14px}
|
||||
.rev h4 .n{color:var(--accent2);font-weight:600;margin-left:8px}
|
||||
.rev .v{color:var(--muted);font-size:13px;margin:0 0 8px}
|
||||
.rev ul{margin:4px 0 8px;padding-left:18px;font-size:13px}
|
||||
.rev .er{color:#e0685f}
|
||||
.rev .kp{color:var(--accent2)}
|
||||
.rev .sh{color:var(--muted);font-variant-numeric:tabular-nums}
|
||||
.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}
|
||||
.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}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -92,14 +101,12 @@
|
|||
navBar("trailer");
|
||||
|
||||
var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = [];
|
||||
var timer = null;
|
||||
var timer = null, EDITING = null;
|
||||
var runId = new URLSearchParams(location.search).get("run") || "";
|
||||
var audio = new Audio();
|
||||
|
||||
if (!runId) { showMsg("msg", "No run was named in the link."); return; }
|
||||
|
||||
// A draft is busy while it is being written or drawn; a run is busy while it
|
||||
// is reviewing or revising. Both are minutes long and report on the row.
|
||||
function busyDraft(d) {
|
||||
var s = String(d.draw_state || "");
|
||||
return d.state === "writing" || (s && !/^(drawn|failed)/.test(s));
|
||||
|
|
@ -107,28 +114,42 @@
|
|||
function busyRun() {
|
||||
return RUN && /^(writing|reviewing|revising)$/.test(String(RUN.stage || ""));
|
||||
}
|
||||
// A shot is busy while it is being written, drawn or filmed. Any of those
|
||||
// means the package on screen is about to change.
|
||||
function busyShots() {
|
||||
return (PKG.shots || []).some(function (s) {
|
||||
return s.writing || s.drawing || s.filming; });
|
||||
}
|
||||
|
||||
async function load(repaint) {
|
||||
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; }
|
||||
var wasBusy = (DRAFTS.length && DRAFTS.some(busyDraft)) || busyRun();
|
||||
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();
|
||||
}
|
||||
|
||||
var busy = DRAFTS.some(busyDraft) || busyRun();
|
||||
if (busy && !timer) timer = setInterval(load, 5000);
|
||||
function tick() {
|
||||
var busy = DRAFTS.some(busyDraft) || busyRun() || busyShots();
|
||||
if (busy && !timer) timer = setInterval(refresh, 5000);
|
||||
if (!busy && timer) { clearInterval(timer); timer = null; }
|
||||
}
|
||||
|
||||
var me = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
|
||||
if (me && me.state === "writing") { paintWriting(); return; }
|
||||
if (repaint !== false && (wasBusy || !document.querySelector(".shot"))) openTab(CUR);
|
||||
else paintBar();
|
||||
// A refresh keeps the tab you are on and the box you are typing in.
|
||||
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();
|
||||
await openTab(CUR);
|
||||
tick();
|
||||
}
|
||||
|
||||
function leadDraftId() {
|
||||
|
|
@ -146,6 +167,7 @@
|
|||
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(" · ");
|
||||
|
|
@ -159,30 +181,21 @@
|
|||
: (s && !/^(drawn|failed)/.test(s)) ? '<span class="st">' + esc(s) + '</span>'
|
||||
: d.score ? '<span class="sc">' + d.score + '</span>'
|
||||
: '';
|
||||
var isLead = d.writer === RUN.lead_writer;
|
||||
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
|
||||
+ (isLead ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
|
||||
+ (d.writer === RUN.lead_writer ? ' lead' : '') + '" data-tab="' + esc(d.id) + '">'
|
||||
+ esc(d.label || d.writer)
|
||||
+ (Number(d.iteration) > 1 ? ' v' + d.iteration : '')
|
||||
+ (isLead ? '<span class="crown" title="head writer">★</span>' : '')
|
||||
+ extra + '</button>';
|
||||
+ (d.writer === RUN.lead_writer ? ' ★' : '') + extra + '</button>';
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function paintWriting() {
|
||||
document.getElementById("pane").innerHTML =
|
||||
'<p class="empty">Working. Reading the whole book takes a few minutes.</p>';
|
||||
}
|
||||
|
||||
document.getElementById("tabs").addEventListener("click", function (e) {
|
||||
var id = e.target.closest ? e.target.closest(".tab") : null;
|
||||
if (!id) return;
|
||||
CUR = id.getAttribute("data-tab");
|
||||
CUR = id.getAttribute("data-tab"); EDITING = null;
|
||||
paintTabs(); openTab(CUR);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ one trailer
|
||||
|
||||
async function openTab(id) {
|
||||
var meta = DRAFTS.filter(function (d) { return d.id === id; })[0];
|
||||
if (!meta) return;
|
||||
|
|
@ -194,8 +207,10 @@
|
|||
+ esc(meta.id) + '">Try this writer again</button></div>';
|
||||
return;
|
||||
}
|
||||
if (meta.state === "writing") { paintWriting(); return; }
|
||||
pane.innerHTML = '<p class="empty">Loading…</p>';
|
||||
if (meta.state === "writing") {
|
||||
pane.innerHTML = '<p class="empty">Working. Reading the whole book takes a few 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 || {};
|
||||
|
|
@ -204,95 +219,77 @@
|
|||
}
|
||||
|
||||
function barHtml(meta) {
|
||||
var shots = (PKG.shots || []).length, cast = (PKG.characters || []).length;
|
||||
var imgs = cast * 3 + shots * 2;
|
||||
var price = imgs * 0.068;
|
||||
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 = cast.length * 3 + shots.length * 2;
|
||||
var s = String(meta.draw_state || "");
|
||||
var drawing = s && !/^(drawn|failed)/.test(s);
|
||||
var drawn = /^drawn/.test(s);
|
||||
var isLead = meta.writer === RUN.lead_writer;
|
||||
var out = [];
|
||||
|
||||
// 1. Whose concept wins. The author's call, not a score's.
|
||||
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 (!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>');
|
||||
|
||||
// 2. Drawing.
|
||||
if (drawing) {
|
||||
out.push('<button type="button" class="btn" disabled>Drawing… ' + esc(s) + '</button>');
|
||||
} else {
|
||||
out.push('<button type="button" class="btn" data-draw="' + esc(meta.id) + '">'
|
||||
+ (drawn ? "Draw it again" : "Draw this storyboard") + ' · $'
|
||||
+ price.toFixed(2) + '</button>');
|
||||
if (!drawn) out.push('<span class="soon">' + imgs + ' pictures</span>');
|
||||
else out.push('<span class="soon">' + esc(s)
|
||||
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 shots')
|
||||
+ ' · $' + (imgs * 0.068).toFixed(2) + '</button>');
|
||||
else out.push('<span class="soon">storyboard drawn'
|
||||
+ (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) : '') + '</span>');
|
||||
}
|
||||
|
||||
// 3. Review, revise, film - only meaningful on the head writer's tab.
|
||||
if (isLead) {
|
||||
var others = DRAFTS.filter(function (d) {
|
||||
return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length;
|
||||
if (RUN.stage === "reviewing") {
|
||||
if (RUN.stage === "reviewing")
|
||||
out.push('<button type="button" class="btn" disabled>The others are reading it…</button>');
|
||||
} else if (others) {
|
||||
else if (others)
|
||||
out.push('<button type="button" class="btn" data-review="1">'
|
||||
+ (REVIEWS.length ? "Review it again" : "Have the others review this")
|
||||
+ ' · ' + others + ' readers</button>');
|
||||
}
|
||||
if (RUN.stage === "revising") {
|
||||
+ (REVIEWS.length ? "Review it again" : "Have the others review this") + '</button>');
|
||||
if (RUN.stage === "revising")
|
||||
out.push('<button type="button" class="btn" disabled>Rewriting…</button>');
|
||||
} else if (REVIEWS.length || Object.keys(NOTES).length) {
|
||||
out.push('<button type="button" class="btn primary" data-revise="1">'
|
||||
+ 'Revise with the notes</button>');
|
||||
}
|
||||
if (drawn) {
|
||||
else if (REVIEWS.length || Object.keys(NOTES).length)
|
||||
out.push('<button type="button" class="btn" data-revise="1">Revise the whole script</button>');
|
||||
|
||||
if (unfilmed && !undrawn)
|
||||
out.push('<button type="button" class="btn primary" data-film="' + esc(meta.id)
|
||||
+ '">Film this trailer</button>');
|
||||
} else {
|
||||
out.push('<span class="soon">Draw the storyboard before filming — '
|
||||
+ 'every shot is filmed from its opening frame.</span>');
|
||||
}
|
||||
+ '">Film ' + (filmed ? 'the ' + unfilmed + ' remaining shots' : 'all ' + shots.length + ' shots')
|
||||
+ ' · $' + (unfilmed * 0.75).toFixed(2) + '</button>');
|
||||
if (filmed)
|
||||
out.push('<button type="button" class="btn primary" data-assemble="' + esc(meta.id)
|
||||
+ '">Make the trailer · ' + filmed + ' of ' + shots.length + ' shots</button>');
|
||||
}
|
||||
return out.join("");
|
||||
}
|
||||
|
||||
function paintBar() {
|
||||
var el = document.querySelector(".bar");
|
||||
var meta = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
|
||||
if (el && meta) el.innerHTML = barHtml(meta);
|
||||
}
|
||||
|
||||
function reviewsHtml(meta) {
|
||||
var mine = REVIEWS.filter(function (r) {
|
||||
return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); });
|
||||
if (!mine.length) return "";
|
||||
return '<div class="revs">' + mine.map(function (r) {
|
||||
var parts = ['<div class="rev"><h4>' + esc(r.label)
|
||||
+ '<span class="n">' + esc(r.score) + '/100</span></h4>'];
|
||||
function list(items, cls, fmt) {
|
||||
if (!items || !items.length) return "";
|
||||
return '<ul class="' + cls + '">' + items.map(fmt).join("") + '</ul>';
|
||||
return (!items || !items.length) ? ""
|
||||
: '<ul class="' + cls + '">' + items.map(fmt).join("") + '</ul>';
|
||||
}
|
||||
parts.push(list(r.errors, "er", function (e) {
|
||||
return '<div class="rev"><h4>' + esc(r.label) + '<span class="n">' + esc(r.score) + '/100</span></h4>'
|
||||
+ list(r.errors, "er", function (e) {
|
||||
return '<li>' + (e.shot ? '<span class="sh">shot ' + esc(e.shot) + '</span> ' : '')
|
||||
+ esc(e.problem || "") + (e.fix ? ' — <i>' + esc(e.fix) + '</i>' : '') + '</li>'; }));
|
||||
parts.push(list(r.suggestions, "", function (s) {
|
||||
+ esc(e.problem || "") + (e.fix ? ' — <i>' + esc(e.fix) + '</i>' : '') + '</li>'; })
|
||||
+ list(r.suggestions, "", function (s) {
|
||||
return '<li>' + (s.shot ? '<span class="sh">shot ' + esc(s.shot) + '</span> ' : '')
|
||||
+ esc(s.change || "") + (s.why ? ' <span class="sh">(' + esc(s.why) + ')</span>' : '') + '</li>'; }));
|
||||
parts.push(list(r.strengths, "kp", function (s) {
|
||||
return '<li>' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '</li>'; }));
|
||||
return parts.join("") + '</div>';
|
||||
+ esc(s.change || "") + '</li>'; })
|
||||
+ list(r.strengths, "kp", function (s) {
|
||||
return '<li>' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '</li>'; })
|
||||
+ '</div>';
|
||||
}).join("") + '</div>';
|
||||
}
|
||||
|
||||
function paneHtml(meta, draft) {
|
||||
var p = PKG, shots = p.shots || [], cast = p.characters || [];
|
||||
var voice = {};
|
||||
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);
|
||||
|
||||
|
|
@ -303,43 +300,58 @@
|
|||
+ (Number(meta.iteration) > 1 ? ' · revision ' + meta.iteration : '')
|
||||
+ ' · ' + shots.length + ' shots · '
|
||||
+ Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0")
|
||||
+ ' · ' + cast.length + ' characters'
|
||||
+ (meta.write_usd ? ' · $' + Number(meta.write_usd).toFixed(2) + ' to write' : '')
|
||||
+ (meta.note ? ' · ' + esc(meta.note) : '')
|
||||
+ '</p>';
|
||||
+ ' · ' + cast.length + ' characters</p>';
|
||||
|
||||
var probs = "";
|
||||
if (draft.problems) {
|
||||
probs = '<div class="probs"><b>The checker still has objections</b><ul>'
|
||||
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>';
|
||||
}
|
||||
return '<li>' + esc(x) + '</li>'; }).join("") + '</ul></div>'
|
||||
: "";
|
||||
|
||||
var castHtml = '<div class="cast">' + cast.map(function (c) {
|
||||
var sheets = "";
|
||||
if (c.head_url || c.front_url || c.side_url) {
|
||||
sheets = '<div class="sheets">'
|
||||
+ [c.head_url, c.front_url, c.side_url].map(function (u) {
|
||||
return u ? '<img src="' + esc(u) + '" alt="">' : ''; }).join("")
|
||||
+ '</div>';
|
||||
}
|
||||
var sheets = (c.head_url || c.front_url || c.side_url)
|
||||
? '<div class="sheets">' + [c.head_url, c.front_url, c.side_url].map(function (u) {
|
||||
return u ? '<img src="' + esc(u) + '" alt="">' : ''; }).join("") + '</div>' : '';
|
||||
return '<div class="ch">' + sheets + '<b>' + esc(c.name || c.id) + '</b>'
|
||||
+ '<span class="vo">' + esc(c.role || "") + (c.age ? " · " + esc(c.age) : "")
|
||||
+ (c.voice_id ? " · voiced" : " · silent") + '</span>'
|
||||
+ '<div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>';
|
||||
+ '<span class="vo">' + esc(c.role || "") + (c.voice_id ? " · voiced" : " · silent")
|
||||
+ '</span><div class="ap">' + esc((c.appearance || "").slice(0, 150)) + '</div></div>';
|
||||
}).join("") + '</div>';
|
||||
|
||||
var body = insertHtml("start");
|
||||
shots.forEach(function (s) {
|
||||
body += shotHtml(s, voice) + insertHtml(s.id);
|
||||
});
|
||||
|
||||
return head + probs + '<div class="bar">' + barHtml(meta) + '</div>'
|
||||
+ reviewsHtml(meta) + castHtml
|
||||
+ shots.map(function (s) { return shotHtml(s, voice); }).join("");
|
||||
+ reviewsHtml(meta) + castHtml + body;
|
||||
}
|
||||
|
||||
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>';
|
||||
}
|
||||
|
||||
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) {
|
||||
var who = voice[a.character_id] || {};
|
||||
return '<div class="line">'
|
||||
+ '<button type="button" class="play" data-shot="' + esc(s.id) + '" title="Hear this shot">▶</button>'
|
||||
+ '<button type="button" class="play" data-shot="' + esc(s.id) + '">▶</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 || "")
|
||||
|
|
@ -347,102 +359,168 @@
|
|||
+ '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>';
|
||||
}).join("");
|
||||
|
||||
var frames = "";
|
||||
if (s.opening_url || s.closing_url) {
|
||||
frames = '<div class="frames">'
|
||||
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 ? ' · ' + Number(s.clip_seconds).toFixed(1) + 's' : '')
|
||||
+ '</figcaption></figure>'
|
||||
+ (s.opening_url ? '<figure><img 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 src="' + esc(s.opening_url) + '" alt=""><figcaption>opens on</figcaption></figure>' : '')
|
||||
+ (s.closing_url ? '<figure><img 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 ? ' — lost “' + esc(String(chk.missing).slice(0, 40)) + '”' : '')
|
||||
+ '</span>';
|
||||
}
|
||||
|
||||
var tools;
|
||||
if (s.drawing) tools = '<span class="busy">drawing…</span>';
|
||||
else if (s.filming) 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') + ' · $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') + ' · $0.75</button>' : '')
|
||||
+ chkHtml
|
||||
+ '<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 || "") + ' · '
|
||||
+ Number(s.edit_seconds || 0).toFixed(1) + 's</div>'
|
||||
+ frames
|
||||
+ media
|
||||
+ '<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
|
||||
+ '<div class="tools">' + tools + '</div>'
|
||||
+ '<textarea class="note" rows="2" data-note="' + esc(s.id) + '" '
|
||||
+ 'placeholder="Your note on this shot. It outranks everything the other writers said.">'
|
||||
+ 'placeholder="Your note on this shot, for the next full revision.">'
|
||||
+ esc(NOTES[s.id] || "") + '</textarea>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ actions
|
||||
|
||||
async function act(name, payload, msg) {
|
||||
var r = await api(name, payload);
|
||||
if (!r.ok) { showMsg("msg", r.error || "That did not work."); return null; }
|
||||
hideMsg("msg");
|
||||
if (msg) showMsg("msg", msg(r), "ok");
|
||||
return r;
|
||||
}
|
||||
|
||||
document.getElementById("pane").addEventListener("click", async function (e) {
|
||||
var t = e.target.closest ? e.target : null;
|
||||
if (!t) return;
|
||||
var hit = function (attr) { var el = e.target.closest("[" + attr + "]"); return el; };
|
||||
var el, r;
|
||||
|
||||
var lead = e.target.closest("[data-lead]");
|
||||
if (lead) {
|
||||
lead.disabled = true;
|
||||
var a = await api("set_lead", { run_id: runId, draft_id: lead.getAttribute("data-lead") });
|
||||
if (!a.ok) { lead.disabled = false; showMsg("msg", a.error || ""); return; }
|
||||
hideMsg("msg"); await load(); openTab(CUR); return;
|
||||
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;
|
||||
}
|
||||
|
||||
var draw = e.target.closest("[data-draw]");
|
||||
if (draw) {
|
||||
draw.disabled = true;
|
||||
var d = await api("draw_storyboard", { draft_id: draw.getAttribute("data-draw") });
|
||||
if (!d.ok) { draw.disabled = false; showMsg("msg", d.error || ""); return; }
|
||||
hideMsg("msg"); load(); 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;
|
||||
}
|
||||
|
||||
var rev = e.target.closest("[data-review]");
|
||||
if (rev) {
|
||||
rev.disabled = true;
|
||||
var v = await api("start_review", { run_id: runId });
|
||||
if (!v.ok) { rev.disabled = false; showMsg("msg", v.error || ""); return; }
|
||||
hideMsg("msg"); load(); 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;
|
||||
}
|
||||
|
||||
var rvs = e.target.closest("[data-revise]");
|
||||
if (rvs) {
|
||||
rvs.disabled = true;
|
||||
var w = await api("start_revision", { run_id: runId });
|
||||
if (!w.ok) { rvs.disabled = false; showMsg("msg", w.error || ""); return; }
|
||||
hideMsg("msg"); load(); return;
|
||||
if ((el = hit("data-filmone"))) {
|
||||
el.disabled = true;
|
||||
r = await act("film_one_shot_now", { draft_id: CUR, shot_id: el.getAttribute("data-filmone") });
|
||||
if (r) await refresh(); return;
|
||||
}
|
||||
|
||||
var film = e.target.closest("[data-film]");
|
||||
if (film) {
|
||||
film.disabled = true;
|
||||
var f = await api("film_draft", { draft_id: film.getAttribute("data-film") });
|
||||
if (!f.ok) { film.disabled = false; showMsg("msg", f.error || ""); return; }
|
||||
showMsg("msg", "Filming " + f.shots + " shots, about $" + f.usd
|
||||
+ ". It appears in My videos as it goes.", "ok");
|
||||
setTimeout(function () { location.href = "/videos.html"; }, 2500);
|
||||
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;
|
||||
}
|
||||
|
||||
var retry = e.target.closest("[data-retry]");
|
||||
if (retry) {
|
||||
retry.disabled = true;
|
||||
var y = await api("retry_writer", { draft_id: retry.getAttribute("data-retry") });
|
||||
if (!y.ok) { retry.disabled = false; showMsg("msg", y.error || ""); return; }
|
||||
hideMsg("msg"); load(); 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-review"))) {
|
||||
el.disabled = true;
|
||||
r = await act("start_review", { run_id: runId }); if (r) await refresh(); return;
|
||||
}
|
||||
if ((el = hit("data-revise"))) {
|
||||
el.disabled = true;
|
||||
r = await act("start_revision", { run_id: runId }); if (r) 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 = "…";
|
||||
var r = await api("shot_audio", { draft_id: CUR, shot_id: sid });
|
||||
var a = await api("shot_audio", { draft_id: CUR, shot_id: sid });
|
||||
btn.disabled = false; btn.innerHTML = "▶";
|
||||
if (!r.ok) { showMsg("msg", r.error || "That would not play."); return; }
|
||||
if (!a.ok) { showMsg("msg", a.error || "That would not play."); return; }
|
||||
hideMsg("msg");
|
||||
if (!r.url) return;
|
||||
// Spoken length against the length the writer planned for. A line that
|
||||
// overruns its shot is the single most common fault in these scripts.
|
||||
document.querySelectorAll('[data-dur="' + sid + '"]').forEach(function (el) {
|
||||
el.textContent = r.seconds.toFixed(1) + "s / " + Number(r.planned).toFixed(1) + "s";
|
||||
el.className = "dur" + (r.seconds > Number(r.planned) ? " over" : "");
|
||||
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 = r.url; audio.play();
|
||||
audio.pause(); audio.src = a.url; audio.play();
|
||||
});
|
||||
|
||||
var noteTimers = {};
|
||||
|
|
@ -452,8 +530,6 @@
|
|||
if (!sid) return;
|
||||
ta.classList.remove("saved");
|
||||
clearTimeout(noteTimers[sid]);
|
||||
// Saved as you stop typing. A note the author has to remember to save is
|
||||
// a note that gets lost between tabs.
|
||||
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"); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue