palette-shader 0.4.0 → 0.5.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 +25 -1
- package/package.json +1 -1
- package/src/index.ts +111 -2
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ All options are optional. The palette defaults to a random 20-colour set.
|
|
|
69
69
|
| `position` | `number` | `0` | 0–1 position along the chosen axis |
|
|
70
70
|
| `invertZ` | `boolean` | `false` | Flip the lightness/value axis |
|
|
71
71
|
| `showRaw` | `boolean` | `false` | Bypass nearest-colour matching (shows the raw colour space) |
|
|
72
|
+
| `outlineWidth` | `number` | `0` | Draw a transparent outline where palette regions meet. Width in physical pixels. `0` disables (no overhead). |
|
|
72
73
|
|
|
73
74
|
---
|
|
74
75
|
|
|
@@ -83,6 +84,7 @@ viz.colorModel = 'okhslPolar';
|
|
|
83
84
|
viz.distanceMetric = 'deltaE2000';
|
|
84
85
|
viz.invertZ = true;
|
|
85
86
|
viz.showRaw = true;
|
|
87
|
+
viz.outlineWidth = 2; // transparent border between regions, in physical pixels
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
Additional read-only properties:
|
|
@@ -133,7 +135,7 @@ viz.removeColor('#a8dadc');
|
|
|
133
135
|
|
|
134
136
|
### `destroy()`
|
|
135
137
|
|
|
136
|
-
Cancel the animation frame, release all WebGL resources (
|
|
138
|
+
Cancel the animation frame, release all WebGL resources (programs, textures, framebuffer, buffer, VAO), and remove the canvas from the DOM.
|
|
137
139
|
|
|
138
140
|
---
|
|
139
141
|
|
|
@@ -217,6 +219,28 @@ document.querySelector('#slider').addEventListener('input', (e) => {
|
|
|
217
219
|
});
|
|
218
220
|
```
|
|
219
221
|
|
|
222
|
+
### Transparent outlines between regions
|
|
223
|
+
|
|
224
|
+
`outlineWidth` draws a transparent gap where one palette colour's region meets another, revealing whatever is behind the canvas. Width is in physical pixels (i.e. it already accounts for `pixelRatio`).
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
const viz = new PaletteViz({
|
|
228
|
+
palette,
|
|
229
|
+
outlineWidth: 2,
|
|
230
|
+
container: document.querySelector('#app'),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// change at runtime — no shader recompile while the value stays > 0
|
|
234
|
+
viz.outlineWidth = 4;
|
|
235
|
+
|
|
236
|
+
// set back to 0 to disable entirely (zero GPU overhead)
|
|
237
|
+
viz.outlineWidth = 0;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Implemented as a two-pass render: pass 1 draws the colour regions into an offscreen framebuffer at the same cost as without outlines; pass 2 runs a tiny edge-detection shader that checks four neighbours via texture reads (no colour-space math). The result is that enabling outlines adds negligible overhead compared to the single-pass approach.
|
|
241
|
+
|
|
242
|
+
When `outlineWidth` is `0` (the default) the framebuffer and outline program are never allocated.
|
|
243
|
+
|
|
220
244
|
### Utility exports
|
|
221
245
|
|
|
222
246
|
```js
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -126,6 +126,35 @@ void main(){
|
|
|
126
126
|
#endif
|
|
127
127
|
}`;
|
|
128
128
|
|
|
129
|
+
// Pass-2 shader: reads from the FBO color texture, detects edges by comparing
|
|
130
|
+
// N/S/E/W neighbors. Only opaque neighbors (a>0) participate in the comparison
|
|
131
|
+
// so polar-disc edges don't bleed into the outline.
|
|
132
|
+
const outlineFragmentShaderSrc = `
|
|
133
|
+
precision highp float;
|
|
134
|
+
in vec2 vUv;
|
|
135
|
+
out vec4 fragColor;
|
|
136
|
+
uniform sampler2D colorMap;
|
|
137
|
+
uniform float outlineWidth;
|
|
138
|
+
uniform vec2 resolution;
|
|
139
|
+
|
|
140
|
+
void main() {
|
|
141
|
+
vec4 center = texture(colorMap, vUv);
|
|
142
|
+
if (center.a == 0.0) { fragColor = vec4(0.0); return; }
|
|
143
|
+
vec2 px = outlineWidth / resolution;
|
|
144
|
+
vec4 n0 = texture(colorMap, vUv + vec2( px.x, 0.0));
|
|
145
|
+
vec4 n1 = texture(colorMap, vUv + vec2(-px.x, 0.0));
|
|
146
|
+
vec4 n2 = texture(colorMap, vUv + vec2(0.0, px.y));
|
|
147
|
+
vec4 n3 = texture(colorMap, vUv + vec2(0.0, -px.y));
|
|
148
|
+
if ((n0.a > 0.0 && any(notEqual(n0.rgb, center.rgb))) ||
|
|
149
|
+
(n1.a > 0.0 && any(notEqual(n1.rgb, center.rgb))) ||
|
|
150
|
+
(n2.a > 0.0 && any(notEqual(n2.rgb, center.rgb))) ||
|
|
151
|
+
(n3.a > 0.0 && any(notEqual(n3.rgb, center.rgb)))) {
|
|
152
|
+
fragColor = vec4(0.0);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
fragColor = center;
|
|
156
|
+
}`;
|
|
157
|
+
|
|
129
158
|
// ── Color parsing ──────────────────────────────────────────────────────────────
|
|
130
159
|
// Use a canvas 2D context as a free CSS color parser — handles hex, rgb(),
|
|
131
160
|
// hsl(), named colors, etc. Lazy-initialised to avoid issues at module load time.
|
|
@@ -270,6 +299,7 @@ export class PaletteViz {
|
|
|
270
299
|
#distanceMetric: DistanceMetric = 'oklab';
|
|
271
300
|
#invertZ = false;
|
|
272
301
|
#showRaw = false;
|
|
302
|
+
#outlineWidth = 0;
|
|
273
303
|
|
|
274
304
|
// uniform value maps
|
|
275
305
|
readonly #axisMap = { x: 0, y: 1, z: 2 } as const;
|
|
@@ -309,6 +339,14 @@ export class PaletteViz {
|
|
|
309
339
|
#uProgress: WebGLUniformLocation | null = null;
|
|
310
340
|
#uPaletteTexture: WebGLUniformLocation | null = null;
|
|
311
341
|
|
|
342
|
+
// outline pass (created/destroyed when outlineWidth toggles between 0 and >0)
|
|
343
|
+
#fbo: WebGLFramebuffer | null = null;
|
|
344
|
+
#fboTexture: WebGLTexture | null = null;
|
|
345
|
+
#outlineProgram: WebGLProgram | null = null;
|
|
346
|
+
#uColorMap: WebGLUniformLocation | null = null;
|
|
347
|
+
#uOutlineWidth: WebGLUniformLocation | null = null;
|
|
348
|
+
#uOutlineResolution: WebGLUniformLocation | null = null;
|
|
349
|
+
|
|
312
350
|
// dom
|
|
313
351
|
#container: HTMLElement | undefined;
|
|
314
352
|
|
|
@@ -324,6 +362,7 @@ export class PaletteViz {
|
|
|
324
362
|
position = 0.0,
|
|
325
363
|
invertZ = false,
|
|
326
364
|
showRaw = false,
|
|
365
|
+
outlineWidth = 0,
|
|
327
366
|
}: PaletteVizOptions = {}) {
|
|
328
367
|
this.#palette = palette;
|
|
329
368
|
this.#width = width;
|
|
@@ -335,6 +374,7 @@ export class PaletteViz {
|
|
|
335
374
|
this.#position = position;
|
|
336
375
|
this.#invertZ = invertZ;
|
|
337
376
|
this.#showRaw = showRaw;
|
|
377
|
+
this.#outlineWidth = outlineWidth;
|
|
338
378
|
this.#container = container;
|
|
339
379
|
|
|
340
380
|
this.#canvas = document.createElement('canvas');
|
|
@@ -361,6 +401,7 @@ export class PaletteViz {
|
|
|
361
401
|
|
|
362
402
|
this.#buildProgram();
|
|
363
403
|
this.#setSize(this.#width, this.#height);
|
|
404
|
+
if (this.#outlineWidth > 0) this.#buildOutlineResources();
|
|
364
405
|
this.#container?.appendChild(this.#canvas);
|
|
365
406
|
this.#paint();
|
|
366
407
|
}
|
|
@@ -383,6 +424,39 @@ export class PaletteViz {
|
|
|
383
424
|
this.#uPaletteTexture = gl.getUniformLocation(this.#program, 'paletteTexture');
|
|
384
425
|
}
|
|
385
426
|
|
|
427
|
+
#buildOutlineResources(): void {
|
|
428
|
+
const gl = this.#gl;
|
|
429
|
+
this.#outlineProgram = buildProgram(gl, {}, outlineFragmentShaderSrc, vertexShaderSrc);
|
|
430
|
+
this.#uColorMap = gl.getUniformLocation(this.#outlineProgram, 'colorMap');
|
|
431
|
+
this.#uOutlineWidth = gl.getUniformLocation(this.#outlineProgram, 'outlineWidth');
|
|
432
|
+
this.#uOutlineResolution = gl.getUniformLocation(this.#outlineProgram, 'resolution');
|
|
433
|
+
|
|
434
|
+
this.#fboTexture = gl.createTexture()!;
|
|
435
|
+
this.#fbo = gl.createFramebuffer()!;
|
|
436
|
+
this.#resizeFBO(this.#canvas.width, this.#canvas.height);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#destroyOutlineResources(): void {
|
|
440
|
+
const gl = this.#gl;
|
|
441
|
+
if (this.#outlineProgram) { gl.deleteProgram(this.#outlineProgram); this.#outlineProgram = null; }
|
|
442
|
+
if (this.#fboTexture) { gl.deleteTexture(this.#fboTexture); this.#fboTexture = null; }
|
|
443
|
+
if (this.#fbo) { gl.deleteFramebuffer(this.#fbo); this.#fbo = null; }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#resizeFBO(pw: number, ph: number): void {
|
|
447
|
+
const gl = this.#gl;
|
|
448
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#fboTexture);
|
|
449
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, pw, ph, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
450
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
451
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
452
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
453
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
454
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
455
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo);
|
|
456
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#fboTexture, 0);
|
|
457
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
458
|
+
}
|
|
459
|
+
|
|
386
460
|
#setSize(w: number, h: number): void {
|
|
387
461
|
const pw = Math.round(w * this.#pixelRatio);
|
|
388
462
|
const ph = Math.round(h * this.#pixelRatio);
|
|
@@ -391,22 +465,43 @@ export class PaletteViz {
|
|
|
391
465
|
this.#canvas.style.width = `${w}px`;
|
|
392
466
|
this.#canvas.style.height = `${h}px`;
|
|
393
467
|
this.#gl.viewport(0, 0, pw, ph);
|
|
468
|
+
if (this.#fboTexture) this.#resizeFBO(pw, ph);
|
|
394
469
|
}
|
|
395
470
|
|
|
396
471
|
#paint(): void {
|
|
397
472
|
if (this.#animationFrame !== null) cancelAnimationFrame(this.#animationFrame);
|
|
398
473
|
this.#animationFrame = requestAnimationFrame(() => {
|
|
399
474
|
const gl = this.#gl;
|
|
400
|
-
gl.useProgram(this.#program);
|
|
401
475
|
|
|
402
|
-
|
|
476
|
+
// ── Pass 1: closest-color render ────────────────────────────────────────
|
|
477
|
+
// Target: FBO when outline is active, canvas otherwise.
|
|
478
|
+
if (this.#fbo) gl.bindFramebuffer(gl.FRAMEBUFFER, this.#fbo);
|
|
403
479
|
|
|
480
|
+
gl.useProgram(this.#program);
|
|
481
|
+
gl.uniform1f(this.#uProgress, this.#position);
|
|
404
482
|
gl.activeTexture(gl.TEXTURE0);
|
|
405
483
|
gl.bindTexture(gl.TEXTURE_2D, this.#texture);
|
|
406
484
|
gl.uniform1i(this.#uPaletteTexture, 0);
|
|
407
485
|
|
|
486
|
+
gl.clearColor(0, 0, 0, 0);
|
|
487
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
408
488
|
gl.bindVertexArray(this.#vao);
|
|
409
489
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
490
|
+
|
|
491
|
+
if (!this.#fbo) { gl.bindVertexArray(null); return; }
|
|
492
|
+
|
|
493
|
+
// ── Pass 2: edge-detection using FBO texture ─────────────────────────────
|
|
494
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
495
|
+
gl.useProgram(this.#outlineProgram);
|
|
496
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
497
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#fboTexture);
|
|
498
|
+
gl.uniform1i(this.#uColorMap, 0);
|
|
499
|
+
gl.uniform1f(this.#uOutlineWidth, this.#outlineWidth);
|
|
500
|
+
gl.uniform2f(this.#uOutlineResolution, this.#canvas.width, this.#canvas.height);
|
|
501
|
+
|
|
502
|
+
gl.clearColor(0, 0, 0, 0);
|
|
503
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
504
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
410
505
|
gl.bindVertexArray(null);
|
|
411
506
|
});
|
|
412
507
|
}
|
|
@@ -436,6 +531,7 @@ export class PaletteViz {
|
|
|
436
531
|
this.#animationFrame = null;
|
|
437
532
|
}
|
|
438
533
|
const gl = this.#gl;
|
|
534
|
+
this.#destroyOutlineResources();
|
|
439
535
|
gl.deleteProgram(this.#program);
|
|
440
536
|
gl.deleteTexture(this.#texture);
|
|
441
537
|
gl.deleteBuffer(this.#quadBuffer);
|
|
@@ -541,6 +637,19 @@ export class PaletteViz {
|
|
|
541
637
|
return this.#showRaw;
|
|
542
638
|
}
|
|
543
639
|
|
|
640
|
+
set outlineWidth(value: number) {
|
|
641
|
+
const wasEnabled = this.#outlineWidth > 0;
|
|
642
|
+
this.#outlineWidth = value;
|
|
643
|
+
if ((value > 0) !== wasEnabled) {
|
|
644
|
+
if (value > 0) this.#buildOutlineResources();
|
|
645
|
+
else this.#destroyOutlineResources();
|
|
646
|
+
}
|
|
647
|
+
this.#paint();
|
|
648
|
+
}
|
|
649
|
+
get outlineWidth() {
|
|
650
|
+
return this.#outlineWidth;
|
|
651
|
+
}
|
|
652
|
+
|
|
544
653
|
static paletteToRGBA = paletteToRGBA;
|
|
545
654
|
/** @deprecated use PaletteViz.paletteToRGBA */
|
|
546
655
|
static paletteToTexture = paletteToRGBA;
|