Skip to content

feat(animation): burst throws a ring of strokes off a node's box edge - #409

Merged
LeadcodeDev merged 1 commit into
mainfrom
feat/burst
Sep 28, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
feat/burst

Conversation

@LeadcodeDev

@LeadcodeDev LeadcodeDev commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #379 (second half — emitter shipped in #407).

The gap

A pill that pops needs a splash of ink to sell the pop. Today that is N hand-placed
line nodes at computed angles, each with its own draw_in timing.

{ "name": "burst", "delay": 0.4, "duration": 0.6, "count": 12, "length": 60,
  "gap": 16, "width": 5, "color": "#FFB020", "seed": 3, "jitter": 0.3 }

The stroke

Each stroke is a head and a tail that each cross the same track once: the head
runs out over the first half of the window (ease_out), the tail follows over the
second (ease_in). The stroke reaches full length at the halfway point and then
closes outward — it flies away, it does not retract into the box.

So the stroke has zero length at both ends of the window by construction, not
because a fade got small.

Why that geometric zero is not redundant with the window test

burst_progress does short-circuit outside [delay, delay + duration). But that
test cannot be exact for every duration: delay + duration - delay != duration in
floating point as soon as duration is not representable — 0.4 is the obvious
case — so a frame landing exactly on the end can fall one ULP inside the window.
It paints nothing anyway, because tail == head there.

a_frame_landing_one_ulp_inside_the_window_still_paints_nothing pins that, and it
is the reason the pixel-identity test alone is not enough: reverting the window
short-circuit to a .clamp(0.0, 1.0) fails
nothing_before_the_delay_and_nothing_at_or_after_the_end but leaves the pixel test
green, exactly as it should.

gap follows the shape

The track starts where the ray leaves the box — measured against the node's own
half-extents — plus gap. A wide pill therefore pushes its horizontal strokes
further out than its vertical ones, which is what makes the ring read as belonging
to the shape rather than sitting on a circle around it. Removing gap fails
burst_leaves_the_node_itself_untouched.

Determinism

Angle, per-stroke length and per-stroke phase all come from (seed, index) through
the same splitmix64-style hash shatter uses. Injecting SystemTime::now() into it
fails burst_two_renders_of_the_same_instant_are_byte_identical immediately.

Placement

Strokes are painted over the node, after its own render and inside its transform,
so they follow a pop_in or a rotation, and they take no layout space. They can
leave the viewport if the node sits near an edge — the geometry validator does not
see them, since a burst has no layout box; rules/burst.md says to leave
gap + length of margin.

Budget-checked like shatter and chromatic_aberration.

Reverts that bite

Revert Test that goes red
drop gap from the track start burst_leaves_the_node_itself_untouched
spacing fixed at TAU/8 instead of TAU/count burst_strokes_sit_on_the_rays_count_asks_for_and_nowhere_else
SystemTime in the angle hash burst_two_renders_of_the_same_instant_are_byte_identical
window short-circuit → .clamp(0.0, 1.0) nothing_before_the_delay_and_nothing_at_or_after_the_end

Gate

cargo fmt --all --check clean · cargo clippy --workspace --all-targets --features rustmotion/studio -D warnings clean · cargo test --workspace 1767 passed · CLI probe validates and renders the ring above.

Second half of #379. Today a pill that pops needs N hand-placed `line`
nodes at computed angles, each with its own draw_in timing, to get the
splash of ink that sells the pop.

burst is that ring as one effect. Each stroke is a head and a tail that
each cross the same track once: the head runs out over the first half of
the window (ease_out), the tail follows over the second (ease_in). The
stroke therefore has zero length at both ends of the window by
construction, not because a fade got small.

That geometric zero matters beyond tidiness. burst_progress does
short-circuit outside [delay, delay + duration), but that test cannot be
exact for every duration: delay + duration - delay != duration in
floating point as soon as duration is not representable (0.4, for
instance), so a frame landing exactly on the end can still fall one ULP
inside the window. It paints nothing anyway, because tail == head there.
Both protections exist and they cover different cases --
a_frame_landing_one_ulp_inside_the_window_still_paints_nothing pins it.

gap keeps every stroke off the box: the track starts at the box edge
along each ray, measured against the node's own half-extents, plus gap.
A wide pill therefore pushes its horizontal strokes further out than its
vertical ones, which is what makes the ring follow the shape.

Angles, per-stroke length and per-stroke phase all come from
(seed, index) through the same splitmix64-style hash shatter uses, so
two renders of the same instant are byte-identical -- injecting
SystemTime into it fails the determinism test immediately.

The strokes are painted over the node, after its own render and inside
its transform, so they follow a pop_in or a rotation and take no layout
space. Budget-checked like shatter and chromatic_aberration.
@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Sep 28, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 28, 2026
@LeadcodeDev
LeadcodeDev merged commit 77b6b71 into main Sep 28, 2026
4 checks passed
@LeadcodeDev
LeadcodeDev deleted the feat/burst branch September 28, 2026 08:34
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.

Radiating graphics: a burst effect around an element, and a radial particle emitter with a lifecycle

1 participant