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.
Install
npm install palette-shaderNo 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) |
isPolar |
boolean |
true |
true = circular wheel, false = rectangular slice |
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 = 'okhsl';
viz.distanceMetric = 'deltaE2000';
viz.isPolar = false;
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); // prependremoveColor(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 wheel or slice is rendered in.
| Value | Description |
|---|---|
'okhsv' |
Default. Hue–Saturation–Value built on OKLab. Gamut-aware hue wheel with perceptually uniform saturation steps. |
'okhsl' |
Hue–Saturation–Lightness built on OKLab. Better lightness uniformity across hues. |
'oklch' |
OKLab in cylindrical form (Lightness, Chroma, Hue). Ideal for chroma or lightness slices. |
'hsv' |
Classic HSV. Not perceptually uniform — hue jumps are uneven — but familiar and fast. |
'hsl' |
Classic HSL. Same caveats as 'hsv'. |
The OK-variants rely on Björn Ottosson's gamut-aware implementation. They produce significantly more even hue distributions than the classic variants, at the same GPU cost.
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: 'okhsv' }),
new PaletteViz({ ...shared, axis: 'y', colorModel: 'okhsl' }),
new PaletteViz({ ...shared, axis: 'z', colorModel: 'oklch' }),
];
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 checkThe 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