Phaser AE

Particles

Free packv3.0.0-52DLook

The 2D particle system: fire, smoke, sparks, rain, snow, magic, debris, confetti and dust. Emitters are game objects like any sprite, every value in their config can vary, and ready-made effects drop in by name.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 19, 2026

Particles: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

particles2d

function particles2d(opts: Particles2dOptions = {}): Pack<{ particles: ParticlesApi }, ParticleTopics>

The 2D particles pack: emitters as roster objects drawn through the aggregate seam in their sorted place, every value an op, effects from vfx-* packs (registry kind vfx). Simulates in the particles-2d.update step after physics; follows in particles-2d.follow. Headless-safe; needs a 2D world.

Helpers

registerVfx

function registerVfx(k: { registry: { add(kind: string, def: VfxDef): void } }, def: VfxDef): void

Register an effect under the registry kind 'vfx' so game.particles.effect(def.name) finds it: the one-liner a vfx-* pack's attach is, and a game's own effect as registerVfx(game.kernel, def). Throws on an emitter without a name. The same name again replaces the definition (a scene that registers its effects every time it enters): the next effect(name) plays the new one, and finished copies of the old one kept to play again are let go. Two packs registering one name still fail at compose.

Types

AnimOp

type AnimOp = { frames: ReadonlyArray<string | number>; fps: number; loop?: boolean } | { frames: ReadonlyArray<string | number>; over: 'life' }

A sprite-sheet animation on each particle: at fps (looping or not), or stretched over its life.

Bounds

interface Bounds

A world-space rectangle particles stay inside.

  • x: number — Left.
  • y: number — Top.
  • w: number — Width.
  • h: number — Height.
  • collide?: ReadonlyArray<'left' | 'right' | 'top' | 'bottom'> — Which sides bounce; default all four.
  • bounce?: number — Velocity kept on a bounce, 0..1. Default 1.
  • kill?: boolean — Kill on contact instead of bouncing. Default false.

Burst

interface Burst

One burst on the emitter clock.

  • at: number — Seconds after the emitter starts.
  • count: Op — Particles; an op.
  • cycles?: number — Repeats. Default 1.
  • interval?: number — Seconds between repeats. Default 0.
  • probability?: number — 0..1 per cycle. Default 1.

BurstSpec

interface BurstSpec extends Omit<EmitterConfig, 'x' | 'y' | 'angle' | 'follow' | 'offset' | 'rate' | 'frequency' | 'bursts' | 'flow' | 'emitting' | 'duration' | 'loop' | 'stopAfter' | 'delay' | 'prewarm' | 'rateOverDistance' | 'space'>

A one-shot burst for game.particles.burst: where and how many, aimed by angle and spread, and the rest a recipe in the emitter config's own keys. Every key but x, y, count, angle and spread makes up the recipe: bursts with the same recipe share one pooled emitter, so vary only those five from call to call.

  • x: number — World x of the burst.
  • y: number — World y of the burst.
  • count?: number — Particles, a whole number of at least 0. Default 12.
  • angle?: number — The burst's direction, degrees (0 is +x, 90 is down the screen). Default 0.
  • spread?: number — The cone it fills around angle, degrees: 360 is all round, 30 a jet. Default 360.
  • recipe?: string — Name the recipe: the first burst under a name sets it and later ones reuse it, whatever else they say. Needed when an op or callback is a function, which cannot be compared; otherwise the recipe is keyed by its values.

ColorOp

type ColorOp = string | ReadonlyArray<string> | { random: ReadonlyArray<string> } | { values: ReadonlyArray<string>; ease?: EaseName | EaseFn } | { min: string; max: string }

A colour op, every colour any CSS colour: one colour · a ramp over life · pick from a list · keyed ramp with an ease · a blend between two.

DeathZone

interface DeathZone

Where particles die: on entering a shape, or on leaving it.

  • shape: ZoneShape — The shape, relative to the emitter (any geometry shape; a rect is centred).
  • type?: 'enter' | 'leave' — 'enter' (default) kills inside, 'leave' kills outside.

EffectHandle

interface EffectHandle

A spawned effect.

  • readonly anchor: Handle — The anchor object (invisible); move it to move the effect.
  • readonly emitters: Readonly<Record<string, EmitterHandle>> — Its emitters by name.
  • readonly name: string — The effect name.
  • readonly complete: boolean — True once every emitter is complete, or what the effect follows has died and its particles with it.
  • kill(): void — Kill everything now.

EffectOptions

interface EffectOptions

What effect() takes.

  • x?: number — World x.
  • y?: number — World y.
  • layer?: string — The layer.
  • z?: number — Depth.
  • follow?: Handle — Follow this object. When it dies the effect stops emitting, a looping one too, and ends once its particles have.
  • offset?: { x?: number; y?: number } — Offset from it.
  • scale?: number — Multiply every distance, speed and size. Default 1.
  • angle?: number — Rotate the whole effect, degrees. Default 0.

EmitterApi

interface EmitterApi extends Required<Pick<EmitterConfig, 'rate' | 'quantity' | 'duration' | 'loop' | 'stopAfter' | 'maxAlive' | 'emitting' | 'timeScale' | 'rotation' | 'space' | 'rateOverDistance'>>

The emitter API on the handle's emitter sub-object. Eleven config keys are live properties here (rate, quantity, duration, loop, stopAfter, maxAlive, emitting, timeScale, rotation, space, rateOverDistance); change any other key with set.

  • start(opts?: { advance?: number; duration?: number }): void — Begin emitting; advance fast-forwards seconds, duration limits.
  • stop(kill?: boolean): void — Stop emitting; living particles finish unless kill.
  • explode(count?: number, x?: number, y?: number, angle?: { min: number; max: number }): void — A burst now, at the emitter or a point (world). angle, degrees, aims this burst alone: each particle's direction is drawn from min to max in place of the angle op (the emitter's rotation still applies).
  • flow(rate: number, quantity?: Op, stopAfter?: number): void — Continuous emission: particles per second, per emission, total.
  • emit(count: number, x?: number, y?: number): void — The primitive: count particles at a point (world; default the emitter).
  • fastForward(seconds: number): void — Run the sim ahead.
  • killAll(): void — Kill every particle.
  • readonly alive: number — Live particles.
  • readonly dead: number — Free slots under max.
  • readonly complete: boolean — Emission is over — stopped, past duration, past stopAfter, or nothing left to emit (no flow, every burst fired) — and the pool has drained. Explode again and it clears.
  • readonly elapsed: number — Seconds since start.
  • set<K extends keyof EmitterConfig>(key: K, value: EmitterConfig[K]): void — Replace a config key (ops recompile). Any key of EmitterConfig.
  • readonly config: EmitterConfig — The live config. Mutate its lists (forces, bursts) in place; a plain assignment to any other key is NOT seen until set recompiles it.

EmitterConfig

interface EmitterConfig

The emitter config. Every number is an Op. Angles in degrees, distances in world units, times in seconds.

  • x?: number — World x of the emitter object. Default 0.
  • y?: number — World y. Default 0.
  • layer?: string — The layer. Default the world's first.
  • z?: number — Depth within the layer. Default 0.
  • follow?: Handle — A handle to follow each step; when it dies the emitter stops, and goes once its last particle has.
  • offset?: { x?: number; y?: number } — Offset from the followed object, world units.
  • space?: 'world' | 'local' — 'world': particles stay where they were born when the emitter moves. 'local': they move with it. Default 'world'.
  • rotation?: number — Rotation of the whole emitter, degrees: added to angle, rotates offsets, zones and forces. Default 0.
  • rate?: number — Particles per second while flowing. 0 = bursts only. Default 0.
  • frequency?: number — Milliseconds between emissions (Phaser's word); overrides rate when set.
  • quantity?: Op — Particles per emission. Default 1.
  • rateOverDistance?: number — Particles per world unit the emitter moves, emitted along its path. Default 0.
  • bursts?: ReadonlyArray<Burst> — Timed bursts on the emitter clock.
  • duration?: number — Seconds of emission after start; 0 = forever. Default 0.
  • loop?: boolean — Restart after duration. Default false.
  • delay?: number — Seconds before the emitter starts emitting. Default 0.
  • stopAfter?: number — Stop after this many particles. 0 = no limit. Default 0.
  • max?: number — Most particles alive at once (the pool's ceiling); the pool starts at 32 slots and doubles as it fills, up to this. Default 1000.
  • maxAlive?: number — A live cap below max (scale an effect down without changing its look). Default max.
  • prewarm?: number — Run the sim this many seconds ahead at start (a smoke column already risen). Default 0.
  • emitting?: boolean — Start emitting at once. Default true.
  • timeScale?: number — Sim speed multiplier. Default 1.
  • seed?: number — Random seed; omitted = from the kernel rng. Set it to replay exactly.
  • lifespan?: Op — Seconds a particle lives. Default 1.
  • hold?: Op — Seconds a particle holds its end state before dying. Default 0.
  • particleDelay?: Op — Seconds a particle waits, invisible, before appearing. Default 0.
  • px?: Op — Birth offset x from the emitter, world units, turned with rotation; a lifetime op ({ start, end }, { values }) also slides the particle by its change over life. Default 0.
  • py?: Op — Birth offset y, as px. Default 0.
  • speed?: Op — Launch speed along angle (when radial). Default 0.
  • speedX?: Op — Launch velocity x (when not radial). Default 0.
  • speedY?: Op — Launch velocity y. Default 0.
  • angle?: Op — Launch direction, degrees (0 right, 90 down). Default [0, 360].
  • radial?: boolean — Read speed+angle (true) or speedX/Y (false). Default true.
  • accelerationX?: Op — Acceleration x, units/s². Default 0.
  • accelerationY?: Op — Acceleration y. Default 0.
  • gravityX?: number — Gravity x, units/s². Default 0.
  • gravityY?: number — Gravity y. Default 0.
  • turnGravity?: boolean — Turn gravity and accelerationX/accelerationY with rotation, as offsets, zones and forces turn: an effect turned on its side falls sideways, as a picture turned would. Default false, so gravity keeps the world's down and a grinder aimed up still rains its sparks down.
  • drag?: number — Velocity damping per second, 0..1. Default 0.
  • maxVelocity?: Op — Speed cap, units/s; 0 = none. Default 0.
  • moveToX?: Op — Arrive at this x (relative to the emitter) at the end of life; overrides speed.
  • moveToY?: Op — Arrive at this y.
  • inheritVelocity?: number — Fraction of the emitter's own velocity a particle is born with. Default 0.
  • size?: Op — World units of the particle's longer side; 0 = the frame's own size. Default 4 for the pixel frame, 0 otherwise.
  • scale?: Op — Size multiplier on both axes, one value per particle so a random scale stays round. On an axis with no scaleX or scaleY of its own it is that axis's curve and takes the over-life forms ({ start, end }, { values }); with scaleX/scaleY set it multiplies them, read once at birth. Default 1.
  • scaleX?: Op — Scale x alone.
  • scaleY?: Op — Scale y alone.
  • alpha?: Op — Opacity 0..1. Default 1.
  • rotate?: Op — Visual rotation, degrees. Default 0.
  • spin?: Op — Rotation rate, degrees/s. Default 0.
  • alignToVelocity?: boolean — Point the frame along the velocity. Default false.
  • stretch?: number — Elongate along the velocity: the frame's length is multiplied by 1 + speed × stretch / 100 (so 1 doubles it at 100 units/s), and the frame turns to face its motion. Motion-blur sparks. Default 0.
  • color?: ColorOp — Colour over life (a ramp), or per particle. Default white.
  • tint?: ColorOp — A second colour op multiplied in per particle (a palette under a ramp).
  • hue?: number — Hue jitter per particle, ± degrees. Default 0.
  • frame?: FrameOp — The atlas frame(s). Default the white pixel.
  • anim?: AnimOp — A sprite-sheet animation per particle.
  • blend?: 'normal' | 'add' | 'multiply' | 'screen' | 'subtract' — Blend for the whole emitter. Default 'normal'.
  • order?: 'index' | 'oldest-front' | 'youngest-front' | 'y' — Draw order within the emitter. Default 'index'.
  • emitZone?: EmitZone | ReadonlyArray<EmitZone> — Where particles are born.
  • deathZone?: DeathZone | ReadonlyArray<DeathZone> — Where they die.
  • bounds?: Bounds — A box they stay in.
  • solid?: SolidTest — Solid geometry they collide with — a tilemap, a heightfield, anything with a point test.
  • forces?: Force[] — Forces, as data.
  • on?: { birth?: SubEmit; death?: SubEmit; interval?: SubEmit } — Sub-emitters by event.
  • onEmit?: (p: ParticleView) => void — Called for each born particle.
  • onDeath?: (p: ParticleView) => void — Called for each dying particle.

EmitterHandle

type EmitterHandle = Handle & { emitter: EmitterApi }

An emitter is a roster handle with an emitter sub-object.

EmitZone

type EmitZone = | { type: 'random'; shape: ZoneShape } | { type: 'edge'; shape: ZoneShape; quantity?: number; stepRate?: number; yoyo?: boolean; seamless?: boolean }

Where particles are born: random — anywhere inside a geometry shape (ON a curve, line or point); edge — along its outline or curve in order, quantity points around it (or one every stepRate units), yoyo to bounce back, seamless (default true) to skip the closing duplicate. Shapes are relative to the emitter; a rect here is centred.

Force

type Force = | { kind: 'well'; x: number; y: number; power: number; epsilon?: number; gravity?: number; world?: boolean } | { kind: 'attract'; x: number; y: number; strength: number; radius?: number; world?: boolean } | { kind: 'vortex'; x: number; y: number; strength: number; radius?: number; world?: boolean } | { kind: 'orbit'; x: number; y: number; strength: number; radius: number; spring?: number; world?: boolean } | { kind: 'wind'; x: number; y: number; gust?: number; gustSpeed?: number; world?: boolean } | { kind: 'noise'; strength: number; scale?: number; speed?: number; octaves?: number; over?: Op } | { kind: 'drag'; amount: number }

A force, as data. Positions are relative to the emitter and directions turn with its rotation, unless world is true.

FrameOp

type FrameOp = string | number | ReadonlyArray<string | number> | { frames: ReadonlyArray<string | number>; cycle?: boolean; quantity?: number }

A frame choice: one atlas key or index, a list (random per particle), or a cycled list.

Inherit

type Inherit = 'velocity' | 'color' | 'scale' | 'rotation' | 'lifetime'

What an inherited child keeps from the parent particle; position always.

Op

type Op = | number | readonly [number, number] | { min: number; max: number; int?: boolean } | { random: ReadonlyArray<number> } | { start: number; end: number; ease?: EaseName | EaseFn; random?: boolean } | { values: ReadonlyArray<number>; ease?: EaseName | EaseFn; interpolation?: 'linear' | 'catmull' | 'stepped' } | { steps: number; start: number; end: number; yoyo?: boolean } | { onEmit: (p: ParticleView, key: string, value: number) => number; onUpdate?: (p: ParticleView, key: string, t: number, value: number) => number }

A numeric op: constant · range · pick · eased over life · keyed over life · stepped per emission · callbacks.

Particles2dOptions

interface Particles2dOptions

Manifest options.

  • maxTotal?: number — A ceiling on live particles across every emitter; a particle that would pass it is not born (nothing throws), and emission resumes as particles die. Default 50000.
  • effectPool?: number — Finished effects kept, per effect name, scale and layer in each scene, to play again rather than make anew: the next effect() of that kind reuses one's anchor and emitters, rewound. 0 makes every effect afresh. Default 4.

ParticlesApi

interface ParticlesApi

game.particles.

  • emitter(config: EmitterConfig): EmitterHandle — Spawn an emitter object. Throws on a bad op, an unknown frame key (naming particle-textures or the file) or an unknown effect in on.
  • effect(name: string, opts?: EffectOptions): EffectHandle — Spawn a registered effect. Throws if no vfx-* pack registered name. A finished effect's emitters are kept (up to the effectPool option) and played again by the next effect of the same name, scale and layer.
  • burst(spec: BurstSpec): EmitterHandle — A one-shot burst: count particles at a point, aimed by angle and spread, on a pooled emitter kept per recipe (per scene), so a burst on every hit costs no new object. Defaults: a soft disc (particle:disc when particle-textures is composed, else the white pixel), speed 60 to 160, lifespan 0.4 to 0.9 s, size 4 to 10, shrinking and fading out. Returns the recipe's emitter. Throws on a bad position, count, angle or spread, or a function in an unnamed recipe.
  • readonly available: ReadonlyArray<string> — Every effect a composed pack registered.
  • readonly count: number — Live particles across every emitter (capped by the maxTotal option).
  • readonly emitters: ReadonlyArray<EmitterHandle> — Live emitters.
  • trail(config?: TrailConfig): TrailHandle — A continuous RIBBON that follows an object, where an emitter is a spray that leaves it. A missile's flame, a blade's arc, a comet's tail. Feed it a position (or give it follow and it feeds itself), release() to let it fade out.
  • readonly trails: ReadonlyArray<TrailHandle> — Live ribbons.

ParticleTopics

interface ParticleTopics

The signals this pack emits, once per step.

  • 'particles.start': { id: string } — An emitter began emitting.
  • 'particles.stop': { id: string } — An emitter stopped emitting.
  • 'particles.complete': { id: string; effect?: string } — An emitter (or a whole effect, with effect) drained after its emission ended.
  • 'particles.death': { id: string; x: number; y: number; why: 'age' | 'zone' | 'bounds' | 'solid' } — A particle died: why is 'age', 'zone', 'bounds' or 'solid'. Clamped at 256 per step.

ParticleView

interface ParticleView

What a particle callback op sees: the particle's live fields, writable.

  • x: number — World x.
  • y: number — World y.
  • vx: number — Velocity x, units/s.
  • vy: number — Velocity y.
  • age: number — Seconds alive.
  • life: number — Lifespan, seconds.
  • t: number — Age / life, 0..1.
  • rotation: number — Rotation, degrees.
  • scaleX: number — Scale x.
  • scaleY: number — Scale y.
  • alpha: number — Alpha 0..1.
  • index: number — Index in the emitter's emission count.
  • seed: number — Per-particle random in [0,1), fixed at birth.

SolidTest

interface SolidTest

SOLID GEOMETRY particles collide with — a tilemap's collision grid, a heightfield, any test you can write.

bounds is a rectangle, which is the right answer for a jar or a playfield and the wrong one for a floor with steps in it: sparks that should skitter down a staircase pile up on one invisible line. This takes a POINT TEST instead, so the geometry can be anything, and resolves per axis — which is what makes a step work. A particle stopped on the way down keeps its sideways motion and slides along the tread; one stopped sideways keeps falling down the riser.

A tilemap is one line: solid: { at: (x, y) => { const { c, r } = map.cell(x, y); return map.solid(c, r); } }.

  • at(x: number, y: number): boolean — Is this WORLD point inside solid geometry? Called once or twice per particle per step.
  • bounce?: number — Speed kept on the bounced axis, 0..1. Default 0.4 — a spark off a stone floor, not a superball.
  • friction?: number — Speed scrubbed off the SLIDING axis on contact, 0..1. Default 0.1.
  • kill?: boolean — Kill on contact instead of bouncing. Default false.

SubEmit

interface SubEmit

A sub-emitter: spawned from a parent PARTICLE at an event. One hidden pool per child config, not one per particle.

  • emitter?: EmitterConfig — A child config, spawned at the parent particle.
  • effect?: string — Or a registered effect, spawned at the parent particle.
  • count?: Op — Particles per event (emitter children). Default 1.
  • inherit?: ReadonlyArray<Inherit> — What the child borrows.
  • probability?: number — 0..1. Default 1.
  • every?: number — Interval children: seconds between spawns per parent particle.

TrailApi

interface TrailApi

A live ribbon.

  • feed(x: number, y: number): void — Add a point. Ignored when it is closer than spacing to the head.
  • release(): void — Stop feeding and let it fade out; it removes itself when the last point dies.
  • clear(): void — Drop every point at once.
  • readonly points: number — Points held right now.
  • width: number — Width at the head, live.
  • alpha: number — Head alpha, live.

TrailConfig

interface TrailConfig

How a ribbon looks and how long it lives.

  • width?: number — Ribbon width at the head, world units. Default 8.
  • life?: number — Seconds a point survives. Default 0.5.
  • colors?: ReadonlyArray<string> — Head→tail colour stops, any CSS colours, spread evenly along the ribbon's age. Default white.
  • add?: boolean — Additive blend — fire, magic, light. Default false.
  • taper?: boolean — Taper the width to nothing at the tail. Default true.
  • alpha?: number — Head alpha. Default 0.8.
  • spacing?: number — Minimum distance between recorded points, world units. Default 3.
  • layer?: string — The layer to draw on.
  • z?: number — Depth. Default 0.
  • follow?: { x: number; y: number } — Follow this object each step, so the game never feeds it by hand.
  • offset?: { x?: number; y?: number } — Offset from the followed object.

TrailHandle

type TrailHandle = Handle & { trail: TrailApi }

A ribbon is a roster object with a trail sub-object.

VfxDef

interface VfxDef

A registered effect: emitters on a timeline, spawned together and anchored to one object.

  • name: string — The name game.particles.effect() is called with.
  • description?: string — One line for the planner.
  • duration?: number — Seconds until the effect is complete; 0 = when every emitter is. Default 0.
  • loop?: boolean — Restart when complete. Default false.
  • emitters: ReadonlyArray<EmitterConfig & { name: string }> — The emitters, in draw order (later is in front). delay starts one late; duration limits it.

ZoneShape

type ZoneShape = Loose<GShape> | { kind: 'circle'; x?: number; y?: number; r: number; inner?: number; arc?: [number, number] } | { kind: 'rect'; x?: number; y?: number; w: number; h: number; inner?: number } | { kind: 'cone'; x?: number; y?: number; r: number; angle: number; spread: number } | { kind: 'ring'; x?: number; y?: number; r: number; width?: number; inner?: number }

A zone shape: any geometry shape with its position optional (default the emitter), plus the particle shorthands circle with inner/arc, rect with inner, ring with width, and cone. Normalised by norm.