@dilemmagx/palette 0.2.0 → 0.2.2
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/dist/cli.js +121 -0
- package/dist/core.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10067,6 +10067,15 @@ function registerBuiltInAlgorithms() {
|
|
|
10067
10067
|
const sourceArgb = argbFromHex(input.baseColor);
|
|
10068
10068
|
return buildMonetPaletteFromSourceArgb(sourceArgb, resolveMonetCount(input.count, 6));
|
|
10069
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
|
+
});
|
|
10070
10079
|
}
|
|
10071
10080
|
registerBuiltInAlgorithms();
|
|
10072
10081
|
async function buildPaletteResult(input, algorithm) {
|
|
@@ -10199,6 +10208,74 @@ async function extractMonetPalette(buffer, count) {
|
|
|
10199
10208
|
const sourceArgb = ranked[0] ?? sampled[0];
|
|
10200
10209
|
return buildMonetPaletteFromSourceArgb(sourceArgb, count);
|
|
10201
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) => {
|
|
10230
|
+
const countDelta = b.count - a.count;
|
|
10231
|
+
if (countDelta !== 0) return countDelta;
|
|
10232
|
+
return rgbKey(a.rgb) - rgbKey(b.rgb);
|
|
10233
|
+
});
|
|
10234
|
+
const totalCount = entries.reduce((sum, entry) => sum + entry.count, 0);
|
|
10235
|
+
const coverageTarget = 0.95;
|
|
10236
|
+
const candidateEntries = [];
|
|
10237
|
+
let coverage = 0;
|
|
10238
|
+
for (const entry of entries) {
|
|
10239
|
+
if (candidateEntries.length >= count) {
|
|
10240
|
+
candidateEntries.push(entry);
|
|
10241
|
+
continue;
|
|
10242
|
+
}
|
|
10243
|
+
candidateEntries.push(entry);
|
|
10244
|
+
coverage += entry.count / Math.max(1, totalCount);
|
|
10245
|
+
if (coverage >= coverageTarget) {
|
|
10246
|
+
break;
|
|
10247
|
+
}
|
|
10248
|
+
}
|
|
10249
|
+
if (candidateEntries.length < count) {
|
|
10250
|
+
candidateEntries.splice(0, candidateEntries.length, ...entries);
|
|
10251
|
+
}
|
|
10252
|
+
if (entries.length <= count) {
|
|
10253
|
+
return entries.map((entry) => rgbToHex(entry.rgb));
|
|
10254
|
+
}
|
|
10255
|
+
const maxCount = Math.max(1, entries[0]?.count ?? 1);
|
|
10256
|
+
const seeds = [candidateEntries[0].rgb];
|
|
10257
|
+
while (seeds.length < count) {
|
|
10258
|
+
let best = null;
|
|
10259
|
+
let bestScore = -1;
|
|
10260
|
+
for (const entry of candidateEntries) {
|
|
10261
|
+
if (seeds.some((seed) => rgbDistanceSq(seed, entry.rgb) === 0)) continue;
|
|
10262
|
+
let minDist = Number.POSITIVE_INFINITY;
|
|
10263
|
+
for (const seed of seeds) {
|
|
10264
|
+
minDist = Math.min(minDist, rgbDistanceSq(seed, entry.rgb));
|
|
10265
|
+
}
|
|
10266
|
+
const weight = entry.count / maxCount;
|
|
10267
|
+
const score = minDist * (0.2 + Math.sqrt(weight));
|
|
10268
|
+
if (score > bestScore + 1e-9 || Math.abs(score - bestScore) <= 1e-9 && best !== null && rgbKey(entry.rgb) < rgbKey(best.rgb)) {
|
|
10269
|
+
bestScore = score;
|
|
10270
|
+
best = entry;
|
|
10271
|
+
}
|
|
10272
|
+
}
|
|
10273
|
+
if (!best) break;
|
|
10274
|
+
seeds.push(best.rgb);
|
|
10275
|
+
}
|
|
10276
|
+
const refined = refineCentroids(seeds, entries);
|
|
10277
|
+
return refined.slice(0, count).map((color) => rgbToHex(color));
|
|
10278
|
+
}
|
|
10202
10279
|
function resolveCount(count, fallback) {
|
|
10203
10280
|
if (!count) return fallback;
|
|
10204
10281
|
return Math.max(2, Math.min(12, Math.floor(count)));
|
|
@@ -10299,6 +10376,50 @@ async function fetchBuffer(url) {
|
|
|
10299
10376
|
function clamp(value, min, max) {
|
|
10300
10377
|
return Math.min(max, Math.max(min, value));
|
|
10301
10378
|
}
|
|
10379
|
+
function rgbDistanceSq(a, b) {
|
|
10380
|
+
const dr = a.r - b.r;
|
|
10381
|
+
const dg = a.g - b.g;
|
|
10382
|
+
const db = a.b - b.b;
|
|
10383
|
+
return dr * dr + dg * dg + db * db;
|
|
10384
|
+
}
|
|
10385
|
+
function rgbKey(color) {
|
|
10386
|
+
return (color.r << 16) + (color.g << 8) + color.b;
|
|
10387
|
+
}
|
|
10388
|
+
function argbToRgb(argb) {
|
|
10389
|
+
return {
|
|
10390
|
+
r: argb >>> 16 & 255,
|
|
10391
|
+
g: argb >>> 8 & 255,
|
|
10392
|
+
b: argb & 255
|
|
10393
|
+
};
|
|
10394
|
+
}
|
|
10395
|
+
function refineCentroids(seeds, entries) {
|
|
10396
|
+
const accum = seeds.map(() => ({ r: 0, g: 0, b: 0, count: 0 }));
|
|
10397
|
+
for (const entry of entries) {
|
|
10398
|
+
let bestIndex = 0;
|
|
10399
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
10400
|
+
for (let i = 0; i < seeds.length; i += 1) {
|
|
10401
|
+
const dist = rgbDistanceSq(seeds[i], entry.rgb);
|
|
10402
|
+
if (dist < bestDist) {
|
|
10403
|
+
bestDist = dist;
|
|
10404
|
+
bestIndex = i;
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
const target = accum[bestIndex];
|
|
10408
|
+
target.r += entry.rgb.r * entry.count;
|
|
10409
|
+
target.g += entry.rgb.g * entry.count;
|
|
10410
|
+
target.b += entry.rgb.b * entry.count;
|
|
10411
|
+
target.count += entry.count;
|
|
10412
|
+
}
|
|
10413
|
+
return seeds.map((seed, index) => {
|
|
10414
|
+
const bucket = accum[index];
|
|
10415
|
+
if (!bucket || bucket.count === 0) return seed;
|
|
10416
|
+
return {
|
|
10417
|
+
r: clamp(bucket.r / bucket.count, 0, 255),
|
|
10418
|
+
g: clamp(bucket.g / bucket.count, 0, 255),
|
|
10419
|
+
b: clamp(bucket.b / bucket.count, 0, 255)
|
|
10420
|
+
};
|
|
10421
|
+
});
|
|
10422
|
+
}
|
|
10302
10423
|
function normalizeHue(hue) {
|
|
10303
10424
|
const normalized = hue % 360;
|
|
10304
10425
|
return normalized < 0 ? normalized + 360 : normalized;
|
package/dist/core.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type InputType = 'hex' | 'datauri' | 'url' | 'path';
|
|
|
5
5
|
/**
|
|
6
6
|
* Built-in palette algorithms.
|
|
7
7
|
*/
|
|
8
|
-
export type BuiltInPaletteAlgorithm = 'analogous' | 'complementary' | 'triadic' | 'tetradic' | 'split-complementary' | 'monochrome' | 'monet';
|
|
8
|
+
export type BuiltInPaletteAlgorithm = 'analogous' | 'complementary' | 'triadic' | 'tetradic' | 'split-complementary' | 'monochrome' | 'monet' | 'dominant';
|
|
9
9
|
/**
|
|
10
10
|
* Palette algorithm name, including custom algorithms.
|
|
11
11
|
*/
|