Concepts
How effects work
What happens in between calling an effect and the last particle disappearing.
Even though calling confetti(button) takes only a single line, a lot happens until the last piece disappears. Particles are spawned, moved, drawn and removed again, many times per second. This page explains the internal lifecycle of an effect. You don’t need any of it to use the built-in effects, but it helps when you customize them, or build your own.
Anatomy of an effect
Section titled “Anatomy of an effect”Every effect, including the built-in ones, is created with createEffect(). Four options describe it:
originis the point that defines the center of the coordinate system of the effect. Particle positions are measured from it.scheduledecides when particles are emitted, and how many.particleInitdescribes how each new particle starts out: where, how fast, in which color and shape, and for how long it lives.behaviorschange particles while they’re alive, for example gravity pulling them down, or fading them out at the end.
Here is an example of an effect that uses all four:
Live Demo
import { cone, createEffect, emitFrom, fade, gravity, measureTarget, pick, randomOrientation, randomSpin, range } from "party-js";
const { origin, area } = measureTarget(button);
createEffect({ origin, schedule: { bursts: [{ count: 30 }] }, particleInit: { position: emitFrom(area), velocity: cone(90, 30, range(200, 400)), orientation: randomOrientation(), angularVelocity: randomSpin(90, 270), size: range(8, 14), color: pick(["#ff5a5f", "#ffb400", "#00a699"]), shape: { type: "circle" }, lifetime: range(1, 2), }, behaviors: [gravity(800), fade(0, 0.5)],});A few more options control where the effect is drawn and how it’s lit, which this page covers further down.
The schedule
Section titled “The schedule”The schedule decides when particles are emitted. It runs for duration seconds (1 by default), and repeats that loops times (once by default). During each loop, particles can be emitted in two ways:
- Bursts emit a group of particles all at once, at a set
timewithin the loop. - A rate emits particles continuously, a number of them per second.
// a single burst of 40 particles, like confetticonst single = { bursts: [{ count: 40 }] };
// two bursts, half a second apartconst double = { bursts: [{ count: 30 }, { time: 0.5, count: 30 }] };
// a steady stream of 20 particles per second, for three secondsconst stream = { duration: 3, rate: 20 };
// a stream that runs until the effect is ended or stoppedconst endless = { loops: Infinity, rate: 20 };A burst’s count can be a sampler, which is sampled each time the burst fires. The rate can be a live value instead, which is read again on every simulation step, so a stream can speed up, slow down, or depend on the size of the window. While it’s 0, the stream pauses, and when it rises again, the stream picks up at that pace, without making up for the particles it skipped.
// emit between 20 and 40 particles each burstconst rangeBurst = { bursts: [{ count: range(20, 40) }] };
// emit more particles the wider the window isconst windowDependent = { rate: () => window.innerWidth / 100 };A schedule that never emits anything, because it has no bursts and a rate of 0, throws an error when the effect is created. The same goes for a burst whose time lies outside the loop’s duration, since it would never fire.
Spawning: when a particle is born
Section titled “Spawning: when a particle is born”Each time the schedule emits particles, they’re created (“spawned”) one after another. For every new particle, each field of particleInit is read once, and the results become the particle’s initial state.
Positions and velocities are in particle space: pixels relative to the origin, with +x pointing right and +y pointing up. That’s the opposite of the page, where +y points down. Particles also have a z coordinate, which lets them rotate in 3D, but it doesn’t change where they’re drawn.
Particles that spawn in the same simulation step, such as all particles of a burst, form a batch. Samplers can use this to spread the particles of a burst evenly, as for example the sparkles do.
Right after a particle’s values are sampled, each behavior that has an init function is called, for example to remember the particle’s starting size.
The life of a particle
Section titled “The life of a particle”Every particle has a position, velocity, orientation, angular velocity, size, color, shape, an age and a lifetime. On each simulation step, party.js does three things to every particle, in this order:
- It ages the particle by the length of the step. Once
agereacheslifetime, the particle is considered dead and skips the rest. - It runs the effect’s behaviors on it, in the order they’re listed in. They can change anything about the particle, most commonly its velocity, size or color.
- It moves the particle by its velocity, and rotates it by its angular velocity.
Dead particles are removed at the end of the step. A particle can’t be revived, and it doesn’t get a final update after its last step.
Because behaviors run before the movement, a behavior that changes the velocity takes effect in the very same step. And because they run in order, the order can matter when one behavior reads a value that another one changes. Built-in behaviors explains the built-in behaviors, and Custom behaviors how to write your own.
Time and the simulation loop
Section titled “Time and the simulation loop”The logic for simulating particles and drawing them is kept separate. The simulation always advances in fixed steps of 1/120 of a second, no matter how fast the screen refreshes. On every animation frame, it runs as many steps as fit into the time that has passed since the last frame, and then draws the particles once. This keeps effects framerate-independent and allows seeded effects to look identical, even on monitors with different refresh rates.
Animation frames don’t always arrive on time: a busy page can hang for a moment, and browsers stop animating tabs that are in the background. If a frame arrives so late that it would need more than 8 steps, only those 8 steps are run and the rest of the time is skipped. This prevents the effect from jumping ahead or freezing the page while it catches up.
Pausing works the same way: a paused effect is still drawn, but no simulation steps are run, and the time spent paused is skipped once it resumes.
The canvas
Section titled “The canvas”Particles are drawn onto a canvas. Unless you pass your own, party.js uses one fullscreen canvas that all running effects share. It’s created when the first effect starts, and removed once the last one ends, so it only exists while there’s something to draw.
The canvas sits on top of the page without getting in the way: it doesn’t react to the mouse, isn’t announced to screen readers, and is shown above other content, including dialogs and fullscreen elements. It follows the size of the window, and is drawn at the screen’s pixel density, so particles stay sharp on high-resolution displays. On the shared canvas, an effect’s origin is in page coordinates, so its particles stay in place on the page while it scrolls.
You can also pass a canvas of your own, to place an effect behind your content, inside a container, or anywhere else on the page:
createEffect({ canvas: document.querySelector("#background"), origin: { x: 0, y: 0 }, // ...});With your own canvas, the origin is measured from the canvas’ top-left corner instead, and the page’s scroll position doesn’t matter. If you want to emit from an element, measureTarget(element, canvas) does this conversion for you. party.js sizes the canvas’ bitmap to match its size on the page, but leaves its styling and placement to you. Effects that share a canvas, the fullscreen one or your own, are drawn together, in the order they were started. Once no effect is drawing on your canvas anymore, it’s cleared and left in place.
How particles are shaded as they turn towards or away from the light source is covered in Colors & lighting.
When an effect ends
Section titled “When an effect ends”An effect ends by itself once its schedule has run all of its loops and its last particle has died. Every effect returns a handle, and its finished promise tells you how the effect ended:
const effect = confetti(button);
const result = await effect.finished;if (result === "completed") { // the confetti is gone, for example to navigate to another page}The promise resolves with:
"completed", when the effect ended by itself. That includes an effect you calledend()on, which stops emitting new particles, and ends once the ones that are alive have died."stopped", when you calledstop(), which removes all particles right away. The same happens when the effect throws an error while running, for example from a custom behavior. The error is still reported in the console, and other effects keep running.
The promise never rejects. An effect whose schedule loops forever only ends when you end or stop it, and a paused effect with particles left doesn’t end until it’s resumed.
When the user has asked their system for reduced motion, effects are skipped by default. A skipped effect draws nothing, and its finished promise resolves with "completed" right away, so code that waits for an effect keeps working.