Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 1049122425962b3fe4811922ba7699d9b2dce8f3
parent fcc479278961d7f1227a2b4c9e79b0958873321a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri,  9 Oct 2026 01:15:39 -0400

Merge umtool/threads-polish (dim dots ahead; outcomes wait for the rail)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mumtool/report-to-video/threads.mjs | 22+++++++++++++++-------
Mumtool/report-to-video/threads.test.mjs | 6++++++
2 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/umtool/report-to-video/threads.mjs b/umtool/report-to-video/threads.mjs @@ -144,7 +144,8 @@ export function validateThreadEntries(manifest) { * - `threads`: each listed thread with its `clips` (`{ segment, at }`: when * its dot fills -- halfway into the dissolve that brings the clip in) and, * with an outcome, `closeAt`: when it is stamped -- `OUTCOME_LEAD` before - * its last clip ends, but never in that clip's first half second; + * its last clip ends, but never in that clip's first half second, and never + * while the rail is aside (then as it is back); * - `runs`: when each thread is the one on screen (`{ thread, from, to }`: * consecutive clips of one thread make one run, ending as the next clip * starts to come in); @@ -168,11 +169,19 @@ export function threadSchedule({ segments, D, total, render, moves = [] }) { t.clips.push({ segment: s.id, at: R(i === 0 ? s.start : s.start + D / 2) }); last.set(t.id, i); }); + const asides = moves.map((m) => { + const i = segments.findIndex((s) => s.id === m.segment); + return { from: R(m.at), to: R(i < 0 ? total : endOf(i)) }; + }); for (const t of threads) { if (!t.outcome || !last.has(t.id)) continue; const i = last.get(t.id); const s = segments[i]; - t.closeAt = R(Math.max(s.start + 0.5, endOf(i) - D - OUTCOME_LEAD)); + let at = Math.max(s.start + 0.5, endOf(i) - D - OUTCOME_LEAD); + // Stamped where it can be seen: a rail stepped aside for a post gets it as it comes back. + const hidden = asides.find((a) => at >= a.from - 0.2 && at < a.to + THREAD_MOTION.aside); + if (hidden) at = hidden.to + THREAD_MOTION.aside + 0.1; + t.closeAt = R(at); } const runs = []; segments.forEach((s, i) => { @@ -183,10 +192,6 @@ export function threadSchedule({ segments, D, total, render, moves = [] }) { prev.lastIdx = i; } else runs.push({ thread: s.thread, from: R(s.start), to: R(endOf(i)), lastIdx: i }); }); - const asides = moves.map((m) => { - const i = segments.findIndex((s) => s.id === m.segment); - return { from: R(m.at), to: R(i < 0 ? total : endOf(i)) }; - }); return { threads, runs: runs.map(({ lastIdx, ...r }) => r), asides }; } @@ -225,6 +230,9 @@ export function planCues(init, events, instant = 0.001) { /** A card's resting opacity before its thread plays, and after. */ export const CARD_REST = Object.freeze({ ahead: 0.42, done: 0.8 }); +/** A dot before its clip plays: there, hollow-looking and small, so a card shows how many clips its thread has. */ +export const DOT_AHEAD = Object.freeze({ opacity: 0.22, scale: 0.7 }); + /** * Everything the rail's timeline does, as data. Card j is `c<j>` (opacity), its * lit wash `w<j>` and string `s<j>` (scaleX from its left), its dots @@ -244,7 +252,7 @@ export function threadCues(sched) { init[`c${j}`] = { opacity: 0 }; init[`w${j}`] = { opacity: 0 }; init[`s${j}`] = { scaleX: 0 }; - t.clips.forEach((_, n) => { init[`d${j}-${n}`] = { opacity: 0, scale: 0.4 }; }); + t.clips.forEach((_, n) => { init[`d${j}-${n}`] = { ...DOT_AHEAD }; }); // A card whose thread is already on screen by the end of its intro comes up lit. const lit = (firstRun.get(t.id) ?? Infinity) <= m.stagger * j + m.intro; add(`c${j}`, m.stagger * j, m.intro, { opacity: lit ? 1 : CARD_REST.ahead }, "power2.out", `intro ${t.id}`); diff --git a/umtool/report-to-video/threads.test.mjs b/umtool/report-to-video/threads.test.mjs @@ -124,6 +124,11 @@ test("the schedule: dots, runs, outcomes on the last clip, asides for posts", () { thread: "aside", from: 47.5, to: 53.5 }, ]); assert.deepEqual(s.asides, [{ from: 30, to: 38 }]); + // An outcome that would land while the rail is aside lands as it comes back. + const hid = threadSchedule({ + segments: SEGS, D: 0.5, total: 53.5, render: RENDER, moves: [{ segment: "s4", at: 40 }], + }); + assert.equal(hid.threads[0].closeAt, 47.5 + 0.4 + 0.1); // A clip too short for the lead is stamped half a second in, never before. const short = threadSchedule({ segments: [{ id: "x", start: 0, duration: 1.2, thread: "proof" }], D: 0.5, total: 1.2, render: RENDER, @@ -146,6 +151,7 @@ test("the cues state every from, light and dim each run, and stamp each outcome assert.equal(rests.length, 2); assert.equal(cues.filter((c) => c.k === "o0" && c.to.autoAlpha === 1).length, 1); assert.equal(init.o2, undefined, "no outcome, no stamp"); + assert.deepEqual(init["d0-0"], { opacity: 0.22, scale: 0.7 }, "a dot is there, small and dim, before its clip"); assert.deepEqual(cues.filter((c) => c.k === "rail").map((c) => c.to.autoAlpha), [0, 1]); // One element's cues never overlap. const byK = {};