@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 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 [...ALGORITHMS];
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
- if (resolvedAlgorithm === "monet") {
9985
- const colors = await extractMonetPalette(buffer, resolveMonetCount(count, 6));
9986
- if (colors.length > 0) {
9987
- return {
9988
- inputType,
9989
- algorithm: resolvedAlgorithm,
9990
- baseColor: rgbToHex(baseRgb),
9991
- colors
9992
- };
9993
- }
9994
- }
9995
- return buildPaletteResultFromBase(baseRgb, inputType, resolvedAlgorithm, count);
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
- if (resolvedAlgorithm === "monet") {
10001
- const sourceArgb = argbFromHex(baseHex);
10002
- return {
10002
+ return buildPaletteResult(
10003
+ {
10003
10004
  inputType,
10004
- algorithm: resolvedAlgorithm,
10005
10005
  baseColor: baseHex,
10006
- colors: buildMonetPaletteFromSourceArgb(sourceArgb, resolveMonetCount(count, 6))
10007
- };
10008
- }
10009
- const baseHsl = rgbToHsl(baseRgb);
10010
- const paletteHsl = buildPaletteHsl(baseHsl, resolvedAlgorithm, count);
10011
- const colors = paletteHsl.map((color) => rgbToHex(hslToRgb(color)));
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
- if (!algorithm) return DEFAULT_ALGORITHM;
10021
- if (!ALGORITHMS.includes(algorithm)) {
10022
- throw new Error(`Unknown algorithm: ${algorithm}`);
10014
+ const resolved = algorithm ?? DEFAULT_ALGORITHM;
10015
+ if (!algorithmRegistry.has(resolved)) {
10016
+ throw new Error(`Unknown algorithm: ${resolved}`);
10023
10017
  }
10024
- return algorithm;
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
- * Supported palette algorithms.
6
+ * Built-in palette algorithms.
7
7
  */
8
- export type PaletteAlgorithm = 'analogous' | 'complementary' | 'triadic' | 'tetradic' | 'split-complementary' | 'monochrome' | 'monet';
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';