seam-craft
Render-correctness doctrine for scene-to-scene seams in HyperFrames launch videos — the prerequisites that make transitions composite correctly on the master timeline. Load when assembling the master timeline / index.html, when a white flash appears at a cut or crossfade seam (especially on dark films), when reasoning about why a transition opacity dip shows through, or when verifying the render-side mechanics of how overlapping scene wrappers blend. Covers the opaque stage-ground (#root background) white-flash guard and how the injector overlaps wrappers, holds final frames, ping-pongs tracks, and stamps lint-clean template code onto the master timeline. Does NOT contain the per-transition catalog — see the transition registry for individual transition entries.
1How It Works & Mechanism
It governs the prerequisites for "enabling any transition to composite correctly" rather than any specific transition—individual transitions belong in the transition registry, whereas this document strictly provides the underlying doctrine (catalogs for crossfade / push-slide / zoom-through / cut-the-curve, etc., reside elsewhere).
This is the **render-correctness doctrine** for PLV scene-to-scene seams: the prerequisites and master-timeline mechanics that make any transition composite correctly, independent of which specific transition is chosen. The per-transition catalog (crossfade, push-slide, zoom-through, cut-the-curve, …) lives in the transition registry — this page is the doctrine that sits underneath all of them.
White-flash guard (core correctness issue): Certain transitions create a window where the sum of both wrappers' opacities is < 1. If #root lacks an opaque background color, the renderer composites this "dip" against the default white page → resulting in a white flash at every seam (particularly jarring in dark films; observed twice by the author in Spotify projects).
Several templates open a window where the two wrappers' summed opacity < 1 (the cut-the-curve mid-window cut, zoom-through's 0.15 floor, plain crossfade's power-curve dip). Whatever is BEHIND the wrappers shows through during that window. If the assembled `index.html` `#root` has no opaque background, the renderer composites the dip over its default **white** page → a white flash at every seam, glaring on dark films (observed on two Spotify runs before the fix).
The fix is a strict prerequisite: Apply an opaque background color to #root, which assemble-index.mjs is responsible for outputting; any other consumers of these templates must provide an equivalent guarantee independently.
**The assembler must paint the stage:** `#root { background: var(--canvas-deep, var(--canvas, #000)) }` — `assemble-index.mjs` now emits this; any other consumer of these templates owns the same guarantee.
The injector's four-step mechanism for applying transitions: Extend the outgoing wrapper's data-duration (freezing the final frame) → advance the incoming wrapper's data-start (creating an overlap window) → rearrange all clips' data-track-index values into a 0/1 ping-pong pattern (same-track overlap is invalid; higher tracks layer on top) → stamp gsap_template into window.__timelines["main"] at T=overlap_start.
1. Extends `#el-<from>` wrapper `data-duration` by `duration_s` (holds its final frame — verified: `core/src/runtime/init.ts:1393-1410` external-slot branch). 2. Pulls `#el-<to>` wrapper `data-start` earlier by `duration_s` (creates the overlap window). 3. Reassigns **all** clip `data-track-index` as a 0/1 ping-pong so the two overlapping wrappers never share a track (same-track overlap is illegal — `core/src/lint/rules/composition.ts`). Higher track composites on top. 4. Stamps the `gsap_template` into `window.__timelines["main"]` at `T = overlap-start`.
Scope is strictly bounded: Covers only Tier-B-ready transitions (pure transform/opacity/filter operations on two scene wrappers without injecting overlay DOM or requiring scene-level cooperation); overlay families (staggered blocks, blinds, light leak, grid dissolve, page burn) and shader transitions are explicitly deferred to later phases.
The transitions this doctrine governs are **Tier-B-ready**: pure transform / opacity / filter on the two scene **clip wrappers** (`#el-<sid>`), no injected overlay DOM, no per-scene cooperation. Overlay families (staggered blocks, blinds, light leak, grid dissolve, page burn) and shader transitions are deferred to later phases.
Template placeholder contract: __OLD__ / __NEW__ (quoted selectors for both wrappers), __T__ (overlap start time on the master clock), __DUR__ (duration_s of the boundary), __DX__ / __DY__ (directional offsets of ±1920 / ±1080), and __ORIGIN_OUT__ / __ORIGIN_IN__ (transformOrigin pairs for squeeze).
| `__OLD__` | `"#el-<from>"` — outgoing clip wrapper selector (quoted) | | `__NEW__` | `"#el-<to>"` — incoming clip wrapper selector (quoted) | | `__T__` | overlap-start time in seconds (master clock) | | `__DUR__` | `duration_s` for this boundary |
A counterintuitive finding: filter / scaleX / transformOrigin are lint-clean on the master timeline—because the property whitelist only constrains the scene-worker prompt, not index.html.
`filter` / `scaleX` / `transformOrigin` are lint-clean on the master timeline (verified: `core/src/lint/rules/gsap.ts` has no per-property whitelist and scopes its checks to `data-composition-id` ranges; the x/y/scale/rotation/opacity whitelist is a _scene-worker_ prompt rule only — it does not bind index.html).
Conclusions include verification methodology and timestamp: Confirmed via prototype render tests (2026-05-31) that master timeline wrapper tweens are sought and rendered (without dual-seek conflicts against child comps' paused timelines, running independently driven at runtime), the extended wrapper freezes scene i's final frame, and the higher-track incoming wrapper correctly overlaps and blends with the outgoing wrapper.
Verified by prototype render (2026-05-31): the master-timeline wrapper tween is seeked and rendered (no double-seek with the sub-comp's own paused timeline — the runtime drives them independently), the extended wrapper holds scene _i_'s final frame, and the higher-track incoming wrapper composites over + blends with the outgoing one.
Load timing explicitly stated in description: When assembling the master timeline / index.html, when white flashes occur (especially on dark films), when diagnosing why transition opacity dips bleed through, and when auditing render-side mechanics for overlapping scene wrappers.
Load when assembling the master timeline / index.html, when a white flash appears at a cut or crossfade seam (especially on dark films), when reasoning about why a transition opacity dip shows through, or when verifying the render-side mechanics of how overlapping scene wrappers blend.
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| package | 同仓组件依赖:assemble-index.mjs(负责给 #root 刷底)、injector / transition registry(gsap_template 的施加方) |
| reference | 框架源码坐标(作为论证依据被引用):core/src/runtime/init.ts、core/src/lint/rules/composition.ts、core/src/lint/rules/gsap.ts |
4Recommended Governance & Evidence
- SKILL.md: Pure prompt directives and normative text; executes no scripts and causes no external side effects
5Second-Pass Verification
- [discrepancy] 文档引用的源码路径在本 pin 是否真实存在(core/src/runtime/init.ts:1393-1410、core/src/lint/rules/composition.ts、core/src/lint/rules/gsap.ts) — 三处在仓库根下均**不存在**(core/src/… 无此目录)。实际已迁到 packages/ 前缀:`packages/core/src/runtime/init.ts` 存在(1393 行附近确为时长/clip 解析相关代码,可见 derivedDuration 等分支,但行号与分支名不能逐字对应 `external-slot branch`);而 `packages/core/src/lint/rules/` 目录不存在,lint 现为 `packages/core/src/lint/index.ts` 单入口。结论:这三条源码坐标是写作时点的旧路径,在本 pin 已失效——不影响其结论(白闪机制、同轨非法),但照抄坐标去查源码会扑空。属上游文档与代码结构漂移,非本 skill 的行为风险。
- [discrepancy] 「assemble-index.mjs now emits this」是否属实,且写法是否与文档一致 — 属实但表达不同。pin 内 assemble-index.mjs 共 4 份(product-launch-video / faceless-explainer / music-to-video / pr-to-video)。以 product-launch-video 版为准:它确实在 <head> 样式里输出 `#root { position: relative; … background: ${groundColor}; }`(groundColor 取自 frame.md 的 canvas 语义色),并在 html,body 上兜底 `background: #000`。而文档写的字面式样是 `var(--canvas-deep, var(--canvas, #000))`——全仓(*.ts/*.mjs/*.html)对 `canvas-deep` **零命中**,即该 CSS 变量名在当前代码里并不存在。行为目标(#root 不透明、不会白闪)一致,具体表达式已演进。
- [ok] 「白闪」结论是否只是传言 — 文档给出机理(summed opacity < 1 的窗口 + 渲染器默认白页)与经验出处(observed on two Spotify runs before the fix),并指认三个会开窗的模板(cut-the-curve 中段、zoom-through 的 0.15 下限、普通 crossfade 的 power 曲线凹坑);后在 cut-the-curve/SKILL.md 里交叉印证(zoom-through 出场透明度确为 0.15)。机理自洽、可验,接受为文档声明。
- [ok] 是否真的没有脚本/网络(green 档前提) — 目录全量仅 SKILL.md;全文 URL、curl、npx、node 调用、process.env 均零命中。文中出现的路径串(core/src/…)是文字引用,不是可执行指令。
- [ok] 「不含 per-transition catalog」的自述是否属实 — SKILL.md 正文只谈前置条件与机序,未列任何具体转场的参数(唯一出现 cut-the-curve/zoom-through 之处是在解释「谁会开出不透明度和 <1 的窗」);转场目录确实不在本目录内。
6Verdict & Scenarios
b3f716c34090b9a1…