Skip to content

fix(motion): let counters land, float_3d honour its timing, and pans dissolve - #141

Merged
LeadcodeDev merged 4 commits into
mainfrom
fix/counter-duration
Aug 7, 2026
Merged

fix(motion): let counters land, float_3d honour its timing, and pans dissolve#141
LeadcodeDev merged 4 commits into
mainfrom
fix/counter-duration

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Three motion defects that all read the same way on screen — movement that ignores what the scenario asked for.

Counters never landed. The ramp was hardcoded to stretch from start_at to the end of the scene, so the figure reached to on the very last frame and the viewer never got to read the number they had just watched count up. New optional counter.duration (seconds, measured from start_at) makes the count finish early and hold. Unset, the old behaviour is preserved exactly. A duration outlasting the scene is honoured rather than clamped — the count simply never arrives, which is what was asked for; a zero or negative one falls back to the scene. Five unit tests pin the ramp, including the two edge cases.

float_3d ignored both delay and duration. Its keyframes were pinned to literal 0.0 / 0.5 / 1.0 seconds, so every floating element in a scene ran one shared 1-second cycle in lockstep no matter what the scenario declared. A row of cards bobbing in unison reads as a dance; the same cards on different phases read as depth, which is the entire point of the preset. The cycle now spans delay..delay+duration, so duration sets the period and delay shifts the phase.

Camera pans slid the foreground as a solid slab. The outgoing plane now dissolves and the incoming one materialises across the pan. Both curves are pinned at their own end — fg_a fully opaque at t=0, fg_b at t=1 — because a transition frame sits directly against a normal frame at each junction and any alpha short of 1 there is a visible step. Mirrored exponents rather than a plain crossfade keep both planes at 67% through the middle, so the frame never washes out half way. Two tests cover it: the junction no-op invariant, and the mid-pan floor.

Known gap

PresetConfig.amplitude is added and read by the preset expansion, but nothing can currently set it. AnimationTiming — the struct scenarios actually deserialize into — has no amplitude field, and to_preset_config() hardcodes amplitude: None, which is the only construction site outside Default. So config.amplitude.unwrap_or(12.0) always yields 12.0 and the knob is dead from a scenario author's point of view. delay and duration do land, since those come from AnimationTiming. Wiring it through is a field on AnimationTiming plus one line in to_preset_config; left out of this PR rather than smuggled in.

Verification

cargo test --workspace green. cargo check --workspace clean.

The ramp always spanned whatever remained of the scene, so a counter only
reached `to` on its very last frame — the figure the card exists to show was
still moving when the camera left, and was never read.

`duration` sets the ramp length explicitly, measured from `start_at`, and the
value holds once reached. Unset, behaviour is unchanged.

A duration outlasting the scene is honoured rather than clamped: silently
speeding up a count the author asked to be slow would be the greater surprise.
Its keyframes were pinned to 0.0/0.5/1.0 seconds, so every floating element
shared one 1-second cycle in lockstep and both `delay` and `duration` were
silently inert. A row of cards then bobs in unison, which reads as a dance
rather than as depth.

The cycle now spans delay..delay+duration, and a new `amplitude` on
PresetConfig sets the travel (default 12px, the previous fixed value) with the
tilt scaled to match. Varying the three across elements in one scene is what
turns a shared bob into parallax.
The two scenes were composited fully opaque, so the outgoing one slid off as a
solid slab while only the background had any differential motion. Each plane
now fades as it leaves or arrives.

Both curves are pinned at their own end — the outgoing plane is fully opaque at
progress 0, the incoming one at progress 1 — because a transition frame sits
directly against a normal frame at each junction, and any alpha short of 1
there is a visible step. Mirrored exponents keep both planes at 67% through the
middle rather than 50%, so the frame never washes out half way across.
La dérive s'applique aussi aux `text`, mais plus discrète que sur les cards
(2,5 → 5 px, cycles de 10 → 16 s) : la typographie est le plan le plus éloigné
de l'objectif et un titre qui oscille visiblement se lit mal.

Note aussi le piège de mesure : le pan caméra est un mode commun d'un ordre de
grandeur supérieur à la dérive, donc corréler les positions absolues renvoie
+0.99 même quand la parallaxe fonctionne. C'est l'écart entre deux éléments
qu'il faut mesurer.
@LeadcodeDev LeadcodeDev self-assigned this Aug 7, 2026
@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Aug 7, 2026
@LeadcodeDev
LeadcodeDev merged commit 61dbf20 into main Aug 7, 2026
2 of 3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/counter-duration branch August 7, 2026 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant