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.
Installation
Install the colormotion
npm module using your favorite package manager:
npm install colormotion
# pnpm install colormotion
# yarn add colormotion
Quick 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
colormotion
leverages the scales feature of chroma.js
under the hood for interpolation.
Theme
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, simple 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,
});
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.
const palette = new ColorPalette({
colors: ['red', 'green', 'blue'],
mode: 'rgb',
nSteps: 10,
});
const theme = new Theme({
palette,
});
The 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 rgb
.
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 20.
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 adding new colors to the palette or generating a random palette. This defaults to 8.
const theme1 = new Theme({
nColors: 5,
maxNumberOfColors: 12,
});
theme.activePalette
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
Returns the hex values of the active ColorPalette
of the Theme
. If the Theme
is currently transitioning between palettes, this will return the hex values of the palette that the theme is transitioning to.
const hexes = theme.activePaletteHexes;
theme.brightness
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.getColor
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.update
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.
theme.update({
colors: ['red', 'green', 'blue'],
mode: 'hsv',
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.setMode
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
. Same rules for the transitionSpeed
apply as before.
theme.setMode('lab');
theme.setMode('lab', {
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.rotateMode
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
. Same rules for the transitionSpeed
apply as before.
theme.rotateMode();
theme.rotateMode({
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.setColors
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
. Same rules for the transitionSpeed
apply as before.
theme.setColors(['red', 'green', 'blue']);
theme.setColors(['red', 'green', 'blue'], {
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.randomFrom
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
. Same rules for the transitionSpeed
apply as before.
You can explicitly set a minBrightness
for the new random colors. Expects a value between 0 and 1. Defaults to 0.
theme.randomFrom('red');
theme.randomFrom('red', {
minBrightness: 0.5, // Defaults to 0
nColors: 5, // Defaults to the number of colors in the current palette
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.randomTheme
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
. Same rules for the transitionSpeed
apply as before.
You can explicitly set a minBrightness
for the new random colors. Expects a value between 0 and 1. Defaults to 0.
theme.randomTheme();
theme.randomTheme({
minBrightness: 0.5, // Defaults to 0
nColors: 5, // Defaults to the number of colors in the current palette
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.pushNewColor
Pushes a new color to the Theme
palette. 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
. Same rules for the transitionSpeed
apply as before.
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
Pushes a new random color to the Theme
palette. 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
. Same rules for the transitionSpeed
apply as before.
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.
theme.pushRandomColor();
theme.pushRandomColor({
minBrightness: 0.5, // Defaults to 0
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.popOldestColor
Drops the oldest color from the Theme
palette. 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
. Same rules for the transitionSpeed
apply as before.
theme.popOldestColor();
theme.popOldestColor({
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.rotateColor
Drops the oldest color from the Theme
palette and adds the new 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
. Same rules for the transitionSpeed
apply as before.
theme.rotateColor('red');
theme.rotateColor('red', {
transitionSpeed: 0.5, // Defaults to 0.1
});
theme.rotateRandomColor
Drops the oldest color from the Theme
palette and adds a random 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
. Same rules for the transitionSpeed
apply as before.
You can explicitly set a minBrightness
for the new random color. Expects a value between 0 and 1. Defaults to 0.
theme.rotateRandomColor();
theme.rotateRandomColor({
minBrightness: 0.5, // Defaults to 0
transitionSpeed: 0.5, // Defaults to 1
});
theme.tick
Arguably the most important method. This advances the color index by a given number of frames, and updates the current palette towards 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. This does not affect the transition speed between palettes.
theme.tick();
theme.tick(10);
theme.subscribe
Subscribe to updates to the Theme
. The callback will be called whenever the target palette of the Theme
is updated and whenever the Theme
reaches the target palette.
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 an example of the actual React hook being used in the demo above to show the currently selected interpolation mode:
import { ThemeUpdateCallback, ThemeUpdateEvent } from 'colormotion';
import { useEffect, useState } from 'react';
import { theme } from '~/components/theme/theme';
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 a given callback from Theme
updates.
theme.unsubscribe(myCallback);