palette-shader 0.15.0 → 0.17.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # palette-shader
2
2
 
3
- A dependency-free WebGL2 shader that maps any color palette across a 3-D perceptual color space and snaps each pixel to the nearest palette color. Visualize how a palette distributes across 30+ color models and ten distance metrics, all on the GPU. Includes 2-D cross-section views (`PaletteViz`) and an interactive 3-D cube/cylinder view (`PaletteViz3D`) with trackball rotation.
3
+ A dependency-free WebGL2 shader that maps any color palette across a 3-D perceptual color space and snaps each pixel to the nearest palette color. Visualize how a palette distributes across 30+ color models and eleven distance metrics, all on the GPU. Includes 2-D cross-section views (`PaletteViz`) and an interactive 3-D cube/cylinder view (`PaletteViz3D`) with trackball rotation.
4
4
 
5
5
  [**Live demo →**](https://meodai.github.io/color-palette-shader/)
6
6
 
@@ -59,6 +59,7 @@ const viz = new PaletteViz({
59
59
  container: document.querySelector('#app'),
60
60
  width: 512,
61
61
  height: 512,
62
+ observeResize: false,
62
63
  });
63
64
 
64
65
  // option B — no container, place the canvas yourself
@@ -78,21 +79,22 @@ new PaletteViz(options?: PaletteVizOptions)
78
79
 
79
80
  All options are optional. The palette defaults to a random 20-color set.
80
81
 
81
- | Option | Type | Default | Description |
82
- | ---------------- | ---------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | `palette` | `[number, number, number][]` | random | sRGB colors as `[r, g, b]` arrays, each component in the `0–1` range |
84
- | `container` | `HTMLElement` | `undefined` | Element the canvas is appended to. Omit and use `viz.canvas` to place it yourself |
85
- | `width` | `number` | `512` | Canvas width in CSS pixels |
86
- | `height` | `number` | `512` | Canvas height in CSS pixels |
87
- | `pixelRatio` | `number` | `devicePixelRatio` | Renderer pixel ratio |
88
- | `colorModel` | `string` | `'okhsv'` | Color space for the visualization (see [Color models](#color-models)) |
89
- | `distanceMetric` | `string` | `'oklab'` | Distance function for nearest-color matching (see [Distance metrics](#distance-metrics)) |
90
- | `axis` | `'x' \| 'y' \| 'z'` | `'y'` | Which axis the `position` value controls |
91
- | `position` | `number` | `0` | 0–1 position along the chosen axis |
92
- | `invertAxes` | `('x' \| 'y' \| 'z')[]` | `[]` | Invert one or more axes, for example `['z']` or `['x', 'z']`. In 2-D polar views, `y` inversion is resolved as a vertical view flip to avoid the mirrored center seam. |
93
- | `showRaw` | `boolean` | `false` | Bypass nearest-color matching (shows the raw color space) |
94
- | `outlineWidth` | `number` | `0` | Draw a transparent outline where palette regions meet. Width in physical pixels. `0` disables (no overhead). |
95
- | `gamutClip` | `boolean` | `false` | Discard out-of-sRGB-gamut pixels instead of clamping. Reveals the true gamut boundary of the color model. |
82
+ | Option | Type | Default | Description |
83
+ | ---------------- | ---------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `palette` | `[number, number, number][]` | random | sRGB colors as `[r, g, b]` arrays, each component in the `0–1` range |
85
+ | `container` | `HTMLElement` | `undefined` | Element the canvas is appended to. Omit and use `viz.canvas` to place it yourself |
86
+ | `width` | `number` | `512` | Canvas width in CSS pixels |
87
+ | `height` | `number` | `512` | Canvas height in CSS pixels |
88
+ | `pixelRatio` | `number` | `devicePixelRatio` | Renderer pixel ratio |
89
+ | `observeResize` | `boolean` | `false` | When `true`, a `ResizeObserver` tracks the laid-out canvas size and updates the backing resolution to match CSS layout. When `false`, `width` and `height` are treated as the explicit display size. |
90
+ | `colorModel` | `string` | `'okhsv'` | Color space for the visualization (see [Color models](#color-models)) |
91
+ | `distanceMetric` | `string` | `'oklab'` | Distance function for nearest-color matching (see [Distance metrics](#distance-metrics)) |
92
+ | `axis` | `'x' \| 'y' \| 'z'` | `'y'` | Which axis the `position` value controls |
93
+ | `position` | `number` | `0` | 0–1 position along the chosen axis |
94
+ | `invertAxes` | `('x' \| 'y' \| 'z')[]` | `[]` | Invert one or more axes, for example `['z']` or `['x', 'z']`. In 2-D polar views, `y` inversion is resolved as a vertical view flip to avoid the mirrored center seam. |
95
+ | `showRaw` | `boolean` | `false` | Bypass nearest-color matching (shows the raw color space) |
96
+ | `outlineWidth` | `number` | `0` | Draw a transparent outline where palette regions meet. Width in physical pixels. `0` disables (no overhead). |
97
+ | `gamutClip` | `boolean` | `false` | Discard out-of-sRGB-gamut pixels instead of clamping. Reveals the true gamut boundary of the color model. |
96
98
 
97
99
  ---
98
100
 
@@ -116,6 +118,12 @@ viz.gamutClip = true; // discard out-of-gamut pixels
116
118
  viz.pixelRatio = window.devicePixelRatio; // update after display changes
117
119
  ```
118
120
 
121
+ Sizing model:
122
+
123
+ - By default, `width` and `height` are the explicit display size and backing resolution source.
124
+ - If you want normal CSS layout to control the canvas size, set `observeResize: true` and style the canvas or its container in CSS.
125
+ - `pixelRatio` only affects backing resolution, not layout size.
126
+
119
127
  Additional read-only properties:
120
128
 
121
129
  | Property | Type | Description |
@@ -191,16 +199,16 @@ Controls the 3-D color space the visualization is rendered in. Polar variants (`
191
199
 
192
200
  **OK — Lab / LCH**
193
201
 
194
- | Value | Shape | Description |
195
- | ---------------- | -------- | ------------------------------------------------------------------------------------ |
196
- | `'oklab'` | cube | Raw OKLab: x→a, y→b, z→L. |
197
- | `'oklch'` | cube | OKLab in cylindrical LCH coordinates. Ideal for chroma or lightness slices. |
198
- | `'oklchPolar'` | wheel | Polar form of OKLch. |
199
- | `'oklchDiag'` | diagonal | Complementary hue plane: lightness on the diagonal, signed chroma on the anti-diagonal. Two opposite hues share the same view — no gray band in the middle. |
200
- | `'oklrab'` | cube | OKLab with toe-corrected lightness (Lr). Better perceptual uniformity in dark tones. |
201
- | `'oklrch'` | cube | OKLrab in cylindrical LCH coordinates. |
202
- | `'oklrchPolar'` | wheel | Polar form of OKLrch. |
203
- | `'oklrchDiag'` | diagonal | Complementary hue plane in OKLrch. Same diagonal layout as `oklchDiag` with toe-corrected lightness. |
202
+ | Value | Shape | Description |
203
+ | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
204
+ | `'oklab'` | cube | Raw OKLab: x→a, y→b, z→L. |
205
+ | `'oklch'` | cube | OKLab in cylindrical LCH coordinates. Ideal for chroma or lightness slices. |
206
+ | `'oklchPolar'` | wheel | Polar form of OKLch. |
207
+ | `'oklchDiag'` | diagonal | Complementary hue plane: lightness on the diagonal, signed chroma on the anti-diagonal. Two opposite hues share the same view — no gray band in the middle. |
208
+ | `'oklrab'` | cube | OKLab with toe-corrected lightness (Lr). Better perceptual uniformity in dark tones. |
209
+ | `'oklrch'` | cube | OKLrab in cylindrical LCH coordinates. |
210
+ | `'oklrchPolar'` | wheel | Polar form of OKLrch. |
211
+ | `'oklrchDiag'` | diagonal | Complementary hue plane in OKLrch. Same diagonal layout as `oklchDiag` with toe-corrected lightness. |
204
212
 
205
213
  **CIE Lab / LCH — D65**
206
214
 
@@ -218,6 +226,13 @@ Controls the 3-D color space the visualization is rendered in. Polar variants (`
218
226
  | `'cielchD50'` | cube | CIELab D50 in cylindrical LCH coordinates. |
219
227
  | `'cielchD50Polar'` | wheel | Polar form of CIELch D50. |
220
228
 
229
+ **CAM16-UCS — D65**
230
+
231
+ | Value | Shape | Description |
232
+ | -------------------- | ----- | -------------------------------------------------------------------------------------- |
233
+ | `'cam16ucsD65'` | cube | CAM16-UCS under fixed D65 CAT16 viewing conditions, rendered with an analytic inverse. |
234
+ | `'cam16ucsD65Polar'` | wheel | Polar CAM16-UCS view under the same fixed D65 conditions. |
235
+
221
236
  **Classic**
222
237
 
223
238
  | Value | Shape | Description |
@@ -232,18 +247,18 @@ Controls the 3-D color space the visualization is rendered in. Polar variants (`
232
247
 
233
248
  **Quantized RGB**
234
249
 
235
- | Value | Shape | Description |
236
- | ------------ | ----- | ---------------------------------------------------------------- |
237
- | `'rgb6bit'` | cube | 2-bit per channel (64 colors). Game Boy–era palettes. |
238
- | `'rgb8bit'` | cube | 3-3-2 bit (256 colors). CGA-style quantization. |
239
- | `'rgb12bit'` | cube | 4-bit per channel (4096 colors). Amiga / NTSC. |
240
- | `'rgb15bit'` | cube | 5-bit per channel (32768 colors). SVGA HiColor. |
241
- | `'rgb18bit'` | cube | 6-bit per channel (262144 colors). VGA. |
250
+ | Value | Shape | Description |
251
+ | ------------ | ----- | ----------------------------------------------------- |
252
+ | `'rgb6bit'` | cube | 2-bit per channel (64 colors). Game Boy–era palettes. |
253
+ | `'rgb8bit'` | cube | 3-3-2 bit (256 colors). CGA-style quantization. |
254
+ | `'rgb12bit'` | cube | 4-bit per channel (4096 colors). Amiga / NTSC. |
255
+ | `'rgb15bit'` | cube | 5-bit per channel (32768 colors). SVGA HiColor. |
256
+ | `'rgb18bit'` | cube | 6-bit per channel (262144 colors). VGA. |
242
257
 
243
258
  **Spectral**
244
259
 
245
- | Value | Shape | Description |
246
- | ------------ | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
260
+ | Value | Shape | Description |
261
+ | ------------ | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
247
262
  | `'spectrum'` | cube | Visible light wavelengths (410–665 nm) plus purple line, modulated in OKLab. Inspired by censor's SpectroBoxWidget. X→wavelength, Y→lightness, Z→chroma scale. |
248
263
 
249
264
  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.
@@ -264,14 +279,15 @@ A practical starting point: use a **polar** model to get an intuitive read on hu
264
279
 
265
280
  Controls how "nearest palette color" is determined per pixel.
266
281
 
267
- **OK**
282
+ **OK / appearance-inspired**
268
283
 
269
- | Value | Description | Cost |
270
- | --------------- | ------------------------------------------------------------------------------------------------------- | ---- |
271
- | `'oklab'` | **Default.** Euclidean distance in OKLab. Fast, perceptually uniform, excellent general-purpose choice. | low |
272
- | `'oklrab'` | Euclidean in OKLab with toe-corrected lightness. Slightly better uniformity in dark tones than OKLab. | low |
273
- | `'okLightness'` | Absolute lightness difference in OKLab (`|ΔL|`). Ignores hue and chroma groups by brightness only. | low |
274
- | `'liMatch'` | Spatially varying blend: full OKLab distance at the left edge, pure lightness match at the right. Inspired by censor's li-match. Useful for visualising tonal structure. | low |
284
+ | Value | Description | Cost |
285
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---- |
286
+ | `'oklab'` | **Default.** Euclidean distance in OKLab. Fast, perceptually uniform, excellent general-purpose choice. | low |
287
+ | `'oklrab'` | Euclidean in OKLab with toe-corrected lightness. Slightly better uniformity in dark tones than OKLab. | low |
288
+ | `'okLightness'` | Absolute lightness difference in OKLab (L`). Ignores hue and chroma, so regions are grouped by brightness only. | low |
289
+ | `'liMatch'` | Spatially varying blend: full OKLab distance at the left edge, pure lightness match at the right. Inspired by censor's li-match. | low |
290
+ | `'cam16ucsD65'` | Euclidean distance in CAM16-UCS under fixed D65 CAT16 viewing conditions. Useful when you want a full appearance-model metric like censor. | high |
275
291
 
276
292
  **CIE — D65**
277
293
 
@@ -1,10 +1,66 @@
1
1
  export declare type Axis = 'x' | 'y' | 'z';
2
2
 
3
+ declare abstract class BasePaletteRenderer {
4
+ protected paletteState: ColorList;
5
+ protected cssWidth: number;
6
+ protected cssHeight: number;
7
+ protected pixelRatioState: number;
8
+ protected readonly canvasElement: HTMLCanvasElement;
9
+ protected readonly glContext: WebGL2RenderingContext;
10
+ protected readonly paletteTexture: WebGLTexture;
11
+ protected readonly metricTexture: WebGLTexture;
12
+ protected metricPaletteDirty: boolean;
13
+ protected animationFrameId: number | null;
14
+ protected destroyed: boolean;
15
+ protected readonly containerElement?: HTMLElement;
16
+ protected readonly observeResize: boolean;
17
+ protected resizeObserver: ResizeObserver | null;
18
+ protected constructor({ palette, width, height, pixelRatio, observeResize, container, canvasClassName, }: BaseRendererOptions);
19
+ protected normalizeInvertAxes(axes: Axis[]): Axis[];
20
+ protected syncCanvasSize(width: number, height: number): {
21
+ pw: number;
22
+ ph: number;
23
+ };
24
+ protected syncCanvasSizeFromLayout(): {
25
+ pw: number;
26
+ ph: number;
27
+ width: number;
28
+ height: number;
29
+ };
30
+ protected schedulePaint(): void;
31
+ protected flushScheduledPaint(): void;
32
+ protected uploadMetricPalette(paletteSizeUniform: WebGLUniformLocation | null): void;
33
+ protected attachCanvas(): void;
34
+ protected beginDestroy(): boolean;
35
+ protected destroyBaseResources(): void;
36
+ protected onSurfaceResized(_pw: number, _ph: number): void;
37
+ protected abstract currentMetricCode(): number;
38
+ protected abstract renderFrame(): void;
39
+ get canvas(): HTMLCanvasElement;
40
+ get width(): number;
41
+ get height(): number;
42
+ resize(width: number, height?: number | null): void;
43
+ set palette(palette: ColorList);
44
+ get palette(): ColorList;
45
+ set pixelRatio(value: number);
46
+ get pixelRatio(): number;
47
+ }
48
+
49
+ declare type BaseRendererOptions = {
50
+ palette: ColorList;
51
+ width: number;
52
+ height: number;
53
+ pixelRatio: number;
54
+ observeResize: boolean;
55
+ container?: HTMLElement;
56
+ canvasClassName: string;
57
+ };
58
+
3
59
  export declare type ColorList = ColorRGB[];
4
60
 
5
61
  export declare type ColorRGB = [number, number, number];
6
62
 
7
- export declare type DistanceMetric = 'rgb' | 'oklab' | 'deltaE76' | 'deltaE94' | 'deltaE2000' | 'kotsarenkoRamos' | 'oklrab' | 'cielabD50' | 'okLightness' | 'liMatch';
63
+ export declare type DistanceMetric = 'rgb' | 'oklab' | 'deltaE76' | 'deltaE94' | 'deltaE2000' | 'kotsarenkoRamos' | 'oklrab' | 'cielabD50' | 'okLightness' | 'liMatch' | 'cam16ucsD65';
8
64
 
9
65
  export declare const fragmentShader: string;
10
66
 
@@ -22,16 +78,13 @@ export declare const paletteToRGBA: (palette: ColorList) => Uint8Array;
22
78
 
23
79
  export declare const paletteToTexture: (palette: ColorList) => Uint8Array;
24
80
 
25
- export declare class PaletteViz {
81
+ export declare class PaletteViz extends BasePaletteRenderer {
26
82
  #private;
27
- constructor({ palette, width, height, pixelRatio, container, colorModel, distanceMetric, axis, position, invertAxes, showRaw, outlineWidth, gamutClip, }?: PaletteVizOptions);
28
- get canvas(): HTMLCanvasElement;
29
- get width(): number;
30
- get height(): number;
31
- resize(width: number, height?: number | null): void;
83
+ constructor({ palette, width, height, pixelRatio, observeResize, container, colorModel, distanceMetric, axis, position, invertAxes, showRaw, outlineWidth, gamutClip, }?: PaletteVizOptions);
84
+ protected currentMetricCode(): number;
85
+ protected onSurfaceResized(pw: number, ph: number): void;
86
+ protected renderFrame(): void;
32
87
  destroy(): void;
33
- set palette(palette: ColorList);
34
- get palette(): ColorList;
35
88
  setColor(color: ColorRGB, index: number): void;
36
89
  addColor(color: ColorRGB, index?: number): void;
37
90
  removeColor(index: number): void;
@@ -49,8 +102,6 @@ export declare class PaletteViz {
49
102
  get invertAxes(): Axis[];
50
103
  set showRaw(value: boolean);
51
104
  get showRaw(): boolean;
52
- set pixelRatio(value: number);
53
- get pixelRatio(): number;
54
105
  set gamutClip(value: boolean);
55
106
  get gamutClip(): boolean;
56
107
  set outlineWidth(value: number);
@@ -60,22 +111,15 @@ export declare class PaletteViz {
60
111
  static paletteToTexture: (palette: ColorList) => Uint8Array;
61
112
  }
62
113
 
63
- export declare class PaletteViz3D {
114
+ export declare class PaletteViz3D extends BasePaletteRenderer {
64
115
  #private;
65
- constructor({ palette, width, height, pixelRatio, container, colorModel, distanceMetric, invertAxes, showRaw, outlineWidth, gamutClip, position, modelMatrix, }?: PaletteViz3DOptions);
66
- /**
67
- * Read the rendered colour at normalised screen coordinates (0–1, y=0 is top).
68
- * Returns [r, g, b] in [0, 1], or null if the pixel is transparent (no geometry).
69
- * Flushes any pending rAF frame to ensure the reading is up to date.
70
- */
116
+ constructor({ palette, width, height, pixelRatio, observeResize, container, colorModel, distanceMetric, invertAxes, showRaw, outlineWidth, gamutClip, position, modelMatrix, }?: PaletteViz3DOptions);
117
+ protected currentMetricCode(): number;
118
+ protected onSurfaceResized(pw: number, ph: number): void;
119
+ protected renderFrame(): void;
71
120
  getColorAtUV(x: number, y: number): [number, number, number] | null;
72
- get canvas(): HTMLCanvasElement;
73
- get width(): number;
74
- get height(): number;
75
121
  resize(width: number, height?: number | null): void;
76
122
  destroy(): void;
77
- set palette(palette: ColorList);
78
- get palette(): ColorList;
79
123
  set colorModel(model: SupportedColorModels);
80
124
  get colorModel(): SupportedColorModels;
81
125
  set distanceMetric(metric: DistanceMetric);
@@ -86,9 +130,8 @@ export declare class PaletteViz3D {
86
130
  get showRaw(): boolean;
87
131
  set position(value: number);
88
132
  get position(): number;
89
- /** Apply an incremental spherical rotation (screen-space dx/dy in radians). */
90
133
  rotate(dx: number, dy: number): void;
91
- set modelMatrix(m: Float32Array);
134
+ set modelMatrix(matrix: Float32Array);
92
135
  get modelMatrix(): Float32Array;
93
136
  set gamutClip(value: boolean);
94
137
  get gamutClip(): boolean;
@@ -103,6 +146,7 @@ export declare type PaletteViz3DOptions = {
103
146
  width?: number;
104
147
  height?: number;
105
148
  pixelRatio?: number;
149
+ observeResize?: boolean;
106
150
  container?: HTMLElement;
107
151
  colorModel?: SupportedColorModels;
108
152
  distanceMetric?: DistanceMetric;
@@ -119,6 +163,7 @@ export declare type PaletteVizOptions = {
119
163
  width?: number;
120
164
  height?: number;
121
165
  pixelRatio?: number;
166
+ observeResize?: boolean;
122
167
  container?: HTMLElement;
123
168
  colorModel?: SupportedColorModels;
124
169
  distanceMetric?: DistanceMetric;
@@ -132,6 +177,6 @@ export declare type PaletteVizOptions = {
132
177
 
133
178
  export declare const randomPalette: (size?: number) => ColorList;
134
179
 
135
- export declare type SupportedColorModels = 'rgb' | 'rgb12bit' | 'rgb8bit' | 'rgb18bit' | 'rgb6bit' | 'rgb15bit' | 'oklab' | 'okhsv' | 'okhsvPolar' | 'okhsl' | 'okhslPolar' | 'oklch' | 'oklchPolar' | 'hsv' | 'hsvPolar' | 'hsl' | 'hslPolar' | 'hwb' | 'hwbPolar' | 'oklrab' | 'oklrch' | 'oklrchPolar' | 'cielab' | 'cielch' | 'cielchPolar' | 'cielabD50' | 'cielchD50' | 'cielchD50Polar' | 'spectrum' | 'oklchDiag' | 'oklrchDiag';
180
+ export declare type SupportedColorModels = 'rgb' | 'rgb12bit' | 'rgb8bit' | 'rgb18bit' | 'rgb6bit' | 'rgb15bit' | 'oklab' | 'okhsv' | 'okhsvPolar' | 'okhsl' | 'okhslPolar' | 'oklch' | 'oklchPolar' | 'hsv' | 'hsvPolar' | 'hsl' | 'hslPolar' | 'hwb' | 'hwbPolar' | 'oklrab' | 'oklrch' | 'oklrchPolar' | 'cielab' | 'cielch' | 'cielchPolar' | 'cielabD50' | 'cielchD50' | 'cielchD50Polar' | 'cam16ucsD65' | 'cam16ucsD65Polar' | 'spectrum' | 'oklchDiag' | 'oklrchDiag';
136
181
 
137
182
  export { }