LeCodesdocs

Particles

A GPU particle system as a node. Give it a Material and emitter settings, add it to the scene, and it streams particles — smoke, sparks, dust. Extends Node: move/rotate the node to move the emitter. Every option is also a live setter, so rate, color, gravity, … can change at runtime.

At a glance

TypeScript
const sparks = new Particles({
  material: Material.unlit({ color: '#ffcc44' }),
  rate: 80,                                                     // particles/second
  shape: { type: 'point', v: [0, 0, 0] },
  startVelocity: { dir: [0, 1, 0], speed: { min: 2, max: 4 } },
  gravity: 9.8,
  lifetime: { min: 0.6, max: 1.2 },
  size: { min: 0.05, max: 0.12 },
  color: dynamicColor('#ffcc44', 0.7, '#ffffff', 1, '#000000'), // fade out over life
})
sparks.position = [0, 1, 0]
scene.add(sparks)

crate.addEventListener('click', () => sparks.spawn(40))         // burst on top of the rate

Options

TypeScript
new Particles(options: {
  material: Material                       // required — how each particle is drawn
  rate?: number                            // particles emitted per second
  shape?: { type: 'point', v: Vec3Like }   // spawn at an offset (emitter-relative)
        | { type: 'box', min: Vec3Like, max: Vec3Like }   // spawn randomly inside the box
  startVelocity?: Vec3Like | VelocityValue // see below
  gravity?: number                         // downward acceleration, world units/s²
  drag?: number                            // velocity damping — higher slows particles faster
  lifetime?: number | { min, max }         // seconds; range = random per particle
  color?: DynamicColor                     // constant, random range, or dynamicColor() curve
  size?: DynamicValue                      // constant, range, or dynamic() curve (alias of custom[0])
  rotation?: DynamicValue                  // same (alias of custom[1])
  noise?: { strength?, frequency?, speed? } | null   // turbulence; all fields default 1
})

Everything but material has a native default — new Particles({ material }) already emits. Fields typed number | { min, max } take a flat value ("always this") or a range ("random per particle").

Not implemented

rate only applies a plain number — a { min, max } range is typed but silently ignored.

startVelocity

A plain Vec3Like is a fixed launch direction; an object picks a direction mode plus optional randomization:

TypeScript
startVelocity: {
  dir?: Vec3Like                                     // launch along this direction, or…
  from?: Vec3Like                                    // …away from this point, or…
  to?: Vec3Like                                      // …toward this point
  speed?: number | { min: number, max: number }      // scales the direction; range = per particle
  randomizeAngle?: { min: Vec3Like, max: Vec3Like }  // jitter the launch angle within bounds
}
TypeScript
sparks.startVelocity = {
  dir: [0, 1, 0],
  speed: { min: 2, max: 5 },
  randomizeAngle: { min: [-15, 0, -15], max: [15, 0, 15] },
}

Live setters & methods

TypeScript
sparks.spawn(count): this      // emit a burst right now, on top of `rate` — chainable
sparks.material                // get/set — swap the draw material live

// write-only live setters (same types as the options):
sparks.rate = 120
sparks.shape = { type: 'box', min: [-1, 0, -1], max: [1, 0, 1] }
sparks.startVelocity = [0, 3, 0]
sparks.gravity = 0
sparks.drag = 0.5
sparks.lifetime = { min: 0.5, max: 1 }
sparks.color = '#88ccff'
sparks.size = dynamic(0.1, 'multiply', 0, 1, 1, 4)
sparks.rotation = { min: 0, max: 360 }
sparks.noise = { strength: 2 }         // or null to disable

sparks.custom[0] = 0.2                 // raw curve-param slots 0..3; size/rotation alias 0/1
sparks.custom[2] = dynamic(0, 'inc', 0, 0, 1, 1)   // slots 2/3 feed a custom material

Lifecycle: the system emits continuously from construction; sparks.destroy() (from Node) removes it. For a one-shot effect, set rate: 0 and call spawn(n).

Curves over a particle's life — dynamic() / dynamicColor()

size, rotation, custom[i], and color can animate across each particle's lifetime. A curve is a flat list of (t, value) stops, t running 0..1 over the particle's life.

TypeScript
dynamic(
  baseValue: number | { min, max },   // value at birth (range = random per particle)
  type: 'multiply' | 'inc',           // curve multiplies the base, or adds to it
  ...stops: (number | { min, max })[] // flat t, value, t, value, … (value range = random per particle)
): DynamicValue

dynamicColor(
  baseValue: string | { min, max },                    // start color (range = random between two colors)
  ...stops: (number | string | { min, max })[]         // flat t, color, t, color, …
): DynamicColor                                        // the curve MULTIPLIES the base color
TypeScript
sparks.size = dynamic(0.1, 'multiply', 0, 1, 1, 4)       // grow to 4× by end of life
sparks.size = dynamic(0.3, 'multiply', 0, 1, 1, 0)       // shrink to nothing
sparks.color = dynamicColor('#ffaa33', 0.7, '#ffffff', 1, '#000000')   // fade to black at the end

dynamicColor multiplies the base, so its main use is fading brightness/alpha toward death. Keep t ascending 0 → 1 and the stop count small — stops are baked into a short native curve, not an arbitrary spline.

Pitfalls

TypeScript
// ✗ per-frame spawn() to fake an emission rate
setLoop(() => sparks.spawn(1))
// ✓ that's what rate is for
sparks.rate = 60

// ✗ expecting a rate range to work
new Particles({ material, rate: { min: 10, max: 50 } })   // ignored — see the note above

See also

  • MaterialMaterial.unlit({...}) is the usual particle material
  • Node — transform (the emitter), destroy()
  • Scene — where the system lives