Files

58 lines
4.3 KiB
JavaScript

// Timeline: thumbnails, cut marks, cut score, shot bars (click = select, drag the white handles, double-click = split).
import { h, hue, fmtT, pill } from "./common.js";
export function timelineCard(c) {
const { plan, an, segs, sel, n, pxPerFrame, tlWidth, maxLen, cuts, hover, tlEl, zoom, play, vid, active } = c;
const S = segs.value, N = n.value, ppf = pxPerFrame.value, fps = plan.fps;
const thumbs = an.value?.thumbs || [];
const thumbW = Math.max(8, thumbs.length ? tlWidth.value / thumbs.length : 96);
const score = an.value?.score || [];
const maxS = Math.max(4, ...score.slice(0, N));
const sparkPts = score.slice(0, N).map((v, i) => `${(i * ppf).toFixed(1)},${(24 - Math.min(1, v / maxS) * 22).toFixed(1)}`).join(" ");
const tooLong = S.filter(s => s.len > maxLen.value).length;
const totalGen = active.value.reduce((a, s) => a + s.gen, 0);
return h("div", { class: "card" }, [
h("div", { class: "row", style: "margin-bottom:6px" }, [
h("span", { class: "sect" }, "Timeline"),
pill(`${S.length} shots`), pill(`${cuts.value.length} cuts`), pill(`generate ${totalGen} frames`),
tooLong ? pill(`${tooLong} too long (> ${maxLen.value} f)`, "warn") : null,
h("span", { class: "grow" }),
h("span", { class: "hint" }, "zoom"),
h("input", { type: "range", min: 1, max: 8, step: 0.5, value: zoom.value, style: "width:100px", onInput: e => { zoom.value = parseFloat(e.target.value); } }),
]),
h("div", { class: "tl", ref: tlEl,
onPointermove: e => { if (!e.buttons) hover.value = c.frameAt(e); }, onPointerleave: () => { hover.value = null; },
onClick: e => { if (e.target.classList.contains("hdl")) return; c.seek(c.frameAt(e).frame); },
onDblclick: e => { if (e.target.classList.contains("hdl")) return; c.splitAt(c.frameAt(e).frame); } }, [
h("div", { class: "tlin", style: `width:${tlWidth.value}px` }, [
h("div", { class: "strip" }, thumbs.map(t => h("img", { src: t.src, style: `width:${thumbW}px` }))),
// outside the start / end range: dimmed, not planned
c.range.value.lo > 0 ? h("div", { class: "outr", style: `left:0;width:${c.range.value.lo * ppf}px`, title: "before the start: not planned" }) : null,
c.range.value.hi < N ? h("div", { class: "outr", style: `left:${c.range.value.hi * ppf}px;width:${(N - c.range.value.hi) * ppf}px`, title: "after the end: not planned" }) : null,
...cuts.value.filter(x => x < N).map(x => h("div", { class: "cut", style: `left:${x * ppf}px`, title: `cut @ ${x}` })),
h("svg", { class: "spark", viewBox: `0 0 ${tlWidth.value} 26`, preserveAspectRatio: "none" },
[h("polyline", { points: sparkPts, fill: "none", stroke: "#ff7a90", "stroke-width": 1, "vector-effect": "non-scaling-stroke" })]),
h("div", { class: "segs" }, [
...S.map((s, i) => h("div", {
class: ["seg", i === sel.value && "sel", !!c.skipWhy(s) && "off", s.len > maxLen.value && "long", play.mode && play.idx === i && "playing"],
style: `left:${s.start * ppf}px;width:${Math.max(2, s.len * ppf - 1)}px;background:${hue(i)}`,
title: `#${i + 1} · frames ${s.start}-${s.end - 1} · ${s.len} → ${s.gen}${c.skipWhy(s) ? " · skip: " + c.skipWhy(s) : ""}`,
onClick: () => { sel.value = i; },
}, s.len * ppf > 26 ? `${i + 1}` : "")),
...plan.bounds.filter(b => b < N).map((b, k) => h("div", { class: "hdl", style: `left:${b * ppf}px`, title: `boundary @ ${b}`, onPointerdown: e => c.drag(k, e) })),
]),
hover.value ? h("div", { class: "ph", style: `left:${hover.value.x}px` }) : null,
vid.value && plan.video ? h("div", { class: "playhead", style: `left:${Math.min(N, play.frame) * ppf}px` }) : null,
]),
]),
// Keep the preview outside the timeline: it must never hide the thumbnails, cuts or handles.
hover.value ? h("div", { class: "tlpreview" }, [
h("img", { src: c.thumbFor(hover.value.frame) }),
h("span", `frame ${hover.value.frame} · ${fmtT(hover.value.frame, fps)}`),
h("span", { class: "hint" }, "click to select · double-click to cut"),
]) : null,
h("div", { class: "hint", style: "margin-top:4px" },
"Click a shot to select it (← → move between shots) · drag the white handles to move a boundary · double-click the shot bar to split · Delete removes the selected shot's cut"),
]);
}