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.

GitHub repotests workflow statusnpm package minimized gzipped size

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 Themepalette, 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);