script: rebuild part 2 of 6
This commit is contained in:
parent
9250a559c3
commit
ab1027e6db
1 changed files with 200 additions and 1 deletions
201
script.html
201
script.html
|
|
@ -394,4 +394,203 @@ function render(r) {
|
||||||
+ 'go straight to filming.</p>'
|
+ 'go straight to filming.</p>'
|
||||||
+ '<h2 class="sec">Hear it first</h2>'
|
+ '<h2 class="sec">Hear it first</h2>'
|
||||||
+ '<div class="card" id="pzcard">'
|
+ '<div class="card" id="pzcard">'
|
||||||
+ '<p class="foot" style="margin:0 0 14px">Filming costs real money; speaking the '<!--PTLCHUNK-->
|
+ '<p class="foot" style="margin:0 0 14px">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.</p>'
|
||||||
|
+ '<button type="button" class="btn ghost" id="pzmake">Speak the script</button>'
|
||||||
|
+ '<button type="button" class="btn" id="alignbtn" style="margin-left:8px">Cut it to the clock</button>'
|
||||||
|
+ '<span class="foot" style="margin-left:12px" id="pznote"></span>'
|
||||||
|
+ '<p class="foot" style="margin:10px 0 0">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.</p>'
|
||||||
|
+ '<div id="alignout" style="margin-top:14px"></div>'
|
||||||
|
+ '<div id="pzout" style="margin-top:16px"></div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<h2 class="sec" style="margin-top:34px">The shots</h2>'
|
||||||
|
+ 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 '<div class="pic">'
|
||||||
|
+ '<img src="' + esc(p.url) + '" alt="' + esc(p.description || "") + '">'
|
||||||
|
+ '<input type="text" value="' + esc(p.description || "") + '" readonly>'
|
||||||
|
+ '<button type="button" class="rm" data-rmpic="' + esc(p.id) + '">remove</button>'
|
||||||
|
+ '</div>';
|
||||||
|
}).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 '<div class="place">shot ' + x.index + ' · '
|
||||||
|
+ esc((p.description || "").slice(0, 60)) + ' — ' + esc(x.why || "") + '</div>';
|
||||||
|
}).join("")
|
||||||
|
+ '<button type="button" class="btn" id="applyp" style="margin-top:14px">Use these</button>'
|
||||||
|
: '<p class="foot">No picture fitted any shot well enough to use.</p>';
|
||||||
|
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 () {
|
||||||
|
<!--PTLCHUNK-->
|
||||||
Loading…
Reference in a new issue