Skip to content

docs(animation): state that effects on a shared property compose - #418

Merged
LeadcodeDev merged 1 commit into
mainfrom
docs/animations-compose-on-a-shared-property
Sep 29, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
docs/animations-compose-on-a-shared-property

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #322.

The decision

Compose, and write it down. Two presets on one property usually should
compose — a pulse layered on a fade_in wants the product, not the second one
winning. Switching to last-wins would change every existing scenario that stacks
two effects, and would need a second syntax to ask for the composition back.

No rendering changes, except the narrow blur fix below.

What was actually missing

resolve_props_for_effects folds each bucket through AnimatedProperties::merge,
which has always composed. Nothing about that was written anywhere an author would
look, and the doc comment that claimed the opposite — "the last effect wins" —
went with the comments in #345. So the contract was nowhere at all.

rules/animations-compose.md now states it:

Behaviour Properties
Product opacity, scale_x, scale_y
Sum translate_x, translate_y, rotation, rotate_x, rotate_y
Last written everything else

plus the grouping rule (presets resolve together, keyframes resolve together) and
the window-bounding recipe for an author who really does want one effect alone.

The blind spot is real, but narrower than it reads

The issue notes that a bucket resolving a property to exactly its neutral value is
skipped, so an animation can never take a property back.

For a composing property that is not a defect: 1 is the identity for a
product and 0 for a sum, so skipping the neutral value and applying it are the
same answer. The guard is an optimisation.
a_neutral_value_in_a_composing_property_is_a_no_op_by_arithmetic pins that.

For a last-written property it is a defect — and it applied to exactly three
fields. Every other one in the struct already rests at a negative sentinel, so
"animated to zero" wins over an earlier value. blur, blur_x and blur_y rested
at 0.0 and merged on > 0.001, so nothing could ever take an element out of a
blur an earlier bucket had put it in.

They now rest at -1.0 and merge on >= 0.0. Every reader already guards on
> 0.0, so a negative resting value reads as no blur exactly as zero did.

Tests

Five on merge directly. Restoring the > 0.001 guard fails
a_later_bucket_can_take_blur_back_to_zero:

blur is last-wins, not a product, so zero is a value and not an absence —
guarding on `> 0.001` left an element blurred for the rest of the scene

Gate

cargo fmt --all --check clean · cargo clippy --workspace --all-targets --features rustmotion/studio -D warnings clean · cargo test --workspace 1802 passed.

Two effects touching the same property combine: opacity and scale
multiply, translate and rotation add, everything else is last-written.
That is what resolve_props_for_effects has always done by folding each
bucket through AnimatedProperties::merge. Nothing about it was written
anywhere an author would look, and the doc comment that claimed the
opposite -- last effect wins -- went with the comments in #345, so the
contract was nowhere at all.

Composing is kept rather than switched to last-wins. Two presets on one
property usually should compose: a pulse layered on a fade_in wants the
product. Switching would change every existing scenario that stacks two
effects, and would need a second syntax to ask for the composition back.

rules/animations-compose.md says it, with the per-property table and the
window-bounding recipe for the case where an author really does want one
effect alone.

The blind spot the issue names is real but narrow. For a composing
property the neutral value is the identity, so skipping it and applying
it are the same answer -- the guard there is an optimisation. For a
last-written property it is not: blur: 0 is a value, not an absence, and
`if other.blur > 0.001` could never take an element out of a blur an
earlier bucket had put it in.

blur, blur_x and blur_y now rest at -1.0 and merge on `>= 0.0`, which is
what every other last-written property in the struct already did. Their
readers all guard on `> 0.0`, so a negative resting value reads as no
blur exactly as zero did.
@LeadcodeDev LeadcodeDev added the documentation Improvements or additions to documentation label Sep 29, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 29, 2026
@LeadcodeDev
LeadcodeDev merged commit a73b169 into main Sep 29, 2026
4 checks passed
@LeadcodeDev
LeadcodeDev deleted the docs/animations-compose-on-a-shared-property branch September 29, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two animations on the same property compose multiplicatively, but the docs promise last-wins

1 participant