Skip to content
party.js logoparty.js logo

Concepts

Samplers & randomness

How different values for particles are picked, and how to write your own samplers.

No two pieces of confetti look exactly alike: each one gets its own size, color, speed and spin. The values come from samplers, small functions that produce a new value every time party.js asks for one. This page explains when that happens, which samplers are built in, and how to write and combine your own.

Wherever party.js accepts a Sampler, you can pass one of two things:

  • A fixed value, like size: 8. Every particle gets exactly this value.
  • A sampler function, like size: range(4, 12). It’s called again for every particle, so each one gets its own value.
import { confetti, range } from "party-js";
confetti(button, { size: 8 }); // every piece is 8px
confetti(button, { size: range(4, 12) }); // every piece is somewhere between 4px and 12px

Functions like range() don’t return a number themselves. They return the sampler, a function that party.js calls whenever it needs a value. That’s why you can create a sampler once and reuse it across effects.

Most samplers are called once for each particle, at the moment it spawns. That includes every field of a custom effect’s particleInit: position, velocity, orientation, spin, size, color, shape and lifetime. After that, the values belong to the particle, and only behaviors change them.

A few values are sampled less often:

  • A burst’s count is sampled each time the burst fires, so a looping effect can emit a different number of particles every loop.
  • Some options of the built-in effects are sampled only once, when the effect starts, and shared by all particles. For example, confetti’s angle and spread decide the direction of the whole burst, not of each piece.

Don’t confuse samplers with live values. A sampler produces variety between particles. A live value, like an effect’s origin or a schedule’s rate, is a function without arguments that party.js calls again and again while the effect runs, so the effect can track something on the page, such as an element or the window size.

Sampler Produces
range(min, max) A number between min and max, with every value equally likely.
normal(mean, stdDev) A number that clusters around mean, following a bell curve. Has no hard limits.
spread(center, deviation) A number around center, spread evenly across each batch.
pick(values, weights?) One of the given values, optionally with weighted chances.
gradient(colors) A random color along a gradient.
randomHue({ l, c }) A color with a random hue, but fixed lightness and intensity.
cone(angle, deviation, speed) A velocity in a direction, fanning out by up to deviation degrees.
randomOrientation() A random rotation, with every orientation equally likely.
randomSpin(min, max) A random spin axis, with a speed between min and max degrees per second.
emitFrom(shape) A position somewhere inside a circle or rectangle.

As an example, here is a mix of mostly circles with the occasional star, in a range of soft colors:

Live Demo

import { confetti, pick, randomHue } from "party-js";
confetti(button, {
shapes: pick([{ type: "circle" }, { type: "star" }], [4, 1]),
colors: randomHue({ l: 0.65, c: 0.08 }),
});

A sampler is just a function. It receives a SamplerContext and returns a value:

// sizes that alternate between small and large
const alternating = (ctx) => (ctx.index % 2 === 0 ? 6 : 14);
confetti(button, { size: alternating });

The context has three fields:

  • rng is the effect’s random number generator. Use it instead of Math.random(), so your sampler stays reproducible.
  • index counts upwards. For a particle’s initial values, it’s the number of particles the effect has spawned before this one. For a burst’s count, it’s the current loop.
  • batch describes the group of particles spawned together with this one. See Batches.

To build a sampler that is based on options, wrap it in an outer function that takes in the options, and returns the sampler itself. All built-in samplers are written this way. For example, a sampler that picks an integer in a given range:

const randomInt = (min, max) => (ctx) =>
Math.floor(min + ctx.rng() * (max - min + 1));

You don’t need to clone objects your sampler returns. party.js copies vectors, colors and shapes before they’re used, so returning the same object for every particle is safe.

All particles that spawn in the same simulation step form a batch. A burst is one batch, and so are several particles of a continuous stream that happen to spawn in the same step. While sampling a particle’s values, ctx.batch tells you its position in the batch (index) and how many particles the batch has (size).

Batches let a sampler coordinate the particles of a burst instead of treating each one on its own. As an example, spread() uses this to split its range into one equal slice per particle in the batch, and give each particle a random value within its own slice. A burst then always covers the whole range evenly, without the clumps and gaps that independent random values can produce. cone() uses spread() for its angle, which is why a burst of sparkles always fans out evenly.

You can use batches in your own samplers too. Here, the particles of each burst are placed on a perfect ring:

Live Demo

import { createEffect } from "party-js";
const ring = (radius) => (ctx) => {
const { index, size } = ctx.batch ?? { index: 0, size: 1 };
const angle = (2 * Math.PI * index) / size;
return {
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius,
z: 0,
};
};
createEffect({
// ...
particleInit: {
position: ring(120),
// ...
},
});

batch is always set while particle values are sampled, but not while sampling a burst’s count, so the type marks it as optional. Falling back to a batch of one keeps the sampler usable everywhere.

Every effect has its own random number generator, which samplers and behaviors receive as ctx.rng. By default, it starts from a random seed, so every run of an effect looks different. Pass a seed to createEffect(), and the same seed with the same options produces the same effect every time:

Live Demo

import { createEffect } from "party-js";
createEffect({
// ...
seed: 42,
});

This is useful for tests, for screenshots, or when an effect should look identical for every visitor. A few things keep it reproducible:

  • All randomness has to come from ctx.rng. A single sampler or behavior that calls Math.random() makes every run different again.
  • The order matters. The generator produces one sequence of numbers, which is shared between all samplers and behaviors. Adding a sampler, or one that uses more random numbers, shifts the values everything after it receives. The same seed only gives the same result with the same options.
  • Live values have to stay the same. An origin or emission rate that depends on the window size can change the effect between runs, even with the same seed.

The simulation runs in fixed steps, independently of the screen’s frame rate, so a fast and a slow device see the same particles. The exception is a device so slow that party.js has to skip time to keep up.

If you need more control, pass your own generator as rng instead. It’s any function that returns a number from 0 (inclusive) to 1 (exclusive), like Math.random() does. When both are given, rng takes precedence over seed.

The built-in effects can take a seed too: confetti(button, { seed: 1 }).

Some samplers accept other samplers as arguments. cone() takes its speed as a sampler, so the direction and the speed of each particle can vary independently:

cone(90, 30, range(300, 500));

To build your own samplers on top of others, resolve them with evaluateSampler(). It calls the sampler if it’s a function, or returns it unchanged if it’s a fixed value, so your sampler accepts both:

import { evaluateSampler } from "party-js";
// a sampler that rounds the values of another sampler
const rounded = (sampler) => (ctx) => Math.round(evaluateSampler(sampler, ctx));
confetti(button, { size: rounded(range(4, 12)) });

The same approach lets you choose between samplers. pick() returns the value it picks as it is, so picking from a list of samplers would hand the sampler itself to the particle, not a value from it. Evaluate the picked sampler as well:

Live Demo

import { confetti, evaluateSampler, pick, range } from "party-js";
// mostly small confetti, with a few large pieces
const sizes = pick([range(4, 8), range(14, 20)], [5, 1]);
const size = (ctx) => evaluateSampler(sizes(ctx), ctx);
confetti(button, { size });

Pass the same ctx along to the samplers you call, so they use the effect’s random number generator and see the same index and batch.