colormotion
colormotion is a JavaScript library of utilities for creating dynamic color palettes. Its primary use case is for generating color palettes that change over time, such as for animations or visualizations in LED art. It is built on top of the fantastic chroma.js library.
Compatible with Node.js and browser environments.
Palette
Transitionspeed 0.1
Randomness5 colors · min 0.6
Shape
Wheel and brightness5/tick · drift
Code and linknew Theme({ … })
Installation
Install the colormotion npm module using your favorite package manager:
npm install colormotion
# pnpm install colormotion
# yarn add colormotionQuick Start
With colormotion you can:
- Generate a color palette that changes over time
- Get colors across the current spectrum that are evenly spaced
- Smoothly transition between color palettes
- Smoothly transition between color interpolation modes
- Adjust the global brightness for a given theme
Import the Theme class from colormotion using CommonJS or ES modules:
// CommonJS
const { Theme } = require('colormotion');
// ES modules
import { Theme } from 'colormotion';Make a theme:
const theme = new Theme();Get a color:
const color = theme.getColor();Here's an example of creating a theme with an initial rainbow palette and using it to control an LED strip:
const theme = new Theme({
colors: ['red', 'green', 'blue'],
nSteps: 2048,
mode: 'rgb',
});
// Suppose this function runs in a loop
function draw() {
// Suppose we have an LED strip with 100 LEDs
for (let i = 0; i < 100; i++) {
const color = theme.getColor(i);
// Set the color of the i-th LED to `color`
// Suppose `setPixel` is a function
// that sets the color of an LED
setPixel(i, color.rgb());
}
// Advance the theme to the next frame
theme.tick();
}Interpolation
Interpolation is the process of generating a sequence of values between two points. When generating colors between the base colors of a palette, colormotion uses the selected interpolation mode to determine how to generate the intermediate colors. The interpolation mode selects the color space in which the interpolation occurs.
The supported color modes for interpolation are:
- RGB
- LAB
- LRGB
- HSL
- LCH
- HSV
- HSI
- HCL
- OKLab
- OKLCH
In code, each mode is its lowercase name, such as 'oklch', typed as InterpolationMode.
colormotion uses the color spaces of chroma.js and follows its interpolation, except near colors without a hue: in LCH, HCL and OKLCH a mix toward black, and in HSI a mix toward white, fades the chroma or saturation so it actually arrives there. And in OKLCH, translucent colors mix as the opaque ones do, with their alpha mixed on its own, where chroma.js would mix alpha as the hue.
LAB, LCH, HCL, OKLab and OKLCH mix through the Lab white point of chroma.js (D65 unless chroma.setLabWhitePoint changes it). A palette mixes its colors when it is built and keeps them, whatever the white point does later; a theme mixes its transitions as it reads each color, under the white point set at the time. The darken brightness mode works in CIELAB too, and darkens each color as it is read, under the white point then.
Theme
new Theme(config?: ThemeConfig);The Theme class is the primary interface for creating dynamic color palettes. It is responsible for maintaining the current color palette, updating the palette over time, and transitioning between palettes.
You'll likely need to construct just a single theme for your entire application. This single theme can then manage the active color palette, global brightness, and other settings, as well as managing the transition between palettes.
To instantiate a new theme, simply create a new instance of the Theme class. The defaults are shown below:
const theme = new Theme({
nColors: 5,
nSteps: 2048,
mode: 'rgb',
deltaEThreshold: 20,
maxNumberOfColors: 8,
brightnessMode: 'darken',
random: Math.random,
});There are three options for selecting the initial palette of the theme.
The palette parameter allows you to pass in an existing ColorPalette instance. This takes precedence if provided. The theme uses a copy of the palette with the theme's nSteps (2048 unless given, whatever the palette's own), and any mode, maxNumberOfColors, deltaEThreshold or random parameter given overrides the palette's. The copy keeps the colors in their positions, with their ages (see theme.rotateColor). A palette that already has all of these settings is used as it is.
const palette = new ColorPalette({
colors: ['red', 'green', 'blue'],
mode: 'lab',
nSteps: 10, // Required here; the theme uses its own nSteps (2048)
});
const theme = new Theme({
palette,
});
theme.mode; // 'lab': the palette's mode, as no mode is given
theme.palette.nSteps; // 2048The colors parameter allows you to pass in a list of color inputs, which can be chroma.js Color objects, hex strings, CSS color names, or any other string that chroma.js can parse. This is the next highest priority, if provided.
const theme = new Theme({
colors: ['red', '#00ff00', 'rgb(0,0,255)'],
});The nColors parameter allows you to specify the number of colors to generate in the initial palette. nColors random colors will be generated for the initial palette. This is the lowest priority, if provided. If none of the three color options are provided, this parameter defaults to 5, resulting in a palette of 5 random colors.
const theme = new Theme({
nColors: 3,
});The nSteps parameter specifies the number of colors in the full color wheel. The colors defined in the Theme palette, along with the defined interpolation mode, will be used to interpolate nSteps colors to fill out the circle of colors. This defaults to 2048.
const theme1 = new Theme({
nColors: 5,
nSteps: 1024,
});The mode parameter specifies the interpolation mode. This defaults to the mode of the palette parameter if one is given, and to rgb otherwise.
const theme1 = new Theme({
nColors: 5,
nSteps: 1024,
mode: 'lab',
});The deltaEThreshold parameter specifies the threshold for the minimum CIEDE2000 color distance between colors in the palette. This threshold is used for all randomization methods. This defaults to the deltaEThreshold of the palette parameter if one is given, and to 20 otherwise.
const theme1 = new Theme({
nColors: 5,
deltaEThreshold: 50,
});The maxNumberOfColors parameter specifies the max number of colors in any given palette allowed in the Theme. This is enforced when setting, adding or generating colors (see theme.maxNumberOfColors). This defaults to the maxNumberOfColors of the palette parameter if one is given, and to 8 otherwise.
const theme1 = new Theme({
nColors: 5,
maxNumberOfColors: 12,
});The brightnessMode parameter controls how brightness is applied. darken (the default) darkens colors in CIELAB space, so light and saturated colors can keep some light even at brightness 0 (white becomes #6d6d6d). linear scales the RGB channels, so brightness 0 is black (LEDs off) and 0.5 halves every channel. See theme.brightnessMode for details.
const theme1 = new Theme({
nColors: 5,
brightnessMode: 'linear',
});The random parameter supplies the random number generator used for all randomization methods. It should return a number in the range [0, 1), like Math.random. This defaults to the generator of the palette parameter if one is given, and to Math.random otherwise. Supply a seeded generator to get reproducible palettes.
const theme1 = new Theme({
nColors: 5,
random: seededRandom(42),
});Theme.random
static random(config: ThemeConfig & { nColors: number }): Theme;Creates a new Theme with a palette of nColors random colors. This is the same as new Theme(config), with nColors required, and takes the same options.
nColors should be a whole number of 1 or more, and is capped at the max number of colors. If palette or colors is also given, it takes precedence as in the constructor, and nColors is ignored.
You can explicitly set a minBrightness for the random colors. Expects a value between 0 and 1. Defaults to 0. With constraints, the colors are drawn within limits on their OKLCH hue and chroma, the color at position i within constraints[i] (see Random colors).
const theme = Theme.random({
nColors: 4,
minBrightness: 0.5, // Defaults to 0
constraints: { chroma: { min: 0.5 } }, // Defaults to none: drawn in HSV
});theme.nSteps
readonly nSteps: number;The number of colors in the Theme's color wheel, set by the nSteps constructor option. Defaults to 2048. It is read-only: set it with the constructor option.
The palette colors are spread evenly around the wheel, with interpolated colors filling the steps between them. theme.getColor wraps its index around nSteps, and each theme.tick() turns the wheel one step, so nSteps ticks turn it all the way round.
Every palette the Theme builds uses it, so theme.activePalette.nSteps equals it, including the copy made of a palette with another nSteps passed to the constructor. Use a whole number of 1 or more.
const theme = new Theme({ colors: ['red', 'green', 'blue'] });
// Spread the whole wheel along a strip of 100 LEDs
// (setPixel stands for a function that sets the color of an LED)
for (let i = 0; i < 100; i++) {
setPixel(i, theme.getColor((i * theme.nSteps) / 100).rgb());
}
// Called on each of 60 ticks a second, turns the wheel
// all the way round every 10 seconds
theme.tick(theme.nSteps / (10 * 60));theme.maxNumberOfColors
readonly maxNumberOfColors: number;The max number of colors in any palette of the Theme, set by the maxNumberOfColors constructor option. Defaults to the maxNumberOfColors of the palette option if one is given, and otherwise to 8. It is read-only: set it with the constructor option. Use a whole number of 1 or more.
Longer color lists are cut to their first maxNumberOfColors colors by the constructor, theme.update and theme.setColors. A larger nColors is capped at it, and pushing a color onto a full palette adds nothing.
const theme = new Theme({
colors: ['red', 'orange', 'yellow', 'green'],
maxNumberOfColors: 3,
});
theme.maxNumberOfColors; // 3
theme.activePaletteHexes; // ['#ff0000', '#ffa500', '#ffff00']
theme.pushNewColor('blue'); // No change: the palette is fulltheme.activePalette
get activePalette(): Readonly<ColorPalette>;Returns the active ColorPalette of the Theme. If the Theme is currently transitioning between palettes, this will return the palette that the theme is transitioning to.
const palette = theme.activePalette;theme.activePaletteHexes
get activePaletteHexes(): string[];Returns the hex values of the active ColorPalette of the Theme, without the closing repeat of the first color. If the Theme is currently transitioning between palettes, this will return the hex values of the palette that the theme is transitioning to.
A rotation or a push can leave a palette whose last color matches its first, such as blue, green, blue. Given back as colors, to theme.setColors, theme.update or the constructor, such a list loses its last color, which is taken for the closing repeat. To give a palette's colors back as they are, pass theme.activePalette.hexes, which ends with the closing repeat, or theme.activePalette.colors.
const theme = new Theme({ colors: ['red', 'green', 'blue'] });
const hexes = theme.activePaletteHexes; // ['#ff0000', '#008000', '#0000ff']
theme.rotateColor('blue');
theme.activePaletteHexes; // ['#0000ff', '#008000', '#0000ff']
theme.setColors(theme.activePalette.hexes); // Keeps all three colorstheme.palette
get palette(): ColorPalette;
set palette(palette: ColorPalette);The ColorPalette the Theme was last at rest on: the initial palette, the one most recently assigned, or the target of the most recent transition that finished. When the Theme is not transitioning, theme.getColor reads from it, and theme.activePalette returns it.
It does not change during a transition: it becomes the target palette only when the transition finishes, and a target that is replaced or cancelled never becomes theme.palette. Meanwhile the colors are a mix that no palette holds, so read them with theme.getColor.
A palette passed to the constructor is copied with the theme's settings, such as its nSteps, when its own differ, so theme.palette is then not that object. A copy keeps the colors' positions and ages.
Assigning a palette puts the Theme at rest on it at once, ending any transition, and notifies subscribers: the same as theme.update with its colors and mode and a transitionDuration of 0, except that the palette's deltaEThreshold, random and color ages are kept. Assigning the palette the Theme is already at rest on, or one with the same colors, settings and ages, changes nothing. One that differs only in its ages takes its place, so the next rotations follow the assigned ages. A palette with a different nSteps or maxNumberOfColors from the theme's is rebuilt with the theme's, as the constructor does, so theme.palette is then a copy of it. To blend into a palette instead, assign it to theme.targetPalette.
const theme = new Theme({ colors: ['red', 'blue'] });
const start = theme.palette;
theme.setColors(['green', 'yellow']);
theme.palette === start; // true until the transition ends
const target = theme.targetPalette;
theme.finishTransition();
theme.palette === target; // true
const lab = new ColorPalette({
colors: ['white', 'black'],
mode: 'lab',
nSteps: 2048,
});
theme.palette = lab; // At rest on it at once
theme.palette === lab; // true
theme.mode; // 'lab'theme.targetPalette
get targetPalette(): ColorPalette | undefined;
set targetPalette(palette: ColorPalette | undefined);The ColorPalette the Theme is transitioning to, or undefined when it is not transitioning. theme.isTransitioning is true exactly when it is set, and theme.activePalette returns it while it is.
The methods below set it when they start a transition. A call for the palette it already holds leaves it as it is; a call for a different palette replaces it, and the colors blend on from where they are. When the transition ends, on a theme.tick or through theme.finishTransition, it becomes theme.palette and returns to undefined. A call that changes nothing when the Theme is not transitioning, such as setting the same colors, leaves it undefined, and so does a transitionDuration of 0, which ends any transition before the call returns.
Assigning a palette starts a transition to it at the default speed, as theme.update does, from wherever the colors are. Assigning the palette it is already heading to, or one with the same colors, settings and ages, changes nothing and keeps the transition's timing; so does assigning the palette it is at rest on. A palette with the colors and settings of theme.activePalette but other ages takes its place without a transition: only the order of the next rotations changes, so the Theme stays at rest, or its transition keeps going as it was. A palette with a different nSteps or maxNumberOfColors from the theme's is rebuilt with the theme's. So reading theme.targetPalette back does not always give the palette assigned: it can be a copy, or undefined when the Theme stays at rest.
Assigning undefined cancels a transition: the Theme returns to theme.palette, and theme.mode to its mode, at once, and notifies subscribers. To end a transition at its target instead, use theme.finishTransition.
const theme = new Theme({ colors: ['red', 'blue'] });
theme.targetPalette; // undefined
theme.setColors(['green', 'yellow']);
theme.isTransitioning; // true
theme.activePalette === theme.targetPalette; // true
theme.targetPalette = undefined; // Back on red and blue at once
theme.targetPalette; // undefinedtheme.mode
get mode(): InterpolationMode;
set mode(mode: InterpolationMode);The interpolation mode of theme.activePalette. On construction, it is the mode option, else the mode of the palette option, else rgb.
It changes when a transition to a palette in another mode starts, not when it ends: during a theme.setMode transition it is already the new mode, while theme.palette.mode is still the old one. Transitions mix the colors toward the target in this mode, subscribers receive it as event.mode, and theme.update uses it when no mode is given.
Assigning a mode is the same as calling theme.setMode with it: the Theme transitions to its palette in that mode at the default speed. To switch at once, call theme.setMode with a transitionDuration of 0. Assigning theme.palette sets the mode to that palette's at once, and cancelling a transition returns it to theme.palette's.
const theme = new Theme({ colors: ['red', 'blue'] });
theme.mode; // 'rgb'
theme.mode = 'oklch'; // The same as theme.setMode('oklch')
theme.mode; // 'oklch'
theme.palette.mode; // 'rgb' until the transition endstheme.brightness
get brightness(): number;
set brightness(brightness: number);Getter and setter for the brightness of the Theme. Values range from 0 to 1. The default is 1.
const brightness = theme.brightness;
theme.brightness = 0.6;theme.brightnessMode
readonly brightnessMode: BrightnessMode;How the brightness is applied to colors from theme.getColor: darken (the default) or linear, set by the brightnessMode constructor option. It is read-only: set it with the constructor option.
linear multiplies the red, green and blue values by the brightness, so hues are kept (until 8-bit rounding near black), 0.5 halves every value and 0 is black. The Theme brightness and the brightness option of theme.getColor multiply: 0.5 and 0.5 give the same color as 0.25.
darken lowers the CIELAB lightness, by up to 54 (of 100) at brightness 0. Whether a color reaches black depends on its lightness and saturation: white keeps some light at 0 (#6d6d6d), mid and dark greys reach black (#777777 at 0, #333333 already at 0.5), and saturated colors can keep a dim tint (#0000ff is #000069 at 0). Hues can shift too (#ff8000 is #a93a00 at 0.5). The Theme brightness and the brightness option of theme.getColor each darken the color, so 0.5 and 0.5 is about as dark as 0.
In either mode, theme.activePalette, theme.activePaletteHexes and the colors in subscriber events are at full brightness.
const theme = new Theme({
colors: ['#ff8000'],
brightnessMode: 'linear',
});
theme.brightnessMode; // 'linear'
theme.brightness = 0.5;
theme.getColor().hex(); // '#804000'
theme.getColor(0, { brightness: 0.5 }).hex(); // '#402000'theme.getColor
getColor(index?: number, options?: { brightness?: number }): Color;Gets the Theme color at the given index. Handles rounding and wrapping around the palette, so you don't need to worry about the index being out of bounds or non-integer. Returns a chroma.js Color object. Index defaults to 0.
const color0 = theme.getColor();
const color100 = theme.getColor(100);
const color100Hex = theme.getColor(100).hex();
const [r, g, b] = theme.getColor(100).rgb();You can optionally adjust the brightness of this specific color, without affecting the overall Theme brightness. This brightness factor compounds with the overall brightness.
const colorDimmed = theme.getColor(100, {
brightness: 0.6,
});Here's an example of creating a theme with an initial rainbow palette and using it to control an LED strip:
const theme = new Theme({
colors: ['red', 'green', 'blue'],
nSteps: 2048,
mode: 'rgb',
});
for (let i = 0; i < 100; i++) {
const color = theme.getColor(i);
// Set the color of the i-th LED to `color`
// Suppose `setPixel` is a function
// that sets the color of an LED
setPixel(i, color.rgb());
}theme.normalizeIndex
normalizeIndex(index?: number): number;Returns the step of the color wheel that theme.getColor reads for the given index: a whole number from 0 to nSteps - 1. The index is an offset from the color index, which theme.tick advances, so the same index reads a different step as the color index moves. Index defaults to 0, so theme.normalizeIndex() is the color index itself, rounded to a whole step and wrapped around the wheel.
The index and the color index are each rounded to the nearest whole step (halves round up), then added and wrapped around the wheel, so negative and out-of-range indexes are fine. An index that is not a finite number (NaN, Infinity) counts as 0. Calling it does not change the Theme.
When the Theme is not transitioning and is at full brightness, getColor(index) is the color at this step of activePalette.scaleColors. During a transition, activePalette is the target palette, so that entry is the color the step is heading to, and getColor returns a mix between it and the color the transition started from.
const theme = new Theme({ nSteps: 100 });
theme.normalizeIndex(5); // 5
theme.normalizeIndex(-1); // 99
theme.normalizeIndex(102.4); // 2
theme.tick(10);
theme.normalizeIndex(); // 10
theme.normalizeIndex(5); // 15
theme.normalizeIndex(-11); // 99
// Not transitioning, at full brightness: the same color
const color = theme.getColor(5);
const sameColor = theme.activePalette.scaleColors[theme.normalizeIndex(5)];theme.fillRgb
fillRgb<T extends Float32Array | Float64Array | number[]>(
out: T,
options?: FillRgbOptions,
): T;Writes every color of the color wheel into an array of numbers, three per color (red, green and blue), without creating a Color object for each: for filling a lookup table on every frame. The values are exactly those of theme.getColor(i, { brightness }).rgb(false) divided by 255, for each index i from 0 to nSteps - 1 in turn, so they run from 0 to 1. Pass max: 255 for the 0-255 channel values themselves.
As with theme.getColor, the colors follow the color index that theme.tick advances (the first color written is the one at step theme.normalizeIndex(0)), a transition in progress, and the brightness and brightness mode. The brightness option compounds with the Theme brightness, as getColor's does. Alpha is not written.
The array can be a Float32Array, a Float64Array or a plain array of numbers, with room for nSteps * 3 values from offset (0 unless given). Values outside that range are left as they are, and a Float32Array holds the values rounded to single precision. theme.fillRgb returns the array, and throws a RangeError, writing nothing, when the array is too short or offset is not a whole number of 0 or more.
At rest, it copies the colors from a buffer kept for each palette. During a transition, it mixes the colors and converts them to RGB in buffers the Theme reuses, once per tick however many times it is called, with the same arithmetic as chroma.js, which is how the values match getColor's exactly. The first fill after the palette changes converts the new colors once. In linear brightness mode, that makes it about ten times as fast as the same loop over getColor, or more.
In darken mode, it keeps the colors darkened by the Theme brightness, and those darkened by the most recent brightness option, so an unchanging brightness at rest costs nothing more. A brightness option that changes from one call to the next, such as a fade or two lookup tables at different brightnesses, darkens every color again on each call, as every tick of a transition does: about five times as fast as getColor, rather than ten or more.
If chroma.setLabWhitePoint has moved chroma.js off its default white point, D65, it reads each color with getColor instead.
const theme = new Theme({
colors: ['#e8b450', '#7a1a2b', '#2b1a12'],
nSteps: 256,
mode: 'oklch',
brightnessMode: 'linear',
});
const lut = new Float32Array(theme.nSteps * 3);
// Once per frame
theme.tick();
theme.fillRgb(lut, { brightness: 0.8 });
// lut[3 * i], lut[3 * i + 1] and lut[3 * i + 2] are the red, green
// and blue of theme.getColor(i, { brightness: 0.8 }), from 0 to 1
// The 0-255 channels, after 3 other values
const channels = new Float64Array(3 + theme.nSteps * 3);
theme.fillRgb(channels, { offset: 3, max: 255 });theme.update
update(
options: {
colors: ColorInput[];
mode?: InterpolationMode;
} & ColorUpdateConfig,
): void;Updates the Theme to a new palette. This will set a target palette for the theme to transition to.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed. The transitionSpeed should be between 0 and 1. The input value will be clamped to this range. A higher value will result in a faster transition, while a lower value will result in a slower transition. Defaults to 0.1.
The mode is optional and defaults to the current interpolation mode.
theme.update({
colors: ['red', 'green', 'blue'],
mode: 'hsv', // Defaults to the current mode
transitionSpeed: 0.5, // Defaults to 0.1
});To end a transition at a set time instead, pass a transitionDuration: the number of ticks the transition takes. The colors ease in and out from where they are and become exactly the new palette on the last tick. The same option works with every method below that sets a new palette, and takes precedence over transitionSpeed. A value that is not a finite number is ignored, and transitionSpeed applies instead.
A duration of 0 applies the palette before the call returns, and subscribers are notified once, with isTransitioning false.
Sending the same duration again for the palette the theme is already transitioning to changes nothing, so an update can be re-sent every tick. A different duration re-times the transition to end that many ticks from now, continuing from the current colors. The easing starts again from rest, so re-send the same duration rather than a countdown of the ticks left.
// 6 seconds when theme.tick() is called 60 times a second
theme.update({
colors: ['red', 'green', 'blue'],
transitionDuration: 6 * 60,
});theme.setMode
setMode(mode?: InterpolationMode, options?: ColorUpdateConfig): void;Updates the Theme to a new interpolation mode. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new interpolation mode.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
theme.setMode('lab');
theme.setMode('lab', {
transitionSpeed: 0.5, // Defaults to 0.1
});theme.rotateMode
rotateMode(options?: ColorUpdateConfig): void;Rotates the Theme interpolation mode. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new interpolation mode.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
theme.rotateMode();
theme.rotateMode({
transitionSpeed: 0.5, // Defaults to 0.1
});theme.setColors
setColors(colorInputs: ColorInput[], options?: ColorUpdateConfig): void;Updates the Theme to a new set of colors. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
theme.setColors(['red', 'green', 'blue']);
theme.setColors(['red', 'green', 'blue'], {
transitionSpeed: 0.5, // Defaults to 0.1
});theme.randomFrom
randomFrom(
color: ColorInput,
options?: ColorUpdateConfig & RandomPaletteConfig,
): void;Updates the Theme to a new set of colors randomized based on a seed color, which will become the first color of the new palette. Defaults to the same number of colors as the current palette, though this can be explicitly set. The max number of colors defined in the theme config is always respected. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
You can explicitly set a minBrightness for the new random colors. Expects a value between 0 and 1. Defaults to 0. With constraints, the random colors are drawn within limits on their OKLCH hue and chroma, the color at position i (from 1) within constraints[i]; the seed is kept as it is (see Random colors).
theme.randomFrom('red');
theme.randomFrom('red', {
minBrightness: 0.5, // Defaults to 0
constraints: { chroma: { min: 0.5 } }, // Defaults to none: drawn in HSV
nColors: 5, // Defaults to the number of colors in the current palette
transitionSpeed: 0.5, // Defaults to 0.1
});theme.randomTheme
randomTheme(options?: ColorUpdateConfig & RandomPaletteConfig): void;Updates the Theme to a new set of random colors. Defaults to the same number of colors as the current palette, though this can be explicitly set. The max number of colors defined in the theme config is always respected. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
You can explicitly set a minBrightness for the new random colors. Expects a value between 0 and 1. Defaults to 0. With constraints, the colors are drawn within limits on their OKLCH hue and chroma, the color at position i within constraints[i] (see Random colors).
theme.randomTheme();
theme.randomTheme({
minBrightness: 0.5, // Defaults to 0
constraints: { chroma: { min: 0.5 } }, // Defaults to none: drawn in HSV
nColors: 5, // Defaults to the number of colors in the current palette
transitionSpeed: 0.5, // Defaults to 0.1
});theme.pushNewColor
pushNewColor(color: ColorInput, options?: ColorUpdateConfig): void;Pushes a new color onto the end of the Theme palette, as its newest color. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The input can be a chroma.js Color object, hex string, CSS color name, or any other string that chroma.js can parse.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
If the number of colors in the palette is already at the maxNumberOfColors, this will not add a new color.
theme.pushNewColor('red');
theme.pushNewColor('red', {
transitionSpeed: 0.5, // Defaults to 0.1
});theme.pushRandomColor
pushRandomColor(options?: ColorUpdateConfig & RandomColorConfig): void;Pushes a new random color onto the end of the Theme palette, as its newest color, drawn at least deltaEThreshold (CIEDE2000) from the current last color. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
If the number of colors in the palette is already at the maxNumberOfColors, this will not add a new color.
You can explicitly set a minBrightness for the new random color. Expects a value between 0 and 1. Defaults to 0. With constraints, it is drawn within limits on its OKLCH hue and chroma: within constraints[nColors], the constraint for the position it takes (see Random colors).
theme.pushRandomColor();
theme.pushRandomColor({
minBrightness: 0.5, // Defaults to 0
constraints: { chroma: { min: 0.5 } }, // Defaults to none: drawn in HSV
transitionSpeed: 0.5, // Defaults to 0.1
});theme.popOldestColor
popOldestColor(options?: ColorUpdateConfig): void;Drops the oldest color from the Theme palette: the first color of the list the colors were set from, or, once the palette has been rotated, the color the next rotation would replace. The other colors keep their order. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
theme.popOldestColor();
theme.popOldestColor({
transitionSpeed: 0.5, // Defaults to 0.1
});theme.rotateColor
rotateColor(color: ColorInput, options?: ColorUpdateConfig): void;Replaces the oldest color in the Theme palette with the new color, in its position. Every other color keeps its place on the wheel, so only the colors between the replaced color's two neighbours change: an LED that shows another part of the wheel keeps its color. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
Successive rotations replace the colors oldest first, starting from the first color of the list the colors were set from, so each color is replaced once before any is replaced again. theme.activePalette.ageOrder lists the positions of the colors, oldest first: its first entry is the color the next rotation replaces. A color added with theme.pushNewColor is the newest, so it is replaced last. During a transition, rotating replaces the oldest color of the target palette.
Rotating in the color the oldest already has changes only the ages, so it starts no transition: the Theme stays at rest, or its transition keeps going as it was, and subscribers are notified once.
The input can be a chroma.js Color object, hex string, CSS color name, or any other string that chroma.js can parse.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
const theme = new Theme({ colors: ['red', 'green', 'blue'] });
theme.rotateColor('purple'); // purple, green, blue
theme.rotateColor('orange'); // purple, orange, blue
theme.activePalette.ageOrder; // [2, 0, 1]: blue is next
theme.rotateColor('red', {
transitionSpeed: 0.5, // Defaults to 0.1
});Before version 4.0, rotating dropped the first color and appended the new one, which moved every color one place along the wheel. To shift the colors that way, set them yourself:
theme.setColors([...theme.activePaletteHexes.slice(1), 'red']);theme.rotateRandomColor
rotateRandomColor(options?: ColorUpdateConfig & RandomColorConfig): void;Replaces the oldest color in the Theme palette with a random color, as theme.rotateColor does. The new color is drawn at least deltaEThreshold (CIEDE2000) from both of the colors it will sit between on the wheel. This will set a target palette for the theme to transition to with the same settings as the current palette other than the new colors.
The transition will be completed gradually over time, with the duration of the transition determined by the set transitionSpeed, or by transitionDuration if given. The same rules apply as for theme.update.
You can explicitly set a minBrightness for the new random color. Expects a value between 0 and 1. Defaults to 0. With constraints, it is drawn within limits on its OKLCH hue and chroma: within the constraint for the position it replaces, constraints[activePalette.ageOrder[0]], so a palette of picks can roll each color near its own pick (see Random colors).
theme.rotateRandomColor();
theme.rotateRandomColor({
minBrightness: 0.5, // Defaults to 0
constraints: { chroma: { min: 0.5 } }, // Defaults to none: drawn in HSV
transitionSpeed: 0.5, // Defaults to 0.1
});Before version 4.0, it dropped the first color and appended a random color drawn at least deltaEThreshold from the last. To shift the colors that way, drop the oldest color and push a random one. With the same random, this draws the same colors as before, as long as the colors are only ever set from lists, pushed or popped; a palette of one color gains a second.
theme.setColors(theme.activePalette.popOldest().pushRandom().colors);theme.tick
tick(n?: number): void;Arguably the most important method. This advances the color index by a given number of frames, and moves the colors toward the target palette if one is set. This should probably be called once per frame in an animation context.
The number of frames defaults to 1. If you want to advance the color index by more than one frame, you can pass the number of frames as an argument. Fractional frames add up, and colors are read at the nearest whole step. This does not affect the transition between palettes, which advances once per call, so theme.tick(0) advances a transition without moving the color index.
theme.tick();
theme.tick(10);
theme.tick(0); // Transition onlytheme.isTransitioning
get isTransitioning(): boolean;Whether the Theme is transitioning to a target palette.
if (theme.isTransitioning) {
// A transition is in progress
}theme.transitionDistance
get transitionDistance(): number | undefined;How far the Theme's colors are from the target palette: the average CIEDE2000 distance between the colors it shows and the target palette's colors, from 0 (identical) to 100. It is estimated from up to 128 evenly spaced colors of the full color wheel, and ignores brightness and the color index. It is an average, so single colors can be further off, and it averages over the whole wheel: after a change to part of it, such as a rotation, the colors that change are further off than the average. A transitionSpeed transition judges when to end by the colors it changes alone, so it ends as smoothly after a rotation as after a whole new palette.
It is undefined whenever theme.isTransitioning is false, so it reads undefined, not 0, once a transition ends. During a transitionDuration transition, it is measured from the current colors when read, so it is defined as soon as the new palette is set. During a transitionSpeed transition, it is measured on each tick, before the colors move: it is undefined until the first tick after the target palette changes, and then one tick behind the colors theme.getColor returns.
It usually falls toward 0, but it can hold or rise before it falls, most often in hue-based interpolation modes: from cyan to red in hsl, it rises from 71 to 87 first. It need not get small before the transition ends: a short transitionDuration transition can end on its last tick while the distance is still above 1, and it then goes straight to undefined.
const theme = new Theme({ colors: ['red', 'blue'] });
theme.setColors(['green', 'yellow']);
// Once per frame
theme.tick();
const distance = theme.transitionDistance;
if (!theme.isTransitioning || (distance !== undefined && distance < 1)) {
// Not transitioning, or on average within about one
// just-noticeable difference of the target palette
}theme.finishTransition
finishTransition(): void;Finishes the current transition now. The target palette is applied exactly, and subscribers are notified as when a transition ends on its own. Does nothing when the Theme is not transitioning.
theme.finishTransition();theme.subscribe
subscribe(callback: ThemeUpdateCallback): ThemeUpdateEvent;Subscribe to updates to the Theme. The callback will be called whenever the target palette of the Theme is updated, whenever the Theme reaches the target palette, and whenever theme.brightness changes.
The subscribe function also returns the current theme state, which can be used to initialize some variable.
const myCallback = (event: ThemeUpdateEvent)=> {
// Do something with the event
}
theme.subscribe(myCallback);Here's a React hook like the one the demo above uses to show the currently selected interpolation mode (the demo's version also follows the playground when it rebuilds its theme):
import { ThemeUpdateCallback, ThemeUpdateEvent } from 'colormotion';
import { useEffect, useState } from 'react';
import { theme } from './theme'; // your Theme instance
export function useInterpolationMode() {
const [mode, setMode] = useState<ThemeUpdateEvent['mode'] | undefined>(
undefined,
);
useEffect(() => {
const updatePalette: ThemeUpdateCallback = (event) => {
setMode(event.mode);
};
// Subscribe to theme updates and capture the current state
const initialState = theme.subscribe(updatePalette);
// Initialize the state upon mounting
updatePalette(initialState);
return () => {
theme.unsubscribe(updatePalette);
};
}, []);
return mode;
}theme.unsubscribe
unsubscribe(callback: ThemeUpdateCallback): void;Unsubscribe a given callback from Theme updates.
theme.unsubscribe(myCallback);Random colors
The random methods draw colors uniformly in HSV by default: any hue, any saturation, and a brightness (HSV value) from minBrightness to 1. Uniform HSV hue is uneven to the eye: about 22 % of fully saturated draws land in the greens between OKLCH hues 120° and 150°, and about 4 % in each 30° of gold or cyan. Uniform saturation draws greys and pastels too: at a minBrightness of 0.537, about a quarter of draws are under relative chroma 0.35.
Give them constraints to draw in OKLCH terms instead: the hue uniform over the OKLCH hues allowed, the relative chroma uniform within a range, and the brightness uniform from minBrightness to 1, as before. Every color drawn meets its constraint, so no draw is rejected. Without constraints, colors are drawn in HSV exactly as before, so a seeded random draws the same colors as in 4.0. Try both in the playground's Randomness group.
Constraints
A ColorConstraint has three optional parts. hues is a list of arcs of OKLCH hue, each a center and a full width in degrees (or built with hueArc); left out, every hue. avoid takes arcs out of them, unless that would leave no hue. chroma bounds the relative chroma: a color's OKLCH chroma as a fraction of the most sRGB allows at its hue and brightness, so every value from 0 (grey) to 1 (as saturated as sRGB gets) is a color at every hue.
// No greys or pastels, and no olive or lime
theme.randomTheme({
minBrightness: 0.5,
constraints: {
chroma: { min: 0.5 },
avoid: [hueArc(95, 135)],
},
});constraints is one constraint for every color, or a list with one for each position in the palette. theme.randomTheme, Theme.random and a theme built with nColors draw the color at position i within constraints[i]; theme.randomFrom keeps its seed at position 0 as it is and draws the rest from position 1; theme.pushRandomColor draws within the constraint for the position it adds; and theme.rotateRandomColor within the constraint for the position it replaces, the oldest (activePalette.ageOrder[0]). So a palette built from a few picks can roll one color at a time and keep each color near its pick. The same goes for the ColorPalette methods they call.
const theme = new Theme({
colors: ['#e8b450', '#7a1a2b', '#2b8f6b'],
mode: 'oklch',
});
const nearEachPick = theme.activePaletteHexes.map((hex) => {
const { hue, chroma } = measureColor(hex);
return {
hues: hue === null ? [] : [{ center: hue, width: 24 }],
chroma: { min: chroma - 0.2, max: chroma + 0.2 },
};
});
// Replace the oldest color with one near the pick in its position
theme.rotateRandomColor({ constraints: nearEachPick });randomColor
function randomColor(options?: RandomColorOptions): Color;Draws one random color, at full precision. With a constraint (even {}) it is drawn in OKLCH within it; without one, in HSV. Each candidate takes three calls to random, in the order the random methods make them, so a seeded random gives the same colors every time.
awayFrom keeps the color at least deltaEThreshold (CIEDE2000, 20 by default) from each of the colors given, as the random methods keep a new color from its neighbours: candidates are drawn until one is far enough, up to 100 (fewer with a candidateBudget, see randomLike), and otherwise the one furthest from its nearest is used. The constraint always holds; the distance is best effort.
const color = randomColor({
constraint: {
hues: [hueArc(20, 60)],
chroma: { min: 0.7 },
},
minBrightness: 0.5, // Defaults to 0
awayFrom: ['#7a1a2b', '#e8b450'], // Defaults to []
deltaEThreshold: 20, // Defaults to 20
random: seededRandom(42), // Defaults to Math.random
});hueArc
function hueArc(from: number, to: number): HueRange;The arc of hue running up from one hue to another, wrapping through 360: hueArc(330, 30) is the 60° of reds from 330° through 0° to 30°, and hueArc(30, 330) the 300° from 30° round to 330°. From a hue to itself is that one hue; a span of 360° or more is every hue.
meetsConstraint
function meetsConstraint(
color: ColorInput,
constraint: ColorConstraint,
options?: { minBrightness?: number; tolerance?: number },
): boolean;Whether a color meets a constraint: its relative chroma within the range and its hue on the hues allowed (a grey, with no hue, passes the hue check). tolerance is the slack at each bound, in relative chroma (0.005 by default). A hue counts as allowed if turning it onto the allowed hues would move the color no further than that, so a less saturated color has more slack in hue. Colors rounded to 8 bits can need more.
measureColor
function measureColor(
color: ColorInput,
options?: { minBrightness?: number },
): ColorMeasure;A color's OKLCH hue (null for greys, white and black), relative chroma and brightness (HSV value). With minBrightness, a darker color is first lifted to it by scaling its channels together, the lift theme.randomFrom gives its seed, so the color is measured as it shows at that floor.
measureColor('#e8b450'); // { hue: 81.18…, chroma: 0.819…, brightness: 0.909… }
measureColor('#808080'); // { hue: null, chroma: 0, brightness: 0.501… }colorFromHue
function colorFromHue(target: {
hue: number;
brightness: number;
chroma: number;
}): Color;The color at an OKLCH hue, brightness and relative chroma, at full precision and in gamut: measureColor gives the three back. Chroma 1 is the most saturated color at that hue and brightness, and chroma 0 the grey of that brightness.
maxChroma
function maxChroma(hue: number, brightness: number): number;The OKLCH chroma of the most saturated sRGB color at a hue and brightness: the one relative chroma 1 stands for. It always lies on an edge of the sRGB cube, where one channel is the brightness and another is 0. Just past blue's hue, from 264.05° to 264.21°, the edge passes three times, and the most saturated of the three counts, so #0000ff measures relative chroma 1. The colors of one brightness there have no chroma between the first two crossings, and relative chroma leaves that gap out, so every value from 0 to 1 is still a color.
Theme relationships
A theme's colors relate to one another: a family of oranges, gold against oxblood, violet and orange bridged by magenta. analyzeTheme finds that relationship, a template turns it into a slot for each color, and colors drawn within the slots keep it: near the theme's own colors (adjacentConstraints), or in the same shape at another hue (randomLike). Try them in the playground's Shape group.
analyzeTheme
function analyzeTheme(
colors: readonly ColorInput[],
options?: AnalyzeOptions,
): ThemeAnalysis;Measures each color (see measureColor), lifted to minBrightness so it is measured as it shows. Vivid colors (relative chroma 0.75 or more) define the hue structure, and hues less than 30° apart chain into a group. Muted tints and tones follow the nearest vivid color within 45°, or form groups of their own, and greys, whites and blacks (under 0.12) take no part. The relationship follows from the groups:
- neutral: none, every color a grey, white or black.
- family: one (or three packed into less than 90°).
- accent: two, less than 90° apart.
- contrast: two, 90° or more apart.
- bridge: three within half the wheel, spanning 90° or more.
- pair-accent: three spread wider, two of them less than 90° apart.
- triad: three, every gap 90° or more.
- spectrum: four or more.
The anchor is the center of the heaviest group. The thresholds are options, and the result (a ThemeAnalysis) is plain JSON. It depends on the colors, not their order.
const analysis = analyzeTheme(
['#e8b450', '#f4dca8', '#7a1a2b', '#b8862f', '#2b1a12'],
{ minBrightness: 0.537 },
);
analysis.relationship; // 'accent': gold against oxblood, 63° apart
analysis.anchor; // 79.2…: the hue of the gold group
analysis.picks[4].role; // 'muted': the umber follows the oxbloodrelationshipTemplate
function relationshipTemplate(
analysis: ThemeAnalysis,
options?: TemplateOptions,
): RelationshipTemplate;
function relationshipTemplate(
kind: Exclude<Relationship, 'neutral'>,
options: TemplateOptions & {
nColors: number;
random?: RandomFunction;
tints?: number;
},
): RelationshipTemplate;From an analysis, a slot for each color, in order: its role, its offset from the anchor, an arc of hue around it and a band of relative chroma that keeps it in its role. The arc reaches hueWidth (12° by default) plus a quarter of its group's span either side, up to maxHueWidth (20°). The slots are then narrowed where they have to be, so that no palette drawn within them changes the relationship or the number of groups: groups are never drawn close enough to merge, nor a group's colors far enough apart to split, nor a muted color far from its group, and where the theme sits near a threshold, no group can move across it (narrowing the chroma too, where the colors' chroma alone could move it). That holds for colors as drawn: rounded to 8 bits, a hue can move by a degree or so, and more for a muted color.
With nColors, it grows a few picks into more slots. The extra slots go to the picks by relative chroma (neutral picks get none), each in a run after its pick's own slot, pickSlots[j], and each copies it.
From a kind ('triad', 'contrast' and so on) and nColors, it draws a template of that kind: its geometry within documented ranges, the slots in hue order so the gradient runs through the relationship, and tints muted slots that follow the groups.
templateConstraints
function templateConstraints(
template: RelationshipTemplate,
anchor: number,
options?: { mirrored?: boolean; avoid?: readonly HueRange[] },
): ColorConstraint[];A constraint for each slot, with the template placed at an anchor hue: each slot's arc centered on anchor + offset, or anchor − offset when mirrored. Neutral slots never rotate or mirror. avoid is added to every slot that does.
adjacentConstraints
function adjacentConstraints(
analysis: ThemeAnalysis,
options?: TemplateOptions,
): ColorConstraint[];The template at the theme's own anchor: the neighbourhood of each color. A color drawn within constraints[i] stays near color i, in its role, so rolling one color at a time within them keeps the theme near its colors:
const nearEachColor = adjacentConstraints(
analyzeTheme(theme.activePaletteHexes, { minBrightness: 0.5 }),
);
// Replace the oldest color with one near the color in its position
theme.rotateRandomColor({
minBrightness: 0.5,
constraints: nearEachColor,
});randomLike
function randomLike(
source: ThemeAnalysis | RelationshipTemplate,
options?: RandomLikeOptions,
): RandomLikeResult;A palette in the same relationship at another anchor hue, optionally mirrored: a contrast pair stays a contrast pair, a bridge a bridge. Each attempt draws the anchor, whether to mirror, and a color per slot, each kept deltaEThreshold from the one before it and the last from the first. The palette is analyzed again, as drawn and rounded to 8 bits, and redrawn until it matches (up to attempts); otherwise the closest comes back with verified: false. The anchor option fixes the anchor or gives the arcs to draw it from, and with avoid no slot is centered in the hues avoided. Keep the constraints it returns to roll single colors in the new shape.
Each color is kept deltaEThreshold from its neighbours by drawing candidates until one is far enough, up to 100. Where the slots are too narrow for that (two groups a few degrees wide at a high minBrightness, say), every color pays all 100 candidates, 300 calls to random, and randomLike pays that for each color of each attempt: up to 33,680 calls for 8 colors. candidateBudget bounds the whole call: each attempt may use half of what is left of it, shared by its colors, and a color that runs out keeps the furthest candidate it found. At 1000, 8 colors take at most 3,416 calls.
The bound costs some spacing where the threshold is hard to reach. At 1000, the first attempt of up to 5 colors is the one drawn without a budget, but 8 colors share 500 in it, about 72 candidates for each color after the first, so a few that would have reached the threshold within 100 do not. And since each attempt may use half of what is left, a palette verified only after several attempts may keep neighbours close: in the costliest case known (olive and green grown to 8 colors at a minBrightness of 0.8), 1 or 2 palettes in 100 have neighbours within 5 ΔE of each other, which none has without a budget.
The random palette methods take it too, shared by the colors they draw, and so does randomColor, though a single color never looks at more than 100 candidates: from 99, a budget changes one only where no distance can be measured. Left out, every draw is as in 4.2; given, a color that stops short leaves random elsewhere, so a seeded random draws differently after it.
const anchor = analysis.anchor ?? 0;
const { colors, constraints } = randomLike(analysis, {
minBrightness: 0.5,
// 30° to 90° away, either way round
anchor: [
hueArc(anchor + 30, anchor + 90),
hueArc(anchor - 90, anchor - 30),
],
// no slot centered in olive or lime
avoid: [hueArc(95, 135)],
candidateBudget: 1000, // bounds the cost; left out, up to 100 candidates a color
});
theme.setColors(colors);
theme.rotateRandomColor({ minBrightness: 0.5, constraints });
// With no theme to start from
theme.setColors(
randomLike(relationshipTemplate('triad', { nColors: 5 })).colors,
);Types
The types of colormotion's API, generated from the source with their documentation. Every type is exported from colormotion.
ThemeConfig
type ThemeConfig = InitialThemeColors & {
nSteps?: number;
mode?: InterpolationMode;
deltaEThreshold?: number;
maxNumberOfColors?: number;
random?: RandomFunction;
brightnessMode?: BrightnessMode;
};nStepsThe number of steps in the color scale. Defaults to 2048.
modeThe color interpolation mode to use between colors in the palette. Defaults to the palette option's mode if one is given, and to RGB otherwise.
deltaEThresholdThe minimum threshold for the CIEDE2000 color distance between colors in the palette. Defaults to the palette option's deltaEThreshold if one is given, and to 20 otherwise.
maxNumberOfColorsMax number of colors in the palette. Defaults to the palette option's maxNumberOfColors if one is given, and to 8 otherwise.
randomRandom number generator used when generating random colors. Supply a seeded generator for reproducible palettes. Defaults to the palette option's random if one is given, and to Math.random otherwise.
brightnessModeHow brightness is applied to colors. 'darken' (the default) darkens in CIELAB space, so light and saturated colors can keep some light even at brightness 0 (white becomes #6d6d6d). 'linear' scales the RGB channels, so brightness 0 is black and 0.5 halves every channel.
InitialThemeColors
type InitialThemeColors =
| {
palette?: ColorPalette;
}
| {
colors?: ColorInput[];
}
| {
nColors?: number;
minBrightness?: number;
constraints?: ColorConstraints;
candidateBudget?: number;
};paletteThe initial color palette. Takes precedence over colors and nColors. The theme uses a copy of the palette with the theme's nSteps and any mode, maxNumberOfColors, deltaEThreshold or random option given in place of the palette's (the palette itself when none of them differs). The copy keeps the colors' positions and ages (see ColorPalette.ageOrder).
colorsThe initial colors in the palette. Their order is also their age order: rotations replace them from the first (see ColorPalette.ageOrder).
nColorsThe initial number of colors in the palette. N random colors will be generated if neither palette nor colors is provided, up to maxNumberOfColors. Defaults to 5.
minBrightnessThe minimum brightness for the random colors. Scale from 0 to 1. Defaults to 0.
constraintsLimits on the random colors' OKLCH hue and relative chroma: one constraint for every color, or a list with one for each position (see RandomPaletteConfig.constraints and ColorPalette.random). Left out, the colors are drawn in HSV, as in 4.0.
candidateBudgetThe most candidates the random colors look at beyond the first of each, while keeping each deltaEThreshold from the one before it (see RandomPaletteConfig.candidateBudget and ColorPalette.random). Left out, up to 100 candidates each, as in 4.2.
ColorUpdateConfig
interface ColorUpdateConfig {
transitionSpeed?: number;
transitionDuration?: number;
}transitionSpeedThe speed of the transition between two palettes. Should be between 0 and 1. Will be clamped to this range. Defaults to 0.1. NaN is treated as not given. The higher the value, the faster the transition: a typical palette change takes about 45 / speed ticks (0.1: about 10 s at 60 ticks a second; 0.01: about 75 s; 0.001: about 12 minutes). For an exact length, use transitionDuration. The transition ends once the colors stop changing within 0.5 CIEDE2000 of the target, averaged over a sample of the scale colors that the transition changes (single colors can be a little further off), or once 99.9999 % of the mix is done. A change to part of the wheel, such as a rotation, ends as smoothly as a whole new palette. A speed of 0 makes no progress, so the transition is treated as settled and the target palette is applied on the second tick. The same goes for a speed too small to move the colors at all (about 5e-16 or less). Ignored when transitionDuration is a finite number. An update to the palette the theme is already transitioning to does not change the transition's speed.
transitionDurationHow many ticks the transition takes, instead of a transitionSpeed. A tick is one call to tick(), whatever its n: 6 * 60 is 6 seconds when tick() is called 60 times a second.
The colors ease in and out (smoothstep) from where they are, and become exactly the target palette on the last tick. A fractional duration ends on the next whole tick (2.5 ends on tick 3), and a value within one part in a billion of a whole number counts as that number (1.1 * 50 ends on tick 55). 0 or less applies the palette before the call returns, notifying subscribers once, with isTransitioning false. Anything that is not a finite number is ignored, and transitionSpeed applies instead.
For the palette the theme is already transitioning to (including a call that leaves it unchanged, such as pushing a color onto a full palette, or one that changes only the ages of its colors, such as rotating in the color the oldest already has):
- The same duration changes nothing and keeps the current end, so an update can safely be re-sent every tick.
- A different duration re-times the transition to end that many ticks from now, continuing from the current colors without notifying subscribers. The easing starts again from rest, so re-send the same duration rather than a countdown of the ticks left.
- 0 or less finishes the transition.
FillRgbOptions
Options for Theme.fillRgb.
interface FillRgbOptions {
brightness?: number;
offset?: number;
max?: number;
}brightnessAdjusts the brightness of every color, as getColor's brightness option does: it compounds with the theme's brightness, in the theme's brightnessMode. 0-1, defaults to 1. NaN counts as 1.
offsetWhere in the array the first value goes. A whole number, 0 or more. Defaults to 0.
maxThe value a channel at full intensity is written as. Defaults to 1, for values from 0 to 1. 255 writes chroma-js's 0-255 channel values, the ones getColor(i).rgb(false) returns (unrounded). Anything that is not a finite number above 0 counts as 1.
RandomPaletteConfig
interface RandomPaletteConfig {
nColors?: number;
minBrightness?: number;
constraints?: ColorConstraints;
candidateBudget?: number;
}nColorsThe number of colors in the palette.
minBrightnessThe minimum brightness for the colors.
constraintsLimits on the random colors' OKLCH hue and relative chroma (see ColorConstraint): one constraint for every color, or a list with one for each position in the palette (a missing entry counts as {}). Each method says which position a color it draws takes. Given (even {}), the colors are drawn in OKLCH, as randomColor draws them with a constraint. Left out, they are drawn in HSV exactly as in 4.0, so a seeded random draws the colors it drew then.
candidateBudgetThe most candidates the method looks at beyond the first of each color it keeps deltaEThreshold from its neighbours (three calls to random each): shared evenly by those colors in the order they are drawn, a color passing what it does not use to the colors after it (see RandomLikeOptions.candidateBudget). A color never looks at more than 100, so a budget of 99 or more for each such color changes nothing, unless no candidate's distance is a number (a NaN minBrightness without constraints, say): the first candidate is then kept, where without a budget a 101st is drawn. Left out (or NaN), up to 100 candidates each, as in 4.2.
RandomColorConfig
interface RandomColorConfig extends Pick<
RandomPaletteConfig,
'minBrightness' | 'constraints' | 'candidateBudget'
> {}minBrightnessThe minimum brightness for the colors.
constraintsLimits on the random colors' OKLCH hue and relative chroma (see ColorConstraint): one constraint for every color, or a list with one for each position in the palette (a missing entry counts as {}). Each method says which position a color it draws takes. Given (even {}), the colors are drawn in OKLCH, as randomColor draws them with a constraint. Left out, they are drawn in HSV exactly as in 4.0, so a seeded random draws the colors it drew then.
candidateBudgetThe most candidates the method looks at beyond the first of each color it keeps deltaEThreshold from its neighbours (three calls to random each): shared evenly by those colors in the order they are drawn, a color passing what it does not use to the colors after it (see RandomLikeOptions.candidateBudget). A color never looks at more than 100, so a budget of 99 or more for each such color changes nothing, unless no candidate's distance is a number (a NaN minBrightness without constraints, say): the first candidate is then kept, where without a budget a 101st is drawn. Left out (or NaN), up to 100 candidates each, as in 4.2.
ColorConstraints
Constraints on the colors of a random palette: one for every color, or a list with one for each position in the palette, in order (a missing entry, or an entry past the end, counts as {}: any color, drawn in OKLCH).
type ColorConstraints =
| ColorConstraint
| readonly (ColorConstraint | undefined)[];ColorConstraint
Limits on the OKLCH hue and relative chroma of a random color. Brightness is set by minBrightness, as for any random color. A color drawn within a constraint always meets it: no draw is rejected.
interface ColorConstraint {
hues?: readonly HueRange[];
avoid?: readonly HueRange[];
chroma?: ChromaRange;
}huesThe hues allowed: the union of the arcs. Left out or empty (or with every range ignored): every hue.
avoidHues taken out of those allowed. Ignored when they would leave no hue, so a color can always be drawn.
chromaThe relative chroma allowed. Left out: from 0 to 1.
HueRange
An arc of OKLCH hue (as measureColor measures it), centered on a hue. It wraps through 360.
interface HueRange {
center: number;
width: number;
}centerThe hue in the middle of the arc, in degrees. Any finite number, wrapped. A center that is not a finite number makes the range ignored, unless it covers every hue.
widthThe full width of the arc, in degrees: center ± width / 2. 0 is the one hue; 360 or more (Infinity included) is every hue. A negative width counts as 0, and NaN makes the range ignored.
ChromaRange
Bounds on relative chroma (see ColorMeasure.chroma): fractions of the most chroma sRGB allows at a color's hue and brightness, so every value within them is a color at every hue and brightness. 0 is grey and 1 is as saturated as sRGB allows.
interface ChromaRange {
min?: number;
max?: number;
}minThe lowest relative chroma. Defaults to 0; clamped to 0–1; NaN counts as not given.
maxThe highest relative chroma. Defaults to 1; clamped to 0–1; NaN counts as not given. A max below min swaps with it.
RandomColorOptions
Options for randomColor.
interface RandomColorOptions {
random?: RandomFunction;
minBrightness?: number;
constraint?: ColorConstraint;
awayFrom?: readonly ColorInput[];
deltaEThreshold?: number;
candidateBudget?: number;
}randomRandom number generator. Defaults to Math.random. Supply a seeded generator for reproducible colors.
minBrightnessThe lowest brightness (HSV value) the color can have, from 0 to 1. Defaults to 0.
constraintLimits on the color's OKLCH hue and relative chroma. Given (even {}), the color is drawn in OKLCH: brightness uniform from minBrightness to 1, hue uniform over the hues allowed, relative chroma uniform within its range. Left out, it is drawn in HSV (hue, saturation, and value from minBrightness to 1), as every random method drew colors in 4.0, so a seeded random draws the colors it drew then.
awayFromColors to keep deltaEThreshold (CIEDE2000) from: candidates are drawn until one is at least that far from every one of them, up to 100 candidates (fewer with candidateBudget); if none is, the one furthest from its nearest is used. The constraint always holds; the distance is best effort.
deltaEThresholdThe distance to keep from awayFrom. Defaults to DEFAULT_DELTA_E_THRESHOLD (20).
candidateBudgetThe most candidates the search for a color deltaEThreshold from awayFrom looks at beyond its first (each takes three calls to random), so 0 takes the first candidate. The candidates are the first of those the search looks at without a budget, in the same order: a color that reaches the threshold within the budget is the same color, and otherwise the furthest candidate so far is used. A color never looks at more than 100, so a budget of 99 or more changes randomColor only where no distance can be measured: with no awayFrom, the first candidate is taken after three calls whatever deltaEThreshold is (without a budget, a NaN threshold draws all 100 and keeps the first), and when no candidate's distance is a number (a NaN minBrightness without a constraint, say), the first candidate is kept (without a budget, a 101st is drawn). Either way a seeded random draws differently after it. The budget matters for randomLike and the random palette methods, where one budget is shared by every color a call draws (see RandomLikeOptions.candidateBudget). Left out (or NaN), up to 100 candidates, as in 4.2. Below 0 counts as 0.
ColorMeasure
The measures of a color.
interface ColorMeasure {
hue: number | null;
chroma: number;
brightness: number;
}hueOKLCH hue in degrees, from 0 to 360. null for a color with no hue: an OKLCH chroma under 1e-4 (greys, white) or black.
chromaRelative chroma, from 0 to 1: the color's OKLCH chroma as a fraction of the most any sRGB color has at its hue and brightness (see maxChroma). 0 is grey and 1 is as saturated as sRGB allows there. In the 0.16° of hue just past blue's, where the colors of one brightness have no chroma between two values, the fraction leaves that gap out, so that every value from 0 to 1 is a color there too.
brightnessBrightness: HSV value, from 0 to 1 (the hottest channel), after any lift to minBrightness.
ThemeAnalysis
A theme's hue relationship, as analyzeTheme finds it. Plain JSON: numbers, strings, arrays and null, with no NaN.
interface ThemeAnalysis {
relationship: Relationship;
anchor: number | null;
groups: HueGroup[];
picks: PickAnalysis[];
neutrals: number[];
duplicates: [number, number][];
span: number;
options: Required<AnalyzeOptions>;
}anchorThe hue the theme is placed by: the center of its heaviest group (ties: the group holding the earliest color). null when every color is neutral.
groupsThe hue groups: the anchor's first, then the others in hue order from it.
picksEvery color analyzed, in order.
neutralsThe indexes of the neutral colors.
duplicatesPairs of indexes of colors closer than duplicateDeltaE, each pair in order.
spanThe arc of hue the groups occupy, in degrees: 360 less the largest gap between group centers. For a single group, its span; 0 when there is none.
optionsThe options the analysis used, defaults filled in, so templates and checks can use the same ones.
PickAnalysis
One of the analyzed colors, measured as shown (lifted to minBrightness), with its part in the theme.
interface PickAnalysis extends ColorMeasure {
index: number;
role: 'structural' | 'muted' | 'neutral';
group: number | null;
offset: number | null;
}indexIts position in the colors analyzed.
rolestructuralcolors define the hue groups,mutedones follow them, andneutralones (greys, whites and blacks) take no part.groupThe index of its group in ThemeAnalysis.groups; null for a neutral color.
offsetIts hue's signed distance from the anchor, in degrees, over −180 up to 180; null for a neutral color.
hueOKLCH hue in degrees, from 0 to 360. null for a color with no hue: an OKLCH chroma under 1e-4 (greys, white) or black.
chromaRelative chroma, from 0 to 1: the color's OKLCH chroma as a fraction of the most any sRGB color has at its hue and brightness (see maxChroma). 0 is grey and 1 is as saturated as sRGB allows there. In the 0.16° of hue just past blue's, where the colors of one brightness have no chroma between two values, the fraction leaves that gap out, so that every value from 0 to 1 is a color there too.
brightnessBrightness: HSV value, from 0 to 1 (the hottest channel), after any lift to minBrightness.
HueGroup
A group of related hues: structural colors chained less than familyJoin apart, and the muted colors that follow them (or muted colors on their own, when no structural color is near).
interface HueGroup {
center: number;
span: number;
members: number[];
weight: number;
}centerIts hue in degrees, 0 to 360: the mean of its structural members' hues, weighted by their relative chroma (for a group of muted colors on their own, the mean of theirs).
spanDegrees from its first member's hue to its last's, muted members included.
membersThe indexes of its colors, in hue order.
weightThe sum of its members' relative chroma.
Relationship
How a theme's hues relate, from the number of hue groups it has and the gaps between them (see analyzeTheme):
neutral: no hue group; every color is a grey, white or black.family: one group, or three packed into less than contrastFrom (a wide analogous run).accent: two groups, less than contrastFrom (90°) apart.contrast: two groups, contrastFrom or more apart.bridge: three groups within half the wheel, spanning contrastFrom or more: the middle one bridges the two ends.pair-accent: three groups spread wider, two of them less than contrastFrom apart.triad: three groups, every gap contrastFrom or more.spectrum: four groups or more.
type Relationship =
| 'neutral'
| 'family'
| 'accent'
| 'contrast'
| 'bridge'
| 'pair-accent'
| 'triad'
| 'spectrum';AnalyzeOptions
Options for analyzeTheme: how colors are measured, and the thresholds of the analysis. A value that is not a finite number counts as not given.
interface AnalyzeOptions {
minBrightness?: number;
neutralChroma?: number;
structuralChroma?: number;
familyJoin?: number;
attachWithin?: number;
contrastFrom?: number;
duplicateDeltaE?: number;
}minBrightnessMeasure the colors as lifted to this brightness (HSV value), as measureColor does: as they show when a theme lifts its colors to a floor. From 0 to 1; defaults to 0.
neutralChromaColors under this relative chroma are neutral: greys, whites and blacks, which take no part in the hue structure. So are colors under brightness 0.02 (black, at a minBrightness of 0). Defaults to 0.12.
structuralChromaColors at or over this relative chroma are structural: they define the hue groups. Less saturated colors are muted tints and tones that follow the groups. When no color reaches it (a theme of pastels), every color that is not neutral is structural. Defaults to 0.75.
familyJoinStructural hues closer than this, in degrees, chain into one group. Defaults to 30.
attachWithinA muted color joins the group of the nearest structural color if that is within this many degrees; muted colors no group takes form groups of their own. Defaults to 45.
contrastFromTwo groups less than this many degrees apart are an accent, and this far or further a contrast. It also bounds a bridge's span and a triad's gaps. Defaults to 90.
duplicateDeltaEPairs of colors closer than this (CIEDE2000) are reported as near-duplicates. Defaults to 3.
RelationshipTemplate
A theme's hue relationship as slots around an anchor hue (see relationshipTemplate). Plain JSON.
interface RelationshipTemplate {
relationship: Relationship;
groups: {
offset: number;
span: number;
}[];
slots: TemplateSlot[];
pickSlots: number[] | null;
options: Required<AnalyzeOptions>;
}relationshipThe relationship a palette drawn in the slots has.
groupsThe hue groups: each one's center in degrees from the anchor, and its span. The anchor's is first.
slotsThe slots, in order: a palette drawn from the template has one color per slot, in this order.
pickSlotsFor a template from an analysis: pickSlots[j] is the index of the slot that holds input color j (its own slot, first in its run). null for a template from a kind.
optionsThe analysis options, defaults filled in: randomLike checks a palette drawn from the template with them.
TemplateSlot
One color's place in a relationship template: an arc of hue relative to the anchor, and a band of relative chroma that keeps a color drawn in it in its role.
interface TemplateSlot {
role: 'structural' | 'muted' | 'neutral';
offset: number | null;
hueWidth: number;
tint: number | null;
chroma: ChromaRange;
group: number | null;
pick: number | null;
}roleThe role of the color the slot stands for (see PickAnalysis.role). Its chroma band keeps a color drawn in it in that role.
offsetThe slot's hue, in degrees from the anchor (structural and muted slots). null for a neutral slot, which never rotates or mirrors.
hueWidthThe full width of the slot's arc of hue, in degrees: half of it either side of the slot's hue (for a neutral slot, of its tint; 360 for a neutral slot with no tint).
tintA neutral slot's own tint, as an absolute OKLCH hue: it keeps the tint of a neutral color with a relative chroma of 0.04 or more, within 30° either side. null for any hue, and for the other slots.
chromaThe relative chroma allowed, both bounds given: the color's own ± chromaWidth, within its role's interval, so a color drawn in the slot keeps its role. The intervals sit a little inside the thresholds, so that a color rounded to 8 bits keeps its role too: with the defaults, 0.77–1 for structural slots, 0.15–0.72 for muted ones and 0–0.08 for neutral ones. In a theme with no color at structuralChroma (pastels, whose colors are all structural), structural slots take 0.15–0.72 as well, so none turns vivid. Narrower where a group sits near a threshold of the relationship (see relationshipTemplate).
groupThe index of the slot's group in the template's groups; null for a neutral slot.
pickThe input color whose neighbourhood this is, for a template from an analysis: its index in the colors analyzed. null for a template from a kind.
TemplateOptions
Options for relationshipTemplate and adjacentConstraints: how wide the slots are, and how many there are. A value that is not a finite number counts as not given.
interface TemplateOptions {
hueWidth?: number;
maxHueWidth?: number;
minHueWidth?: number;
chromaWidth?: number;
nColors?: number;
}hueWidthThe base half-width of a slot's arc of hue, in degrees (see relationshipTemplate). Defaults to 12 for a template from an analysis, and to 8 for one from a kind (and for randomLike, given an analysis).
maxHueWidthThe widest half-width a slot takes before its caps, in degrees. Defaults to 20.
minHueWidthThe narrowest half-width a slot takes before its caps, in degrees. Defaults to 3.
chromaWidthHow far a slot's chroma band reaches either side of its color's relative chroma. Defaults to 0.2.
nColorsFor a template from an analysis: the number of slots to lay out. Defaults to the number of colors analyzed, which is also the least it takes (see relationshipTemplate).
RandomLikeOptions
Options for randomLike.
interface RandomLikeOptions {
random?: RandomFunction;
minBrightness?: number;
deltaEThreshold?: number;
anchor?: number | HueRange | readonly HueRange[];
mirror?: boolean;
avoid?: readonly HueRange[];
attempts?: number;
nColors?: number;
candidateBudget?: number;
}randomRandom number generator. Defaults to Math.random. Supply a seeded generator for reproducible palettes.
minBrightnessThe lowest brightness (HSV value) of the colors, from 0 to 1. Defaults to the minBrightness the source was analyzed at (0 for a template from a kind).
deltaEThresholdHow far each color is kept (CIEDE2000) from the one before it, and the last from the first as well, since the wheel closes there. Best effort, as for any random color. Defaults to DEFAULT_DELTA_E_THRESHOLD (20).
anchorThe anchor: a hue to place the template at, or the arcs (one or several) to draw it from, uniformly. Left out, any hue.
mirrorAllows the mirrored geometry, drawn half the time. Defaults to true.
avoidHues to keep out: added to every rotating slot's avoid, and no drawn anchor puts a slot's hue inside them (when any anchor allowed can).
attemptsHow many palettes to draw, at most, until one has the source's relationship. Defaults to 16.
nColorsFor an analysis: the number of colors (see TemplateOptions.nColors). Ignored for a template.
candidateBudgetA bound on what keeping the colors deltaEThreshold apart costs: the most candidates the call looks at beyond the first of each color it draws (three calls to random each). Each attempt may use half of what is left of it, shared evenly by the attempt's colors in slot order, a color passing what it does not use to the colors after it; a color stops at the first candidate that reaches the threshold, as without a budget, or keeps the furthest candidate it looked at. The calls to random are then at most attempts × (2 + 3 × colors) + 3 × candidateBudget: 3,416 for 8 colors and a budget of 1000, against 33,680 without one. Left out (or NaN), each color looks at up to 100 candidates, as in 4.2: a threshold out of reach (narrow slots at a high minBrightness) then costs all 100 for every color of every attempt.
The bound costs some spacing where the threshold is hard to reach. At 1000, the first attempt of up to 5 colors is the one drawn without a budget, but 8 colors share 500 in it (about 72 candidates for each color after the first, where they need them all), so a few colors that would have reached the threshold within 100 candidates do not. Each attempt may use half of what is left, so an attempt after others that spent their share has little: the palettes verified only after several attempts are the least far apart. In the costliest case known (olive and green grown to 8 colors at a minBrightness of 0.8), 1 or 2 palettes in 100 have neighbours within 5 ΔE of each other, which none has without a budget.
RandomLikeResult
A palette randomLike drew, and where.
interface RandomLikeResult {
colors: Color[];
anchor: number;
mirrored: boolean;
constraints: ColorConstraint[];
relationship: Relationship;
verified: boolean;
}colorsOne color per slot, in the template's slot order, at full precision.
anchorThe anchor the template was placed at, in degrees.
mirroredWhether it was mirrored.
constraintsThe constraint for each color, as templateConstraints gives them for this anchor and mirroring: keep them for rolling single colors in the same shape.
relationshipThe relationship the colors have, analyzed with the source's options.
verifiedWhether the colors have the source's relationship and number of groups, both as they are and rounded to 8 bits (as hex). When no attempt did, the closest is returned, with verified false.
ThemeUpdateEvent
interface ThemeUpdateEvent {
palette: Readonly<ColorPalette>;
isTransitioning: boolean;
colors: Color[];
mode: InterpolationMode;
brightness: number;
}paletteThe active palette: the target palette while transitioning.
isTransitioningIs the theme transitioning between palettes.
colorsThe colors in the wheel of the active palette (the target palette while transitioning): the palette colors and the interpolated colors between them.
modeThe current interpolation mode.
brightnessThe current brightness factor (0-1).
ThemeUpdateCallback
type ThemeUpdateCallback = (event: ThemeUpdateEvent) => void;InterpolationMode
Supported interpolation modes.
type InterpolationMode =
| 'rgb'
| 'lab'
| 'lrgb'
| 'hsl'
| 'lch'
| 'hsv'
| 'hsi'
| 'hcl'
| 'oklab'
| 'oklch';BrightnessMode
Supported brightness modes.
type BrightnessMode = 'darken' | 'linear';darkenDarkens colors in CIELAB space, lowering the lightness by up to 54 (of 100) at brightness 0. Colors lighter than lightness 54 keep some light at brightness 0 (white becomes #6d6d6d), and many saturated colors keep a dim tint (#0000ff becomes #000069); mid and dark greys and dark, muted colors reach black. Colors are darkened as they are read, under chroma's Lab white point at the time.
linearScales the RGB channels linearly. Brightness 0 is black (LEDs off) and 0.5 halves every channel.
RandomFunction
A function returning a pseudo-random number in the range [0, 1), like Math.random. Supply your own to get reproducible palettes.
type RandomFunction = () => number;ColorPalette
An immutable ColorPalette.
class ColorPalette {
readonly colors: Color[];
readonly hexes: string[];
readonly key: string;
readonly nColors: number;
readonly mode: InterpolationMode;
readonly nSteps: number;
readonly scale: Scale;
readonly scaleColors: Color[];
readonly maxNumberOfColors: number;
readonly deltaEThreshold: number;
readonly random: RandomFunction;
static normalizeColors(colorInputs: ColorInput[]): Color[];
static clampColors(
colors: ColorInput[],
maxNumberOfColors: number,
): ColorInput[];
static random(
options: Omit<ColorPaletteConfig, 'colors' | 'normalizedColors'> &
RandomPaletteConfig,
): ColorPalette;
constructor(options: ColorPaletteConfig);
get ageOrder(): readonly number[];
newConfig(options: {
colors: ColorInput[];
mode: InterpolationMode;
nSteps: number;
maxNumberOfColors?: number;
deltaEThreshold?: number;
random?: RandomFunction;
}): ColorPalette;
newColors(colors: ColorInput[]): ColorPalette;
newMode(mode: InterpolationMode): ColorPalette;
rotateMode(): ColorPalette;
randomizeFrom(
seed: ColorInput,
options?: RandomPaletteConfig,
): ColorPalette;
randomize(options?: RandomPaletteConfig): ColorPalette;
push(color: ColorInput): ColorPalette;
pushRandom(randomColorConfig?: RandomColorConfig): ColorPalette;
popOldest(): ColorPalette;
rotateOn(color: ColorInput): ColorPalette;
rotateRandomOn(options?: RandomColorConfig): ColorPalette;
}scalechroma-js's scale over the palette colors. The palette's own colors (scaleColors, and so a Theme's) follow chroma's interpolation except near hue-less ends, where colormotion's mix reaches the end and chroma's stops short: toward black in LCH, HCL and OKLCH, and toward white in HSI. And in OKLCH, where chroma's mix reads a translucent color's alpha as its hue, they mix translucent colors as the opaque ones, with alpha mixed on its own. The scale mixes when it is called, under chroma's Lab white point at the time.
scaleColorsThe colors of the wheel, one per step, sampled from the scale once, when the palette is built. Mixing in lab, lch, hcl, oklab and oklch depends on chroma's Lab white point (see chroma.setLabWhitePoint), and these keep the one set then: changing it later does not change them. newColors, newMode and newConfig return this same palette when its colors, mode and settings would not change, and build any other palette they return under the white point set at the time.
static normalizeColorsNormalizes the input colors to chroma-js colors.
static randomReturns a new random palette with the specified number of colors. Defaults to 5 colors. Each color after the first is drawn at least deltaEThreshold (CIEDE2000) from the one before it. The color at position i meets constraints[i] (or the one constraint given for every color). With a candidateBudget, the colors after the first share it (see RandomPaletteConfig.candidateBudget).
ageOrderThe positions of the palette's colors (indexes into colors and hexes), oldest first: the order in which rotations replace them. rotateOn and rotateRandomOn replace the color at ageOrder[0] in its position, which moves to the end; popOldest drops the color at ageOrder[0]; push adds its new position at the end. So the next rotations replace ageOrder[0], ageOrder[1] and so on, in turn.
A palette built from a list (the constructor, newColors, newConfig with other colors, randomize, randomizeFrom and ColorPalette.random) takes the list order: the first color is the oldest, so its rotations go first in, first out. newMode, rotateMode and newConfig with the same colors keep the ages, and so does any method that gives back this palette. The array is frozen.
newConfigCreates a new palette with the specified configuration. Options not provided are inherited from this palette. The same colors keep their positions and ages (a lower maxNumberOfColors keeps those of the colors it keeps); other colors take their list order as their age order, as in newColors.
newColorsGets a new palette with new colors but the same mode. The list order is the new colors' age order: the first is the oldest (see ageOrder), so new colors start the rotation order again from the first. The same colors, or a longer list whose first colors are the ones this palette keeps, give back this palette, ages included.
newModeGets a new palette with the same colors, in the same positions and with the same ages, but a different mode.
rotateModeGets a new palette with the same colors and ages but the next mode.
randomizeFromRandomizes the palette starting with the input color. Maintains the number of colors in the palette. Each random color is drawn at least deltaEThreshold (CIEDE2000) from the one before it.
seed: The color to start with, at position 0. It is kept as it is (it meets no constraint), lifted to minBrightness if it is darker.options: Options for randomizing the palette. The random color at position i (from 1) meets constraints[i] (or the one constraint given for every color). With a candidateBudget, the random colors share it (see RandomPaletteConfig.candidateBudget).
Returns: A new palette with randomized colors.
randomizeRandomizes the whole palette. Maintains the number of colors in the palette. Draws the first color, then the others as randomizeFrom does. The color at position i meets constraints[i] (or the one constraint given for every color). With a candidateBudget, the colors after the first share it, as in randomizeFrom.
pushAdds the color at the end, as the newest (the last position in ageOrder). Gives back this palette if it already has maxNumberOfColors colors.
pushRandomAdds a random color at the end, as push does, drawn at least deltaEThreshold (CIEDE2000) from the current last color. The new color meets constraints[nColors], the constraint for the position it takes (or the one constraint given for every color). With a candidateBudget, no more than 1 + candidateBudget candidates are looked at (never more than 100).
popOldestDrops the oldest color (the one at ageOrder[0]). The others keep their order and their ages, closing up the gap. Gives back this palette if it has only one color.
rotateOnReplaces the oldest color (the one at ageOrder[0]) with the color, in its position, and makes the new color the newest. Every other color keeps its position, so the scale changes only between the replaced color's two neighbours around the wheel (for the first color, the last color and the second). Successive rotations of a palette built from a list replace its colors in list order: first in, first out. Replacing the oldest color with the same color still makes it the newest; only a single color replaced by itself gives back this palette.
To shift the colors instead, as rotateOn did before 4.0 (dropping the first color and appending the new one, so every color moves one position): newColors([...hexes.slice(1, -1), color]).
rotateRandomOnReplaces the oldest color with a random color, as rotateOn does. The new color is drawn at least deltaEThreshold (CIEDE2000) from both of the colors it will sit between around the wheel: in a palette of two, the other color, and in a palette of one, the color it replaces. If no candidate qualifies within a bounded number of draws, the one furthest from the nearer of them is used. The new color meets constraints[ageOrder[0]], the constraint for the position it replaces (or the one constraint given for every color), so the colors a palette of picks rotates in stay near each pick. With a candidateBudget, no more than 1 + candidateBudget candidates are looked at (never more than 100).
To shift the colors instead, as rotateRandomOn did before 4.0 (dropping the first color and appending a random color drawn deltaEThreshold from the last): popOldest().pushRandom(options). With the same random, it draws the same colors as before 4.0, for a palette of two or more colors whose ages are in list order (as they are when its colors are only ever set from lists, pushed or popped). A single color gains a second.
ColorPaletteConfig
type ColorPaletteConfig = ColorPaletteColors & {
mode: InterpolationMode;
nSteps: number;
deltaEThreshold?: number;
maxNumberOfColors?: number;
random?: RandomFunction;
};modeThe interpolation mode to use between colors in the palette.
nStepsThe number of steps in the color scale.
deltaEThresholdThe minimum threshold for the CIEDE2000 color distance between colors in the palette. Defaults to 20.
maxNumberOfColorsMax number of colors in the palette. Defaults to 8.
randomRandom number generator used when generating random colors. Defaults to Math.random.
ColorPaletteColors
Exactly one of colors or normalizedColors must be provided.
type ColorPaletteColors =
| {
colors: ColorInput[];
normalizedColors?: never;
}
| {
colors?: never;
normalizedColors: Color[];
};colorsThe colors to use in the palette. Their order is also their age order: the first is the oldest (see ColorPalette.ageOrder).
normalizedColorsThe normalized colors to use in the palette. Don't use this unless you know what you're doing. As with colors, their order is their age order.