trailer.html: draw the storyboard, and show the cast sheets
The draw button is live and priced from what that writer actually wrote - three sheets a character plus an opening and a closing frame a shot - so a 14-shot trailer costs $3.33 and a 30-shot one costs more, rather than both being quoted an average. Cast sheets render as head/front/side, because they are the thing that stops a face drifting between shots and the author should be able to reject a face before seventy pictures are drawn from it. Progress is per picture rather than a spinner: drawing takes minutes and a spinner that might mean "working" or "hung" is worse than a count.
This commit is contained in:
parent
d1c9320966
commit
c6788afba5
1 changed files with 79 additions and 24 deletions
103
trailer.html
103
trailer.html
|
|
@ -17,11 +17,14 @@
|
|||
.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:10px;margin:0 0 22px}
|
||||
.ch{border:1px solid var(--line);border-radius:10px;padding:9px 12px;font-size:13px;max-width:330px}
|
||||
.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:3px}
|
||||
.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: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 .body{flex:1;min-width:0}
|
||||
|
|
@ -83,36 +86,51 @@
|
|||
|
||||
if (!runId) { showMsg("msg", "No run was named in the link."); return; }
|
||||
|
||||
// ------------------------------------------------------------ polling
|
||||
// A draft is busy while it is being written or drawn. Both are minutes long
|
||||
// and both report progress on the row, so one poll covers them.
|
||||
function busyState(d) {
|
||||
var s = String(d.draw_state || "");
|
||||
return d.state === "writing" || (s && !/^(drawn|failed)/.test(s));
|
||||
}
|
||||
|
||||
async function load() {
|
||||
async function load(repaint) {
|
||||
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(busyState);
|
||||
RUN = r.run; DRAFTS = r.drafts || [];
|
||||
document.getElementById("title").textContent = RUN.book_title || "Trailers";
|
||||
document.getElementById("sub").textContent = sub();
|
||||
if (!CUR && DRAFTS.length) CUR = DRAFTS[0].id;
|
||||
paintTabs();
|
||||
var busy = DRAFTS.some(function (d) { return d.state === "writing"; });
|
||||
if (busy && !timer) timer = setInterval(load, 4000);
|
||||
|
||||
var busy = DRAFTS.some(busyState);
|
||||
if (busy && !timer) timer = setInterval(load, 5000);
|
||||
if (!busy && timer) { clearInterval(timer); timer = null; }
|
||||
if (!busy) openTab(CUR);
|
||||
else paintWriting();
|
||||
|
||||
var me = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
|
||||
if (me && me.state === "writing") { paintWriting(); return; }
|
||||
// Repaint when asked, on first paint, or when work has just finished.
|
||||
if (repaint !== false && (wasBusy || !document.querySelector(".shot"))) openTab(CUR);
|
||||
else paintBar();
|
||||
}
|
||||
|
||||
function sub() {
|
||||
var bits = [];
|
||||
if (RUN.stage) bits.push(RUN.stage);
|
||||
bits.push(DRAFTS.length + " writer" + (DRAFTS.length === 1 ? "" : "s"));
|
||||
if (RUN.cost_usd) bits.push("$" + Number(RUN.cost_usd).toFixed(2) + " so far");
|
||||
var spent = DRAFTS.reduce(function (a, d) {
|
||||
return a + Number(d.write_usd || 0) + Number(d.draw_usd || 0); }, 0);
|
||||
if (spent) bits.push("$" + spent.toFixed(2) + " so far");
|
||||
if (RUN.created_at) bits.push(when(RUN.created_at));
|
||||
return bits.join(" · ");
|
||||
}
|
||||
|
||||
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">' + d.score + '</span>'
|
||||
: '';
|
||||
return '<button type="button" class="tab' + (d.id === CUR ? ' on' : '')
|
||||
|
|
@ -154,16 +172,42 @@
|
|||
pane.innerHTML = paneHtml(meta, r.draft);
|
||||
}
|
||||
|
||||
function drawBtn(meta) {
|
||||
var shots = (PKG.shots || []).length, cast = (PKG.characters || []).length;
|
||||
var imgs = cast * 3 + shots * 2;
|
||||
var price = imgs * 0.068;
|
||||
var s = String(meta.draw_state || "");
|
||||
if (s && !/^(drawn|failed)/.test(s)) {
|
||||
return '<button type="button" class="btn" disabled>Drawing… ' + esc(s)
|
||||
+ '</button><span class="soon">Cast sheets first, then every shot. '
|
||||
+ 'This takes a few minutes.</span>';
|
||||
}
|
||||
if (/^drawn/.test(s)) {
|
||||
return '<button type="button" class="btn" data-draw="' + esc(meta.id)
|
||||
+ '">Draw it again · $' + price.toFixed(2) + '</button>'
|
||||
+ '<span class="soon">' + esc(s)
|
||||
+ (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) + ' spent' : '')
|
||||
+ '</span>';
|
||||
}
|
||||
return '<button type="button" class="btn primary" data-draw="' + esc(meta.id)
|
||||
+ '">Draw this storyboard · $' + price.toFixed(2) + '</button>'
|
||||
+ '<span class="soon">' + imgs + ' pictures — ' + cast
|
||||
+ ' cast sheets and ' + shots + ' shots, opening and closing.</span>'
|
||||
+ (s === "failed" && meta.note ? '<span class="soon">' + esc(meta.note) + '</span>' : '');
|
||||
}
|
||||
|
||||
function paintBar() {
|
||||
var el = document.querySelector(".bar");
|
||||
var meta = DRAFTS.filter(function (d) { return d.id === CUR; })[0];
|
||||
if (el && meta) el.innerHTML = drawBtn(meta);
|
||||
}
|
||||
|
||||
function paneHtml(meta, draft) {
|
||||
var p = PKG, shots = p.shots || [], cast = p.characters || [];
|
||||
var voice = {};
|
||||
cast.forEach(function (c) { voice[c.id] = c; });
|
||||
|
||||
var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0);
|
||||
// Drawing is priced off what this writer actually wrote, not an estimate:
|
||||
// three sheets a character plus an opening and a closing frame a shot.
|
||||
var imgs = cast.length * 3 + shots.length * 2;
|
||||
var price = imgs * 0.068;
|
||||
|
||||
var head = '<div class="head"><h2>' + esc(p.title || meta.label) + '</h2>'
|
||||
+ (p.tagline ? '<span class="tag">' + esc(p.tagline) + '</span>' : '') + '</div>'
|
||||
|
|
@ -182,25 +226,26 @@
|
|||
+ '</ul></div>';
|
||||
}
|
||||
|
||||
var bar = '<div class="bar">'
|
||||
+ '<button type="button" class="btn" disabled>Draw this storyboard · $'
|
||||
+ price.toFixed(2) + '</button>'
|
||||
+ '<span class="soon">' + imgs + ' pictures. Not wired up yet.</span>'
|
||||
+ '</div>';
|
||||
|
||||
var castHtml = '<div class="cast">' + cast.map(function (c) {
|
||||
return '<div class="ch"><b>' + esc(c.name || c.id) + '</b>'
|
||||
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>';
|
||||
}
|
||||
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>';
|
||||
}).join("") + '</div>';
|
||||
|
||||
return head + probs + bar + castHtml
|
||||
return head + probs + '<div class="bar">' + drawBtn(meta) + '</div>' + castHtml
|
||||
+ shots.map(function (s) { return shotHtml(s, voice); }).join("");
|
||||
}
|
||||
|
||||
function shotHtml(s, voice) {
|
||||
var lines = (s.audio || []).map(function (a, i) {
|
||||
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>'
|
||||
|
|
@ -234,9 +279,19 @@
|
|||
+ '</div></div>';
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ play + notes
|
||||
// ------------------------------------------------------------ actions
|
||||
|
||||
document.getElementById("pane").addEventListener("click", async function (e) {
|
||||
var draw = e.target.closest ? e.target.closest("[data-draw]") : null;
|
||||
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 || "That did not start."); return; }
|
||||
hideMsg("msg");
|
||||
load();
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = e.target.closest ? e.target.closest(".play") : null;
|
||||
if (!btn) return;
|
||||
var sid = btn.getAttribute("data-shot");
|
||||
|
|
|
|||
Loading…
Reference in a new issue