emitter — objects that spawn, fall, and die
The emitter is the only system whose population is not fixed. Every other
scene draws the same number of things every frame; this one throws objects from a
source line just below the frame, lets each ride its own parabola, and retires it
when its life runs out or it falls out of shot. Nothing wraps — that is the whole
difference from the swarm, whose world is a torus and whose particles
categorically cannot leave
(ADR-0057).
The path is fixed at spawn and it is a closed form, p0 + v0*t + 0.5*g*t².
Two things follow for authoring. There is no mid-flight force: no drag, no
swirl, no steering, and easing gravity or launch_speed changes only the
objects thrown after the change — the ones already in the air keep the arc they
were launched on. And the motion is exactly the same on every device, because
there is no dt in it at all.
The throw
| param | what it does |
|---|---|
spawn_rate | objects a second. The population settles at roughly spawn_rate * flight time, so this is the density lever |
gravity | downward acceleration, world units per second squared |
launch_speed | speed at the source, world units a second |
launch_angle | radians clockwise from straight up; 0 throws vertically, 1.57 throws to the right |
lifetime | seconds an object lives, if it has not left the frame first |
The geometry is one line of arithmetic and it is worth doing before tuning: a mark
launched at v against gravity g turns over after v / g seconds, v² / (2g)
world units above the source line (at y = -1.12 unless source_y moves it).
A frame is |y| <= 1, so
v² / (2g) - 1.12 is where the crest of the shower sits. Every object shares
launch_speed, so a crest inside the frame draws a visible horizontal ceiling
where the population piles up at the top of its arc; emitter_perseids.toml puts
its crest at y = 1.48 (v = 2.6, g = 1.3), off frame, for exactly that reason.
lifetime past the flight time is wasted pool. An object that has left the frame
is retired the moment it does, so the only thing a long lifetime buys is slots
held by objects thrown straight up into a gravity = 0 sky.
The source
The line objects are thrown from is geometry the preset owns (ADR-0104). Four scalars, and they are one mechanism rather than four knobs: the first two say where the source is, and the last two answer the two things that go wrong once you move it — a mark that pops on arrival, and a world that takes a lifetime to fill. Every default is exactly the behaviour this scene shipped with, so a preset that binds none of them is unchanged to the pixel.
| param | default | what it does |
|---|---|---|
source_y | -1.12 | the line’s world y. Just below the frame by default, so an upward throw rises into shot |
source_width | 1 | the line’s half-width as a fraction of the frame’s. 1 spans the frame, 0 is a point source |
spawn_fade | 0 | fraction of an object’s life spent ramping up from black. 0 is no ramp beyond the engine’s own 8 % attack |
prewarm | 0 | lifetimes of spawns to back-date at scene start, so the population begins at its steady state instead of ramping to it. Clamped at 2 |
source_width is a fraction, not world units, so it means the same thing on every
display — the engine has already reconciled the aspect
(ADR-0037) and an
author never should. source_y and source_width are both clamped to the
retirement bound (1.6 frames), past which an object would be born already outside
it — a pool churning against itself rather than anything visible.
A point fountain is source_width = 0; an off-centre jet is that plus pan_x.
The source line is centred on the world origin, and pan_x carries the whole scene,
so a narrow source at pan_x = 0.5 is a jet three-quarters of the way across the
frame. spread still opens the cone — close it too if the throw should be a
column rather than a fan.
source_y may sit inside the frame, which is what makes a drift slow enough to
read as a sky reachable at all: from below the frame an object has to travel 2.12
world units before it crosses, and at sky speeds that is seconds of empty picture.
The price is a spawn pop — an object switched on at full brightness where the eye is.
spawn_fade is what you pay it with. It ramps an object’s brightness from
black over the first fraction of its life, so a mark born mid-frame arrives instead
of appearing. 0.3 on a two-second life is a 0.6 s arrival. Nothing validates the
pair — an inside-frame source_y at spawn_fade = 0 pops, and the engine lets it
(coupling them was rejected: the preset surface is a flat set of independent
scalars, and a rule like that would make a legal preset briefly illegal under a
[smoothing]-eased fade passing through zero). It is also worth having on its own:
a ramp on a short-lived object is a soft spark, which no brightness can express.
Judged in the app, 2026-08-15: at the pop’s most visible —
a source on the screen midline, source_y = 0 — spawn_fade = 0.35 hides it,
against a paired spawn_fade = 0 probe that shows the artifact plainly. So the
price above is real but payable, and there is no “keep the marks faint” caveat to
work around. The same sitting judged a prewarmed world’s one-frame arrival as
right rather than jarring, so nothing about a preset switch argues against
prewarm either.
prewarm is the other half of a slow world, and moving the source does not do
its job. Wherever the line sits, the pool starts empty and fills at
spawn_rate, so the population climbs toward spawn_rate * lifetime over a whole
lifetime. On an eight-second sky that is eight seconds of a picture arriving.
prewarm = 1 back-dates a lifetime of spawns at scene start, so the first frame is
already the steady state — exactly, not approximately: an emitter object’s path is
a closed form in its own age, so an object born back-dated is indistinguishable
from one that had been flying all along.
Measured on a slow drifting draft (source_y = -1.05, launch_speed = 0.3,
lifetime = 8), prewarm = 0 against prewarm = 1, everything else identical:
| statistic | prewarm = 0 | prewarm = 1 |
|---|---|---|
coverage (sanity, floor 0.25) | 0.0074, 0/10 radial shells — reads as blank | 0.1470, 10/10 shells — under the floor but structurally present, which passes |
motion (animation, floor 0.01) | 0.0629 | 0.1702 |
best band (reactivity, floor 0.02) | 0.0002 | 0.0195 |
A prewarmed world is full on its first frame, which is right for a sky and wrong for a cascade — a preset switching into one arrives already populated rather than building. That is the whole reason it is a param and not a fix.
Individuation — the distribution params
A binding is evaluated once per frame for the whole scene, so no expression can make one object differ from another. That is what these are for: each says how wide a per-object draw is, and the object’s own seed picks within it, once, at spawn. The preset controls the distribution; the seed controls the member.
| param | what varies, per object |
|---|---|
spread | launch angle, within a cone of this width in radians, centred on launch_angle |
size_spread | the mark’s size, as a fraction either side of size |
lifetime_spread | how long it lives, as a fraction either side of lifetime |
spin | how fast the mark turns, in radians a second — signed per object, so the field turns both ways |
twinkle | a brightness oscillation whose rate and phase both come off the seed |
spread is the one that changes the geometry. At 0 every object rides the
same vertical parabola and the shower is a column of beads; open it and the arcs
cross. Defaults are non-zero for spread, size_spread and lifetime_spread —
a population with no variation is the defect this exists to fix.
twinkle is the answer to “make the stars blink and they all flash together”.
Because each object draws its own rate as well as its own phase, the whole-frame
brightness stays steady while every member of the field swings; a shared rate
would flash as one sheet whatever the phases were.
spin turns the mark, and there is something to turn. The emitter’s default mark
is a soft elongated glint rather than a perfect disc —
a disc is rotationally symmetric, so on one spin would be invisible — and that
glint is what shape = disc means on this scene. Select any other silhouette
and spin rotates the figure itself, which is what makes a shaped mark read as
an object rather than a stamp. See
Shaped marks.
What it does not have
No per-object expressions (hash(index)-style authoring is out of reach — widen
a spread, but “every seventh object is gold” is not expressible), no collision or
inter-object forces, no stamped trail. trails reads differently here than on the
swarm and that is not a defect: a decaying smear behind an object that leaves
is a comet tail, where behind a wrapping particle it is a current. Keep it short
if the arcs should read as arcs.
No source shape either — the source is a line, so a ring or an area is engine
feedback; the two scalars above reach every shape that has been asked for. And
there is no fade-out to match spawn_fade: an object vanishes at its death time,
which the retirement margin usually puts off-frame — an inside-frame source_y
is exactly the case that undoes that.
Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.