feat(animation): move a component along a path, visibly to the validator - #177
Open
LeadcodeDev wants to merge 1 commit into
Open
feat(animation): move a component along a path, visibly to the validator#177LeadcodeDev wants to merge 1 commit into
LeadcodeDev wants to merge 1 commit into
Conversation
None of the 41 `AnimationEffect` variants could make a component follow a
trajectory. `PathMeasure` was already in the repository — `svg.rs` and
`arrow.rs` use it for stroke dash-reveal — so the primitive existed; nothing
used it to position.
`motion_path` takes SVG path data, parsed with the same `Path::from_svg`
call `shapes.rs` already uses for `shape: { "type": "path" }`, and walks it
with `PathMeasure::pos_tan`. No third path format was invented. `orient`
turns the component along the tangent, with an `orient_offset` for artwork
that does not point right at rest.
**The position goes through `css.transform`, and that is the point.**
`--strict-anim` folds transforms to find overflow, and it only reads
`css.transform`. An effect that positioned a component through any other
channel would be invisible to it, and a component sailing off-frame along
its curve would validate clean. That hole already exists here —
`AnimatedProperties` carries `width`, `height` and `font_size` that its own
doc calls painter-only, with no CSS bridge — and this does not widen it:
`motion_path` writes `translate_x`/`translate_y`/`rotation`, the same fields
`orbit` uses, which the existing bridge already translates.
Verified by removing the fix and re-running the decisive test, rather than
by argument: an out-of-frame path then reported `Valid scenario`. With it,
`--strict-anim` names the instant and the transform that does it:
bbox: [2046, 700] -> [2102, 756] (viewport: 1920x1080)
hint: at t=1.70s (57% of scene), animation transforms (tx=1886, ty=0, …)
push the bbox out of the viewport
The mid-path test was validated the same way — the implementation was
temporarily replaced by a straight lerp between the path's endpoints, which
returned 50 where the real curve gives 100 on an L-shaped path. A test that
only checked t=0 and t=1 would have passed either way.
Coordinates are deltas from the layout position, like `orbit`.
`resolve_props_for_effects` receives neither the resolved box nor the
viewport — those are computed later in files frozen for this change — so an
absolute space was not implementable without widening that boundary.
Degenerate paths hold position with zero rotation rather than producing a
NaN that would contaminate layout, and a non-positive duration is both
floored in the solver and rejected by name in `validate` — the same posture
the spring solver took after `mass: 0` produced NaN in round 4. Worth
recording: `PathMeasure::pos_tan` happens to return `None` at zero length in
this skia-safe version, so the explicit guard is not strictly load-bearing
today. It stays because it is the only place the degenerate case is *named*
rather than absorbed by non-contractual behaviour.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the "path animation and path geometry utilities" gap (High/M).
None of the 41
AnimationEffectvariants could make a component follow a trajectory.PathMeasurewas already in the repository —svg.rs:184andarrow.rs:126use it for stroke dash-reveal — so the primitive existed; nothing used it to position.The shape
{ "type": "shape", "shape": "circle", "fill": "#F68F2B", "position": "absolute", "x": 160, "y": 700, "style": { "width": "56px", "height": "56px", "animation": [{ "name": "motion_path", "path": "M0,0 C300,-320 750,-320 1100,-60 C1300,90 1450,-140 1600,-260", "delay": 0.2, "duration": 2.4, "orient": true, "orient_offset": 90, "easing": "ease_in_out" }] } }pathis SVG path data, parsed with the samePath::from_svgcallshapes.rs:88already uses forshape: { "type": "path" }. No third format was invented.orientturns the component along the tangent;orient_offsethandles artwork that does not point right at rest.The position goes through
css.transform, and that is the whole point--strict-animfolds transforms to find overflow, and it readscss.transformonly. An effect positioning a component through any other channel would be invisible to it — a component sailing off-frame along its curve would validate clean.That hole already exists in this repository:
AnimatedPropertiescarrieswidth,heightandfont_sizethat its own doc calls "component-internal, painter-only", with no CSS bridge. This change does not widen it.motion_pathwritestranslate_x/translate_y/rotation— the same fieldsorbituses, which the existing bridge already translates.Proved by removing the fix, not by argument. With
apply_motion_pathsdisabled, the decisive test reported:With it in place,
--strict-animnames the instant and the transform responsible:Three black-box tests against the compiled binary pin all of it: detected under
--strict-anim, not detected without it (so the sampling is what finds it), and no false positive on a path that stays on screen.The mid-path test was validated the same way
A test checking only t=0 and t=1 would pass even with a straight lerp between endpoints. So the implementation was temporarily replaced by exactly that lerp, on an L-shaped path
M0,0 L100,0 L100,100:The test discriminates. It is not decoration.
Coordinates
Deltas from the layout position, like
orbit.resolve_props_for_effectsreceives neither the resolved box nor the viewport — both computed later, in files frozen for this change — so an absolute coordinate space was not implementable without widening that boundary. Stated rather than left for a reader to infer.Degenerate paths
Empty or unparsable path data is rejected at parse. A single-point or zero-length path holds position with zero rotation. A non-positive duration is floored in the solver and rejected by name in
validate— the same two-ended posture the spring solver took aftermass: 0produced a NaN that contaminated layout in round 4.One honest note:
PathMeasure::pos_tanhappens to returnNoneat zero length in this skia-safe version, so the explicit guard is not strictly load-bearing today. It stays because it is the only place the degenerate case is named rather than absorbed by behaviour Skia does not contract.Verification
cargo test --workspace: 30 targets, 1114 tests, 0 failurescargo fmt --all --checkandcargo clippy --workspace --all-targets -- -D warnings: cleanpaint_pass.rs,box_builder.rsandgeometry.rsstayed read-only — no need aroseNot covered
AnimatedProperties.motion_progress, an existing painter-only field, is deliberately left without a producer: giving it one would widen the very surface this change was told not to widen.