Computed<T> is defined in crates/rustmotion-core/src/expr/mod.rs and no field uses it.
$ grep -rn "Computed<" crates/*/src --include='*.rs' | grep -v "expr/mod.rs" | wc -l
0
$ grep -n "pub opacity" crates/rustmotion-core/src/css/style.rs
179: pub opacity: Option<f32>,
So a scenario writing "opacity": "= $keyDraw" fails at deserialization, before any per-frame machinery runs:
Error: invalid component — invalid type: string "= $keyDraw", expected f32
Static folding is unaffected: it rewrites the JSON to a literal before CssStyle deserializes, which is why a for-each over eight computed cosines already renders correctly. The per-frame tier has nowhere to land, and three landed workstreams depend on it — the expression engine's dynamic half (#327), animated variables (#329), and node references (#328). All three resolve correct values into a scope that no field can read from.
This was missed when the chantier was decomposed. #326's design says an expression is allowed on any numeric or colour property; the work of making a field able to carry one was specified and never assigned.
The shape this should take
Retyping CssStyle's fields to Computed<f32> would ripple through every consumer of a style property. There is already a per-frame override path built for exactly this problem: css::animation::apply_animated_props(css: &mut CssStyle, props: &AnimatedProperties), called from rustmotion-components/src/box_builder.rs, patches a resolved animation's values onto the style each frame. An expression is a second source of the same kind of override, and should reuse that path rather than introduce a parallel one.
That leaves one real problem to solve: a = ... string has to survive deserialization somewhere, since the typed field rejects it and CssStyle is deny_unknown_fields.
Acceptance
A scenario file on disk with "style": { "opacity": "= $keyDraw" }, where keyDraw is an animated variable, passes rustmotion validate, renders, and the rendered opacity actually differs between frames — demonstrated by sampled stills, not by a unit test alone. The same must hold for a node reference ("= node(\"badge\", \"tx\")") once #328's wiring lands.
A scenario using no expression must render byte-identical frames to today, and must pay nothing per frame for a feature it does not use.
Computed<T>is defined incrates/rustmotion-core/src/expr/mod.rsand no field uses it.So a scenario writing
"opacity": "= $keyDraw"fails at deserialization, before any per-frame machinery runs:Static folding is unaffected: it rewrites the JSON to a literal before
CssStyledeserializes, which is why afor-eachover eight computed cosines already renders correctly. The per-frame tier has nowhere to land, and three landed workstreams depend on it — the expression engine's dynamic half (#327), animated variables (#329), and node references (#328). All three resolve correct values into a scope that no field can read from.This was missed when the chantier was decomposed. #326's design says an expression is allowed on any numeric or colour property; the work of making a field able to carry one was specified and never assigned.
The shape this should take
Retyping
CssStyle's fields toComputed<f32>would ripple through every consumer of a style property. There is already a per-frame override path built for exactly this problem:css::animation::apply_animated_props(css: &mut CssStyle, props: &AnimatedProperties), called fromrustmotion-components/src/box_builder.rs, patches a resolved animation's values onto the style each frame. An expression is a second source of the same kind of override, and should reuse that path rather than introduce a parallel one.That leaves one real problem to solve: a
= ...string has to survive deserialization somewhere, since the typed field rejects it andCssStyleisdeny_unknown_fields.Acceptance
A scenario file on disk with
"style": { "opacity": "= $keyDraw" }, wherekeyDrawis an animated variable, passesrustmotion validate, renders, and the rendered opacity actually differs between frames — demonstrated by sampled stills, not by a unit test alone. The same must hold for a node reference ("= node(\"badge\", \"tx\")") once #328's wiring lands.A scenario using no expression must render byte-identical frames to today, and must pay nothing per frame for a feature it does not use.