palette-shader 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # palette-shader
2
+
3
+ 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.
4
+
5
+ [**Live demo →**](https://meodai.github.io/color-palette-shader/)
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install palette-shader
13
+ ```
14
+
15
+ No dependencies — only a browser with WebGL support is required.
16
+
17
+ ---
18
+
19
+ ## Quick start
20
+
21
+ ```js
22
+ import { PaletteViz } from 'palette-shader';
23
+
24
+ // option A — pass a container, canvas is appended automatically
25
+ const viz = new PaletteViz({
26
+ palette: ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'],
27
+ container: document.querySelector('#app'),
28
+ width: 512,
29
+ height: 512,
30
+ });
31
+
32
+ // option B — no container, place the canvas yourself
33
+ const viz = new PaletteViz({ palette: ['#264653', '#2a9d8f', '#e9c46a'] });
34
+ document.querySelector('#app').appendChild(viz.canvas);
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Constructor
40
+
41
+ ```ts
42
+ new PaletteViz(options?: PaletteVizOptions)
43
+ ```
44
+
45
+ All options are optional. The palette defaults to a random 20-colour set.
46
+
47
+ | Option | Type | Default | Description |
48
+ |---|---|---|---|
49
+ | `palette` | `string[]` | random | CSS colour strings (`#hex`, `rgb()`, `hsl()`, …) |
50
+ | `container` | `HTMLElement` | `undefined` | Element the canvas is appended to. Omit and use `viz.canvas` to place it yourself |
51
+ | `width` | `number` | `512` | Canvas width in CSS pixels |
52
+ | `height` | `number` | `512` | Canvas height in CSS pixels |
53
+ | `pixelRatio` | `number` | `devicePixelRatio` | Renderer pixel ratio |
54
+ | `colorModel` | `string` | `'okhsv'` | Colour space for the visualisation (see [Colour models](#colour-models)) |
55
+ | `distanceMetric` | `string` | `'oklab'` | Distance function for nearest-colour matching (see [Distance metrics](#distance-metrics)) |
56
+ | `isPolar` | `boolean` | `true` | `true` = circular wheel, `false` = rectangular slice |
57
+ | `axis` | `'x' \| 'y' \| 'z'` | `'y'` | Which axis the `position` value controls |
58
+ | `position` | `number` | `0` | 0–1 position along the chosen axis |
59
+ | `invertLightness` | `boolean` | `false` | Flip the lightness/value axis |
60
+ | `showRaw` | `boolean` | `false` | Bypass nearest-colour matching (shows the raw colour space) |
61
+
62
+ ---
63
+
64
+ ## Properties
65
+
66
+ Every constructor option is also a live setter/getter. Assigning any of them re-renders immediately via `requestAnimationFrame`.
67
+
68
+ ```js
69
+ viz.palette = ['#ff0000', '#00ff00', '#0000ff'];
70
+ viz.position = 0.5;
71
+ viz.colorModel = 'okhsl';
72
+ viz.distanceMetric = 'deltaE2000';
73
+ viz.isPolar = false;
74
+ viz.invertLightness = true;
75
+ viz.showRaw = true;
76
+ ```
77
+
78
+ Additional read-only properties:
79
+
80
+ | Property | Type | Description |
81
+ |---|---|---|
82
+ | `canvas` | `HTMLCanvasElement` | The underlying canvas element |
83
+ | `width` | `number` | Current width in CSS pixels |
84
+ | `height` | `number` | Current height in CSS pixels |
85
+
86
+ ---
87
+
88
+ ## Methods
89
+
90
+ ### `resize(width, height?)`
91
+
92
+ Resize the canvas. If `height` is omitted the canvas stays square.
93
+
94
+ ```js
95
+ window.addEventListener('resize', () => viz.resize(window.innerWidth * 0.5));
96
+ ```
97
+
98
+ ### `setColor(color, index)`
99
+
100
+ Update a single palette entry without rebuilding the whole texture.
101
+
102
+ ```js
103
+ viz.setColor('#e63946', 2);
104
+ ```
105
+
106
+ ### `addColor(color, index?)`
107
+
108
+ Insert a colour at `index` (appends if omitted).
109
+
110
+ ```js
111
+ viz.addColor('#a8dadc'); // append
112
+ viz.addColor('#457b9d', 0); // prepend
113
+ ```
114
+
115
+ ### `removeColor(index | color)`
116
+
117
+ Remove a palette entry by index or by colour string.
118
+
119
+ ```js
120
+ viz.removeColor(0);
121
+ viz.removeColor('#a8dadc');
122
+ ```
123
+
124
+ ### `destroy()`
125
+
126
+ Cancel the animation frame, release all WebGL resources (program, texture, buffer, VAO), and remove the canvas from the DOM.
127
+
128
+ ---
129
+
130
+ ## Colour models
131
+
132
+ Controls the 3-D colour space the wheel or slice is rendered in.
133
+
134
+ | Value | Description |
135
+ |---|---|
136
+ | `'okhsv'` | **Default.** Hue–Saturation–Value built on OKLab. Gamut-aware hue wheel with perceptually uniform saturation steps. |
137
+ | `'okhsl'` | Hue–Saturation–Lightness built on OKLab. Better lightness uniformity across hues. |
138
+ | `'oklch'` | OKLab in cylindrical form (Lightness, Chroma, Hue). Ideal for chroma or lightness slices. |
139
+ | `'hsv'` | Classic HSV. Not perceptually uniform — hue jumps are uneven — but familiar and fast. |
140
+ | `'hsl'` | Classic HSL. Same caveats as `'hsv'`. |
141
+
142
+ 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.
143
+
144
+ ---
145
+
146
+ ## Distance metrics
147
+
148
+ Controls how "nearest palette colour" is determined per pixel.
149
+
150
+ | Value | Description | Cost |
151
+ |---|---|---|
152
+ | `'oklab'` | **Default.** Euclidean distance in OKLab. Fast, perceptually uniform, excellent general-purpose choice. | low |
153
+ | `'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 |
154
+ | `'deltaE76'` | CIE 1976: plain Euclidean distance in CIELab. Classic standard, decent uniformity. | medium |
155
+ | `'deltaE94'` | CIE 1994: adds chroma and hue weighting on top of ΔE76. Better than ΔE76, cheaper than ΔE2000. | medium |
156
+ | `'deltaE2000'` | CIEDE2000: weighted colour difference with per-channel corrections for hue, chroma, and lightness. Most accurate, most expensive. | high |
157
+ | `'rgb'` | Plain Euclidean in sRGB. Not perceptually uniform. Useful as a baseline. | lowest |
158
+
159
+ ---
160
+
161
+ ## Advanced usage
162
+
163
+ ### Accessing the canvas
164
+
165
+ ```js
166
+ // with no container, manage placement yourself
167
+ const viz = new PaletteViz({ palette });
168
+ document.querySelector('#app').appendChild(viz.canvas);
169
+
170
+ // or style it after the fact
171
+ viz.canvas.style.borderRadius = '50%';
172
+ ```
173
+
174
+ ### Multiple synchronised views
175
+
176
+ ```js
177
+ const palette = ['#264653', '#2a9d8f', '#e9c46a'];
178
+ const shared = { palette, width: 256, height: 256, container: document.querySelector('#views') };
179
+
180
+ const views = [
181
+ new PaletteViz({ ...shared, axis: 'x', colorModel: 'okhsv' }),
182
+ new PaletteViz({ ...shared, axis: 'y', colorModel: 'okhsl' }),
183
+ new PaletteViz({ ...shared, axis: 'z', colorModel: 'oklch' }),
184
+ ];
185
+
186
+ document.querySelector('#slider').addEventListener('input', (e) => {
187
+ views.forEach((v) => { v.position = +e.target.value; });
188
+ });
189
+ ```
190
+
191
+ ### Utility exports
192
+
193
+ ```js
194
+ import { paletteToRGBA, randomPalette, fragmentShader } from 'palette-shader';
195
+
196
+ // Get raw RGBA bytes (Uint8Array, sRGB, 4 bytes per color)
197
+ // Useful for building your own WebGL texture or processing palette data
198
+ const rgba = paletteToRGBA(['#ff0000', '#00ff00', '#0000ff']);
199
+
200
+ // Quick random palette for prototyping
201
+ const palette = randomPalette(16);
202
+
203
+ // Access the raw GLSL fragment shader string
204
+ console.log(fragmentShader);
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Dependencies
210
+
211
+ None. The library uses raw WebGL 2 and the browser's native CSS color parser. No runtime dependencies.
212
+
213
+ ## Browser support
214
+
215
+ Requires **WebGL 2** (supported in all modern browsers and most mobile devices since ~2017). Use `canvas.getContext('webgl2')` availability to feature-detect if needed.
216
+
217
+ ---
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ git clone https://github.com/meodai/color-palette-shader.git
223
+ cd color-palette-shader
224
+ npm install
225
+
226
+ npm run dev # start demo dev server → http://localhost:5173
227
+ npm run build # build library → dist/
228
+ npm run typecheck # TypeScript type check
229
+ ```
230
+
231
+ 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.
232
+
233
+ ---
234
+
235
+ ## License
236
+
237
+ MIT © [David Aerne](https://elastiq.ch)
@@ -0,0 +1,167 @@
1
+ import { DataTexture } from 'three';
2
+
3
+ declare type Axis = 'x' | 'y' | 'z';
4
+
5
+ declare type ColorList = ColorString[];
6
+
7
+ declare type ColorString = string;
8
+
9
+ declare type DistanceMetric = 'rgb' | 'oklab' | 'deltaE76' | 'deltaE2000' | 'kotsarenkoRamos';
10
+
11
+ export declare const fragmentShader: string;
12
+
13
+ export declare const paletteToTexture: (palette: ColorList) => DataTexture;
14
+
15
+ export declare class PaletteViz {
16
+ #private;
17
+ constructor({ palette, width, height, pixelRatio, container, colorModel, distanceMetric, isPolar, axis, position, invertLightness, showRaw, }?: PaletteVizOptions);
18
+ get canvas(): HTMLCanvasElement;
19
+ get width(): number;
20
+ get height(): number;
21
+ resize(width: number, height?: number | null): void;
22
+ destroy(): void;
23
+ set palette(palette: ColorList);
24
+ get palette(): ColorList;
25
+ setColor(color: ColorString, index: number): void;
26
+ addColor(color: ColorString, index?: number): void;
27
+ removeColor(index: number): void;
28
+ removeColor(color: ColorString): void;
29
+ set position(value: number);
30
+ get position(): number;
31
+ set axis(axis: Axis);
32
+ get axis(): Axis;
33
+ set colorModel(model: SupportedColorModels);
34
+ get colorModel(): SupportedColorModels;
35
+ set distanceMetric(metric: DistanceMetric);
36
+ get distanceMetric(): DistanceMetric;
37
+ set isPolar(value: boolean);
38
+ get isPolar(): boolean;
39
+ set invertLightness(value: boolean);
40
+ get invertLightness(): boolean;
41
+ set showRaw(value: boolean);
42
+ get showRaw(): boolean;
43
+ static paletteToTexture: (palette: ColorList) => DataTexture;
44
+ }
45
+
46
+ export declare class PaletteViz3D {
47
+ #private;
48
+ constructor({ palette, width, height, pixelRatio, container, colorModel, distanceMetric, invertLightness, showRaw, isPolar, yaw, pitch, zoom, slices, sliceResolution, sliceOpacity, }?: PaletteViz3DOptions);
49
+ get canvas(): HTMLCanvasElement;
50
+ get width(): number;
51
+ get height(): number;
52
+ resize(width: number, height?: number | null): void;
53
+ destroy(): void;
54
+ set palette(palette: ColorList);
55
+ get palette(): ColorList;
56
+ setColor(color: ColorString, index: number): void;
57
+ set colorModel(model: SupportedColorModels);
58
+ get colorModel(): SupportedColorModels;
59
+ set distanceMetric(metric: DistanceMetric);
60
+ get distanceMetric(): DistanceMetric;
61
+ set isPolar(value: boolean);
62
+ get isPolar(): boolean;
63
+ set invertLightness(value: boolean);
64
+ get invertLightness(): boolean;
65
+ set showRaw(value: boolean);
66
+ get showRaw(): boolean;
67
+ set yaw(value: number);
68
+ get yaw(): number;
69
+ set pitch(value: number);
70
+ get pitch(): number;
71
+ set zoom(value: number);
72
+ get zoom(): number;
73
+ }
74
+
75
+ declare type PaletteViz3DOptions = {
76
+ palette?: ColorList;
77
+ width?: number;
78
+ height?: number;
79
+ pixelRatio?: number;
80
+ container?: HTMLElement;
81
+ colorModel?: SupportedColorModels;
82
+ distanceMetric?: DistanceMetric;
83
+ invertLightness?: boolean;
84
+ showRaw?: boolean;
85
+ isPolar?: boolean;
86
+ yaw?: number;
87
+ pitch?: number;
88
+ zoom?: number;
89
+ /** Number of slices along the value/lightness axis. Default 32. */
90
+ slices?: number;
91
+ /** Pixel resolution of each slice (square). Default 64. */
92
+ sliceResolution?: number;
93
+ /** Per-slice alpha. Defaults to 5/slices. */
94
+ sliceOpacity?: number;
95
+ };
96
+
97
+ export declare class PaletteViz3DVoronoi {
98
+ #private;
99
+ constructor({ palette, width, height, pixelRatio, container, colorModel, distanceMetric, invertLightness, showRaw, isPolar, yaw, pitch, zoom, resolution, tubeRadius, tubeSegments, }?: PaletteViz3DVoronoiOptions);
100
+ get canvas(): HTMLCanvasElement;
101
+ get width(): number;
102
+ get height(): number;
103
+ resize(width: number, height?: number | null): void;
104
+ destroy(): void;
105
+ set palette(palette: ColorList);
106
+ get palette(): ColorList;
107
+ setColor(color: ColorString, index: number): void;
108
+ set colorModel(model: SupportedColorModels);
109
+ get colorModel(): SupportedColorModels;
110
+ set distanceMetric(metric: DistanceMetric);
111
+ get distanceMetric(): DistanceMetric;
112
+ set isPolar(value: boolean);
113
+ get isPolar(): boolean;
114
+ set invertLightness(value: boolean);
115
+ get invertLightness(): boolean;
116
+ set showRaw(value: boolean);
117
+ get showRaw(): boolean;
118
+ set yaw(value: number);
119
+ get yaw(): number;
120
+ set pitch(value: number);
121
+ get pitch(): number;
122
+ set zoom(value: number);
123
+ get zoom(): number;
124
+ }
125
+
126
+ declare type PaletteViz3DVoronoiOptions = {
127
+ palette?: ColorList;
128
+ width?: number;
129
+ height?: number;
130
+ pixelRatio?: number;
131
+ container?: HTMLElement;
132
+ colorModel?: SupportedColorModels;
133
+ distanceMetric?: DistanceMetric;
134
+ invertLightness?: boolean;
135
+ showRaw?: boolean;
136
+ isPolar?: boolean;
137
+ yaw?: number;
138
+ pitch?: number;
139
+ zoom?: number;
140
+ /** Grid resolution for Voronoi voxelization (NxNxN). Default 32. */
141
+ resolution?: number;
142
+ /** Tube radius in world units. Default 0.008. */
143
+ tubeRadius?: number;
144
+ /** Circumference segments per tube. Default 8. */
145
+ tubeSegments?: number;
146
+ };
147
+
148
+ declare type PaletteVizOptions = {
149
+ palette?: ColorList;
150
+ width?: number;
151
+ height?: number;
152
+ pixelRatio?: number;
153
+ container?: HTMLElement;
154
+ colorModel?: SupportedColorModels;
155
+ distanceMetric?: DistanceMetric;
156
+ isPolar?: boolean;
157
+ axis?: Axis;
158
+ position?: number;
159
+ invertLightness?: boolean;
160
+ showRaw?: boolean;
161
+ };
162
+
163
+ export declare const randomPalette: (size?: number) => ColorList;
164
+
165
+ declare type SupportedColorModels = 'hsv' | 'okhsv' | 'hsl' | 'okhsl' | 'oklch';
166
+
167
+ export { }