feat(animation): burst throws a ring of strokes off a node's box edge - #409
Merged
Merged
Conversation
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.
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 free
to 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 #379 (second half —
emittershipped in #407).The gap
A pill that pops needs a splash of ink to sell the pop. Today that is N hand-placed
linenodes at computed angles, each with its owndraw_intiming.{ "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 thesecond (
ease_in). The stroke reaches full length at the halfway point and thencloses 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_progressdoes short-circuit outside[delay, delay + duration). But thattest cannot be exact for every duration:
delay + duration - delay != durationinfloating point as soon as
durationis not representable —0.4is the obviouscase — so a frame landing exactly on the end can fall one ULP inside the window.
It paints nothing anyway, because
tail == headthere.a_frame_landing_one_ulp_inside_the_window_still_paints_nothingpins that, and itis the reason the pixel-identity test alone is not enough: reverting the window
short-circuit to a
.clamp(0.0, 1.0)failsnothing_before_the_delay_and_nothing_at_or_after_the_endbut leaves the pixel testgreen, exactly as it should.
gapfollows the shapeThe 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 strokesfurther 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
gapfailsburst_leaves_the_node_itself_untouched.Determinism
Angle, per-stroke length and per-stroke phase all come from
(seed, index)throughthe same splitmix64-style hash
shatteruses. InjectingSystemTime::now()into itfails
burst_two_renders_of_the_same_instant_are_byte_identicalimmediately.Placement
Strokes are painted over the node, after its own render and inside its transform,
so they follow a
pop_inor a rotation, and they take no layout space. They canleave 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.mdsays to leavegap + lengthof margin.Budget-checked like
shatterandchromatic_aberration.Reverts that bite
gapfrom the track startburst_leaves_the_node_itself_untouchedTAU/8instead ofTAU/countburst_strokes_sit_on_the_rays_count_asks_for_and_nowhere_elseSystemTimein the angle hashburst_two_renders_of_the_same_instant_are_byte_identical.clamp(0.0, 1.0)nothing_before_the_delay_and_nothing_at_or_after_the_endGate
cargo fmt --all --checkclean ·cargo clippy --workspace --all-targets --features rustmotion/studio -D warningsclean ·cargo test --workspace1767 passed · CLI probe validates and renders the ring above.