Skip to content
party.js logoparty.js logo

Concepts

Shapes

How particles obtain their drawn shape.

The way a particle is drawn depends on its shape. party.js ships with a few shapes that cover most effects, like squares, circles and stars, as well as text and images. You can also add your own shapes, as paths or custom draw functions. This page explains how shapes are sized and placed, which ones are built in and how to add your own.

All shapes are drawn in the same coordinate system, called shape space. A shape fits into a 1x1 box, centered at (0, 0), so it reaches from -0.5 to 0.5 in both directions. Like on a regular canvas, +y points down.

When a particle is drawn, this box is scaled to the particle’s size in pixels, rotated by its orientation, and moved to its position. For example, a circle with a size of 10 will be drawn as 10 pixels wide, as would a square. Shapes that aren’t as wide as they are tall, like a narrow strip, fill the box in their longer direction.

The most common shapes for effects are built-in, and easily be used and configured.

PlaygroundsTry it in the shapes playground

Shape Description
square A square, or a rectangle with an aspectRatio. cornerRadius rounds its corners.
circle A circle.
star A star with 5 points, or as many as you like. innerRadius makes its points thinner or wider.
polygon A regular polygon with the given number of sides.

confetti() uses circles, rounded squares and thin, rounded strips by default, and sparkles() uses slim, four-pointed stars. Confetti’s shapes option takes a list to pick from:

Live Demo

import { confetti } from "party-js";
confetti(button, {
shapes: [
{ type: "square" },
{ type: "square", aspectRatio: 0.4, cornerRadius: 0.2 },
{ type: "circle" },
{ type: "star", points: 5 },
{ type: "polygon", sides: 6 },
],
});

Like other values, shapes are checked before they’re used. A polygon with 2 sides or a negative corner radius throws a PartyJSError, either when the effect is created, or for shapes that come from a sampler, when a particle spawns.

For any shape you can describe as an outline, use a Path2D. The path has to be drawn in shape space, inside the 1 by 1 box around (0, 0):

Live Demo

import { confetti } from "party-js";
// a plus sign, drawn inside the 1x1 box around (0, 0)
const plus = new Path2D("M-0.15-0.5h0.3v0.35h0.35v0.3h-0.35v0.35h-0.3v-0.35h-0.35v-0.3h0.35z");
confetti(button, {
shapes: [{ type: "path", path: plus }],
});

Icons are also a good source of shapes. Most icons are drawn on a larger grid, like 24 by 24 pixels, so they need to be scaled into the box first:

// an icon from a 24x24 grid, moved to (0, 0) and scaled to a size of 1
const icon = new Path2D();
icon.addPath(new Path2D("M12 2 L22 22 L2 22 Z"), new DOMMatrix().scale(1 / 24).translate(-12, -12));

Single characters or plain text can also be used as a shape.

Live Demo

import { confetti } from "party-js";
confetti(button, {
// ...
shapes: { type: "text", text: "party.js" },
});

You can also use emojis, however they will not be affected by the particle’s color or lighting. Opacity works fine though.

Live Demo

import { confetti } from "party-js";
confetti(button, {
// ...
shapes: [
{ type: "text", text: "😄" },
{ type: "text", text: "😝" },
],
});

Particles can also be pictures, like your logo. Give the image shape a URL, and party.js loads the image for you:

Live Demo

import { confetti } from "party-js";
confetti(button, {
shapes: [{ type: "image", image: "/favicon.svg" }],
size: 24,
});

An image keeps its aspect ratio, with its longer side filling the box. It also keeps its own colors: it fades out with the particle, but isn’t tinted by its color or lit.

Until the image has loaded, its particles simply aren’t drawn. Loading starts when the first particle spawns, so a large image can miss the first burst. To show it right away, preload it in your page’s <head>:

<link rel="preload" as="image" href="/logo.png" />

Instead of a URL, you can also pass anything a canvas can draw, like an <img> that has already loaded, a <canvas> or an ImageBitmap. If you pass an SVG as an <img>, give it a width and height, since some browsers report a size of 0 otherwise.

Each URL is only loaded once, and kept for later effects. If it can’t be loaded, party.js reports it once in the console, and the effect plays without those particles.

A custom shape draws the particle with your own function, using the canvas’ context. The context is already set up when your function is called: it’s moved, rotated and scaled into shape space, and its fillStyle is the particle’s lit color, including its opacity. Here, each particle is drawn as three circles, which makes it look like it glows:

Live Demo

import { createEffect } from "party-js";
createEffect({
// ...
particleInit: {
// ...
shape: {
type: "custom",
draw(ctx) {
// three circles on top of each other, fainter towards the outside
for (const [radius, alpha] of [
[0.5, 0.15],
[0.3, 0.35],
[0.12, 1],
]) {
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.fill();
}
},
},
},
// ...
lighting: null,
});

globalAlpha is multiplied with the opacity of the fillStyle, so the glow still fades out with the particle. You don’t need to reset anything you change, like globalAlpha here: the context is saved before your function is called, and restored afterwards.

Your function also receives the particle being drawn, so the drawing can change with it. Here, each particle is a ring that gets thinner as it ages:

Live Demo

import { createEffect } from "party-js";
createEffect({
// ...
particleInit: {
// ...
shape: {
type: "custom",
draw(ctx, particle) {
const progress = particle.age / particle.lifetime;
ctx.strokeStyle = ctx.fillStyle;
ctx.lineWidth = 0.2 * (1 - progress);
ctx.beginPath();
ctx.arc(0, 0, 0.4, 0, 2 * Math.PI);
ctx.stroke();
},
},
},
// ...
lighting: null,
});

Only read from the particle. Drawing happens once per frame, not once per simulation step, so to change particles, write a behavior.

Keep custom drawing functions quick, they run for every particle, on every frame. Prepare anything you can, like images or gradients, outside of the function.

Each particle has a position, a size, and an orientation in 3D. When it’s drawn, party.js takes its shape from shape space, scales it to the particle’s size, turns it by its orientation, and then looks at the result straight from the front:

  • Its position becomes a point on the canvas, measured from the effect’s origin. Particle space has +y pointing up, the canvas has it pointing down, so party.js flips it.
  • Its depth is ignored. There’s no perspective, so a particle doesn’t get smaller as it moves away, and particles don’t hide each other based on their depth. Particles are drawn in the order they spawned, so newer ones end up on top.
  • Its orientation decides how the flat shape lies in space. Facing you, it’s drawn as it is. As it turns away, it gets narrower, until it’s only a line when you see it edge-on, and then it shows its back side.

Here, a row of squares turns around the vertical axis, each one a little further than the one before:

Live Demo

import { createEffect } from "party-js";
createEffect({
// ...
particleInit: {
// ...
angularVelocity: { x: 0, y: 120, z: 0 },
},
lighting: null,
});