Cutaways are laid across the joins between shots, '
+ 'so the picture changes while the voice keeps going and the join is never on screen.
'
+ + '
'
+ + '
'
+ + ''
+ + '
'
+ + '
'
+ + ''
+ + 'Nothing is rendered — this switches the picture as it plays.'
+ + ''
+ + ''
+ + '
'
+ + ''
+ + ''
+ + '
'
+ ''
+ ''
@@ -549,7 +575,152 @@ async function bindFilm(p) {
};
}
+// ---------------------------------------------------------------- the cut
+// EDL is {duration, cuts:[{shot,start,seconds,kind,src,in,moved}], segments}.
+// Only the cut positions are editable; the server composes the rest, so the
+// page never has to be trusted about source urls or trim points.
+var EDL = null, DIRTY = false;
+
+function tlDraw() {
+ var tl = document.getElementById("tl");
+ if (!tl || !EDL) return;
+ var dur = EDL.duration || 1, w = tl.clientWidth || 1;
+ var html = "";
+ (EDL.cuts || []).forEach(function (c, i) {
+ var left = (c.start / dur) * 100, wide = (c.seconds / dur) * 100;
+ html += '
'
+ + 'shot ' + c.shot + '
';
+ });
+ html += '';
+ html += '
';
+ for (var t = 0; t <= dur; t += 8) {
+ html += '' + t + 's';
+ }
+ html += '
';
+ tl.innerHTML = html;
+ var n = (EDL.cuts || []).length;
+ document.getElementById("tlinfo").textContent = n
+ ? n + " cutaway" + (n === 1 ? "" : "s") + " over " + dur + " seconds. Drag to move one, or pull its right edge to change how long it runs. The voice underneath never stops."
+ : "No cutaways on this one, so the talking head runs the whole way.";
+ document.getElementById("edlsave").disabled = !DIRTY;
+}
+
+function tlBind() {
+ var tl = document.getElementById("tl");
+ if (!tl) return;
+ var drag = null;
+
+ tl.addEventListener("pointerdown", function (e) {
+ var grip = e.target.closest("[data-grip]");
+ var block = e.target.closest("[data-i]");
+ if (!block) return;
+ var i = parseInt((grip || block).getAttribute(grip ? "data-grip" : "data-i"), 10);
+ drag = { i: i, mode: grip ? "len" : "move", x0: e.clientX,
+ start0: EDL.cuts[i].start, secs0: EDL.cuts[i].seconds };
+ tl.setPointerCapture(e.pointerId);
+ e.preventDefault();
+ });
+
+ tl.addEventListener("pointermove", function (e) {
+ if (!drag) return;
+ var dur = EDL.duration || 1, w = tl.clientWidth || 1;
+ var d = ((e.clientX - drag.x0) / w) * dur;
+ var c = EDL.cuts[drag.i];
+ if (drag.mode === "move") {
+ c.start = Math.max(0, Math.min(drag.start0 + d, dur - c.seconds));
+ } else {
+ c.seconds = Math.max(0.8, Math.min(drag.secs0 + d, Math.min(20, dur - c.start)));
+ }
+ c.start = Math.round(c.start * 20) / 20; // quarter-frame is plenty
+ c.seconds = Math.round(c.seconds * 20) / 20;
+ c.moved = true; DIRTY = true;
+ tlDraw();
+ });
+
+ function end(e) {
+ if (!drag) return;
+ drag = null;
+ try { tl.releasePointerCapture(e.pointerId); } catch (x) {}
+ }
+ tl.addEventListener("pointerup", end);
+ tl.addEventListener("pointercancel", end);
+}
+
+// The preview never renders anything. The talking head plays with its audio
+// throughout and the picture is covered while a cutaway is due, which is
+// exactly what the finished cut does.
+function previewBind(p) {
+ var v = document.getElementById("pvid");
+ if (!v) return;
+ if (p.video_url) v.src = p.video_url;
+ var ovl = document.getElementById("povl"),
+ oi = document.getElementById("povlimg"),
+ ov = document.getElementById("povlvid");
+
+ function activeCut(t) {
+ var cs = (EDL && EDL.cuts) || [];
+ for (var i = 0; i < cs.length; i++) {
+ if (t >= cs[i].start && t < cs[i].start + cs[i].seconds) return cs[i];
+ }
+ return null;
+ }
+
+ var showing = null;
+ v.addEventListener("timeupdate", function () {
+ var head = document.getElementById("tlhead");
+ if (head && EDL) head.style.left = ((v.currentTime / (EDL.duration || 1)) * 100) + "%";
+ var c = activeCut(v.currentTime);
+ if (c === showing) return;
+ showing = c;
+ if (!c) { ovl.style.display = "none"; ov.pause(); return; }
+ ovl.style.display = "block";
+ if (c.kind === "video") {
+ oi.style.display = "none"; ov.style.display = "block";
+ if (ov.src !== c.src) ov.src = c.src;
+ try { ov.currentTime = c["in"] || 0; } catch (x) {}
+ ov.play().catch(function () {});
+ } else {
+ ov.style.display = "none"; ov.pause();
+ oi.style.display = "block"; oi.src = c.src;
+ }
+ });
+
+ document.getElementById("pplay").onclick = function () {
+ if (v.paused) { v.play(); this.innerHTML = "▮▮ Pause"; }
+ else { v.pause(); this.innerHTML = "▶ Preview the cut"; }
+ };
+}
+
+async function loadEdl(p) {
+ var r = await api("preview_edl", { email: email, project_id: pid });
+ if (!r.ok) return;
+ EDL = r.edl; DIRTY = false;
+ tlDraw(); tlBind(); previewBind(p);
+
+ document.getElementById("edlsave").onclick = async function () {
+ var b = this; b.disabled = true; b.textContent = "Saving…";
+ var s = await api("save_edl_cuts", { email: email, project_id: pid,
+ cuts: (EDL.cuts || []).map(function (c) {
+ return { shot: c.shot, start: c.start, seconds: c.seconds }; }) });
+ b.textContent = "Save these positions";
+ if (!s.ok) { b.disabled = false; showMsg("msg", s.error || "Those positions were refused."); return; }
+ hideMsg("msg");
+ EDL = s.edl; DIRTY = false; tlDraw();
+ showMsg("msg", s.status_text || "Saved.", "ok");
+ };
+
+ document.getElementById("edlreset").onclick = async function () {
+ var s = await api("reset_edl_cuts", { email: email, project_id: pid });
+ if (!s.ok) { showMsg("msg", s.error || "Could not reset."); return; }
+ EDL = s.edl; DIRTY = false; tlDraw();
+ showMsg("msg", s.status_text || "Back to the suggested cut.", "ok");
+ };
+}
+
function bindPost() {
+ if (document.getElementById("tl")) loadEdl((DATA && DATA.project) || {});
var sb = document.getElementById("seams");
function paintSeams(r) {
var el = document.getElementById("seamout");