diff --git a/script.html b/script.html index a69b320..bebb78c 100644 --- a/script.html +++ b/script.html @@ -394,4 +394,203 @@ function render(r) { + 'go straight to filming.

' + '

Hear it first

' + '
' - + '

Filming costs real money; speaking the ' \ No newline at end of file + + '

Filming costs real money; speaking the ' + + 'words costs about a hundredth of a cent. Run it through in your pet’s voice ' + + 'over the opening stills first, and only film once the timing and the jokes land.

' + + '' + + '' + + '' + + '

Cutting to the clock speaks the whole piece once, ' + + 'measures where every word actually falls, and moves the shot boundaries to real word ' + + 'endings. It rewrites how the lines are divided — not the words themselves.

' + + '
' + + '
' + + '
' + + '

The shots

' + + s.segments.map(function (x) { return shotHtml(x, false); }).join("") + + own + + film; + } else if (VIEW === 3) { + body = head + film; + } else { + body = head + post + film; + } + + out.innerHTML = cams + stepper(stage, VIEW) + body; + + wire(locked); +} + +var PICS = [], SUGGESTED = []; + +function renderPics() { + var el = document.getElementById("pics"); + if (!el) return; + el.innerHTML = PICS.map(function (p) { + return '
' + + '' + esc(p.description || ' + + '' + + '' + + '
'; + }).join(""); + [].forEach.call(el.querySelectorAll("[data-rmpic]"), function (b) { + b.onclick = async function () { + var r = await api("delete_project_image", { email: email, image_id: b.getAttribute("data-rmpic") }); + if (!r.ok) { showMsg("msg", r.error || "Could not remove that."); return; } + await loadPics(); + }; + }); +} + +async function loadPics() { + var r = await api("list_project_images", { email: email, project_id: pid }); + PICS = (r.ok && r.images) || []; + renderPics(); +} + +async function addPic(file) { + hideMsg("msg"); + if (!file || !/^image\//.test(file.type)) { showMsg("msg", "That is not an image."); return; } + // Asked for up front: a picture with no description cannot be matched to + // anything, so it would just sit there unused. + var desc = prompt("What is in this picture? A short description is what lets it be matched to the script."); + if (!desc || desc.trim().length < 3) { showMsg("msg", "A description is needed to place the picture."); return; } + var drop = document.getElementById("drop"); + if (drop) drop.textContent = "Uploading…"; + var img; + try { img = await resizeImage(file, 1600, 0.85); } + catch (e) { showMsg("msg", e.message || "Could not read that image."); return; } + var r = await api("add_project_image", { email: email, project_id: pid, + description: desc.trim(), data_b64: img.b64 }); + if (drop) drop.textContent = "Paste an image, drop one here, or click to choose"; + if (!r.ok) { showMsg("msg", r.error || "That upload did not work."); return; } + await loadPics(); +} + +function bindPics() { + var drop = document.getElementById("drop"); + if (!drop) return; + var picker = document.getElementById("picker"); + + drop.onclick = function () { picker.click(); }; + picker.onchange = function () { + [].forEach.call(picker.files || [], addPic); + picker.value = ""; + }; + drop.addEventListener("dragover", function (e) { e.preventDefault(); drop.classList.add("hot"); }); + drop.addEventListener("dragleave", function () { drop.classList.remove("hot"); }); + drop.addEventListener("drop", function (e) { + e.preventDefault(); drop.classList.remove("hot"); + [].forEach.call((e.dataTransfer && e.dataTransfer.files) || [], addPic); + }); + // Paste anywhere on the page, which is how people actually do this. + document.addEventListener("paste", function (e) { + var items = (e.clipboardData && e.clipboardData.items) || []; + for (var i = 0; i < items.length; i++) { + if (items[i].type && items[i].type.indexOf("image") === 0) { + var f = items[i].getAsFile(); + if (f) { e.preventDefault(); addPic(f); } + return; + } + } + }); + + var sg = document.getElementById("suggest"); + if (sg) sg.onclick = async function () { + hideMsg("msg"); + if (!PICS.length) { showMsg("msg", "Add a picture first."); return; } + sg.disabled = true; sg.textContent = "Reading the script…"; + var r = await api("suggest_image_placements", { email: email, project_id: pid }); + if (!r.ok) { sg.disabled = false; sg.textContent = "Suggest where they fit"; showMsg("msg", r.error || "Could not suggest."); return; } + // Reading the script is a model call, far too slow to answer inside one + // request, so it runs in the background and we wait for it here. + if (r.pending) { + document.getElementById("sugnote").textContent = r.status_text || "Reading the script…"; + var waited = 0; + r = await new Promise(function (done) { + var t = setInterval(async function () { + waited += 4; + var g = await api("suggest_image_placements", { email: email, project_id: pid }); + if (g.ok && !g.pending) { clearInterval(t); done(g); return; } + if (waited > 150) { clearInterval(t); done({ ok: false, error: "That is taking unusually long." }); } + }, 4000); + }); + } + sg.disabled = false; sg.textContent = "Suggest where they fit"; + document.getElementById("sugnote").textContent = r.status_text || ""; + if (!r.ok) { showMsg("msg", r.error || "Could not suggest."); return; } + SUGGESTED = r.placements || []; + var byId = {}; + PICS.forEach(function (p) { byId[p.id] = p; }); + document.getElementById("places").innerHTML = SUGGESTED.length + ? SUGGESTED.map(function (x) { + var p = byId[x.image_id] || {}; + return '
shot ' + x.index + ' · ' + + esc((p.description || "").slice(0, 60)) + ' — ' + esc(x.why || "") + '
'; + }).join("") + + '' + : '

No picture fitted any shot well enough to use.

'; + var ap = document.getElementById("applyp"); + if (ap) ap.onclick = async function () { + ap.disabled = true; ap.textContent = "Applying…"; + var res = await api("apply_image_placements", { email: email, project_id: pid, + placements: SUGGESTED }); + if (!res.ok) { ap.disabled = false; ap.textContent = "Use these"; showMsg("msg", res.error || "Could not apply."); return; } + showMsg("msg", res.status_text || "Placed.", "ok"); + load(); + }; + }; + + loadPics(); +} + +function wire(locked) { + [].forEach.call(document.querySelectorAll("[data-stage]"), function (b) { + b.onclick = function () { + var want = parseInt(b.getAttribute("data-stage"), 10); + if (want === VIEW) return; + VIEW = want; + hideMsg("msg"); + render(DATA); + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + }); + bindPics(); + bindPreviz(); + bindAlign(); + bindFilm((DATA && DATA.project) || {}); + bindPost(); + if (locked) { bindBottom(); return; } + + // Save edits when the field loses focus, so typing is never interrupted. + [].forEach.call(document.querySelectorAll("[data-scene],[data-cam],[data-photo]"), function (el) { + var evt = el.tagName === "SELECT" ? "change" : "blur"; + el.addEventListener(evt, async function () { + var idx = el.getAttribute("data-scene") || el.getAttribute("data-cam") || el.getAttribute("data-photo"); + var body = { email: email, project_id: pid, index: parseInt(idx, 10) }; + if (el.hasAttribute("data-scene")) body.scene = el.value; + if (el.hasAttribute("data-cam")) body.camera = el.value; + if (el.hasAttribute("data-photo")) body.photo_id = el.value; + var r = await api("update_shot", body); + if (!r.ok) showMsg("msg", r.error || "That change did not save."); + else hideMsg("msg"); + }); + }); + + // Switching source changes which fields apply, so redraw the card after. + [].forEach.call(document.querySelectorAll("[data-srcgrp]"), function (g) { + g.addEventListener("click", async function (e) { + var c = e.target.closest("[data-src]"); + if (!c) return; + var idx = parseInt(g.getAttribute("data-srcgrp"), 10); + var r = await api("update_shot", { email: email, project_id: pid, index: idx, + source: c.getAttribute("data-src") }); + if (!r.ok) { showMsg("msg", r.error || "Could not change that."); return; } + hideMsg("msg"); + load(); + }); + }); + + [].forEach.call(document.querySelectorAll("[data-broll]"), function (b) { + b.addEventListener("click", async function () { + \ No newline at end of file