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.
Fixed values and samplers
Section titled “Fixed values and samplers”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 8pxconfetti(button, { size: range(4, 12) }); // every piece is somewhere between 4px and 12pxFunctions 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.
When values are picked
Section titled “When values are picked”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
countis 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
angleandspreaddecide 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.
Built-in samplers
Section titled “Built-in samplers”| 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 }),});Writing your own sampler
Section titled “Writing your own sampler”A sampler is just a function. It receives a SamplerContext and returns a value:
// sizes that alternate between small and largeconst alternating = (ctx) => (ctx.index % 2 === 0 ? 6 : 14);
confetti(button, { size: alternating });The context has three fields:
rngis the effect’s random number generator. Use it instead ofMath.random(), so your sampler stays reproducible.indexcounts upwards. For a particle’s initial values, it’s the number of particles the effect has spawned before this one. For a burst’scount, it’s the current loop.batchdescribes 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.
Batches
Section titled “Batches”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.
Reproducible randomness
Section titled “Reproducible randomness”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 callsMath.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 }).
Combining samplers
Section titled “Combining samplers”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 samplerconst 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 piecesconst 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.