palette-shader 0.16.0 → 0.18.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 |
@@ -174,6 +182,33 @@ Returns the current shader result at normalized UV coordinates (`0–1` on both
174
182
  const color = viz.getColorAtUV(0.5, 0.5); // center
175
183
  ```
176
184
 
185
+ ### `getColorAtUV_float(x, y)`
186
+
187
+ Returns the color at normalized UV coordinates as `[r, g, b]` in **unclamped linear RGB** with full float precision. Unlike `getColorAtUV`, values are not quantized to 8-bit and are not clamped to `[0, 1]` — out-of-gamut colors preserve their original value. The sRGB transfer function is bypassed so the consumer receives linear-light values suitable for further color-space conversions without double-gamma.
188
+
189
+ Renders a single pixel on demand into a 1×1 `RGBA16F` framebuffer — zero per-frame cost.
190
+
191
+ ```js
192
+ const [r, g, b] = viz.getColorAtUV_float(0.5, 0.5);
193
+ // r, g, b are linear RGB — may be < 0 or > 1 for out-of-gamut colors
194
+ ```
195
+
196
+ Converting to OKLab (or any other color space) with [culori](https://culorijs.org/):
197
+
198
+ ```js
199
+ import { converter, formatCss } from 'culori';
200
+
201
+ const toOklab = converter('oklab');
202
+ const [r, g, b] = viz.getColorAtUV_float(0.5, 0.5);
203
+
204
+ // Feed linear RGB directly into culori's linear-sRGB mode
205
+ const oklab = toOklab({ mode: 'lrgb', r, g, b });
206
+ // → { mode: 'oklab', l: 0.72, a: 0.08, b: 0.12 }
207
+
208
+ formatCss(oklab);
209
+ // → 'oklab(0.72 0.08 0.12)'
210
+ ```
211
+
177
212
  ---
178
213
 
179
214
  ## Color models
@@ -218,6 +253,13 @@ Controls the 3-D color space the visualization is rendered in. Polar variants (`
218
253
  | `'cielchD50'` | cube | CIELab D50 in cylindrical LCH coordinates. |
219
254
  | `'cielchD50Polar'` | wheel | Polar form of CIELch D50. |
220
255
 
256
+ **CAM16-UCS — D65**
257
+
258
+ | Value | Shape | Description |
259
+ | -------------------- | ----- | -------------------------------------------------------------------------------------- |
260
+ | `'cam16ucsD65'` | cube | CAM16-UCS under fixed D65 CAT16 viewing conditions, rendered with an analytic inverse. |
261
+ | `'cam16ucsD65Polar'` | wheel | Polar CAM16-UCS view under the same fixed D65 conditions. |
262
+
221
263
  **Classic**
222
264
 
223
265
  | Value | Shape | Description |
@@ -264,14 +306,15 @@ A practical starting point: use a **polar** model to get an intuitive read on hu
264
306
 
265
307
  Controls how "nearest palette color" is determined per pixel.
266
308
 
267
- **OK**
309
+ **OK / appearance-inspired**
268
310
 
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 |
311
+ | Value | Description | Cost |
312
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---- |
313
+ | `'oklab'` | **Default.** Euclidean distance in OKLab. Fast, perceptually uniform, excellent general-purpose choice. | low |
314
+ | `'oklrab'` | Euclidean in OKLab with toe-corrected lightness. Slightly better uniformity in dark tones than OKLab. | low |
315
+ | `'okLightness'` | Absolute lightness difference in OKLab (L`). Ignores hue and chroma, so regions are grouped by brightness only. | low |
316
+ | `'liMatch'` | Spatially varying blend: full OKLab distance at the left edge, pure lightness match at the right. Inspired by censor's li-match. | low |
317
+ | `'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
318
 
276
319
  **CIE — D65**
277
320
 
@@ -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,21 +78,19 @@ 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;
38
91
  removeColor(color: ColorRGB): void;
39
92
  getColorAtUV(x: number, y: number): ColorRGB;
93
+ getColorAtUV_float(x: number, y: number): ColorRGB;
40
94
  set position(value: number);
41
95
  get position(): number;
42
96
  set axis(axis: Axis);
@@ -49,8 +103,6 @@ export declare class PaletteViz {
49
103
  get invertAxes(): Axis[];
50
104
  set showRaw(value: boolean);
51
105
  get showRaw(): boolean;
52
- set pixelRatio(value: number);
53
- get pixelRatio(): number;
54
106
  set gamutClip(value: boolean);
55
107
  get gamutClip(): boolean;
56
108
  set outlineWidth(value: number);
@@ -60,22 +112,15 @@ export declare class PaletteViz {
60
112
  static paletteToTexture: (palette: ColorList) => Uint8Array;
61
113
  }
62
114
 
63
- export declare class PaletteViz3D {
115
+ export declare class PaletteViz3D extends BasePaletteRenderer {
64
116
  #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
- */
117
+ constructor({ palette, width, height, pixelRatio, observeResize, container, colorModel, distanceMetric, invertAxes, showRaw, outlineWidth, gamutClip, position, modelMatrix, }?: PaletteViz3DOptions);
118
+ protected currentMetricCode(): number;
119
+ protected onSurfaceResized(pw: number, ph: number): void;
120
+ protected renderFrame(): void;
71
121
  getColorAtUV(x: number, y: number): [number, number, number] | null;
72
- get canvas(): HTMLCanvasElement;
73
- get width(): number;
74
- get height(): number;
75
122
  resize(width: number, height?: number | null): void;
76
123
  destroy(): void;
77
- set palette(palette: ColorList);
78
- get palette(): ColorList;
79
124
  set colorModel(model: SupportedColorModels);
80
125
  get colorModel(): SupportedColorModels;
81
126
  set distanceMetric(metric: DistanceMetric);
@@ -86,9 +131,8 @@ export declare class PaletteViz3D {
86
131
  get showRaw(): boolean;
87
132
  set position(value: number);
88
133
  get position(): number;
89
- /** Apply an incremental spherical rotation (screen-space dx/dy in radians). */
90
134
  rotate(dx: number, dy: number): void;
91
- set modelMatrix(m: Float32Array);
135
+ set modelMatrix(matrix: Float32Array);
92
136
  get modelMatrix(): Float32Array;
93
137
  set gamutClip(value: boolean);
94
138
  get gamutClip(): boolean;
@@ -103,6 +147,7 @@ export declare type PaletteViz3DOptions = {
103
147
  width?: number;
104
148
  height?: number;
105
149
  pixelRatio?: number;
150
+ observeResize?: boolean;
106
151
  container?: HTMLElement;
107
152
  colorModel?: SupportedColorModels;
108
153
  distanceMetric?: DistanceMetric;
@@ -119,6 +164,7 @@ export declare type PaletteVizOptions = {
119
164
  width?: number;
120
165
  height?: number;
121
166
  pixelRatio?: number;
167
+ observeResize?: boolean;
122
168
  container?: HTMLElement;
123
169
  colorModel?: SupportedColorModels;
124
170
  distanceMetric?: DistanceMetric;
@@ -132,6 +178,6 @@ export declare type PaletteVizOptions = {
132
178
 
133
179
  export declare const randomPalette: (size?: number) => ColorList;
134
180
 
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';
181
+ 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
182
 
137
183
  export { }