Particles
The @yagejs/particles package provides a lightweight particle system with
pooled rendering, configurable emitters, and built-in presets for common effects.
ParticlesPlugin Setup
Section titled “ParticlesPlugin Setup”import { ParticlesPlugin } from "@yagejs/particles";
engine.use(new ParticlesPlugin());The plugin depends on @yagejs/renderer.
Creating an Emitter
Section titled “Creating an Emitter”Add a ParticleEmitterComponent to an entity that has a Transform. Particles
spawn centred on the entity’s world position, so a child entity emits where it
is drawn.
The Transform is required, not optional: the system that drives emitters looks
for entities that have both, so an emitter on its own never emits and never ages
the particles a burst already spawned. The first emit() or burst() on an
entity with no Transform logs a warning to the console.
The smallest emitter needs only a lifetime — with no texture, particles are
white squares you can color with tint. The default shape is a single pixel,
so set scale to the size you want:
import { ParticleEmitterComponent } from "@yagejs/particles";
const sparkle = entity.add(new ParticleEmitterComponent({ lifetime: [0.3, 0.6], speed: [60, 120], angle: [0, Math.PI * 2], scale: [2, 4], tint: 0xffdd55,}));
sparkle.burst(20);For an image asset, pass texture (or textureKey, which also survives
save/load):
import { texture } from "@yagejs/renderer";
const Particle = texture("assets/particle.png");
const emitter = new ParticleEmitterComponent({ texture: Particle, maxParticles: 200, rate: 20, // particles per second lifetime: [0.5, 1.5], // seconds (random range) speed: [50, 150], // px/s angle: [-Math.PI, Math.PI], // emission direction (radians) scale: { start: 1, end: 0 }, alpha: { start: 1, end: 0 }, tint: 0xff6600, gravity: { x: 0, y: 200 }, layer: "effects",});
entity.add(emitter);emitter.emit(); // start continuous emissionEmitter Config
Section titled “Emitter Config”| Property | Type | Default | Description |
|---|---|---|---|
texture | TextureInput | — | Particle texture |
textureKey | string | — | Asset key (serializable alternative to texture) |
shape | ParticleShape | ShapeConfig | "pixel" | Built-in shape, optionally sized |
maxParticles | number | 100 | Maximum live particles |
rate | number | 10 | Emission rate (particles/sec) |
lifetime | NumberRange | — | Particle lifetime in seconds (required) |
speed | NumberRange | 0 | Initial speed (px/s) |
angle | NumberRange | 0 | Emission angle (radians) |
scale | NumberRange | Lerped | 1 | Size, or start→end interpolation |
alpha | NumberRange | Lerped | 1 | Opacity, or start→end interpolation |
rotation | NumberRange | 0 | Initial rotation (radians) |
rotationSpeed | NumberRange | 0 | Rotation speed (rad/s) |
tint | number | 0xffffff | Color tint |
blendMode | BlendMode | "normal" | How the particles combine with what is beneath |
gravity | { x, y } | — | Per-particle gravity (px/s²) |
damping | number | 0 | Velocity damping (0–1) |
spawnOffset | { x?, y? } | — | Random offset from emitter position |
layer | string | "default" | Render layer name |
NumberRange can be a single number or a [min, max] tuple for random values.
Lerped interpolates from start to end over the particle’s lifetime:
scale: { start: [0.8, 1.2], end: 0 } // shrink to nothingalpha: { start: 1, end: [0, 0.3] } // fade out with variationThe three texture options are mutually exclusive: setting more than one is a
type error. An emitter that sets none of them uses the "pixel" shape.
Blend Mode
Section titled “Blend Mode”blendMode decides how the particles combine with what is already drawn
beneath them. Fire, sparks, and magic usually want "add", which brightens
the background instead of covering it:
const emitter = new ParticleEmitterComponent({ ...ParticlePresets.fire(), blendMode: "add",});
emitter.blendMode = "normal"; // also a live accessorThe mode applies to the emitter as a whole, so every one of its particles
blends the same way. "add", "multiply", "screen" and the other GPU-native
modes work with no setup; the photoshop-style ones need a side-effect import.
See Blend modes for the full list.
Built-in Shapes
Section titled “Built-in Shapes”Six shapes ship with the package, so effects work before you have any art:
| Shape | Looks like | Default size |
|---|---|---|
"pixel" | A white rectangle | 1×1 |
"circle" | A solid disc, an ellipse on a non-square size | 64×64 |
"softCircle" | A disc fading to transparent at the edge — smoke, glows, embers | 64×64 |
"diamond" | A solid diamond | 64×64 |
"softDiamond" | A diamond fading to transparent, which reads as a four-point sparkle | 64×64 |
"line" | A filled streak — rain, speed trails | 64×8 |
entity.add(new ParticleEmitterComponent({ shape: "softCircle", lifetime: [0.6, 1.2], speed: [30, 60], angle: [-Math.PI / 2 - 0.3, -Math.PI / 2 + 0.3], scale: { start: 0.5, end: 1.2 }, alpha: { start: 0.5, end: 0 }, tint: 0x888888, // shapes are white; tint gives them their color}));Every shape is white so tint fully controls the color. The first emitter to
ask for a shape at a given size builds it; the rest share that one texture.
Sizing a Shape
Section titled “Sizing a Shape”Pass an object instead of the shape name to choose the texture size — one
number for a square, or [width, height]:
{ shape: "softCircle" } // 64×64{ shape: { type: "softCircle", size: 16 } } // 16×16 texture{ shape: { type: "circle", size: [32, 16] } } // an ellipse{ shape: { type: "line", size: [4, 32] } } // a vertical streak, for rainsize is the generated texture’s size in pixels, which at the default
scale: 1 is also the size a particle covers on screen. The two options do
different jobs:
sizesets the shape’s own dimensions. Keep to a few fixed values — every distinct size generates and caches its own texture.scalesizes each particle relative to that and animates over its lifetime, and generates nothing. Vary particle size here.
A size has to be a finite number above 0; anything else throws instead of producing an empty texture.
No shape forces an aspect ratio: a "circle" in a [32, 16] texture is an
ellipse that fills it. "line" is horizontal at its default 64×8, so for
falling rain either size it vertically ([4, 32]) or set rotation to aim it
along the direction of travel — diagonal rain needs rotation either way.
Every shape stays visible at every size, down to 1×1. "pixel" and "line"
fill their texture edge to edge. The other four draw an outline inside the
texture with a one-pixel antialiased edge, and fill their texture instead once
they get too thin to hold one — at 3 pixels or less on either axis, a 1-pixel
border would be the whole shape.
An emitter using a shape saves and restores like one using textureKey — the
shape and its size go into the snapshot.
Runtime Control
Section titled “Runtime Control”emitter.emit(); // start continuous emissionemitter.stop(); // stop emitting (existing particles continue)emitter.burst(50); // spawn 50 at the entity's world positionemitter.burst(10, 400, 300); // burst at an explicit world position
emitter.isEmitting; // booleanemitter.activeCount; // number of live particlesemitter.blendMode = "add"; // read/writePresets
Section titled “Presets”ParticlePresets provides ready-made configs for common effects. Each preset
returns a complete EmitterConfig and works with no arguments at all:
import { ParticlePresets } from "@yagejs/particles";
// Fire — upward, warm colors, shrinkingentity.add(new ParticleEmitterComponent(ParticlePresets.fire()));
// Smoke — slow, expanding, fadingentity.add(new ParticleEmitterComponent(ParticlePresets.smoke()));
// Sparks — fast, short-lived, gravity-affectedentity.add(new ParticleEmitterComponent(ParticlePresets.sparks()));
// Rain — downward, uniform, long-livedentity.add(new ParticleEmitterComponent(ParticlePresets.rain()));Each one falls back to a built-in shape sized for the effect: a 32px soft circle for fire, a 40px one for smoke, a 10×3 streak for sparks, a 2×20 one for rain.
Pass your own art as the argument — a texture, a texture handle, or an asset key:
entity.add(new ParticleEmitterComponent(ParticlePresets.fire(Particle)));entity.add(new ParticleEmitterComponent(ParticlePresets.fire("assets/flame.png")));The presets keep on-screen size in the shape’s size and use scale only for
lifetime animation and per-particle variation, centred on 1. That is why the
same preset reads correctly both ways: with your texture the effect animates it
at its natural size instead of scaling it against a size the preset guessed.
Overriding a Preset
Section titled “Overriding a Preset”Spread a preset to override anything except where its particles get their look:
entity.add(new ParticleEmitterComponent({ ...ParticlePresets.fire(), rate: 50, tint: 0x00ccff, // blue fire}));A preset already sets a texture source, and the three sources are mutually exclusive, so adding another by spreading does not compile:
// Type error — the preset's shape and your texture are both sources{ ...ParticlePresets.fire(), texture: Particle }
// Pass it as the argument instead{ ...ParticlePresets.fire(Particle), rate: 50 }- Position the emitter entity where you want particles to spawn — particles take the entity’s world position at the moment they’re created, including when the entity is a child of another one.
- A particle is centred on its spawn point, and
rotationSpeedturns it about its own centre. That holds for your own textures too, so a 64px sprite covers 32px either side of the emitter rather than hanging down and to the right. - Particles live in world space. Once spawned they keep going on their own path; moving the emitter afterwards does not drag them along.
- Destroying the emitter entity stops emission and cleans up all particles.
- Keep
maxParticlesreasonable. Hundreds are fine; thousands may impact performance on lower-end devices. - Use render layers to control whether particles appear above or below game entities.