feat(scene): animated zab-transition (mount fade/scale + leaf declaration); playout reloads browser to replay#97
Open
ClodoCapeo wants to merge 3 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #96 (
forge/pulsar-m10-smooth-fade-playout). Makes the M10transition VISIBLY animated, white+logo, and self-delivering the Blue rule
leaf. Refs #79.
Three coupled fixes
1.
zab-transition.lsml.json— animate + declare the leafanimatedirective on the logo (LSMLAnimateDirective: a singletransition —
transition{duration:550ms, easing:ease-out}+opacity+transform.scale). Authored mount fade/scale-in, no keyframes-compiler, nomulti-step. Verified it compiles strict through
@lumencast/compiler(Prism's vendored copy) to per-prop
transitions(opacity/scaletween,duration_ms=550→ runtime0.55s,ease cubic-out); the bundleround-trips byte-stable.
operator_inputsdeclares__inputs.blue.m10-scene-control.scene_control(calque onm10-orion-scene.lsml.json), so Orion'ssceneAcceptsPath(F2) fans theBlue 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-coverrender element (leaf declared, not loweredto a cover).
2. Playout — replays the animation at each transition
@keyframeskeyed to page load (opacity 0→1, scale 0.85→1, the directive'sduration/easing).
browser_source(
re-SetCaptureSourcewith a cache-busted_replay=URL → a fresh CEF pageload → the mount animation replays).
obs-browser's same-URLUpdate()early-return would otherwise skip the reload, so the URL must differ.
3. Anti-faux-positif — prove the animation is VISIBLE
prove a RAMP (
prove_mount_ramp): the logo footprint grows from ~0 toits 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-blankdegrades toINCONCLUSIVE on a headless box. Keeps the settled white+logo MID + A + B +
the Blue-rule target. C-SEC,
--no-broadcast/--broadcastkept.Known limitation (authoring info, not a defect)
In vanilla Solar today, the
animatedirective lowers to per-prop transitiontiming only, and the runtime primitives (
Image/Frame) set frameranimatewithoutinitial— so a static-propanimatedoes not itselfmount-play. The VISIBLE mount animation is produced by the
browser_sourceremount (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/compilerstrict compile of theanimatedirective + the leafdeclaration (✅, no warnings).
pytest scripts/test_zab_transition.py— 24 tests (inverted staticinvariants → animate+leaf, +
animate_mount_params,prove_mount_rampramp/settled/blank/too-few,
_bust_url). Full CI offline set(
test_m10_setup.py+test_probe_m10_real_orion.py+test_zab_transition.pycontracts/scene_control/) → 103 passed.ruff checkclean on the changed Python.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