Skip to content

feat(transition,shape): silhouette and blob masks, feathered wipes, path trim and morph - #403

Merged
LeadcodeDev merged 2 commits into
mainfrom
feat/mask-transitions-and-path-trim
Sep 27, 2026
Merged

LeadcodeDev merged 2 commits into
mainfrom
feat/mask-transitions-and-path-trim

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #383. Refs #377, #388 — #377 is partial by necessity, see below.

#383 — reveal through any silhouette

mask grows an arbitrary polygon or SVG path about its own bounding-box centre; blob builds a smooth closed silhouette from lobes noise-perturbed points joined by quadratic curves. feather and band_color are shared by both and by all four existing wipe_* through one new primitive that paints the boundary into an Alpha8 mask, blurs it, and tints where the alpha is near 0.5.

feather <= 0 keeps the original hard-clip path byte-for-byte, so no existing scenario changes.

Zero at both ends is a hard short-circuit before any silhouette or blur maths, like zoom_blur and whip: byte-identity with the raw frames at from_scale: 0.2, to_scale: 40, at lobes: 12, wobble: 0.6, and at feather: 40 with a band colour.

A bug the tests caught, not the spec. blob first used iris_max_radius unscaled as its target, and a wobble-shrunk lobe could fall short of a frame corner — leaving a sliver of the outgoing scene visible:

just short of the end the blob's automatically solved covering radius must
already reveal the whole frame, whatever origin it grows from

Fixed by dividing by (1 − wobble). That auto-solve is not in the issue at all.

Reused: iris_max_radius directly, the origin type, and the kind: polygon|path vocabulary from clip_path_to_skia. Not reused: clip_path_to_skia itself, because it resolves percentages against a layout box — a concept that does not exist for two already-painted frame buffers.

⚠️ #377 is shape-only, and stays open

shape.draw_start and shape.path_morph work, with two real primitives behind them: trim_path_between using PathMeasure::get_segment (not a dash-effect hack, so it cannot reproduce the zero-length-dash round-cap dot that #376 just fixed in line.rs), and interpolate_path_data lerping point-by-point when both keyframes share a verb sequence. A mismatch prints to stderr and holds the earlier shape — the node-path precedent, not a snap.

svg, line, arrow and connector still have no draw_start. That needs a new AnimatedProperties field, in a file another workstream owned. So the issue's literal JSON — a style.animation keyframe on any drawable — is not what shipped, and #377 stays open for it.

A real Rust bug worth recording

Path::iter() yields PathIterRec, which is self-referential: its .points() slice points into the Rec's own inline storage. Collecting Vec<PathIterRec> and reading .points() afterwards reads dangling data — it surfaced as Rect { left: 10.0, top: 0.0, right: 1.1598642e29, bottom: 10.0 } instead of the path's actual bounds. Fixed by extracting owned points inside the iterator's map.

Verification

34 new tests across two new files plus five inline. cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (1713) all clean.

Two things flagged rather than buried

  • mask's origin is pixels, following iris and zoom_blur, although the issue's example looks fractional. Documented as a deliberate deviation rather than a second inconsistent convention.
  • Seven pre-existing Shape { .. } test literals in box_builder.rs and legacy_dispatch.rs needed two None fields each — Rust struct literals are exhaustive and none used ..Default::default(). Mechanical, outside the stated ownership, and named by the agent.

Written comment-free, per the codebase-wide rule from #345.

…ipes, and shape draw_start/path_morph

#383: generalises iris to an arbitrary silhouette. `mask` scales a
`polygon`/`path` (reusing the clip-path `kind` vocabulary) about its own
bounding-box centre from `from_scale` to `to_scale`, centred on `origin`.
`blob` grows a procedural organic silhouette (lobes/wobble/seed) with its
covering radius solved for automatically, the way iris's is, including a
safety margin for wobble-shrunk lobes. `feather`+`band_color` add a soft,
optionally tinted edge to `mask`/`blob` and the `wipe_*` family, built on a
shared `composite_through_mask` (alpha-blurred mask, tint peaking at the
alpha=0.5 boundary). Every one of these hard-short-circuits to the raw
source/destination frame at progress<=0/>=1, matching zoom_blur/whip's
"exactly zero at both ends" contract instead of an asymptotic fade.

#377: draw_start (the other end of draw_progress's trim) and path_morph
(keyframed `d`) land on `shape` specifically, as a component-local
mechanism (`Computed<f32>` expr, and a small keyframe list) rather than
through AnimatedProperties/animator.rs, which is owned by another
workstream right now. Both are built on two new pure primitives in
renderer/shapes.rs: `trim_path_between` (a real PathMeasure segment
extraction, not a dash-effect hack — avoiding the zero-length-dash-paints-
a-dot bug documented for line.rs/svg.rs) and `interpolate_path_data`
(point-by-point lerp when two keyframes share the same verb structure;
a structural mismatch is reported on stderr and holds the earlier shape,
same precedent as clip_path_to_skia's node-path handling).

Also patches 7 pre-existing test-only `Shape { .. }` struct literals in
box_builder.rs/legacy_dispatch.rs (mechanical `draw_start: None, path_morph:
None` additions) — required for the crate to compile once Shape grows new
fields; nothing else in those files was touched.

New tests: mask_blob_transitions.rs (18), path_geometry.rs (11), plus 5
inline in shape.rs — all proving the zero-at-both-ends and trim/interpolate
contracts explicitly, several catching real bugs during development (a
wobble-shrunk blob lobe under-covering the frame; a stale pointer from
collecting skia's PathIterRec before reading its points()).
@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Sep 27, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 27, 2026
@LeadcodeDev
LeadcodeDev merged commit 9f2136f into main Sep 27, 2026
4 checks passed
LeadcodeDev added a commit that referenced this pull request Sep 28, 2026
…ties (#404)

Both were blocked on the same thing: a new field on `AnimatedProperties`, in a
file another workstream owned while #363/#380 and #377 were in flight. Neither
agent reached across the boundary, and one of them explicitly refused to add
`letter_spacing` to `KNOWN_MOTION_PROPERTIES` on its own — that alone would have
made it validate and stay inert, which is the exact defect #363 was about.

**`letter_spacing`.** The intrinsic measurement keeps the static
`style.letter-spacing`, so animating it does not re-run layout — the same policy
every other layout property follows. The consequence is real and documented
rather than hidden: a text whose spacing opens overflows its box.

Two mechanisms now share a name. A `timeline` changing `letter-spacing` snaps,
and the validator warns; a `keyframes` track on `letter_spacing` interpolates.
Different subsystems, deliberately different behaviour, said plainly in the rule
file.

**`draw_start`.** `draw_progress` advances the head of a stroke; this advances
its tail. Together they are a window travelling along the path — a line that
runs rather than one that grows. On its own it erases the beginning of a
finished stroke.

`line` and `svg` both trim through `PathMeasure::get_segment`, the primitive
#403 added, rather than a dash effect. That is what keeps a zero-length dash
with a round cap from painting the stray dot #376 had to remove.

`shape` keeps its own component-level `draw_start` from #403: it carries
`path_morph` too, and the two belong together.

Five tests. Three fail without the wiring — `tight=48px, loose=48px` for
spacing, and a painted first half for the trim. Two assert equality and pass
both ways on purpose: an absent animated value must leave the declared one
alone, which is why the field defaults to NaN rather than zero.

One probe stops six pixels short of the cut on purpose, with the reason in the
assertion: a round cap extends half the stroke width back past the trim, which
is the cap working, not the trim failing.

Closes #380
Closes #377
@LeadcodeDev
LeadcodeDev deleted the feat/mask-transitions-and-path-trim branch September 29, 2026 18:42
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.

Transition edges: a mask transition with any silhouette, an organic blob, and feathered or tinted wipes

1 participant