commit 061d4aa48ec9a6d7f27079b3f4beb627e42abf43
parent c83389e06967a52149a336d97bd784f72c32bd87
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 8 Oct 2026 23:19:05 -0400
report-to-video README: the thread rail, the flips panel, panning screenshots, a source line that keeps its date
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
1 file changed, 63 insertions(+), 3 deletions(-)
diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md
@@ -411,7 +411,9 @@ characters, trimmed; an empty value deletes the key, the way the attribution
fields do. Without it the deck's title is empty (a card falls back to
`heading`) and the subtitle is auto-built: a clip's channel · title · date (the
channel only when the cut spans more than one), an image's `title · date`, a
-card's `sub`. The QR follows a clip's corner-QR rule unchanged (`citeUrl`, else
+card's `sub`. A line too long for its column gives way in its middle parts
+(the title ends in "…"): its first part and its last (the date) always show
+whole. The QR follows a clip's corner-QR rule unchanged (`citeUrl`, else
the site link at the clip's start); an image draws one only with an explicit
`citeUrl`; a card never does.
@@ -481,8 +483,10 @@ footage, so it rides on a clip.
its day), `@handle · Bluesky` (or X), the words — paragraphs kept, clamped to
`maxLines` with an ellipsis — and a QR of the post's page on the archive
(below), or of its own `url`. A post with a `shot` draws the screenshot in
- place of the words, as wide as the card's text and no taller than a full card
- of words, or than `shotMaxHeight` px when that is set.
+ place of the words, always as wide as the card's text (so it reads), in a
+ viewport no taller than a full card of words, or than `shotMaxHeight` px when
+ that is set; a taller screenshot holds on its top for 1.2 s once the card has
+ landed, pans to its bottom, and holds there 1.2 s before the card leaves.
- **Marks.** A post's own `accent`, `logo` and `flag` set one kind of card apart
from another at a glance — a source document's sentence beside a platform post,
say: the rail and rim in the accent, the logo above the QR in the top corner,
@@ -597,6 +601,62 @@ a verdict named without a colour keeps the default's), the stamp's seconds and
corner, and whether and where the tally is drawn; `validateChrome()` refuses an
unknown key there as everywhere in the block.
+### `thread` and `render.chrome.threads` — the thread rail
+
+A cut whose clips make a few lines of argument can draw them as a rail of cards
+down the frame's left side. Each clip names its thread; the list names the
+threads in the rail's order, each with an optional outcome.
+
+```jsonc
+"render": { "chrome": { …, "threads": { "list": [
+ { "id": "bet", "label": "The bet: her career", // ≤ 32 characters, one line
+ "outcome": { "verdict": "CONTRADICTED", "label": "Walked back" } }, // label optional (≤ 24): else the verdict's
+ { "id": "aside", "label": "An aside" } ] } } } // no outcome: never stamped
+{ "type": "clip", "id": "c07", …, "thread": "bet" }
+```
+
+- **The layout.** With the rail on, the footage box moves to the frame's right
+ edge (24 px in) and the rail takes the left, as tall as the footage: a card
+ per thread (1–8), each its number, a dot per clip and its label.
+- **The motion.** The card of the thread on screen is lit and a string draws
+ from it to the picture; a clip's dot fills as the clip comes in; when the
+ thread's last clip ends (1.8 s before it hands over), its outcome is stamped
+ on its card in the verdict's colour. Before its thread plays a card is dim,
+ after it rests quieter, so by the last clip the rail is the whole argument.
+ The rail steps aside while a popup post has moved the footage over it.
+- **Checks.** `validateThreads()` (via `validateChrome()`) refuses a bad list
+ and the feed layout beside it; `validateThreadEntries()` refuses an entry
+ naming an unlisted thread, a teaser in a thread and a listed thread with no
+ clip. `schedule.json` gains `threads: { threads, runs, asides }` only when
+ the rail is on. `chrome-threads.mjs` is the page; `compose-chrome.mjs
+ --region threads` renders it; the build lays it last, like the stamps.
+
+### `render.chrome.flips` — THEN and NOW, back to back
+
+A cut built of pairs: a line from THEN and the opposite line from NOW. A panel
+down the frame's left (the rail's region: a cut has the rail or the panel)
+holds one pair while it plays.
+
+```jsonc
+"render": { "transition": 0.12, "chrome": { …, "deck": { "footageScale": 0.78 }, "flips": { "pairs": [
+ { "id": "vax", "topic": "Vaccines", // ≤ 32 characters
+ "then": { "entry": "c01", "when": "2019", "words": "…" }, // when ≤ 18, words ≤ 110: verbatim
+ "now": { "entry": "c02", "when": "2025", "words": "…" } } ] } } }
+```
+
+- **The motion.** The pair rises in with its THEN clip: its place (`03 / 11`),
+ the topic, the THEN card (tag, when, words) lit. As the NOW clip starts its
+ card slams in under it with a flash, the THEN card dims, and when both
+ `when`s carry a year the years between roll up on an odometer
+ ("+7 years later"). The pair lifts away as its NOW clip ends.
+- **Checks.** `validateFlips()` (via `validateChrome()`) refuses a bad pair
+ and the rail beside it; `validateFlipEntries()` refuses a side naming no
+ timeline entry, a teaser, a THEN after its NOW and an entry in two pairs.
+ `schedule.json` gains `flips: { pairs, asides }`. `chrome-flips.mjs` is the
+ page; `compose-chrome.mjs --region flips` renders it.
+- **Punch.** The panel is built for short clips — the line and nothing else —
+ and near-hard cuts (`transition` 0.12); a smaller `footageScale` gives it room.
+
### The `image` entry type
A still: the receipts a clip cannot say out loud — a post, a thread, a DM, a