Skip to content

feat(emitter): a radial particle field with a closed-form lifecycle - #407

Merged
LeadcodeDev merged 2 commits into
mainfrom
feat/burst-and-emitter
Sep 28, 2026
Merged

LeadcodeDev merged 2 commits into
mainfrom
feat/burst-and-emitter

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

First half of #379. burst remains open — see the note at the bottom.

The gap

particle is deprecated and its five frozen presets have no lifecycle. The
hand-rolled alternative — for-each + rand($seed, $i) + sin($t) — has none
either: nothing tracks birth, travel and death, so the field either freezes at a
single age or needs an explicit keyframed node per particle. A warp-tunnel study
in the reel produced 460 nodes and hundreds of KB of JSON for three seconds.

The shape

Each particle's age is derived from (seed, index, time) in closed form
rather than simulated:

phase[i] = rand(seed, i) * life[i]
age(t)   = (t + phase[i]) mod life[i]

That is what makes the component seekable: still --time 1.5, a full render, and
a re-render of that same frame all evaluate the same pure function. Two
still --time 1.5 runs produce byte-identical PNGs.

Concurrency falls out of rate × average(life) by Little's law, so there is
deliberately no particle-count field to desync from the other knobs. A
6000-particle cap bounds the cost.

speed.{from,to} set the total travel distance (average velocity × life);
speed.easing then decides how that distance distributes across the particle's
life, so ease_in reads as acceleration outward.

Registered as a decorative full-bleed component like particle, which it
supersedes: 100%/100% default sizing, exempt from the viewport-overflow check.

Docs

rules/emitter-lifecycle.md, indexed from SKILL.md. CLAUDE.md also claimed
53 components while the enum carries 61 — the audit test pins README.md and
rustmotion-components/Cargo.toml to the enum count but never covered
CLAUDE.md, so it had drifted silently. Corrected, and emitter added to the
Média section.

Not in this PR

burst, the other half of #379, needs a new AnimationEffect::Burst variant in
rustmotion-core/src/schema/video.rs. That file was owned by the shatter
workstream (#378) while this was written, so #379 stays open for it.

Gate

cargo fmt --all --check clean · cargo clippy --workspace --all-targets -D warnings clean · cargo test --workspace 1746 passed.

particle is deprecated and the for-each + rand + sin($t) recipe has no
lifecycle: a warp-tunnel style effect needs hundreds of hand-keyframed
nodes (reel-10's study: 460 nodes, hundreds of KB of JSON for 3s)
because nothing tracks birth, travel and death per particle.

emitter fixes this by deriving each particle's age from (seed, index,
time) in closed form instead of simulating: age(t) = (t + phase[i]) mod
life[i]. That keeps it deterministic and seekable (still --time,
render, and re-rendering the same frame all hit the same pure
function), and lets a handful of scalar fields (rate, life, speed,
spawn_radius, length) stand in for what used to be an explicit node per
particle. Concurrency falls out of rate * average(life) by Little's
law, so there's no separate particle count to desync from the other
knobs. speed.{from,to} set the physically-motivated total travel
distance (average velocity * life), and speed.easing then decides how
that distance distributes over the particle's life -- ease_in reads as
acceleration outward.

Registered as a new decorative, full-bleed component (like particle,
which it supersedes): default 100%/100% sizing, exempt from the
viewport-overflow check.

burst (issue #379's other half) is blocked: it needs a new
AnimationEffect::Burst variant in rustmotion-core/src/schema/video.rs,
which another workstream owns right now. Not touched.

Also bumps the component count in README.md, the rustmotion-components
crate description, and the component-tag corpus test in
crates/rustmotion/src/tests.rs -- all three are asserted against the
Component enum's variant count by existing tests and would otherwise
fail cargo test --workspace for any new component, not just this one.
The rule file arrived in English. The corpus is roughly half and half, but
the eight most recently written rules are all French, so the sample that
suggested English was unrepresentative -- rewritten in French, content
unchanged.

CLAUDE.md still claimed 53 components while the enum carries 61; the
audit test that pins README.md and rustmotion-components/Cargo.toml to
the enum count never covered CLAUDE.md, so it drifted silently.
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.

1 participant