JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q93896F
  • License MIT

Dependency-free WebGL2 shader that maps any colour palette across perceptual colour spaces — OKHsv, OKHsl, OKLCH and more.

Package Exports

  • palette-shader

Readme

palette-shader

A dependency-free WebGL2 shader that maps any colour palette across a 3-D perceptual colour space and snaps each pixel to the nearest palette colour. Visualise how a palette distributes across HSV, HSL, LCH or their perceptual OK-variants, and compare results across six colour-distance metrics — all on the GPU.

Live demo →


What is this for?

It shows you how a color palette distributes across "all possible colors." Each region of the wheel or grid represents a color — and whichever palette color is closest to it claims that region.

So if one of your palette colors only claims a tiny sliver, it lives very close to another color already in your palette — it's almost redundant. If it claims a large region, it's doing a lot of unique work. At a glance you can tell:

  • How distinct each color is from the others
  • How balanced the palette is overall — even regions mean even coverage
  • Whether a new color is worth adding — if it doesn't carve out its own space, it's probably not pulling its weight

Install

npm install palette-shader

No dependencies — only a browser with WebGL support is required.


Quick start

import { PaletteViz } from 'palette-shader';

// option A — pass a container, canvas is appended automatically
const viz = new PaletteViz({
  palette: ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'],
  container: document.querySelector('#app'),
  width: 512,
  height: 512,
});

// option B — no container, place the canvas yourself
const viz = new PaletteViz({ palette: ['#264653', '#2a9d8f', '#e9c46a'] });
document.querySelector('#app').appendChild(viz.canvas);

Constructor

new PaletteViz(options?: PaletteVizOptions)

All options are optional. The palette defaults to a random 20-colour set.

Option Type Default Description
palette string[] random CSS colour strings (#hex, rgb(), hsl(), …)
container HTMLElement undefined Element the canvas is appended to. Omit and use viz.canvas to place it yourself
width number 512 Canvas width in CSS pixels
height number 512 Canvas height in CSS pixels
pixelRatio number devicePixelRatio Renderer pixel ratio
colorModel string 'okhsv' Colour space for the visualisation (see Colour models)
distanceMetric string 'oklab' Distance function for nearest-colour matching (see Distance metrics)
axis 'x' | 'y' | 'z' 'y' Which axis the position value controls
position number 0 0–1 position along the chosen axis
invertLightness boolean false Flip the lightness/value axis
showRaw boolean false Bypass nearest-colour matching (shows the raw colour space)

Properties

Every constructor option is also a live setter/getter. Assigning any of them re-renders immediately via requestAnimationFrame.

viz.palette = ['#ff0000', '#00ff00', '#0000ff'];
viz.position = 0.5;
viz.colorModel = 'okhslPolar';
viz.distanceMetric = 'deltaE2000';
viz.invertLightness = true;
viz.showRaw = true;

Additional read-only properties:

Property Type Description
canvas HTMLCanvasElement The underlying canvas element
width number Current width in CSS pixels
height number Current height in CSS pixels

Methods

resize(width, height?)

Resize the canvas. If height is omitted the canvas stays square.

window.addEventListener('resize', () => viz.resize(window.innerWidth * 0.5));

setColor(color, index)

Update a single palette entry without rebuilding the whole texture.

viz.setColor('#e63946', 2);

addColor(color, index?)

Insert a colour at index (appends if omitted).

viz.addColor('#a8dadc');       // append
viz.addColor('#457b9d', 0);   // prepend

removeColor(index | color)

Remove a palette entry by index or by colour string.

viz.removeColor(0);
viz.removeColor('#a8dadc');

destroy()

Cancel the animation frame, release all WebGL resources (program, texture, buffer, VAO), and remove the canvas from the DOM.


Colour models

Controls the 3-D colour space the visualisation is rendered in. Polar variants (*Polar) map hue to angle and show a circular wheel; non-polar variants show a rectangular slice.

Value Shape Description
'okhsv' cube Default. Hue–Saturation–Value built on OKLab. Gamut-aware with perceptually uniform saturation steps.
'okhsvPolar' wheel Polar (cylindrical) form of OKHsv.
'okhsl' cube Hue–Saturation–Lightness built on OKLab. Better lightness uniformity across hues.
'okhslPolar' wheel Polar form of OKHsl.
'oklch' cube OKLab in cylindrical coordinates (L, C, h). Ideal for chroma or lightness slices.
'oklchPolar' wheel Polar form of OKLch.
'hsv' cube Classic HSV. Not perceptually uniform, but familiar and fast.
'hsvPolar' wheel Polar form of HSV.
'hsl' cube Classic HSL. Same caveats as 'hsv'.
'hslPolar' wheel Polar form of HSL.
'oklab' cube Raw OKLab cube: x→a, y→b, z→L. Cube only — no polar variant.
'rgb' cube Raw sRGB cube. Useful as a baseline. Cube only — no polar variant.

The OK-variants rely on Björn Ottosson's gamut-aware implementation and produce significantly more even hue distributions than the classic variants at the same GPU cost.

Cube vs. polar — which to use?

Both shapes render the same underlying colour space; they just arrange it differently on screen.

Cube (rectangular slice) lays the three axes out as a flat grid. One axis is fixed by the position slider, the other two fill the canvas. This makes it easy to read absolute values — you can see exactly where on the hue, saturation and lightness axes each palette colour falls, and compare palettes side-by-side without any projection distortion.

Polar (wheel) wraps the hue axis around a circle. Hue runs around the circumference, saturation (or chroma) runs outward from the centre, and the third axis is controlled by position. This matches the intuition most designers have for colour — it's immediately obvious whether two colours are complementary, analogous or triadic. Voronoi regions that are nearly circular indicate a well-balanced palette; lopsided regions reveal hue bias.

A practical starting point: use a polar model to get an intuitive read on hue distribution and harmony, then switch to a cube slice to inspect individual lightness or saturation bands in detail. rgb and oklab have no polar variant because they aren't hue-based cylindrical spaces.


Distance metrics

Controls how "nearest palette colour" is determined per pixel.

Value Description Cost
'oklab' Default. Euclidean distance in OKLab. Fast, perceptually uniform, excellent general-purpose choice. low
'kotsarenkoRamos' Weighted Euclidean in sRGB — no colour-space conversion. Weights R and B by the mean red value for quick perceptual improvement over plain RGB. lowest
'deltaE76' CIE 1976: plain Euclidean distance in CIELab. Classic standard, decent uniformity. medium
'deltaE94' CIE 1994: adds chroma and hue weighting on top of ΔE76. Better than ΔE76, cheaper than ΔE2000. medium
'deltaE2000' CIEDE2000: weighted colour difference with per-channel corrections for hue, chroma, and lightness. Most accurate, most expensive. high
'rgb' Plain Euclidean in sRGB. Not perceptually uniform. Useful as a baseline. lowest

Advanced usage

Accessing the canvas

// with no container, manage placement yourself
const viz = new PaletteViz({ palette });
document.querySelector('#app').appendChild(viz.canvas);

// or style it after the fact
viz.canvas.style.borderRadius = '50%';

Multiple synchronised views

const palette = ['#264653', '#2a9d8f', '#e9c46a'];
const shared = { palette, width: 256, height: 256, container: document.querySelector('#views') };

const views = [
  new PaletteViz({ ...shared, axis: 'x', colorModel: 'okhslPolar' }),
  new PaletteViz({ ...shared, axis: 'y', colorModel: 'okhslPolar' }),
  new PaletteViz({ ...shared, axis: 'z', colorModel: 'okhslPolar' }),
];

document.querySelector('#slider').addEventListener('input', (e) => {
  views.forEach((v) => { v.position = +e.target.value; });
});

Utility exports

import { paletteToRGBA, randomPalette, fragmentShader } from 'palette-shader';

// Get raw RGBA bytes (Uint8Array, sRGB, 4 bytes per color)
// Useful for building your own WebGL texture or processing palette data
const rgba = paletteToRGBA(['#ff0000', '#00ff00', '#0000ff']);

// Quick random palette for prototyping
const palette = randomPalette(16);

// Access the raw GLSL fragment shader string
console.log(fragmentShader);

Dependencies

None. The library uses raw WebGL 2 and the browser's native CSS color parser. No runtime dependencies.

Browser support

Requires WebGL 2 (supported in all modern browsers and most mobile devices since ~2017). Use canvas.getContext('webgl2') availability to feature-detect if needed.


Development

git clone https://github.com/meodai/color-palette-shader.git
cd color-palette-shader
npm install

npm run dev        # start demo dev server → http://localhost:5173
npm run build      # build library → dist/
npm run typecheck  # TypeScript type check

The demo lives in demo/ and is a private workspace package. It resolves the library from src/ via a Vite alias so changes to the library are reflected immediately without a build step.


License

MIT © David Aerne