Package Exports
- facette
Readme
Facette
Perceptual color palette generation. Give it a few seed colors and a target size, and it produces a palette where every color is visually distinct and belongs to the same chromatic family.
Zero dependencies. Works in Node, browsers, and edge runtimes.
Installation
npm install facetteUsage
import { generatePalette } from 'facette';
const result = generatePalette(
['#e63946', '#457b9d', '#1d3557'], // seed colors
8 // palette size
);
console.log(result.colors);
// ['#e63946', '#457b9d', '#1d3557', '#7b2d3e', '#2e6a85', ...]Options
const result = generatePalette(seeds, 8, {
vividness: 0.06, // 0 = auto (default), range [0.005, 0.10]
gamma: 1.5, // chroma preservation strength, >= 1 (default: 1)
});vividness— controls how aggressively the algorithm avoids low-chroma colors. Higher values push the palette toward more saturated colors. At0(default), it adapts automatically based on how vivid your seeds are.gamma— controls chroma preservation on intermediate colors between vivid seeds. At1(default), the algorithm uses standard gray avoidance. Values above1(e.g.1.5-2) produce stronger outward bowing of the hull surface in OKLab, keeping midpoint colors more vivid.
Stepper API
For inspecting the optimization process frame by frame:
import { createPaletteStepper } from 'facette';
const stepper = createPaletteStepper(['#e63946', '#457b9d', '#1d3557'], 8);
// Step through the optimization frame by frame
for (const frame of stepper.frames()) {
console.log(`Iteration ${frame.iteration}: energy=${frame.energy.toFixed(4)}, minDeltaE=${frame.minDeltaE.toFixed(4)}`);
}
// Or get everything at once
const trace = stepper.run();
console.log(trace.finalColors); // hex strings
console.log(trace.frames.length); // number of iterations
console.log(trace.geometry.kind); // 'line' or 'hull'API Reference
generatePalette(seeds, size, options?)
| Parameter | Type | Description |
|---|---|---|
seeds |
string[] |
Hex colors (e.g. ['#ff0000', '#0000ff']). Minimum 2, must be distinct. |
size |
number |
Total palette size including seeds. Must be >= seed count. |
options.vividness |
number |
Gray avoidance strength. 0 = auto. Range [0.005, 0.10]. |
options.gamma |
number |
Chroma preservation strength. Default 1. Must be >= 1. |
Returns PaletteResult:
{
colors: string[]; // hex sRGB colors
seeds: string[]; // input seeds echoed back
metadata: {
minDeltaE: number; // minimum pairwise perceptual distance
iterations: number; // optimization steps taken
clippedCount: number; // colors that needed gamut clipping
};
}createPaletteStepper(seeds, size, options?)
Same parameters as generatePalette. Returns a PaletteStepper:
{
geometry: Geometry; // hull or line segment topology
seeds: Particle[]; // classified seed particles
frames(): Generator<OptimizationFrame>; // iterate frame by frame
run(): OptimizationTrace; // run to completion
}How it works
Facette treats palette generation as a physics simulation in a radially lifted OKLab color space:
- Radial lift — a convex transform contracts the low-chroma region, making gray positions scarce, while anchoring vivid seeds as fixed points
- Convex hull — the hull of lifted seeds defines the palette's chromatic family
- Particle repulsion — free particles on the hull surface repel each other via Riesz energy until they reach maximum separation
- Inverse lift — final positions are mapped back to OKLab and clipped to sRGB
The algorithm handles everything automatically: 2 seeds produce a gradient, 3+ seeds define a surface, and the convex hull geometry adapts to any configuration.
License
MIT