Concepts
Colors & lighting
How particles are colored and lit.
Wherever party.js takes a color, you can pass a CSS color string:
- hex colors, like
#ff5a5f,#f00or#ff000080, - the
rgb(),hsl(),oklab()andoklch()functions, likergb(255 90 95)oroklch(0.7 0.15 20).
Named colors like red or transparent aren’t supported, to save on bundle size.
Strings are converted with color(), which you can also call yourself. It returns a Color, an object in the Oklab color space. Oklab is designed to match how we perceive colors: equal steps in it look like equal steps to the eye. That’s why party.js uses it internally. Colors blend smoothly, without muddy or overly bright spots in between, and lighting can make a color darker or brighter without changing its hue.
import { color } from "party-js";
color("#ff5a5f"); // { l: 0.69, a: 0.19, b: 0.08, alpha: 1 }, roundedA Color has a lightness l, from 0 (black) to 1 (white), two coordinates a and b that describe its hue and how intense it is, and an alpha for its opacity. You’ll rarely need to touch them directly, except when you change a particle’s color in a custom behavior.
Picking colors
Section titled “Picking colors”Colors work like any other sampled value. The built-in effects take a list of colors and pick one for each particle, and three samplers give you more control:
pick(colors, weights?)picks one of the given colors, optionally with weighted chances.gradient(colors)picks a random color along a gradient, including the colors in between.randomHue({ l, c })picks a color with a random hue, but a fixed lightnessland intensity (chroma)c.
A gradient blends its colors in Oklch, the polar form of Oklab, which goes around the color wheel. Between two colors, it passes through the hues in between, instead of a dull gray:
Live Demo
import { confetti, gradient } from "party-js";
confetti(button, { colors: gradient(["#7b61ff", "#ff5a5f", "#ffb400"]),});The colors of a gradient are spaced evenly by default. To make some of them more common, place them yourself with stops. A wider gap between two stops means more particles get a color from that part of the gradient:
// mostly pinks, with a short blend into gold at the endgradient([ { offset: 0, color: "#ff5a5f" }, { offset: 0.8, color: "#ff8fa3" }, { offset: 1, color: "#ffb400" },]);randomHue() makes the most of Oklab: since all of its colors share the same lightness, none of them stands out, which is hard to get right with hsl(). Lightnesses from 0.6 to 0.8, and chromas from 0.1 to 0.2 give vivid, even mixes. Much higher chromas go beyond what a screen can show, and get clipped, which can shift their hue a little.
Lighting
Section titled “Lighting”A turning piece of paper is brightest when it faces the light, and darker when it’s tilted away. party.js imitates this by adjusting the color of each particle on every frame, depending on how directly its face points towards the light.
How strongly a particle faces the light is a number from -1 to 1, the lighting coefficient. It’s 1 when the front of the particle faces the light directly, 0 when the light hits it edge-on, and -1 when its back faces the light. An effect’s lighting option turns this number into a color.
By default, effects use createLighting(). It multiplies the particle’s lightness by a factor between ambient, for particles hit edge-on, and peak, for particles facing the light, from either side. With the default values of 0.5 and 1, a particle facing the light is drawn in its own color, and one seen edge-on at half its lightness.
A peak above 1 makes particles brighter than their own color when they face the light, which makes them flash as they turn. Confetti uses a peak of 1.5.
Live Demo
import { createEffect, createLighting } from "party-js";
createEffect({ // ... lighting: createLighting({ ambient: 0.2, peak: 1.5 }),});Since lighting only changes the lightness, and party.js stores colors in the Oklab color space, a lit particle keeps its hue. A red piece of confetti gets darker or brighter, but never turns brown or pink.
The light’s direction
Section titled “The light’s direction”By default, the light comes from the front, from where the viewer is. The light option changes its direction, as a vector in particle space, where +x points right, +y up and +z towards the viewer. Its length doesn’t matter:
createEffect({ // ... light: { x: -1, y: 1, z: 1 }, // from the top left, in front of the canvas});The direction is a live value, so it’s read again on every frame. Pass a function, and the light can move while the effect runs, for example to follow the cursor.
Custom lighting
Section titled “Custom lighting”The lighting option takes any function that receives the particle’s color and its lighting coefficient, and returns the color to draw:
Live Demo
import { createEffect } from "party-js";
// darker at an angle, with a white flash when facing the light directlyconst glint = (color, coefficient) => { const facing = Math.abs(coefficient); const shine = facing ** 16; // close to 0, unless facing the light return { ...color, l: color.l * (0.5 + 0.5 * facing) + (1 - color.l) * shine, };};
createEffect({ // ... lighting: glint,});glint darkens particles that are tilted away, like the default lighting, but makes particles that face the light almost directly flash white. Raising the coefficient to a high power keeps the flash short: 0.9 to the power of 16 is less than 0.2.
The coefficient tells the front and the back of a particle apart, so your function can treat them differently. The default lighting ignores the sign, since paper looks the same from both sides, but a material with a different back side could darken it, or give it a different color altogether.
Two things to keep in mind:
- Return a new color. The color your function receives belongs to the particle. Changing it would change the particle itself, and the lighting would apply again on the next frame.
- Keep it quick. The function runs for every particle on every frame.