Skip to content

feat(scene): animated zab-transition (mount fade/scale + leaf declaration); playout reloads browser to replay#97

Open
ClodoCapeo wants to merge 3 commits into
forge/pulsar-m10-smooth-fade-playoutfrom
forge/pulsar-m10-animated-zab-transition
Open

feat(scene): animated zab-transition (mount fade/scale + leaf declaration); playout reloads browser to replay#97
ClodoCapeo wants to merge 3 commits into
forge/pulsar-m10-smooth-fade-playoutfrom
forge/pulsar-m10-animated-zab-transition

Conversation

@ClodoCapeo

Copy link
Copy Markdown
Contributor

Stacked on #96 (forge/pulsar-m10-smooth-fade-playout). Makes the M10
transition VISIBLY animated, white+logo, and self-delivering the Blue rule
leaf. Refs #79.

Three coupled fixes

1. zab-transition.lsml.json — animate + declare the leaf

  • animate directive on the logo (LSMLAnimateDirective: a single
    transition — transition{duration:550ms, easing:ease-out} + opacity +
    transform.scale). Authored mount fade/scale-in, no keyframes-compiler, no
    multi-step. Verified it compiles strict through @lumencast/compiler
    (Prism's vendored copy) to per-prop transitions (opacity/scale tween,
    duration_ms=550 → runtime 0.55s, ease cubic-out); the bundle
    round-trips byte-stable.
  • Leaf declaration: operator_inputs declares
    __inputs.blue.m10-scene-control.scene_control (calque on
    m10-orion-scene.lsml.json), so Orion's sceneAcceptsPath (F2) fans the
    Blue delta out while this white+logo scene is active — no silent-drop, no
    magenta wipe-cover workaround. The render root stays white+logo: no
    keyframes block, no wipe-cover render element (leaf declared, not lowered
    to a cover).

2. Playout — replays the animation at each transition

  • The local render page now plays the authored mount animation as CSS
    @keyframes keyed to page load (opacity 0→1, scale 0.85→1, the directive's
    duration/easing).
  • Before each cut, the playout re-mounts the transition browser_source
    (re-SetCaptureSource with a cache-busted _replay= URL → a fresh CEF page
    load → the mount animation replays). obs-browser's same-URL Update()
    early-return would otherwise skip the reload, so the URL must differ.

3. Anti-faux-positif — prove the animation is VISIBLE

  • Capture a sequence of frames (~8 @ ~80ms) across the mount window and
    prove a RAMP (prove_mount_ramp): the logo footprint grows from ~0 to
    its settled peak, with an intermediate frame neither blank nor already-
    settled. If every frame is identical/settled → NOT VISIBLE, said
    franchement (never declared "animated"); --allow-blank degrades to
    INCONCLUSIVE on a headless box. Keeps the settled white+logo MID + A + B +
    the Blue-rule target. C-SEC, --no-broadcast/--broadcast kept.

Known limitation (authoring info, not a defect)

In vanilla Solar today, the animate directive lowers to per-prop transition
timing only, and the runtime primitives (Image/Frame) set framer
animate without initial — so a static-prop animate does not itself
mount-play. The VISIBLE mount animation is produced by the browser_source
remount (a fresh page load replays the page's mount animation). The
real-Solar render at the antenna replays on reload the same way. Flagged for the
authoring session.

Tested

  • @lumencast/compiler strict compile of the animate directive + the leaf
    declaration (✅, no warnings).
  • pytest scripts/test_zab_transition.py — 24 tests (inverted static
    invariants → animate+leaf, + animate_mount_params, prove_mount_ramp
    ramp/settled/blank/too-few, _bust_url). Full CI offline set
    (test_m10_setup.py + test_probe_m10_real_orion.py + test_zab_transition.py
    • contracts/scene_control/) → 103 passed.
  • ruff check clean on the changed Python.
  • The visible animation (the ramp on a real CEF) is the Keeper run — the
    capture-sequence + ramp proof are wired to PROVE it; on a headless box it
    degrades to INCONCLUSIVE rather than a false "animated".

No test artefacts pushed (frames/VOD/pages live under gitignored build/).

🤖 Generated with Claude Code

ClodoCapeo and others added 2 commits June 9, 2026 15:49
…te) + leaf declaration; playout reloads browser to replay

The M10 zab-transition scene now (1) ANIMATES on mount and (2) DECLARES the
Blue scene_control leaf, so the white+logo passage both moves visibly and
delivers the Blue rule itself — no more magenta wipe-cover workaround.

scripts/fixtures/zab-transition.lsml.json
- Add an `animate` directive on the logo (LSMLAnimateDirective: a single
  transition — transition{duration:550ms,easing:ease-out} + opacity +
  transform.scale). Authored mount fade/scale-in, no keyframes-compiler.
  Verified it compiles strict through @lumencast/compiler to per-prop
  `transitions` (opacity/scale tween, duration_ms=550 → runtime 0.55s).
- Add operator_inputs declaring __inputs.blue.m10-scene-control.scene_control
  (calque on m10-orion-scene.lsml.json) so Orion's sceneAcceptsPath (F2) fans
  the Blue delta out while THIS scene is active — no silent-drop, no magenta.
- Render root STAYS white+logo: no keyframes block, no wipe-cover render
  element (the leaf is declared for fan-out, not lowered to a cover).

scripts/probe-m10-canvas-live.py
- Local render page now plays the authored mount animation as CSS @Keyframes
  keyed to page load (opacity 0→1, scale 0.85→1, the directive's
  duration/easing) — a fresh CEF load replays it.
- Playout RE-MOUNTS the transition browser_source before each cut
  (re-SetCaptureSource with a cache-busted _replay= URL → fresh CEF load →
  the mount animation replays; obs-browser's same-URL Update() early-return
  would otherwise skip the reload).
- Capture a SEQUENCE of frames (~8 @ ~80ms) across the mount window and
  prove a RAMP (prove_mount_ramp): an intermediate frame neither blank nor
  already-settled. If every frame is identical/settled → NOT VISIBLE, said
  franchement (anti-faux-positif); --allow-blank degrades to INCONCLUSIVE.
- load_transition_bundle now asserts the animate directive + the leaf
  declaration (and still that the render root stays white+logo).
- Keeps C-SEC, --no-broadcast/--broadcast, the white settled MID + A + B +
  the Blue-rule target.

scripts/test_zab_transition.py — invert the static invariants (animate + leaf
declared), add coverage for animate_mount_params, prove_mount_ramp (ramp /
settled / blank / too-few), _bust_url. 24 tests, all green (103 across the CI
offline set).

KNOWN LIMITATION (authoring info, not a defect): in vanilla Solar today the
`animate` directive lowers to per-prop transition TIMING only and the runtime
primitives (Image/Frame) set framer `animate` WITHOUT `initial`, so a
static-prop `animate` does NOT itself mount-play. The VISIBLE mount animation
is produced by the browser_source REMOUNT (fresh page load replays the page's
mount animation). The real-Solar render at the antenna replays on reload too.

Carries the base-branch (#96 final run) WIP: the runbook addendum documenting
the real antenna run + the topology tension this resolves, and the env-driven
Blue rule `target` trigger input.

Refs #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt-play initial state)

Author the mount-play INITIAL STATE on the fixture itself: the logo's
`animate` directive now carries `from: {opacity:0, transform:{scale:0.85}}`
(LSML 1.1 / @Lumencast 0.3.0, lumencast-js PR #23 / Solar v0.2.3). With
the patched runtime, this directive IS a native mount-play in Solar
(compiler lowers from -> animate_initial; primitives pass framer-motion
initial={from}) — the browser_source remount re-triggers the mount and
the runtime plays the fade+scale itself, no CSS-page trick needed for
the real bundle.

animate_mount_params now reads the authored `from` (single source of
truth for the local dry-render page) and falls back to the prior 0/0.85
hidden-start defaults when `from` is absent (older fixtures keep their
ramp — covered by a regression test).

Tests: from-state authored + strictly below settled targets; fallback
without from; full offline suite 105 passed.

Refs #79, refs #97.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erdict against blank->settled false positive

Keeper's real run (frames in hand) proved the trap: the replay re-created
the CEF browser WHILE the transition scene was program, a cold CEF takes
~2-3s before its first paint, and the 550ms mount animation played inside
the 703->2781ms sampling hole — the antenna saw white -> settled logo,
never the ramp. Worse, prove_mount_ramp took the pre-paint blank
(early_low=0.000) for the bottom of a ramp: a false positive.

Three-pronged fix (strategy = longer ramp + cut-on-first-paint):
- OFF-AIR reload + paint-gated cut: replay_transition_mount now navigates
  the EXISTING PulsarSceneSource via SetInputSettings (no program flip;
  the plugin creates it shutdown=false so it renders off-air), then
  wait_mount_first_paint polls GetSourceScreenshot ON THE SOURCE at ~60ms
  (reload blank, then first logo pixels) and the fade #1 cut fires THEN.
- 1200ms authored ramp: zab-transition.lsml.json animate.transition is
  now 1200ms ease-out (MOUNT_ANIM_MS mirrors it, test-pinned) so the
  residual detection->cut latency still leaves a generous on-air ramp;
  --hold-ms defaults to 1800 and clamps up to ramp+400ms.
- Dense capture + hardened verdict: ~28 downscaled (480x270) program
  grabs at ~70ms nominal cadence (full-res decode was ~170ms/frame — the
  hole itself); prove_mount_ramp now REQUIRES >=1 true intermediate frame
  (15-85% of the settled peak), a monotone-ish rise across intermediates,
  excludes crossfade-blend frames (foreign modal), and EXPLICITLY FAILS
  the blank->settled jump with the capture-hole diagnostic.

CTest proof-only stays intact: --allow-blank still downgrades a CEF that
never paints to INCONCLUSIVE (antenna run), and run-probes.ps1 keeps its
floor hold (the probe clamps it up when the CEF renders). New offline
cases: blank->settled FAILS, dense intermediates PASS, falling
intermediates FAIL, blend frames excluded, fixture/MOUNT_ANIM_MS pinned.

Refs #79

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant