pixfx

Concepts

The ideas every pixfx package shares: effects and instances, the colour and light layers, back and front, origins and margins, time, and seeds.

Effects and instances

An effect is a static description: its metadata plus a way to spawn state. Effects are listed in one registry (pixfx_effects::EFFECTS) and looked up by id, a category/name string such as fire/candle.

An instance is one playing copy of an effect, with its own options, seed and clock. You can spawn any number of instances of the same effect.

Metadata

Every effect has a Meta. In JavaScript, meta(id) returns it as an object.

Field (JS name)Meaning
idcategory/name.
name, desc, tagsDisplay name, one-sentence description, search tags.
size[w, h]: the effect's canvas in pixels.
origin[x, y]: where the effect's local (0, 0) sits on its canvas. For a fire, the point on the ground under it.
durationSeconds: one loop, or the whole one-shot.
loop (Rust looping)Whether it loops. A one-shot holds its last moment and is done at duration.
fpsFrame rate of exported sheets and GIFs. Live playback runs at any rate.
seedThe seed used when you give none.
glowMargin (Rust glow_margin())Empty pixels worth adding round the canvas so glow that spills past it is not cut: half the canvas's longer side.

Colour and light

A frame holds two layers of the same size:

Keeping light separate is what lets a fire light up the ground and the sprites standing next to it. Every live renderer draws colour first, then light with additive blending (Canvas 2D "lighter", Phaser BlendModes.ADD, gpui SpriteBlend::Additive).

Additive light only shows over an opaque, darker background. Over a transparent target there is nothing to add to: the canvas renderer and the exports then turn light into colour whose alpha is its brightest channel, which reads correctly once composited over a dark ground. Phaser's WebGL renderer adds nothing over a transparent render target; see Phaser.

Compositing

To flatten both layers into RGBA bytes, composite draws colour over an optional background, then adds light:

The composite sheet the CLI writes and Fx.rgba() in JS both use this.

Back and front layers

An effect draws in two passes:

When drawing Both, back is drawn first, then front, into the same frame. An effect with nothing behind a character draws nothing for back. The three effects shipped today (candle, campfire, brazier) draw everything in front; the split is part of the effect contract, so an effect can wrap around a character.

To put your own sprite inside an effect, draw the back layer, your sprite, then the front layer:

stage.update(dt);
drawGround();
stage.drawLayer("back");
drawPlayer();
stage.drawLayer("front");

The Phaser plugin does the same with depths: its back images sit at one depth and front images at another, and you put your sprite between them.

Origin, placement and margin

When you play an effect "at" a point, that point is where the effect's origin lands. Its canvas extends from there by -origin to size - origin. The candle is 56×76 with origin (28, 64): its canvas reaches 28 pixels left, 28 right, 64 up and 12 down from the point you give.

Positions are rounded to whole pixels as JavaScript's Math.round does.

An instance can draw on a canvas bigger than size by a margin of empty pixels on every side. Light often spills past an effect's canvas, so:

Time

Instances advance in seconds: update(dt).

Stopping

stop() lets an instance end: a loop fades out over 0.3 s (FADE), a one-shot is cut short the same way. An instance is done when that fade has finished, or when a one-shot reaches its duration. The canvas renderer and Phaser plugin remove done instances on their own. Later stop() calls keep the first stop time.

Options

Every instance takes the same common options, all optional:

OptionTypeDefaultMeaning
seednon-negative integermeta.seedPicks the random variation. Same seed and same steps give the same pixels.
flipbooleanfalseMirror left-right about the origin.
anglenumber, radians0Rotate about the origin.
lightnumber1Glow strength. 0 draws no light; values above 1 brighten it.
palettearray of hex coloursthe effect's rampThe effect's main colour ramp, dark to bright.

Effects also take options of their own, each a number or an array of hex colours. The Effect catalog lists them. An option an effect does not know is ignored; a number given where a ramp is expected is ignored too.

Palettes and ramps

A ramp is a list of colours from dark to bright. Colours are "#rgb", "#rrggbb" or "#rrggbbaa", with or without the #; they are always opaque (alpha digits are ignored, as in pixelfx). A ramp of a different length from the default is resampled to the default's length, so a 3-colour ramp works on a 9-colour slot. An empty ramp falls back to the default.

stage.play("fire/campfire", 64, 112, {
  palette: ["#0d1033", "#3a62e0", "#7fb4ff", "#ffffff"], // a blue fire
  wood: ["#1a1a1a", "#4a4a4a"],                          // the campfire's own ramp
  height: 30,                                            // the campfire's own number
});

Determinism

Everything an effect draws comes from its seed, its options and the steps it was given. pixfx reproduces JavaScript's number semantics (rounding, min/max with NaN, 32-bit integer conversions, clamped byte stores) and uses its own math functions instead of the platform's, so a native build and a wasm build of the same effect produce the same pixels, bit for bit. A frame-hash test checks this; see Parity and tests.