@dilemmagx/palette 0.1.0 → 0.2.1
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 +16 -1
- package/dist/cli.js +192 -43
- package/dist/core.d.ts +46 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/dist/presets.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @dilemmagx/palette
|
|
2
2
|
|
|
3
3
|
Palette is a CLI + TypeScript toolkit that reads a single input (HEX, data URI, URL, or local path), detects its type automatically, and generates palettes.
|
|
4
|
+
It supports both built-in and custom algorithms.
|
|
4
5
|
|
|
5
6
|
## Install
|
|
6
7
|
|
|
@@ -42,7 +43,7 @@ Output:
|
|
|
42
43
|
- URL: `http://` or `https://`
|
|
43
44
|
- Path: local image path (default fallback)
|
|
44
45
|
|
|
45
|
-
## Algorithms
|
|
46
|
+
## Built-in Algorithms
|
|
46
47
|
|
|
47
48
|
- analogous
|
|
48
49
|
- complementary
|
|
@@ -54,6 +55,20 @@ Output:
|
|
|
54
55
|
|
|
55
56
|
Monet uses image quantization and color scoring inspired by Material Color Utilities.
|
|
56
57
|
|
|
58
|
+
## Custom Algorithms
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { registerAlgorithm, generatePaletteFromHex } from '@dilemmagx/palette';
|
|
62
|
+
|
|
63
|
+
registerAlgorithm('duotone', (input) => {
|
|
64
|
+
const base = input.baseColor;
|
|
65
|
+
return [base, '#000000'];
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = await generatePaletteFromHex('#ff7a18', { algorithm: 'duotone' });
|
|
69
|
+
console.log(result.colors);
|
|
70
|
+
```
|
|
71
|
+
|
|
57
72
|
## Presets
|
|
58
73
|
|
|
59
74
|
- `getGameBoyPalette()`: Returns the classic Game Boy colors (#9bbc0f, #8bac0f, #306230, #0f380f)
|
package/dist/cli.js
CHANGED
|
@@ -9933,18 +9933,20 @@ function hexFromArgb(argb) {
|
|
|
9933
9933
|
}
|
|
9934
9934
|
|
|
9935
9935
|
// src/core.ts
|
|
9936
|
-
var ALGORITHMS = [
|
|
9937
|
-
"analogous",
|
|
9938
|
-
"complementary",
|
|
9939
|
-
"triadic",
|
|
9940
|
-
"tetradic",
|
|
9941
|
-
"split-complementary",
|
|
9942
|
-
"monochrome",
|
|
9943
|
-
"monet"
|
|
9944
|
-
];
|
|
9945
9936
|
var DEFAULT_ALGORITHM = "analogous";
|
|
9937
|
+
var algorithmRegistry = /* @__PURE__ */ new Map();
|
|
9938
|
+
var builtInsRegistered = false;
|
|
9939
|
+
function registerAlgorithm(name, generator) {
|
|
9940
|
+
if (!name.trim()) {
|
|
9941
|
+
throw new Error("Algorithm name is required");
|
|
9942
|
+
}
|
|
9943
|
+
algorithmRegistry.set(name, generator);
|
|
9944
|
+
}
|
|
9945
|
+
function getAlgorithm(name) {
|
|
9946
|
+
return algorithmRegistry.get(name);
|
|
9947
|
+
}
|
|
9946
9948
|
function listAlgorithms() {
|
|
9947
|
-
return
|
|
9949
|
+
return Array.from(algorithmRegistry.keys()).sort();
|
|
9948
9950
|
}
|
|
9949
9951
|
function detectInputType(input) {
|
|
9950
9952
|
const trimmed = input.trim();
|
|
@@ -9981,47 +9983,39 @@ async function generatePaletteFromPath(filePath, options = {}) {
|
|
|
9981
9983
|
async function buildPaletteResultFromBuffer(buffer, inputType, algorithm, count) {
|
|
9982
9984
|
const resolvedAlgorithm = resolveAlgorithm(algorithm);
|
|
9983
9985
|
const baseRgb = await getAverageColor(buffer);
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
}
|
|
9994
|
-
|
|
9995
|
-
|
|
9986
|
+
const baseHex = rgbToHex(baseRgb);
|
|
9987
|
+
return buildPaletteResult(
|
|
9988
|
+
{
|
|
9989
|
+
inputType,
|
|
9990
|
+
baseColor: baseHex,
|
|
9991
|
+
baseRgb,
|
|
9992
|
+
baseHsl: rgbToHsl(baseRgb),
|
|
9993
|
+
count,
|
|
9994
|
+
buffer
|
|
9995
|
+
},
|
|
9996
|
+
resolvedAlgorithm
|
|
9997
|
+
);
|
|
9996
9998
|
}
|
|
9997
|
-
function buildPaletteResultFromBase(baseRgb, inputType, algorithm, count) {
|
|
9999
|
+
async function buildPaletteResultFromBase(baseRgb, inputType, algorithm, count) {
|
|
9998
10000
|
const resolvedAlgorithm = resolveAlgorithm(algorithm);
|
|
9999
10001
|
const baseHex = rgbToHex(baseRgb);
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
return {
|
|
10002
|
+
return buildPaletteResult(
|
|
10003
|
+
{
|
|
10003
10004
|
inputType,
|
|
10004
|
-
algorithm: resolvedAlgorithm,
|
|
10005
10005
|
baseColor: baseHex,
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
return {
|
|
10013
|
-
inputType,
|
|
10014
|
-
algorithm: resolvedAlgorithm,
|
|
10015
|
-
baseColor: baseHex,
|
|
10016
|
-
colors
|
|
10017
|
-
};
|
|
10006
|
+
baseRgb,
|
|
10007
|
+
baseHsl: rgbToHsl(baseRgb),
|
|
10008
|
+
count
|
|
10009
|
+
},
|
|
10010
|
+
resolvedAlgorithm
|
|
10011
|
+
);
|
|
10018
10012
|
}
|
|
10019
10013
|
function resolveAlgorithm(algorithm) {
|
|
10020
|
-
|
|
10021
|
-
if (!
|
|
10022
|
-
throw new Error(`Unknown algorithm: ${
|
|
10014
|
+
const resolved = algorithm ?? DEFAULT_ALGORITHM;
|
|
10015
|
+
if (!algorithmRegistry.has(resolved)) {
|
|
10016
|
+
throw new Error(`Unknown algorithm: ${resolved}`);
|
|
10023
10017
|
}
|
|
10024
|
-
return
|
|
10018
|
+
return resolved;
|
|
10025
10019
|
}
|
|
10026
10020
|
function buildPaletteHsl(base, algorithm, count) {
|
|
10027
10021
|
const hue = normalizeHue(base.h);
|
|
@@ -10047,6 +10041,56 @@ function buildPaletteHsl(base, algorithm, count) {
|
|
|
10047
10041
|
}
|
|
10048
10042
|
return buildMonochrome(hue, sat, light, resolveCount(count, 6));
|
|
10049
10043
|
}
|
|
10044
|
+
function buildHslPaletteColors(input, algorithm) {
|
|
10045
|
+
const paletteHsl = buildPaletteHsl(input.baseHsl, algorithm, input.count);
|
|
10046
|
+
return paletteHsl.map((color) => rgbToHex(hslToRgb(color)));
|
|
10047
|
+
}
|
|
10048
|
+
function registerBuiltInAlgorithms() {
|
|
10049
|
+
if (builtInsRegistered) return;
|
|
10050
|
+
builtInsRegistered = true;
|
|
10051
|
+
registerAlgorithm("analogous", (input) => buildHslPaletteColors(input, "analogous"));
|
|
10052
|
+
registerAlgorithm("complementary", (input) => buildHslPaletteColors(input, "complementary"));
|
|
10053
|
+
registerAlgorithm("triadic", (input) => buildHslPaletteColors(input, "triadic"));
|
|
10054
|
+
registerAlgorithm("tetradic", (input) => buildHslPaletteColors(input, "tetradic"));
|
|
10055
|
+
registerAlgorithm(
|
|
10056
|
+
"split-complementary",
|
|
10057
|
+
(input) => buildHslPaletteColors(input, "split-complementary")
|
|
10058
|
+
);
|
|
10059
|
+
registerAlgorithm("monochrome", (input) => buildHslPaletteColors(input, "monochrome"));
|
|
10060
|
+
registerAlgorithm("monet", async (input) => {
|
|
10061
|
+
if (input.buffer) {
|
|
10062
|
+
const colors = await extractMonetPalette(input.buffer, resolveMonetCount(input.count, 6));
|
|
10063
|
+
if (colors.length > 0) {
|
|
10064
|
+
return colors;
|
|
10065
|
+
}
|
|
10066
|
+
}
|
|
10067
|
+
const sourceArgb = argbFromHex(input.baseColor);
|
|
10068
|
+
return buildMonetPaletteFromSourceArgb(sourceArgb, resolveMonetCount(input.count, 6));
|
|
10069
|
+
});
|
|
10070
|
+
registerAlgorithm("dominant", async (input) => {
|
|
10071
|
+
if (input.buffer) {
|
|
10072
|
+
const colors = await extractDominantPalette(input.buffer, resolveCount(input.count, 8));
|
|
10073
|
+
if (colors.length > 0) {
|
|
10074
|
+
return colors;
|
|
10075
|
+
}
|
|
10076
|
+
}
|
|
10077
|
+
return buildHslPaletteColors(input, "monochrome");
|
|
10078
|
+
});
|
|
10079
|
+
}
|
|
10080
|
+
registerBuiltInAlgorithms();
|
|
10081
|
+
async function buildPaletteResult(input, algorithm) {
|
|
10082
|
+
const generator = getAlgorithm(algorithm);
|
|
10083
|
+
if (!generator) {
|
|
10084
|
+
throw new Error(`Unknown algorithm: ${algorithm}`);
|
|
10085
|
+
}
|
|
10086
|
+
const colors = await generator(input);
|
|
10087
|
+
return {
|
|
10088
|
+
inputType: input.inputType,
|
|
10089
|
+
algorithm,
|
|
10090
|
+
baseColor: input.baseColor,
|
|
10091
|
+
colors
|
|
10092
|
+
};
|
|
10093
|
+
}
|
|
10050
10094
|
function isHex(value) {
|
|
10051
10095
|
return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
|
10052
10096
|
}
|
|
@@ -10164,6 +10208,70 @@ async function extractMonetPalette(buffer, count) {
|
|
|
10164
10208
|
const sourceArgb = ranked[0] ?? sampled[0];
|
|
10165
10209
|
return buildMonetPaletteFromSourceArgb(sourceArgb, count);
|
|
10166
10210
|
}
|
|
10211
|
+
async function extractDominantPalette(buffer, count) {
|
|
10212
|
+
const { data, info } = await (0, import_sharp.default)(buffer).resize({ width: 96, height: 96, fit: "inside" }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
10213
|
+
const step = info.channels;
|
|
10214
|
+
const pixels = [];
|
|
10215
|
+
for (let i = 0; i < data.length; i += step) {
|
|
10216
|
+
const r = data[i] ?? 0;
|
|
10217
|
+
const g = data[i + 1] ?? 0;
|
|
10218
|
+
const b = data[i + 2] ?? 0;
|
|
10219
|
+
pixels.push(argbFromRgb(r, g, b));
|
|
10220
|
+
}
|
|
10221
|
+
if (pixels.length === 0) return [];
|
|
10222
|
+
const sampled = downsamplePixels(pixels, 2400);
|
|
10223
|
+
const quantized = QuantizerCelebi.quantize(sampled, 128);
|
|
10224
|
+
const entries = Array.from(quantized.entries()).map(([argb, population]) => ({
|
|
10225
|
+
rgb: argbToRgb(argb),
|
|
10226
|
+
count: population
|
|
10227
|
+
}));
|
|
10228
|
+
if (entries.length === 0) return [];
|
|
10229
|
+
entries.sort((a, b) => b.count - a.count);
|
|
10230
|
+
const totalCount = entries.reduce((sum, entry) => sum + entry.count, 0);
|
|
10231
|
+
const coverageTarget = 0.95;
|
|
10232
|
+
const candidateEntries = [];
|
|
10233
|
+
let coverage = 0;
|
|
10234
|
+
for (const entry of entries) {
|
|
10235
|
+
if (candidateEntries.length >= count) {
|
|
10236
|
+
candidateEntries.push(entry);
|
|
10237
|
+
continue;
|
|
10238
|
+
}
|
|
10239
|
+
candidateEntries.push(entry);
|
|
10240
|
+
coverage += entry.count / Math.max(1, totalCount);
|
|
10241
|
+
if (coverage >= coverageTarget) {
|
|
10242
|
+
break;
|
|
10243
|
+
}
|
|
10244
|
+
}
|
|
10245
|
+
if (candidateEntries.length < count) {
|
|
10246
|
+
candidateEntries.splice(0, candidateEntries.length, ...entries);
|
|
10247
|
+
}
|
|
10248
|
+
if (entries.length <= count) {
|
|
10249
|
+
return entries.map((entry) => rgbToHex(entry.rgb));
|
|
10250
|
+
}
|
|
10251
|
+
const maxCount = Math.max(1, entries[0]?.count ?? 1);
|
|
10252
|
+
const seeds = [candidateEntries[0].rgb];
|
|
10253
|
+
while (seeds.length < count) {
|
|
10254
|
+
let best = null;
|
|
10255
|
+
let bestScore = -1;
|
|
10256
|
+
for (const entry of candidateEntries) {
|
|
10257
|
+
if (seeds.some((seed) => rgbDistanceSq(seed, entry.rgb) === 0)) continue;
|
|
10258
|
+
let minDist = Number.POSITIVE_INFINITY;
|
|
10259
|
+
for (const seed of seeds) {
|
|
10260
|
+
minDist = Math.min(minDist, rgbDistanceSq(seed, entry.rgb));
|
|
10261
|
+
}
|
|
10262
|
+
const weight = entry.count / maxCount;
|
|
10263
|
+
const score = minDist * (0.2 + Math.sqrt(weight));
|
|
10264
|
+
if (score > bestScore) {
|
|
10265
|
+
bestScore = score;
|
|
10266
|
+
best = entry;
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
if (!best) break;
|
|
10270
|
+
seeds.push(best.rgb);
|
|
10271
|
+
}
|
|
10272
|
+
const refined = refineCentroids(seeds, entries);
|
|
10273
|
+
return refined.slice(0, count).map((color) => rgbToHex(color));
|
|
10274
|
+
}
|
|
10167
10275
|
function resolveCount(count, fallback) {
|
|
10168
10276
|
if (!count) return fallback;
|
|
10169
10277
|
return Math.max(2, Math.min(12, Math.floor(count)));
|
|
@@ -10264,6 +10372,47 @@ async function fetchBuffer(url) {
|
|
|
10264
10372
|
function clamp(value, min, max) {
|
|
10265
10373
|
return Math.min(max, Math.max(min, value));
|
|
10266
10374
|
}
|
|
10375
|
+
function rgbDistanceSq(a, b) {
|
|
10376
|
+
const dr = a.r - b.r;
|
|
10377
|
+
const dg = a.g - b.g;
|
|
10378
|
+
const db = a.b - b.b;
|
|
10379
|
+
return dr * dr + dg * dg + db * db;
|
|
10380
|
+
}
|
|
10381
|
+
function argbToRgb(argb) {
|
|
10382
|
+
return {
|
|
10383
|
+
r: argb >>> 16 & 255,
|
|
10384
|
+
g: argb >>> 8 & 255,
|
|
10385
|
+
b: argb & 255
|
|
10386
|
+
};
|
|
10387
|
+
}
|
|
10388
|
+
function refineCentroids(seeds, entries) {
|
|
10389
|
+
const accum = seeds.map(() => ({ r: 0, g: 0, b: 0, count: 0 }));
|
|
10390
|
+
for (const entry of entries) {
|
|
10391
|
+
let bestIndex = 0;
|
|
10392
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
10393
|
+
for (let i = 0; i < seeds.length; i += 1) {
|
|
10394
|
+
const dist = rgbDistanceSq(seeds[i], entry.rgb);
|
|
10395
|
+
if (dist < bestDist) {
|
|
10396
|
+
bestDist = dist;
|
|
10397
|
+
bestIndex = i;
|
|
10398
|
+
}
|
|
10399
|
+
}
|
|
10400
|
+
const target = accum[bestIndex];
|
|
10401
|
+
target.r += entry.rgb.r * entry.count;
|
|
10402
|
+
target.g += entry.rgb.g * entry.count;
|
|
10403
|
+
target.b += entry.rgb.b * entry.count;
|
|
10404
|
+
target.count += entry.count;
|
|
10405
|
+
}
|
|
10406
|
+
return seeds.map((seed, index) => {
|
|
10407
|
+
const bucket = accum[index];
|
|
10408
|
+
if (!bucket || bucket.count === 0) return seed;
|
|
10409
|
+
return {
|
|
10410
|
+
r: clamp(bucket.r / bucket.count, 0, 255),
|
|
10411
|
+
g: clamp(bucket.g / bucket.count, 0, 255),
|
|
10412
|
+
b: clamp(bucket.b / bucket.count, 0, 255)
|
|
10413
|
+
};
|
|
10414
|
+
});
|
|
10415
|
+
}
|
|
10267
10416
|
function normalizeHue(hue) {
|
|
10268
10417
|
const normalized = hue % 360;
|
|
10269
10418
|
return normalized < 0 ? normalized + 360 : normalized;
|
package/dist/core.d.ts
CHANGED
|
@@ -3,9 +3,30 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type InputType = 'hex' | 'datauri' | 'url' | 'path';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Built-in palette algorithms.
|
|
7
7
|
*/
|
|
8
|
-
export type
|
|
8
|
+
export type BuiltInPaletteAlgorithm = 'analogous' | 'complementary' | 'triadic' | 'tetradic' | 'split-complementary' | 'monochrome' | 'monet' | 'dominant';
|
|
9
|
+
/**
|
|
10
|
+
* Palette algorithm name, including custom algorithms.
|
|
11
|
+
*/
|
|
12
|
+
export type PaletteAlgorithm = BuiltInPaletteAlgorithm | (string & {
|
|
13
|
+
__paletteAlgorithm?: never;
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Input for palette generators.
|
|
17
|
+
*/
|
|
18
|
+
export interface PaletteGeneratorInput {
|
|
19
|
+
inputType: InputType;
|
|
20
|
+
baseColor: string;
|
|
21
|
+
baseRgb: RgbColor;
|
|
22
|
+
baseHsl: HslColor;
|
|
23
|
+
count?: number;
|
|
24
|
+
buffer?: Buffer;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Palette generator contract.
|
|
28
|
+
*/
|
|
29
|
+
export type PaletteGenerator = (input: PaletteGeneratorInput) => Promise<string[]> | string[];
|
|
9
30
|
/**
|
|
10
31
|
* Options used when generating palettes.
|
|
11
32
|
*/
|
|
@@ -40,6 +61,28 @@ export interface PaletteResult {
|
|
|
40
61
|
*/
|
|
41
62
|
colors: string[];
|
|
42
63
|
}
|
|
64
|
+
interface RgbColor {
|
|
65
|
+
r: number;
|
|
66
|
+
g: number;
|
|
67
|
+
b: number;
|
|
68
|
+
}
|
|
69
|
+
interface HslColor {
|
|
70
|
+
h: number;
|
|
71
|
+
s: number;
|
|
72
|
+
l: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Registers a custom palette algorithm.
|
|
76
|
+
*/
|
|
77
|
+
export declare function registerAlgorithm(name: string, generator: PaletteGenerator): void;
|
|
78
|
+
/**
|
|
79
|
+
* Unregisters a palette algorithm.
|
|
80
|
+
*/
|
|
81
|
+
export declare function unregisterAlgorithm(name: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Returns a registered algorithm by name.
|
|
84
|
+
*/
|
|
85
|
+
export declare function getAlgorithm(name: string): PaletteGenerator | undefined;
|
|
43
86
|
/**
|
|
44
87
|
* Returns the list of supported palette algorithms.
|
|
45
88
|
*/
|
|
@@ -79,3 +122,4 @@ export declare function generatePaletteFromUrl(url: string, options?: PaletteOpt
|
|
|
79
122
|
* @param options Optional palette options.
|
|
80
123
|
*/
|
|
81
124
|
export declare function generatePaletteFromPath(filePath: string, options?: PaletteOptions): Promise<PaletteResult>;
|
|
125
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { InputType, PaletteAlgorithm, PaletteOptions, PaletteResult } from './core';
|
|
2
|
-
export { detectInputType, generatePalette, generatePaletteFromHex, generatePaletteFromDataUri, generatePaletteFromUrl, generatePaletteFromPath, listAlgorithms, } from './core';
|
|
1
|
+
export type { BuiltInPaletteAlgorithm, InputType, PaletteAlgorithm, PaletteGenerator, PaletteGeneratorInput, PaletteOptions, PaletteResult, } from './core';
|
|
2
|
+
export { detectInputType, generatePalette, generatePaletteFromHex, generatePaletteFromDataUri, generatePaletteFromUrl, generatePaletteFromPath, getAlgorithm, listAlgorithms, registerAlgorithm, unregisterAlgorithm, } from './core';
|
|
3
3
|
export { getGameBoyPalette } from './presets';
|
|
4
4
|
export type { RgbaColor } from './presets';
|