Skip to content

fix(animation): make every animated property reach layout and the frame - #443

Merged
LeadcodeDev merged 2 commits into
mainfrom
ws-a-text-animation
Sep 29, 2026
Merged

LeadcodeDev merged 2 commits into
mainfrom
ws-a-text-animation

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #426. Closes #430. Part of #438.

Two issues, one architecture. #426 reported that an animation touching only
border_radius, clip_path_progress or font_size never applied; #430 reported
letter_spacing having no effect on rich_text and gradient_text, animated
tracking overflowing because layout used the static value, and a static
opacity: 0 cancelling a fade-in. Three separate defects sat behind them, and
only one was the shared root cause the decomposition predicted.

The gate. props_has_paint_overrides decided whether apply_animated_props
ran at all. It listed translate, scale, rotation, opacity, blur, glow,
perspective, width and height, and had gone stale against every property added to
AnimatedProperties since. It is deleted rather than completed:
apply_animated_props already guards each branch against the resting value, so
the list bought nothing and left the same trap for the next property anyone adds.
Reverting it turns seven tests red.

The cascade. with_cascaded_style folded the box's own resolved style onto a
clone of the same component through inherit_from, which only fills fields that
are None. A component declaring its own font-size or letter-spacing
therefore never saw the animated value — at measurement time, which is why the
painted run could be right while the layout was wrong. inherit_from is correct
for what it is named after, parent to child, so the re-cascade gets its own
function: overlay_resolved_typography, which overwrites. Conflating the two
was the bug, and a boolean flag on inherit_from would have preserved it.

rich_text. It ignored style.letter-spacing outright, static or animated:
compute_layout discarded the resolved value into a binding literally named
_letter_spacing_unused, and resolve_span_fonts defaulted every span to 0.0.
Neither fix above would have reached it. gradient_text needed no change — it
already reads its own cascaded style correctly, and was broken only upstream.

⚠️ One visible rendering change

An animation on opacity now replaces the declared static value instead of
multiplying with it, which is how every other property in apply_animated_props
already treats its declaration. A node at opacity: 0.6 carrying a pulse used
to paint 0.6 × pulse and now paints the pulse.

This does not contradict rules/animations-compose.md. That rule is about several
effects on one property composing with each other, which happens upstream in
AnimatedProperties::merge and is untouched — by the time apply_animated_props
sees props.opacity, the effects are already multiplied together. The axis here
is the static declaration against the merged result, which the rule does not
cover.

Tests

Eight full-pipeline render tests in box_builder.rs — scenario, layout, paint,
read pixels — plus unit tests on apply_animated_props, overlay_resolved_typography
and with_cascaded_style. Every fix was reverted individually and watched go red:

an_animated_font_size_reaches_layout_not_only_paint      baseline=395.75, animated=395.75
an_animated_letter_spacing_reaches_rich_text...          extent at t=0 566px, at t=1 566px
an_animation_touching_only_gap_reaches_the_painted_frame rgba [0,0,170,255] bled into the gap
a_static_opacity_zero_is_overridden_by_an_active_fade_in center pixel rgba [0,0,0,0]

cargo test -p rustmotion-core --lib css:: 114 passed. Clippy and the full suite
run at integration; a sibling workstream is still mid-flight in this checkout.

Two issues, one architecture. #426 reported that an animation touching
only border_radius, clip_path_progress or font_size never applied; #430
reported letter_spacing having no effect on rich_text and gradient_text,
animated tracking overflowing because layout used the static value, and
a static opacity: 0 cancelling a fade-in. Three separate defects sat
behind them.

props_has_paint_overrides gated whether apply_animated_props ran at all.
It listed translate, scale, rotation, opacity, blur, glow, perspective,
width and height, and had gone stale against every property added to
AnimatedProperties since. The gate is deleted rather than completed:
apply_animated_props already guards each branch against the resting
value, so the list bought nothing and left the same trap for the next
property anyone adds.

with_cascaded_style folded the box's own resolved style onto a clone of
the same component through inherit_from, which only fills fields that
are None. A component declaring its own font-size or letter-spacing
therefore never saw the animated value. inherit_from is right for what
it is named after -- parent to child -- so the re-cascade gets its own
function, overlay_resolved_typography, which overwrites. Conflating the
two was the bug.

rich_text ignored style.letter-spacing outright, static or animated:
compute_layout discarded the resolved value into a binding named
_letter_spacing_unused and resolve_span_fonts defaulted every span to
0.0. Neither fix above would have reached it.

An animation on opacity now replaces the declared static value instead
of multiplying with it, which is how every other property in
apply_animated_props already treats its declaration. This changes the
render of a scenario that declares an opacity and animates it: a node at
opacity 0.6 with a pulse used to paint 0.6 * pulse and now paints the
pulse. Effects still compose with each other -- that happens upstream in
AnimatedProperties::merge and is untouched.

Closes #426
Closes #430
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 29, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 29, 2026
with_cascaded_style had two meanings depending on who called it. The
render path passes the node's own already-resolved style, where
overwriting is right because that style has already applied the
component's own declaration over its parent's. These two tests passed it
a bare parent style instead, where filling only the gaps was right.

Making the function overwrite fixed the first caller and broke the
second, which is what turned table_own_color_wins_over_cascaded_card_color
red on CI while every local scoped run stayed green.

The invariant both tests assert is unchanged and still worth asserting.
They now build the resolved style the box tree builds -- the component's
own, with inherit_from filling what it left unset -- and pass that, which
is the shape the render path actually uses.
@LeadcodeDev
LeadcodeDev merged commit 0ce65d9 into main Sep 29, 2026
4 checks passed
@LeadcodeDev
LeadcodeDev deleted the ws-a-text-animation branch September 29, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

1 participant