// The FLIPS panel's composition: one HyperFrames page for a whole cut, the // frame's left side beside the footage (deck.mjs threadsGeometry -- the region // the thread rail would take), one pair on it at a time (flips.mjs): its // topic, the THEN card, the years between, the NOW card slamming in. // // PURE, like chrome-threads.mjs: a schedule and a render block in, an HTML // string out; compose-chrome.mjs renders it (`region: "flips"`). import { pageDuration, threadsGeometry } from "./deck.mjs"; import { mix, rgba } from "./chrome-deck.mjs"; import { flipCues } from "./flips.mjs"; const esc = (s) => String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); const r4 = (v) => Math.round(v * 10000) / 10000; /** THEN's colour when the palette names none (`palette.then`): a cool blue against the accent's NOW. */ export const THEN_COLOR = "#6fa8dc"; /** * The panel's HTML, for the whole cut -- or a window of it (`from`/`duration`). * `fonts` = `{ regular, bold }`; `gsap` the vendored script. `?still=` and * the preview's `deck:seek` take CUT seconds. */ export function flipsHtml(schedule, render, opts = {}) { const sched = schedule.flips; if (!sched?.pairs?.length) throw new Error("flips: the schedule has no pairs"); const geo = threadsGeometry(render); const pal = render.palette; const W = geo.width, H = geo.height; const C = geo.cards; const fonts = opts.fonts ?? {}; const gsapSrc = opts.gsap ?? "assets/gsap.min.js"; const total = schedule.total; const from = Number(opts.from ?? 0); const dur = opts.duration != null ? r4(Number(opts.duration)) : r4(total - from); if (!(dur > 0)) throw new Error(`flips: nothing to render from ${from}s of a ${total}s cut`); const windowed = from > 0 || Math.abs(dur - total) > 1e-6; const { init, cues } = flipCues(sched); const thenC = /^#[0-9a-fA-F]{6}$/.test(pal.then ?? "") ? pal.then : THEN_COLOR; const nowC = pal.accent; // Type from the panel's width: 298 px at the deck's default footage, wider with less footage. const k = Math.max(0.8, Math.min(1.4, C.width / 300)); const S = (v) => Math.round(v * k); const topicSize = S(30), idxSize = S(15), whenSize = S(50), wordsSize = S(27), tagSize = S(14), gapSize = S(22); const wordsLine = Math.round(wordsSize * 1.22); const gapLine = Math.round(gapSize * 1.2); const odo = (years) => Array.from({ length: years + 1 }, (_, i) => `+${i}`).join(""); const sideHtml = (cls, tag, s, color, key) => `
` + `
${tag}${esc(s.when)}
` + `
“${esc(s.words)}”
`; const pairsHtml = sched.pairs .map((p, j) => `
` + `
${String(p.index).padStart(2, "0")} / ${String(p.of).padStart(2, "0")}` + `
${esc(p.topic)}
` + sideHtml("then", "THEN", p.then, thenC, `t${j}`) + `
` + (p.years != null ? `
${odo(p.years)}` + `${p.years === 1 ? "year" : "years"} later
` : `
`) + sideHtml("now", "NOW", p.now, nowC, `n${j}`) + `
` + ``) .join("\n "); const data = { total: r4(total), window: windowed ? { from: r4(from), dur } : null, wordsSize, ids: sched.pairs.map((p) => p.id), init, cues: cues.map(({ why, ...c }) => c), }; const json = JSON.stringify(data).replace(/
${pairsHtml}
`; }