Storyboard: per-shot background - generate from an edited description, same as previous, carry through the scene

This commit is contained in:
gator 2026-09-22 20:30:29 +00:00
parent 72f76c7f0f
commit d15f469d45

View file

@ -111,6 +111,13 @@
.line .say{flex:1;min-width:0;font-size:14px} .line .say{flex:1;min-width:0;font-size:14px}
.line .who{color:var(--accent);font-size:12px} .line .who{color:var(--accent);font-size:12px}
.line .dir{color:var(--muted);font-size:12px} .line .dir{color:var(--muted);font-size:12px}
.bg{display:flex;gap:12px;align-items:flex-start;border:1px dashed var(--line);border-radius:10px;padding:8px 10px;margin:8px 0}
.bg img{width:180px;border-radius:6px;border:1px solid var(--line);cursor:zoom-in}
.bg .bgtxt{flex:1;min-width:200px}
.bg .bgtxt b{font-size:13px}
.bg textarea{width:100%;box-sizing:border-box;font:inherit;font-size:13px;min-height:64px;margin:4px 0}
.bg .tools{display:flex;gap:6px;flex-wrap:wrap}
.bg .tiny{font-size:12px;color:var(--muted)}
.play{border:1px solid var(--line);background:none;color:var(--ink);border-radius:999px; .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} width:30px;height:30px;flex:none;cursor:pointer;font:inherit;line-height:1}
.play:hover{border-color:var(--accent)} .play:hover{border-color:var(--accent)}
@ -1363,6 +1370,41 @@
}).join("") + '</div>'; }).join("") + '</div>';
} }
// THE BACKGROUND of a shot: the place's picture with nobody in it, which
// every picture of that place is drawn from. Dialled in one shot at a time
// and carried forward until it should change ("crawl before we walk").
var BGEDIT = null;
function placeOf(s) {
if (s.place) return s.place;
var t = String(s.scene || "").toLowerCase().replace(/^(int|ext)\.?\s*\/?\s*(int|ext)?\.?\s*/, "").replace(/\s*[-–—]\s*(day|night|dawn|dusk|morning|evening|afternoon|late afternoon|continuous).*$/, "").replace(/[^a-z0-9]+/g, " ").trim();
return t;
}
function backgroundHtml(s) {
var sets = PKG.sets || {}, key = placeOf(s), st = sets[key] || {};
var url = s.set_ref || st.ref_url || "";
var busy = st.state === "drawing";
var editing = BGEDIT === s.id || (!url && !busy && BGEDIT === "auto:" + s.id);
var desc = st.description || ((s.setting || s.scene || "") + ". " + String(s.opening_image || "").replace(/\b(Dorothy|Toto|Em|Aunt Em|Henry|Uncle Henry)\b[^.]*\./g, "").trim() + " Nobody in the picture.");
var pic = url ? '<img src="' + esc(url) + '" data-zoom="' + esc("Background — " + (st.name || s.scene || key)) + '" alt="">'
: '<div class="tiny" style="width:180px">' + (busy ? 'drawing the background…' : 'no background yet') + '</div>';
var body = '<b>Background</b> <span class="tiny">' + esc(st.name || s.scene || key) + (s.place ? ' · set by you' : '') + '</span>';
if (st.error) body += '<div class="warn">' + esc(st.error) + '</div>';
if (editing) {
body += '<textarea data-bgdesc="' + esc(s.id) + '" placeholder="The place with nobody in it: what is there, where the light comes from, the time of day.">' + esc(desc) + '</textarea>'
+ '<div class="tools"><button type="button" class="btn small primary" data-bggen="' + esc(s.id) + '">' + (url ? 'Generate it again' : 'Generate background image') + ' &middot; $0.07</button>'
+ '<button type="button" class="btn small" data-bgcancel="1">Cancel</button></div>';
} else {
body += '<div class="tools">'
+ '<button type="button" class="btn small" data-bgedit="' + esc(s.id) + '"' + (busy ? ' disabled' : '') + '>' + (url ? 'Change the background' : 'Generate background image') + '</button>'
+ '<button type="button" class="btn small" data-bgcopy="' + esc(s.id) + '" title="This shot uses the background of the shot before it">Same as the previous shot</button>'
+ (url ? '<button type="button" class="btn small" data-bgforward="' + esc(s.id) + '" title="The following shots of this scene use this background">Use it for the rest of the scene</button>' : '')
+ (s.place ? '<button type="button" class="btn small" data-bgclear="' + esc(s.id) + '">Back to the scene\'s own</button>' : '')
+ '</div>'
+ (st.description && url ? '<div class="tiny" style="margin-top:4px">' + esc(String(st.description).slice(0, 220)) + '</div>' : '');
}
return '<div class="bg">' + pic + '<div class="bgtxt">' + body + '</div></div>';
}
function shotHtml(s, voice) { function shotHtml(s, voice) {
if (s.writing) { if (s.writing) {
return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">' return '<div class="shot"><div class="num">' + esc(s.id) + '</div><div class="body">'
@ -1397,7 +1439,7 @@
+ '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>'; + '</div><span class="dur" data-dur="' + esc(s.id) + '"></span></div>';
}).join(""); }).join("");
var media = ""; var media = backgroundHtml(s);
if (s.clip_url) { if (s.clip_url) {
media = '<div class="frames"><figure><video src="' + esc(s.clip_url) + '" controls ' media = '<div class="frames"><figure><video src="' + esc(s.clip_url) + '" controls '
+ 'preload="metadata"></video><figcaption>the filmed shot' + 'preload="metadata"></video><figcaption>the filmed shot'
@ -1454,7 +1496,7 @@
} else { } else {
tools = '<button type="button" class="btn" data-edit="' + esc(s.id) + '">Change this shot</button>' 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) + '">' + '<button type="button" class="btn" data-drawone="' + esc(s.id) + '">'
+ (s.opening_url ? 'Redraw' : 'Draw') + ' &middot; $0.14</button>' + (s.opening_url ? 'Redraw the shot picture' : 'Draw the shot picture (actors in the background)') + ' &middot; $0.14</button>'
+ (s.opening_url ? '<button type="button" class="btn" data-filmone="' + esc(s.id) + '">' + (s.opening_url ? '<button type="button" class="btn" data-filmone="' + esc(s.id) + '">'
+ (s.clip_url ? 'Film it again' : 'Film this shot') + ' &middot; about $' + filmCost(s).toFixed(2) + '</button>' : '') + (s.clip_url ? 'Film it again' : 'Film this shot') + ' &middot; about $' + filmCost(s).toFixed(2) + '</button>' : '')
+ (s.clip_url && s.take_url ? '<button type="button" class="btn" data-recut="' + esc(s.id) + (s.clip_url && s.take_url ? '<button type="button" class="btn" data-recut="' + esc(s.id)
@ -1508,6 +1550,19 @@
var el, r; var el, r;
if ((el = hit("data-cancel"))) { EDITING = null; openTab(CUR); return; } if ((el = hit("data-cancel"))) { EDITING = null; openTab(CUR); return; }
if ((el = hit("data-bgedit"))) { BGEDIT = el.getAttribute("data-bgedit"); openTab(CUR); return; }
if ((el = hit("data-bgcancel"))) { BGEDIT = null; openTab(CUR); return; }
if ((el = hit("data-bggen"))) {
var sidb = el.getAttribute("data-bggen"), ta = document.querySelector('[data-bgdesc="' + sidb + '"]');
el.disabled = true;
r = await act("background", { draft_id: CUR, shot_id: sidb, op: "generate", description: ta ? ta.value : "" });
if (r) { BGEDIT = null; showMsg("msg", "Drawing the background (about $" + r.usd + ").", "ok"); await refresh(); setTimeout(refresh, 25000); }
else el.disabled = false;
return;
}
if ((el = hit("data-bgcopy"))) { r = await act("background", { draft_id: CUR, shot_id: el.getAttribute("data-bgcopy"), op: "copy" }); if (r) await refresh(); return; }
if ((el = hit("data-bgforward"))) { r = await act("background", { draft_id: CUR, shot_id: el.getAttribute("data-bgforward"), op: "forward" }); if (r) { showMsg("msg", "Carried to " + r.shots + " following shots.", "ok"); await refresh(); } return; }
if ((el = hit("data-bgclear"))) { r = await act("background", { draft_id: CUR, shot_id: el.getAttribute("data-bgclear"), op: "clear" }); if (r) await refresh(); return; }
if ((el = hit("data-drawcast"))) { if ((el = hit("data-drawcast"))) {
el.disabled = true; el.disabled = true;
r = await act("draw_cast", { draft_id: el.getAttribute("data-drawcast") }); r = await act("draw_cast", { draft_id: el.getAttribute("data-drawcast") });