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 +66 -23
- package/dist/palette-shader.d.ts +72 -26
- package/dist/palette-shader.js +992 -701
- package/dist/palette-shader.js.map +1 -1
- package/dist/palette-shader.umd.cjs +305 -58
- package/dist/palette-shader.umd.cjs.map +1 -1
- package/package.json +1 -1
- package/src/PaletteViz.ts +189 -232
- package/src/PaletteViz3D.ts +129 -297
- package/src/mesh.ts +1 -1
- package/src/rendererShared.ts +260 -0
- package/src/shaderSrc.ts +101 -22
- package/src/shaders/cam16ucs.frag.glsl +180 -0
- package/src/shaders/closestColor.frag.glsl +4 -2
- package/src/types.ts +6 -1
- package/src/webgl.ts +72 -0
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
|
|
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
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
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
|
|
270
|
-
| --------------- |
|
|
271
|
-
| `'oklab'` | **Default.** Euclidean distance in OKLab. Fast, perceptually uniform, excellent general-purpose choice.
|
|
272
|
-
| `'oklrab'` | Euclidean in OKLab with toe-corrected lightness. Slightly better uniformity in dark tones than OKLab.
|
|
273
|
-
| `'okLightness'` | Absolute lightness difference in OKLab (
|
|
274
|
-
| `'liMatch'` | Spatially varying blend: full OKLab distance at the left edge, pure lightness match at the right. Inspired by censor's li-match.
|
|
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
|
|
package/dist/palette-shader.d.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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(
|
|
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 { }
|