Skip to content

feat(engine): close the dark-premium capability gaps and ship the template - #122

Merged
LeadcodeDev merged 10 commits into
mainfrom
chantier/dark-premium-template
Aug 1, 2026
Merged

feat(engine): close the dark-premium capability gaps and ship the template#122
LeadcodeDev merged 10 commits into
mainfrom
chantier/dark-premium-template

Conversation

@LeadcodeDev

@LeadcodeDev LeadcodeDev commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes the gaps a capability audit found between the engine and the two reference
films (Machina by 1600.agency, Aikido), ships a template that exercises them, and
fixes what rendering that template exposed.

Parent: #117 · Sub-issues: #118 #119 #120 #121

Capabilities the audit asked for

Requirement Before After
Word-by-word reveal like the references six char presets, none blurred char_blur_in — blur, opacity and drift from one eased progress
Ambient glow layer alpha buried in hex strings; opaque colours gave a flat wall HaloZone.opacity, default 1.0, multiplied with the colour's own alpha
Glow bleeding past the frame rejected as viewport overflow; the obvious workaround drew a hard seam bleed: true, extending the existing marquee/cursor exemption
Composing a seamless video the world view existed in no authoring rule documented end to end, with the coordinate model that caught the auditor twice
A template in this register only the brutalist one examples/dark-premium.json, 7 beats

Then the template was rendered, and three defects surfaced

The halo preset strobed. Each zone breathes on 1 + 0.15*sin(time*freq), and
freq came straight from speed — shared with the scrolling presets, where it means
pixels per second and defaults to 30. Used as an angular frequency that is 30 rad/s: a
period of 0.16s, so the ambience pulsed at roughly 5 Hz. Converting it to a breathing
rate gives 8–15s instead. Mean frame-to-frame change on the dark background: 2.04 →
0.71, with the slow luminance swing that carries the breathing preserved (9.3 → 8.8).

A camera pan could not carry a per-scene background. It drew one background — the
outgoing scene's — as a fixed backdrop. That is right when every beat shares an
ambience, and it is why a multi-beat video reads as one shot, so it stays the default.
transition.background: travel renders both and locks each to its own scene.

Travelling backgrounds joined on a hard line. Two opaque full-frame images tile
exactly, so they meet on an edge, and an edge between two ambiences is a scene
boundary. Fixed with three things that only work together: backgrounds drift at a
fraction of the foreground's distance (real parallax, and it makes them overlap rather
than abut); each is overscaled by its own current displacement, since translating an
opaque image uncovers a strip that reads as a hard edge too; and the outgoing one stays
opaque while the incoming dissolves over it. Largest adjacent-column luminance step:
7.74 → 5.48, and what remains sits mid-frame on a gradient rather than on a layer edge.

Two of the three approaches tried were regressions, caught by measurement rather than by
eye — fading both backgrounds, which looked like the obvious answer, measured worse
(12.25) than the seam it replaced.

Verification

cargo test --workspace → 15 suites green. cargo clippy --workspace --all-targets
clean apart from a transitive dependency. All 8 example scenarios validate.

Every visual claim is measured, not asserted:

  • char_blur_in — Laplacian variance over a word's bounding box: 1.29 mid-reveal →
    3712 settled, converging exactly at that word's unit_end.
  • halo opacity — centre pixel R = 255 / 128 / 51 at opacity 1.0 / 0.5 / 0.2;
    no-op proven by byte-identical PNGs.
  • bleed — the same bleeding shape validates with the flag and still errors without
    it; a bleed: true component whose content overflows its own box is still reported.

Known, unfixed

  • Background jumps at transition junctions. A frame-by-frame pass over all 486
    frames shows isolated single-frame spikes right after each transition, 47–62× the
    median background change. They reproduce with background: static, so they
    predate the travelling backgrounds. Lead, unverified: the normal render path does its
    own background crossfade via render_scene_frame_scaled_with_prev_bg(prev_bg) while
    the transition path draws the background raw — two independent crossfades that do not
    know about each other, meeting at the boundary. Three earlier hypotheses were tested
    and refuted; this one is not proven and should not be treated as a conclusion.
  • validate reports 20.8s for a render that is 16.2s. Transition duration is
    subtracted from the scenes rather than overlapping them, so the announced duration is
    22% off.
  • char_blur_in resolves from style.animation directly rather than through
    extract_effects, so unlike its five siblings it ignores container-level stagger
    shifting and timeline-step copies.
  • scene.transition is silently ignored inside a world view, with no validator warning.
  • TransitionType::CameraPan means two different things depending on context: a real
    foreground pan between scenes of a slide view, a plain crossfade at a view boundary.

Both reference films reveal a line word by word, each word arriving blurred
and settling sharp. The per-unit machinery already existed — granularity,
stagger, delay, easing, overshoot — but none of the six char presets blurred,
so the one detail that reads as expensive was the one thing not expressible.

char_blur_in drives blur, opacity and a slight upward drift from a single eased
progress, so a word is one continuous animation rather than three stacked
effects. Sigma is configurable and defaults to 14px, chosen because at 150px
type it reads as unmistakably blurred early while letterforms stay
ghost-legible.

Measured on the rendered frames: Laplacian variance over a word's bounding box
climbs from 1.29 mid-reveal to 3712 once settled, converging exactly at that
word's unit_end and staying flat after.

Known limitation, documented in the code: the preset is resolved directly from
style.animation rather than through extract_effects, so unlike its five
siblings it does not pick up container-level stagger shifting or timeline-step
copies.

Refs #118
The halo preset is the right home for the ambient glow of a dark video: it is a
background, so it escapes geometry validation, and the renderer draws it
continuously rather than per scene. But HaloZone exposed only colour, position
and radius, so the only way to soften a glow was to bury alpha inside the hex
string. With opaque colours it renders a flat saturated wall instead of glows —
reproducing the reference look took two attempts and a lot of guessing.

opacity defaults to 1.0 and multiplies whatever alpha the colour already
carries, so it never overrides an author's intent. The default is a true no-op:
a scenario with the field absent and one with opacity 1.0 render byte-identical
PNGs, which is guaranteed rather than incidental since f32 * 1.0 is exact.

Measured falloff at 1.0 / 0.5 / 0.2: centre pixel R = 255 / 128 / 51.

Refs #119
A radial glow running past the frame edge is the base layer of the reference
style, and the validator rejected it as viewport overflow. The obvious
workaround — wrapping it in a full-frame overflow: hidden container — passes
validation and then destroys the effect it was protecting: the container draws
a hard rectangle, and during a camera pan that edge sits in plain view.

bleed: true exempts a component from viewport_overflow and, under --strict-anim,
from animated_text_overflow. It deliberately does not exempt
content_overflows_box: a component may extend past the frame on purpose, but it
is still responsible for its own contents. Nor does it suppress checks on
children.

This extends the existing exemption — geometry.rs already exempts marquee and
cursor, with a comment saying their job is to bleed — rather than adding a
second mechanism. Chosen over inferring which shapes look decorative: a shape is
very often real content, and a wrong guess would silently hide a genuine
overflow.

Also adds bleed to WRAPPER_KEYS. Without it the attribute checker, blocking by
default since it was hardened, rejected bleed as unknown — the geometry was
correct while the CLI refused the file, which would have shipped the feature
dead.

Refs #120
The world view is the engine's spatial composition mode and appeared in no
authoring rule — not composition, not world-position, not camera_pan_duration,
not persist. A generator cannot produce what is not written down, which is why
the capability audit had to rediscover it from source, getting the coordinate
model wrong twice on the way.

Two things the docs now state explicitly because they cost real time:
world-position is the camera waypoint, not the scene's origin, and its default
is vw/2 + i*vw; and children use scene-local coordinates, so a child placed at
world coordinates is rejected as viewport overflow.

Corrects a claim the audit made: CameraPan is not merely a crossfade. Between
scenes of a slide view it dispatches to a real foreground pan over a static
background; it degrades to a crossfade only at a view boundary. That one enum
variant meaning two different things is noted as a footgun.

Also notes that scene.transition is silently ignored inside a world view, and
the world-position kebab / freeze_at snake casing inconsistency.

Every fenced JSON block added was extracted and validated.

Refs #121
A second template, in the register of the Machina and Aikido reference films —
deliberately not replacing the brutalist 1600-style one, which is coherent in
its own language.

Seven beats, 20.8s, and no beat that reads as a cut. The seamlessness rests on
three things, in order of importance: camera_pan transitions with a
world-position delta per scene, so the background is drawn once and stays put
while foregrounds slide across it; an ambient halo layer living in the scene
background rather than in shapes, since shapes would need clipping and a clip
rectangle is exactly the hard edge being avoided; and a camera that never
returns to where it started, so no beat is a still frame.

Exercises the three features landing alongside it: char_blur_in for word
reveals, halo opacity for the ambience, and bleed on the decorative orbs.

Refs #117
Each halo zone breathes: its radius is scaled by 1 + 0.15*sin(time*freq).
`freq` came straight from `speed`, which is shared with the scrolling presets
where it means pixels per second and defaults to 30. Used as an angular
frequency that is 30 rad/s — a period of 0.16 to 0.30 seconds, so the ambience
pulsed at roughly 5 Hz. It read as flicker, not as light, and on a dark
gradient it was the most visible thing in the frame.

Convert the scroll speed into a breathing rate instead. At the default it now
gives a period of 8 to 15 seconds — about one cycle over a short video, which
reads as the glow being alive rather than broken.

Measured on the dark background of examples/dark-premium.json, mean absolute
difference between consecutive frames: 2.04 before, 0.71 after, while the
slow luminance swing that carries the breathing is preserved (8.8 vs 9.3).
A camera pan drew one background — the outgoing scene's — as a fixed backdrop,
and slid only the foregrounds over it. That is what makes a shared ambience
survive a junction, and it is the right default: it is precisely why a
multi-beat video reads as a single shot rather than a sequence of cuts.

But it also made a per-beat background impossible. A scene whose ambience
differed from its predecessor's never showed it during the pan; the new
background appeared only once the transition had finished, as a pop.

`transition.background` now chooses. `static` keeps the current behaviour and
stays the default, so no existing scenario changes. `travel` renders both
backgrounds and locks each to its own foreground, so a beat moves as one plane
and the whole world changes at the junction rather than just the words.

The second background is only rendered when it will actually be used.
The template shared one halo layer across all seven beats. That was the safe
choice while a pan could only hold a fixed backdrop, but it left the ambience
inert: the words changed, the world did not.

Each beat now carries its own palette and the pans use `background: travel`,
so the ambience arrives with the scene. The progression follows the argument —
cold and thin while the problem is stated, darkest and most crowded at the beat
about too many tools, opening into blue as it turns, resolving on magenta for
the number and on both accents at the call to action.

Also drops the decorative orbs. They were foreground children, and during a pan
the two foreground planes are adjacent rather than overlapping, so ambience
living there breaks at the join. All of it belongs in the background — which is
what this template's own recipe says, and what it was not doing.
…hem together

`background: travel` slid each background locked to its foreground, at the same
distance. The two are opaque full-frame images that tile exactly, so they met
on a line — and a line between two different ambiences is a scene boundary,
which is the one thing this transition exists to avoid. It was clearly visible
whenever consecutive beats had different palettes.

Three changes, each needed for the others to work:

Backgrounds now drift at a fraction of the foreground's distance. That is how
parallax behaves — what is far away moves less — and it makes them overlap
across nearly the whole frame rather than sharing an edge.

They are drawn overscaled by the largest drift either can take. Translating an
opaque image uncovers a strip on the opposite side, and that strip reads as a
hard edge exactly as a join would; the overscale means neither ever exposes one.

The outgoing background stays opaque and the incoming one dissolves over it.
Fading both leaves the frame under-covered wherever only one of them reaches,
which trades the seam for a dark band — measurably worse than the seam it
replaced.

Largest adjacent-column luminance step across the background of
examples/dark-premium.json: 7.74 before, 5.48 after, and what remains sits mid
-frame on a gradient rather than on a layer edge.
Each travelling background was overscaled by the largest drift it could take,
which made the margin constant across the whole transition — including at both
ends, where the displacement is zero. The background therefore switched between
a normal frame and an enlarged one at every junction.

Tie the margin to each layer's current offset instead: just enough to cover the
strip its own translation uncovers, and exactly zero where a transition meets a
normal frame.

This is correct rather than dramatic. Measured on the background of
examples/dark-premium.json, peak frame-to-frame change went from 10.65 to
10.47 — the remaining jumps have a different cause, which reproduces with
`background: static` and therefore predates travelling backgrounds entirely.
@LeadcodeDev
LeadcodeDev merged commit d2881d5 into main Aug 1, 2026
3 checks passed
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