@dloizides/color-science 0.1.0 → 0.2.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/dist/index.d.mts CHANGED
@@ -55,6 +55,26 @@ declare function simulateCvd(rgb: Rgb, type: CvdType): Rgb;
55
55
  /** Smallest deltaEOk between two colours across normal vision and all three CVD types. */
56
56
  declare function visionDistance(x: Rgb, y: Rgb): number;
57
57
 
58
+ /** WCAG 2 non-text contrast floor a bead colour must reach against what sits behind it. */
59
+ declare const MIN_BEAD_CONTRAST = 3;
60
+ /** Under-3:1 shares this close count as equal, and delta-E decides. */
61
+ declare const BACKDROP_TIE_MARGIN = 0.01;
62
+ interface BeadCount {
63
+ readonly hex: string;
64
+ readonly count: number;
65
+ }
66
+ interface BackdropScore {
67
+ readonly hex: string;
68
+ readonly underShare: number;
69
+ readonly medianDeltaE: number;
70
+ }
71
+ /** WCAG 2 contrast ratio of two hex colours, 1 to 21. */
72
+ declare function contrastRatio(x: string, y: string): number;
73
+ /** Measures one backdrop against a pattern's beads, weighted by how many of each bead it uses. */
74
+ declare function scoreBackdrop(beads: readonly BeadCount[], backdropHex: string): BackdropScore;
75
+ /** Lowest under-3:1 share wins; inside the margin the higher median delta-E wins; first wins a tie; null when empty. */
76
+ declare function pickBackdrop(beads: readonly BeadCount[], backdrops: readonly string[], margin?: number): BackdropScore | null;
77
+
58
78
  /** Euclidean distance in OKLab, the CSS Color 4 deltaEOK. About 0.02 is just noticeable. */
59
79
  declare function deltaEOk(x: Oklab, y: Oklab): number;
60
80
  /** Index of the palette entry nearest the target; first wins a tie, -1 when empty. */
@@ -70,4 +90,4 @@ declare const MIN_HARMONY_DISTANCE = 0.1;
70
90
  */
71
91
  declare function harmony(baseHex: string, count: HarmonyCount): string[];
72
92
 
73
- export { CvdType, type HarmonyCount, type LinearRgb, MIN_HARMONY_DISTANCE, type Oklab, type Oklch, type Rgb, deltaEOk, fitChromaToSrgb, harmony, hexToOklab, hexToRgb, isInSrgbGamut, linearToOklab, linearToRgb, nearestInPalette, oklabToHex, oklabToLinear, oklabToOklch, oklchToOklab, rgbToHex, rgbToLinear, simulateCvd, visionDistance };
93
+ export { BACKDROP_TIE_MARGIN, type BackdropScore, type BeadCount, CvdType, type HarmonyCount, type LinearRgb, MIN_BEAD_CONTRAST, MIN_HARMONY_DISTANCE, type Oklab, type Oklch, type Rgb, contrastRatio, deltaEOk, fitChromaToSrgb, harmony, hexToOklab, hexToRgb, isInSrgbGamut, linearToOklab, linearToRgb, nearestInPalette, oklabToHex, oklabToLinear, oklabToOklch, oklchToOklab, pickBackdrop, rgbToHex, rgbToLinear, scoreBackdrop, simulateCvd, visionDistance };
package/dist/index.d.ts CHANGED
@@ -55,6 +55,26 @@ declare function simulateCvd(rgb: Rgb, type: CvdType): Rgb;
55
55
  /** Smallest deltaEOk between two colours across normal vision and all three CVD types. */
56
56
  declare function visionDistance(x: Rgb, y: Rgb): number;
57
57
 
58
+ /** WCAG 2 non-text contrast floor a bead colour must reach against what sits behind it. */
59
+ declare const MIN_BEAD_CONTRAST = 3;
60
+ /** Under-3:1 shares this close count as equal, and delta-E decides. */
61
+ declare const BACKDROP_TIE_MARGIN = 0.01;
62
+ interface BeadCount {
63
+ readonly hex: string;
64
+ readonly count: number;
65
+ }
66
+ interface BackdropScore {
67
+ readonly hex: string;
68
+ readonly underShare: number;
69
+ readonly medianDeltaE: number;
70
+ }
71
+ /** WCAG 2 contrast ratio of two hex colours, 1 to 21. */
72
+ declare function contrastRatio(x: string, y: string): number;
73
+ /** Measures one backdrop against a pattern's beads, weighted by how many of each bead it uses. */
74
+ declare function scoreBackdrop(beads: readonly BeadCount[], backdropHex: string): BackdropScore;
75
+ /** Lowest under-3:1 share wins; inside the margin the higher median delta-E wins; first wins a tie; null when empty. */
76
+ declare function pickBackdrop(beads: readonly BeadCount[], backdrops: readonly string[], margin?: number): BackdropScore | null;
77
+
58
78
  /** Euclidean distance in OKLab, the CSS Color 4 deltaEOK. About 0.02 is just noticeable. */
59
79
  declare function deltaEOk(x: Oklab, y: Oklab): number;
60
80
  /** Index of the palette entry nearest the target; first wins a tie, -1 when empty. */
@@ -70,4 +90,4 @@ declare const MIN_HARMONY_DISTANCE = 0.1;
70
90
  */
71
91
  declare function harmony(baseHex: string, count: HarmonyCount): string[];
72
92
 
73
- export { CvdType, type HarmonyCount, type LinearRgb, MIN_HARMONY_DISTANCE, type Oklab, type Oklch, type Rgb, deltaEOk, fitChromaToSrgb, harmony, hexToOklab, hexToRgb, isInSrgbGamut, linearToOklab, linearToRgb, nearestInPalette, oklabToHex, oklabToLinear, oklabToOklch, oklchToOklab, rgbToHex, rgbToLinear, simulateCvd, visionDistance };
93
+ export { BACKDROP_TIE_MARGIN, type BackdropScore, type BeadCount, CvdType, type HarmonyCount, type LinearRgb, MIN_BEAD_CONTRAST, MIN_HARMONY_DISTANCE, type Oklab, type Oklch, type Rgb, contrastRatio, deltaEOk, fitChromaToSrgb, harmony, hexToOklab, hexToRgb, isInSrgbGamut, linearToOklab, linearToRgb, nearestInPalette, oklabToHex, oklabToLinear, oklabToOklch, oklchToOklab, pickBackdrop, rgbToHex, rgbToLinear, scoreBackdrop, simulateCvd, visionDistance };
package/dist/index.js CHANGED
@@ -186,6 +186,54 @@ function visionDistance(x, y) {
186
186
  );
187
187
  }
188
188
 
189
+ // src/backdrop.ts
190
+ var RED_WEIGHT = 0.2126;
191
+ var GREEN_WEIGHT = 0.7152;
192
+ var BLUE_WEIGHT = 0.0722;
193
+ var FLARE = 0.05;
194
+ var HALF2 = 0.5;
195
+ var MIN_BEAD_CONTRAST = 3;
196
+ var BACKDROP_TIE_MARGIN = 0.01;
197
+ function luminance(hex) {
198
+ const { r, g, b } = rgbToLinear(hexToRgb(hex));
199
+ return RED_WEIGHT * r + GREEN_WEIGHT * g + BLUE_WEIGHT * b;
200
+ }
201
+ function weightedMedian(entries, total) {
202
+ const sorted = [...entries].sort((x, y) => x.value - y.value);
203
+ let seen = 0;
204
+ for (const entry of sorted) {
205
+ seen += entry.count;
206
+ if (seen >= total * HALF2) {
207
+ return entry.value;
208
+ }
209
+ }
210
+ return 0;
211
+ }
212
+ function contrastRatio(x, y) {
213
+ const lumX = luminance(x);
214
+ const lumY = luminance(y);
215
+ return (Math.max(lumX, lumY) + FLARE) / (Math.min(lumX, lumY) + FLARE);
216
+ }
217
+ function scoreBackdrop(beads, backdropHex) {
218
+ const counted = beads.filter((bead) => bead.count > 0);
219
+ const total = counted.reduce((sum, bead) => sum + bead.count, 0);
220
+ if (total === 0) {
221
+ return { hex: backdropHex, underShare: 0, medianDeltaE: 0 };
222
+ }
223
+ const backdrop = hexToOklab(backdropHex);
224
+ const under = counted.filter((bead) => contrastRatio(bead.hex, backdropHex) < MIN_BEAD_CONTRAST).reduce((sum, bead) => sum + bead.count, 0);
225
+ const distances = counted.map((bead) => ({ value: deltaEOk(hexToOklab(bead.hex), backdrop), count: bead.count }));
226
+ return { hex: backdropHex, underShare: under / total, medianDeltaE: weightedMedian(distances, total) };
227
+ }
228
+ function pickBackdrop(beads, backdrops, margin = BACKDROP_TIE_MARGIN) {
229
+ const scores = backdrops.map((hex) => scoreBackdrop(beads, hex));
230
+ if (scores.length === 0) {
231
+ return null;
232
+ }
233
+ const lowest = Math.min(...scores.map((score) => score.underShare));
234
+ return scores.filter((score) => score.underShare <= lowest + margin).reduce((best, score) => score.medianDeltaE > best.medianDeltaE ? score : best);
235
+ }
236
+
189
237
  // src/harmony.ts
190
238
  var MIN_HARMONY_DISTANCE = 0.1;
191
239
  var HUE_OFFSETS = {
@@ -229,8 +277,11 @@ function harmony(baseHex, count) {
229
277
  return colours.map(rgbToHex);
230
278
  }
231
279
 
280
+ exports.BACKDROP_TIE_MARGIN = BACKDROP_TIE_MARGIN;
232
281
  exports.CvdType = CvdType;
282
+ exports.MIN_BEAD_CONTRAST = MIN_BEAD_CONTRAST;
233
283
  exports.MIN_HARMONY_DISTANCE = MIN_HARMONY_DISTANCE;
284
+ exports.contrastRatio = contrastRatio;
234
285
  exports.deltaEOk = deltaEOk;
235
286
  exports.fitChromaToSrgb = fitChromaToSrgb;
236
287
  exports.harmony = harmony;
@@ -244,8 +295,10 @@ exports.oklabToHex = oklabToHex;
244
295
  exports.oklabToLinear = oklabToLinear;
245
296
  exports.oklabToOklch = oklabToOklch;
246
297
  exports.oklchToOklab = oklchToOklab;
298
+ exports.pickBackdrop = pickBackdrop;
247
299
  exports.rgbToHex = rgbToHex;
248
300
  exports.rgbToLinear = rgbToLinear;
301
+ exports.scoreBackdrop = scoreBackdrop;
249
302
  exports.simulateCvd = simulateCvd;
250
303
  exports.visionDistance = visionDistance;
251
304
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/convert.ts","../src/CvdType.ts","../src/deltaE.ts","../src/cvd.ts","../src/harmony.ts"],"names":["CvdType"],"mappings":"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","file":"index.js","sourcesContent":["import type { LinearRgb, Oklab, Oklch, Rgb } from './types';\n\nconst BYTE_MAX = 255;\nconst HEX_RADIX = 16;\nconst HEX_BYTE_WIDTH = 2;\nconst SHORT_HEX_LENGTH = 3;\nconst RED_SHIFT = 16;\nconst GREEN_SHIFT = 8;\nconst HEX_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\n\nconst SRGB_ENCODED_CUTOFF = 0.04045;\nconst SRGB_LINEAR_CUTOFF = 0.0031308;\nconst SRGB_LINEAR_SLOPE = 12.92;\nconst SRGB_OFFSET = 0.055;\nconst SRGB_SCALE = 1.055;\nconst SRGB_GAMMA = 2.4;\n\nconst DEGREES_PER_TURN = 360;\nconst DEGREES_PER_HALF_TURN = 180;\nconst GAMUT_EPSILON = 1e-4;\nconst CHROMA_SEARCH_STEPS = 20;\nconst HALF = 0.5;\n\nfunction clampByte(value: number): number {\n return Math.min(BYTE_MAX, Math.max(0, Math.round(value)));\n}\n\nfunction toHexByte(value: number): string {\n return clampByte(value).toString(HEX_RADIX).padStart(HEX_BYTE_WIDTH, '0');\n}\n\nfunction byteToLinear(byte: number): number {\n const encoded = byte / BYTE_MAX;\n return encoded <= SRGB_ENCODED_CUTOFF\n ? encoded / SRGB_LINEAR_SLOPE\n : ((encoded + SRGB_OFFSET) / SRGB_SCALE) ** SRGB_GAMMA;\n}\n\nfunction linearToByte(linear: number): number {\n const encoded =\n linear <= SRGB_LINEAR_CUTOFF\n ? linear * SRGB_LINEAR_SLOPE\n : SRGB_SCALE * linear ** (1 / SRGB_GAMMA) - SRGB_OFFSET;\n return clampByte(encoded * BYTE_MAX);\n}\n\nfunction isUnitChannel(channel: number): boolean {\n return channel >= -GAMUT_EPSILON && channel <= 1 + GAMUT_EPSILON;\n}\n\n/** Parses `#rgb` or `#rrggbb`, any case. Throws RangeError on anything else. */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX_PATTERN.test(hex)) {\n throw new RangeError(`Not a hex colour: ${hex}`);\n }\n const digits = hex.slice(1);\n const full = digits.length === SHORT_HEX_LENGTH ? digits.replace(/./g, '$&$&') : digits;\n const value = parseInt(full, HEX_RADIX);\n return {\n r: (value >> RED_SHIFT) & BYTE_MAX,\n g: (value >> GREEN_SHIFT) & BYTE_MAX,\n b: value & BYTE_MAX,\n };\n}\n\n/** Formats as lowercase `#rrggbb`, rounding and clamping each channel. */\nexport function rgbToHex(rgb: Rgb): string {\n return `#${toHexByte(rgb.r)}${toHexByte(rgb.g)}${toHexByte(rgb.b)}`;\n}\n\n/** Decodes sRGB bytes to linear light, 0 to 1. */\nexport function rgbToLinear(rgb: Rgb): LinearRgb {\n return { r: byteToLinear(rgb.r), g: byteToLinear(rgb.g), b: byteToLinear(rgb.b) };\n}\n\n/** Encodes linear light to sRGB bytes, clamping anything out of gamut. */\nexport function linearToRgb(linear: LinearRgb): Rgb {\n return { r: linearToByte(linear.r), g: linearToByte(linear.g), b: linearToByte(linear.b) };\n}\n\n/** Converts linear sRGB to OKLab (Ottosson 2020 matrices). */\nexport function linearToOklab({ r, g, b }: LinearRgb): Oklab {\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n return {\n L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n };\n}\n\n/** Converts OKLab to linear sRGB, unclamped, so out-of-gamut values stay visible. */\nexport function oklabToLinear({ L, a, b }: Oklab): LinearRgb {\n const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;\n const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;\n const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;\n return {\n r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n };\n}\n\n/** Parses a hex colour straight to OKLab. */\nexport function hexToOklab(hex: string): Oklab {\n return linearToOklab(rgbToLinear(hexToRgb(hex)));\n}\n\n/** Formats OKLab as `#rrggbb`, clamping anything out of gamut. */\nexport function oklabToHex(lab: Oklab): string {\n return rgbToHex(linearToRgb(oklabToLinear(lab)));\n}\n\n/** Converts OKLab to OKLCH, with hue in degrees from 0 up to 360. */\nexport function oklabToOklch({ L, a, b }: Oklab): Oklch {\n const degrees = (Math.atan2(b, a) * DEGREES_PER_HALF_TURN) / Math.PI;\n return { L, C: Math.hypot(a, b), h: (degrees + DEGREES_PER_TURN) % DEGREES_PER_TURN };\n}\n\n/** Converts OKLCH, hue in degrees, to OKLab. */\nexport function oklchToOklab({ L, C, h }: Oklch): Oklab {\n const radians = (h * Math.PI) / DEGREES_PER_HALF_TURN;\n return { L, a: C * Math.cos(radians), b: C * Math.sin(radians) };\n}\n\n/** True when the colour lands inside sRGB, within a tiny rounding allowance. */\nexport function isInSrgbGamut(lab: Oklab): boolean {\n const { r, g, b } = oklabToLinear(lab);\n return isUnitChannel(r) && isUnitChannel(g) && isUnitChannel(b);\n}\n\n/** Lowers chroma until the colour fits sRGB; lightness and hue are kept. */\nexport function fitChromaToSrgb(lch: Oklch): Oklch {\n if (isInSrgbGamut(oklchToOklab(lch))) {\n return lch;\n }\n let low = 0;\n let high = lch.C;\n for (let step = 0; step < CHROMA_SEARCH_STEPS; step += 1) {\n const middle = (low + high) * HALF;\n if (isInSrgbGamut(oklchToOklab({ ...lch, C: middle }))) {\n low = middle;\n } else {\n high = middle;\n }\n }\n return { ...lch, C: low };\n}\n","export const enum CvdType {\n Protanopia = 'protanopia',\n Deuteranopia = 'deuteranopia',\n Tritanopia = 'tritanopia',\n}\n","import type { Oklab } from './types';\n\nfunction squaredDistance(x: Oklab, y: Oklab): number {\n const dL = x.L - y.L;\n const da = x.a - y.a;\n const db = x.b - y.b;\n return dL * dL + da * da + db * db;\n}\n\n/** Euclidean distance in OKLab, the CSS Color 4 deltaEOK. About 0.02 is just noticeable. */\nexport function deltaEOk(x: Oklab, y: Oklab): number {\n return Math.sqrt(squaredDistance(x, y));\n}\n\n/** Index of the palette entry nearest the target; first wins a tie, -1 when empty. */\nexport function nearestInPalette(target: Oklab, palette: readonly Oklab[]): number {\n let bestIndex = -1;\n let bestDistance = Infinity;\n palette.forEach((entry, index) => {\n const distance = squaredDistance(target, entry);\n if (distance < bestDistance) {\n bestDistance = distance;\n bestIndex = index;\n }\n });\n return bestIndex;\n}\n","import { linearToOklab, linearToRgb, rgbToLinear } from './convert';\nimport { CvdType } from './CvdType';\nimport { deltaEOk } from './deltaE';\nimport type { LinearRgb, Oklab, Rgb } from './types';\n\ntype MatrixRow = readonly [number, number, number];\ntype Matrix = readonly [MatrixRow, MatrixRow, MatrixRow];\n\nconst MACHADO_2009_FULL_SEVERITY: Readonly<Record<CvdType, Matrix>> = {\n [CvdType.Protanopia]: [\n [0.152286, 1.052583, -0.204868],\n [0.114503, 0.786281, 0.099216],\n [-0.003882, -0.048116, 1.051998],\n ],\n [CvdType.Deuteranopia]: [\n [0.367322, 0.860646, -0.227968],\n [0.280085, 0.672501, 0.047413],\n [-0.01182, 0.04294, 0.968881],\n ],\n [CvdType.Tritanopia]: [\n [1.255528, -0.076749, -0.178779],\n [-0.078411, 0.930809, 0.147602],\n [0.004733, 0.691367, 0.3039],\n ],\n};\n\nconst ALL_CVD_TYPES = [CvdType.Protanopia, CvdType.Deuteranopia, CvdType.Tritanopia];\n\nfunction applyRow([x, y, z]: MatrixRow, { r, g, b }: LinearRgb): number {\n return x * r + y * g + z * b;\n}\n\nfunction toLab(rgb: Rgb): Oklab {\n return linearToOklab(rgbToLinear(rgb));\n}\n\n/** What a viewer with full dichromacy of this type sees (Machado 2009, severity 1). */\nexport function simulateCvd(rgb: Rgb, type: CvdType): Rgb {\n const linear = rgbToLinear(rgb);\n const [red, green, blue] = MACHADO_2009_FULL_SEVERITY[type];\n return linearToRgb({\n r: applyRow(red, linear),\n g: applyRow(green, linear),\n b: applyRow(blue, linear),\n });\n}\n\n/** Smallest deltaEOk between two colours across normal vision and all three CVD types. */\nexport function visionDistance(x: Rgb, y: Rgb): number {\n const normalVision = deltaEOk(toLab(x), toLab(y));\n return ALL_CVD_TYPES.reduce(\n (smallest, type) =>\n Math.min(smallest, deltaEOk(toLab(simulateCvd(x, type)), toLab(simulateCvd(y, type)))),\n normalVision,\n );\n}\n","import {\n fitChromaToSrgb,\n hexToOklab,\n hexToRgb,\n linearToRgb,\n oklabToLinear,\n oklabToOklch,\n oklchToOklab,\n rgbToHex,\n} from './convert';\nimport { visionDistance } from './cvd';\nimport type { Oklch, Rgb } from './types';\n\nexport type HarmonyCount = 2 | 3 | 4;\n\n/** Every pair in a harmony is at least this far apart in every vision type. */\nexport const MIN_HARMONY_DISTANCE = 0.1;\n\nconst HUE_OFFSETS: Readonly<Record<HarmonyCount, readonly number[]>> = {\n 2: [180],\n 3: [120, 240],\n 4: [90, 180, 270],\n};\n\nconst LIGHTNESS_STEPS = 100;\n\ninterface Candidate {\n readonly rgb: Rgb;\n readonly clearance: number;\n readonly lightnessShift: number;\n}\n\nfunction buildCandidate(lch: Oklch, baseLightness: number, chosen: readonly Rgb[]): Candidate {\n const rgb = linearToRgb(oklabToLinear(oklchToOklab(fitChromaToSrgb(lch))));\n const distances = chosen.map((other) => visionDistance(rgb, other));\n return {\n rgb,\n clearance: Math.min(MIN_HARMONY_DISTANCE, ...distances),\n lightnessShift: Math.abs(lch.L - baseLightness),\n };\n}\n\nfunction pickBetter(best: Candidate, next: Candidate): Candidate {\n if (next.clearance !== best.clearance) {\n return next.clearance > best.clearance ? next : best;\n }\n return next.lightnessShift < best.lightnessShift ? next : best;\n}\n\nfunction pickAtHue(base: Oklch, hueOffset: number, chosen: readonly Rgb[]): Rgb {\n const candidates = Array.from({ length: LIGHTNESS_STEPS + 1 }, (_, step) =>\n buildCandidate(\n { L: step / LIGHTNESS_STEPS, C: base.C, h: base.h + hueOffset },\n base.L,\n chosen,\n ),\n );\n return candidates.reduce(pickBetter).rgb;\n}\n\n/**\n * Base colour first, then the others at fixed OKLCH hue offsets: 2 = 180, 3 = 120, 4 = 90.\n * Lightness moves as little as it can to clear MIN_HARMONY_DISTANCE; chroma only drops to fit sRGB.\n * @returns `count` lowercase `#rrggbb` values.\n */\nexport function harmony(baseHex: string, count: HarmonyCount): string[] {\n const base = oklabToOklch(hexToOklab(baseHex));\n const colours = HUE_OFFSETS[count].reduce<readonly Rgb[]>(\n (chosen, hueOffset) => [...chosen, pickAtHue(base, hueOffset, chosen)],\n [hexToRgb(baseHex)],\n );\n return colours.map(rgbToHex);\n}\n"]}
1
+ {"version":3,"sources":["../src/convert.ts","../src/CvdType.ts","../src/deltaE.ts","../src/cvd.ts","../src/backdrop.ts","../src/harmony.ts"],"names":["CvdType","HALF"],"mappings":"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","file":"index.js","sourcesContent":["import type { LinearRgb, Oklab, Oklch, Rgb } from './types';\n\nconst BYTE_MAX = 255;\nconst HEX_RADIX = 16;\nconst HEX_BYTE_WIDTH = 2;\nconst SHORT_HEX_LENGTH = 3;\nconst RED_SHIFT = 16;\nconst GREEN_SHIFT = 8;\nconst HEX_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\n\nconst SRGB_ENCODED_CUTOFF = 0.04045;\nconst SRGB_LINEAR_CUTOFF = 0.0031308;\nconst SRGB_LINEAR_SLOPE = 12.92;\nconst SRGB_OFFSET = 0.055;\nconst SRGB_SCALE = 1.055;\nconst SRGB_GAMMA = 2.4;\n\nconst DEGREES_PER_TURN = 360;\nconst DEGREES_PER_HALF_TURN = 180;\nconst GAMUT_EPSILON = 1e-4;\nconst CHROMA_SEARCH_STEPS = 20;\nconst HALF = 0.5;\n\nfunction clampByte(value: number): number {\n return Math.min(BYTE_MAX, Math.max(0, Math.round(value)));\n}\n\nfunction toHexByte(value: number): string {\n return clampByte(value).toString(HEX_RADIX).padStart(HEX_BYTE_WIDTH, '0');\n}\n\nfunction byteToLinear(byte: number): number {\n const encoded = byte / BYTE_MAX;\n return encoded <= SRGB_ENCODED_CUTOFF\n ? encoded / SRGB_LINEAR_SLOPE\n : ((encoded + SRGB_OFFSET) / SRGB_SCALE) ** SRGB_GAMMA;\n}\n\nfunction linearToByte(linear: number): number {\n const encoded =\n linear <= SRGB_LINEAR_CUTOFF\n ? linear * SRGB_LINEAR_SLOPE\n : SRGB_SCALE * linear ** (1 / SRGB_GAMMA) - SRGB_OFFSET;\n return clampByte(encoded * BYTE_MAX);\n}\n\nfunction isUnitChannel(channel: number): boolean {\n return channel >= -GAMUT_EPSILON && channel <= 1 + GAMUT_EPSILON;\n}\n\n/** Parses `#rgb` or `#rrggbb`, any case. Throws RangeError on anything else. */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX_PATTERN.test(hex)) {\n throw new RangeError(`Not a hex colour: ${hex}`);\n }\n const digits = hex.slice(1);\n const full = digits.length === SHORT_HEX_LENGTH ? digits.replace(/./g, '$&$&') : digits;\n const value = parseInt(full, HEX_RADIX);\n return {\n r: (value >> RED_SHIFT) & BYTE_MAX,\n g: (value >> GREEN_SHIFT) & BYTE_MAX,\n b: value & BYTE_MAX,\n };\n}\n\n/** Formats as lowercase `#rrggbb`, rounding and clamping each channel. */\nexport function rgbToHex(rgb: Rgb): string {\n return `#${toHexByte(rgb.r)}${toHexByte(rgb.g)}${toHexByte(rgb.b)}`;\n}\n\n/** Decodes sRGB bytes to linear light, 0 to 1. */\nexport function rgbToLinear(rgb: Rgb): LinearRgb {\n return { r: byteToLinear(rgb.r), g: byteToLinear(rgb.g), b: byteToLinear(rgb.b) };\n}\n\n/** Encodes linear light to sRGB bytes, clamping anything out of gamut. */\nexport function linearToRgb(linear: LinearRgb): Rgb {\n return { r: linearToByte(linear.r), g: linearToByte(linear.g), b: linearToByte(linear.b) };\n}\n\n/** Converts linear sRGB to OKLab (Ottosson 2020 matrices). */\nexport function linearToOklab({ r, g, b }: LinearRgb): Oklab {\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n return {\n L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n };\n}\n\n/** Converts OKLab to linear sRGB, unclamped, so out-of-gamut values stay visible. */\nexport function oklabToLinear({ L, a, b }: Oklab): LinearRgb {\n const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;\n const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;\n const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;\n return {\n r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n };\n}\n\n/** Parses a hex colour straight to OKLab. */\nexport function hexToOklab(hex: string): Oklab {\n return linearToOklab(rgbToLinear(hexToRgb(hex)));\n}\n\n/** Formats OKLab as `#rrggbb`, clamping anything out of gamut. */\nexport function oklabToHex(lab: Oklab): string {\n return rgbToHex(linearToRgb(oklabToLinear(lab)));\n}\n\n/** Converts OKLab to OKLCH, with hue in degrees from 0 up to 360. */\nexport function oklabToOklch({ L, a, b }: Oklab): Oklch {\n const degrees = (Math.atan2(b, a) * DEGREES_PER_HALF_TURN) / Math.PI;\n return { L, C: Math.hypot(a, b), h: (degrees + DEGREES_PER_TURN) % DEGREES_PER_TURN };\n}\n\n/** Converts OKLCH, hue in degrees, to OKLab. */\nexport function oklchToOklab({ L, C, h }: Oklch): Oklab {\n const radians = (h * Math.PI) / DEGREES_PER_HALF_TURN;\n return { L, a: C * Math.cos(radians), b: C * Math.sin(radians) };\n}\n\n/** True when the colour lands inside sRGB, within a tiny rounding allowance. */\nexport function isInSrgbGamut(lab: Oklab): boolean {\n const { r, g, b } = oklabToLinear(lab);\n return isUnitChannel(r) && isUnitChannel(g) && isUnitChannel(b);\n}\n\n/** Lowers chroma until the colour fits sRGB; lightness and hue are kept. */\nexport function fitChromaToSrgb(lch: Oklch): Oklch {\n if (isInSrgbGamut(oklchToOklab(lch))) {\n return lch;\n }\n let low = 0;\n let high = lch.C;\n for (let step = 0; step < CHROMA_SEARCH_STEPS; step += 1) {\n const middle = (low + high) * HALF;\n if (isInSrgbGamut(oklchToOklab({ ...lch, C: middle }))) {\n low = middle;\n } else {\n high = middle;\n }\n }\n return { ...lch, C: low };\n}\n","export const enum CvdType {\n Protanopia = 'protanopia',\n Deuteranopia = 'deuteranopia',\n Tritanopia = 'tritanopia',\n}\n","import type { Oklab } from './types';\n\nfunction squaredDistance(x: Oklab, y: Oklab): number {\n const dL = x.L - y.L;\n const da = x.a - y.a;\n const db = x.b - y.b;\n return dL * dL + da * da + db * db;\n}\n\n/** Euclidean distance in OKLab, the CSS Color 4 deltaEOK. About 0.02 is just noticeable. */\nexport function deltaEOk(x: Oklab, y: Oklab): number {\n return Math.sqrt(squaredDistance(x, y));\n}\n\n/** Index of the palette entry nearest the target; first wins a tie, -1 when empty. */\nexport function nearestInPalette(target: Oklab, palette: readonly Oklab[]): number {\n let bestIndex = -1;\n let bestDistance = Infinity;\n palette.forEach((entry, index) => {\n const distance = squaredDistance(target, entry);\n if (distance < bestDistance) {\n bestDistance = distance;\n bestIndex = index;\n }\n });\n return bestIndex;\n}\n","import { linearToOklab, linearToRgb, rgbToLinear } from './convert';\nimport { CvdType } from './CvdType';\nimport { deltaEOk } from './deltaE';\nimport type { LinearRgb, Oklab, Rgb } from './types';\n\ntype MatrixRow = readonly [number, number, number];\ntype Matrix = readonly [MatrixRow, MatrixRow, MatrixRow];\n\nconst MACHADO_2009_FULL_SEVERITY: Readonly<Record<CvdType, Matrix>> = {\n [CvdType.Protanopia]: [\n [0.152286, 1.052583, -0.204868],\n [0.114503, 0.786281, 0.099216],\n [-0.003882, -0.048116, 1.051998],\n ],\n [CvdType.Deuteranopia]: [\n [0.367322, 0.860646, -0.227968],\n [0.280085, 0.672501, 0.047413],\n [-0.01182, 0.04294, 0.968881],\n ],\n [CvdType.Tritanopia]: [\n [1.255528, -0.076749, -0.178779],\n [-0.078411, 0.930809, 0.147602],\n [0.004733, 0.691367, 0.3039],\n ],\n};\n\nconst ALL_CVD_TYPES = [CvdType.Protanopia, CvdType.Deuteranopia, CvdType.Tritanopia];\n\nfunction applyRow([x, y, z]: MatrixRow, { r, g, b }: LinearRgb): number {\n return x * r + y * g + z * b;\n}\n\nfunction toLab(rgb: Rgb): Oklab {\n return linearToOklab(rgbToLinear(rgb));\n}\n\n/** What a viewer with full dichromacy of this type sees (Machado 2009, severity 1). */\nexport function simulateCvd(rgb: Rgb, type: CvdType): Rgb {\n const linear = rgbToLinear(rgb);\n const [red, green, blue] = MACHADO_2009_FULL_SEVERITY[type];\n return linearToRgb({\n r: applyRow(red, linear),\n g: applyRow(green, linear),\n b: applyRow(blue, linear),\n });\n}\n\n/** Smallest deltaEOk between two colours across normal vision and all three CVD types. */\nexport function visionDistance(x: Rgb, y: Rgb): number {\n const normalVision = deltaEOk(toLab(x), toLab(y));\n return ALL_CVD_TYPES.reduce(\n (smallest, type) =>\n Math.min(smallest, deltaEOk(toLab(simulateCvd(x, type)), toLab(simulateCvd(y, type)))),\n normalVision,\n );\n}\n","import { hexToOklab, hexToRgb, rgbToLinear } from './convert';\nimport { deltaEOk } from './deltaE';\n\nconst RED_WEIGHT = 0.2126;\nconst GREEN_WEIGHT = 0.7152;\nconst BLUE_WEIGHT = 0.0722;\nconst FLARE = 0.05;\nconst HALF = 0.5;\n\n/** WCAG 2 non-text contrast floor a bead colour must reach against what sits behind it. */\nexport const MIN_BEAD_CONTRAST = 3;\n\n/** Under-3:1 shares this close count as equal, and delta-E decides. */\nexport const BACKDROP_TIE_MARGIN = 0.01;\n\nexport interface BeadCount {\n readonly hex: string;\n readonly count: number;\n}\n\nexport interface BackdropScore {\n readonly hex: string;\n readonly underShare: number;\n readonly medianDeltaE: number;\n}\n\nfunction luminance(hex: string): number {\n const { r, g, b } = rgbToLinear(hexToRgb(hex));\n return RED_WEIGHT * r + GREEN_WEIGHT * g + BLUE_WEIGHT * b;\n}\n\nfunction weightedMedian(entries: readonly { value: number; count: number }[], total: number): number {\n const sorted = [...entries].sort((x, y) => x.value - y.value);\n let seen = 0;\n for (const entry of sorted) {\n seen += entry.count;\n if (seen >= total * HALF) {return entry.value;}\n }\n return 0;\n}\n\n/** WCAG 2 contrast ratio of two hex colours, 1 to 21. */\nexport function contrastRatio(x: string, y: string): number {\n const lumX = luminance(x);\n const lumY = luminance(y);\n return (Math.max(lumX, lumY) + FLARE) / (Math.min(lumX, lumY) + FLARE);\n}\n\n/** Measures one backdrop against a pattern's beads, weighted by how many of each bead it uses. */\nexport function scoreBackdrop(beads: readonly BeadCount[], backdropHex: string): BackdropScore {\n const counted = beads.filter((bead) => bead.count > 0);\n const total = counted.reduce((sum, bead) => sum + bead.count, 0);\n if (total === 0) {return { hex: backdropHex, underShare: 0, medianDeltaE: 0 };}\n const backdrop = hexToOklab(backdropHex);\n const under = counted\n .filter((bead) => contrastRatio(bead.hex, backdropHex) < MIN_BEAD_CONTRAST)\n .reduce((sum, bead) => sum + bead.count, 0);\n const distances = counted.map((bead) => ({ value: deltaEOk(hexToOklab(bead.hex), backdrop), count: bead.count }));\n return { hex: backdropHex, underShare: under / total, medianDeltaE: weightedMedian(distances, total) };\n}\n\n/** Lowest under-3:1 share wins; inside the margin the higher median delta-E wins; first wins a tie; null when empty. */\nexport function pickBackdrop(\n beads: readonly BeadCount[],\n backdrops: readonly string[],\n margin: number = BACKDROP_TIE_MARGIN,\n): BackdropScore | null {\n const scores = backdrops.map((hex) => scoreBackdrop(beads, hex));\n if (scores.length === 0) {return null;}\n const lowest = Math.min(...scores.map((score) => score.underShare));\n return scores\n .filter((score) => score.underShare <= lowest + margin)\n .reduce((best, score) => (score.medianDeltaE > best.medianDeltaE ? score : best));\n}\n","import {\n fitChromaToSrgb,\n hexToOklab,\n hexToRgb,\n linearToRgb,\n oklabToLinear,\n oklabToOklch,\n oklchToOklab,\n rgbToHex,\n} from './convert';\nimport { visionDistance } from './cvd';\nimport type { Oklch, Rgb } from './types';\n\nexport type HarmonyCount = 2 | 3 | 4;\n\n/** Every pair in a harmony is at least this far apart in every vision type. */\nexport const MIN_HARMONY_DISTANCE = 0.1;\n\nconst HUE_OFFSETS: Readonly<Record<HarmonyCount, readonly number[]>> = {\n 2: [180],\n 3: [120, 240],\n 4: [90, 180, 270],\n};\n\nconst LIGHTNESS_STEPS = 100;\n\ninterface Candidate {\n readonly rgb: Rgb;\n readonly clearance: number;\n readonly lightnessShift: number;\n}\n\nfunction buildCandidate(lch: Oklch, baseLightness: number, chosen: readonly Rgb[]): Candidate {\n const rgb = linearToRgb(oklabToLinear(oklchToOklab(fitChromaToSrgb(lch))));\n const distances = chosen.map((other) => visionDistance(rgb, other));\n return {\n rgb,\n clearance: Math.min(MIN_HARMONY_DISTANCE, ...distances),\n lightnessShift: Math.abs(lch.L - baseLightness),\n };\n}\n\nfunction pickBetter(best: Candidate, next: Candidate): Candidate {\n if (next.clearance !== best.clearance) {\n return next.clearance > best.clearance ? next : best;\n }\n return next.lightnessShift < best.lightnessShift ? next : best;\n}\n\nfunction pickAtHue(base: Oklch, hueOffset: number, chosen: readonly Rgb[]): Rgb {\n const candidates = Array.from({ length: LIGHTNESS_STEPS + 1 }, (_, step) =>\n buildCandidate(\n { L: step / LIGHTNESS_STEPS, C: base.C, h: base.h + hueOffset },\n base.L,\n chosen,\n ),\n );\n return candidates.reduce(pickBetter).rgb;\n}\n\n/**\n * Base colour first, then the others at fixed OKLCH hue offsets: 2 = 180, 3 = 120, 4 = 90.\n * Lightness moves as little as it can to clear MIN_HARMONY_DISTANCE; chroma only drops to fit sRGB.\n * @returns `count` lowercase `#rrggbb` values.\n */\nexport function harmony(baseHex: string, count: HarmonyCount): string[] {\n const base = oklabToOklch(hexToOklab(baseHex));\n const colours = HUE_OFFSETS[count].reduce<readonly Rgb[]>(\n (chosen, hueOffset) => [...chosen, pickAtHue(base, hueOffset, chosen)],\n [hexToRgb(baseHex)],\n );\n return colours.map(rgbToHex);\n}\n"]}
package/dist/index.mjs CHANGED
@@ -184,6 +184,54 @@ function visionDistance(x, y) {
184
184
  );
185
185
  }
186
186
 
187
+ // src/backdrop.ts
188
+ var RED_WEIGHT = 0.2126;
189
+ var GREEN_WEIGHT = 0.7152;
190
+ var BLUE_WEIGHT = 0.0722;
191
+ var FLARE = 0.05;
192
+ var HALF2 = 0.5;
193
+ var MIN_BEAD_CONTRAST = 3;
194
+ var BACKDROP_TIE_MARGIN = 0.01;
195
+ function luminance(hex) {
196
+ const { r, g, b } = rgbToLinear(hexToRgb(hex));
197
+ return RED_WEIGHT * r + GREEN_WEIGHT * g + BLUE_WEIGHT * b;
198
+ }
199
+ function weightedMedian(entries, total) {
200
+ const sorted = [...entries].sort((x, y) => x.value - y.value);
201
+ let seen = 0;
202
+ for (const entry of sorted) {
203
+ seen += entry.count;
204
+ if (seen >= total * HALF2) {
205
+ return entry.value;
206
+ }
207
+ }
208
+ return 0;
209
+ }
210
+ function contrastRatio(x, y) {
211
+ const lumX = luminance(x);
212
+ const lumY = luminance(y);
213
+ return (Math.max(lumX, lumY) + FLARE) / (Math.min(lumX, lumY) + FLARE);
214
+ }
215
+ function scoreBackdrop(beads, backdropHex) {
216
+ const counted = beads.filter((bead) => bead.count > 0);
217
+ const total = counted.reduce((sum, bead) => sum + bead.count, 0);
218
+ if (total === 0) {
219
+ return { hex: backdropHex, underShare: 0, medianDeltaE: 0 };
220
+ }
221
+ const backdrop = hexToOklab(backdropHex);
222
+ const under = counted.filter((bead) => contrastRatio(bead.hex, backdropHex) < MIN_BEAD_CONTRAST).reduce((sum, bead) => sum + bead.count, 0);
223
+ const distances = counted.map((bead) => ({ value: deltaEOk(hexToOklab(bead.hex), backdrop), count: bead.count }));
224
+ return { hex: backdropHex, underShare: under / total, medianDeltaE: weightedMedian(distances, total) };
225
+ }
226
+ function pickBackdrop(beads, backdrops, margin = BACKDROP_TIE_MARGIN) {
227
+ const scores = backdrops.map((hex) => scoreBackdrop(beads, hex));
228
+ if (scores.length === 0) {
229
+ return null;
230
+ }
231
+ const lowest = Math.min(...scores.map((score) => score.underShare));
232
+ return scores.filter((score) => score.underShare <= lowest + margin).reduce((best, score) => score.medianDeltaE > best.medianDeltaE ? score : best);
233
+ }
234
+
187
235
  // src/harmony.ts
188
236
  var MIN_HARMONY_DISTANCE = 0.1;
189
237
  var HUE_OFFSETS = {
@@ -227,6 +275,6 @@ function harmony(baseHex, count) {
227
275
  return colours.map(rgbToHex);
228
276
  }
229
277
 
230
- export { CvdType, MIN_HARMONY_DISTANCE, deltaEOk, fitChromaToSrgb, harmony, hexToOklab, hexToRgb, isInSrgbGamut, linearToOklab, linearToRgb, nearestInPalette, oklabToHex, oklabToLinear, oklabToOklch, oklchToOklab, rgbToHex, rgbToLinear, simulateCvd, visionDistance };
278
+ export { BACKDROP_TIE_MARGIN, CvdType, MIN_BEAD_CONTRAST, MIN_HARMONY_DISTANCE, contrastRatio, deltaEOk, fitChromaToSrgb, harmony, hexToOklab, hexToRgb, isInSrgbGamut, linearToOklab, linearToRgb, nearestInPalette, oklabToHex, oklabToLinear, oklabToOklch, oklchToOklab, pickBackdrop, rgbToHex, rgbToLinear, scoreBackdrop, simulateCvd, visionDistance };
231
279
  //# sourceMappingURL=index.mjs.map
232
280
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/convert.ts","../src/CvdType.ts","../src/deltaE.ts","../src/cvd.ts","../src/harmony.ts"],"names":["CvdType"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { LinearRgb, Oklab, Oklch, Rgb } from './types';\n\nconst BYTE_MAX = 255;\nconst HEX_RADIX = 16;\nconst HEX_BYTE_WIDTH = 2;\nconst SHORT_HEX_LENGTH = 3;\nconst RED_SHIFT = 16;\nconst GREEN_SHIFT = 8;\nconst HEX_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\n\nconst SRGB_ENCODED_CUTOFF = 0.04045;\nconst SRGB_LINEAR_CUTOFF = 0.0031308;\nconst SRGB_LINEAR_SLOPE = 12.92;\nconst SRGB_OFFSET = 0.055;\nconst SRGB_SCALE = 1.055;\nconst SRGB_GAMMA = 2.4;\n\nconst DEGREES_PER_TURN = 360;\nconst DEGREES_PER_HALF_TURN = 180;\nconst GAMUT_EPSILON = 1e-4;\nconst CHROMA_SEARCH_STEPS = 20;\nconst HALF = 0.5;\n\nfunction clampByte(value: number): number {\n return Math.min(BYTE_MAX, Math.max(0, Math.round(value)));\n}\n\nfunction toHexByte(value: number): string {\n return clampByte(value).toString(HEX_RADIX).padStart(HEX_BYTE_WIDTH, '0');\n}\n\nfunction byteToLinear(byte: number): number {\n const encoded = byte / BYTE_MAX;\n return encoded <= SRGB_ENCODED_CUTOFF\n ? encoded / SRGB_LINEAR_SLOPE\n : ((encoded + SRGB_OFFSET) / SRGB_SCALE) ** SRGB_GAMMA;\n}\n\nfunction linearToByte(linear: number): number {\n const encoded =\n linear <= SRGB_LINEAR_CUTOFF\n ? linear * SRGB_LINEAR_SLOPE\n : SRGB_SCALE * linear ** (1 / SRGB_GAMMA) - SRGB_OFFSET;\n return clampByte(encoded * BYTE_MAX);\n}\n\nfunction isUnitChannel(channel: number): boolean {\n return channel >= -GAMUT_EPSILON && channel <= 1 + GAMUT_EPSILON;\n}\n\n/** Parses `#rgb` or `#rrggbb`, any case. Throws RangeError on anything else. */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX_PATTERN.test(hex)) {\n throw new RangeError(`Not a hex colour: ${hex}`);\n }\n const digits = hex.slice(1);\n const full = digits.length === SHORT_HEX_LENGTH ? digits.replace(/./g, '$&$&') : digits;\n const value = parseInt(full, HEX_RADIX);\n return {\n r: (value >> RED_SHIFT) & BYTE_MAX,\n g: (value >> GREEN_SHIFT) & BYTE_MAX,\n b: value & BYTE_MAX,\n };\n}\n\n/** Formats as lowercase `#rrggbb`, rounding and clamping each channel. */\nexport function rgbToHex(rgb: Rgb): string {\n return `#${toHexByte(rgb.r)}${toHexByte(rgb.g)}${toHexByte(rgb.b)}`;\n}\n\n/** Decodes sRGB bytes to linear light, 0 to 1. */\nexport function rgbToLinear(rgb: Rgb): LinearRgb {\n return { r: byteToLinear(rgb.r), g: byteToLinear(rgb.g), b: byteToLinear(rgb.b) };\n}\n\n/** Encodes linear light to sRGB bytes, clamping anything out of gamut. */\nexport function linearToRgb(linear: LinearRgb): Rgb {\n return { r: linearToByte(linear.r), g: linearToByte(linear.g), b: linearToByte(linear.b) };\n}\n\n/** Converts linear sRGB to OKLab (Ottosson 2020 matrices). */\nexport function linearToOklab({ r, g, b }: LinearRgb): Oklab {\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n return {\n L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n };\n}\n\n/** Converts OKLab to linear sRGB, unclamped, so out-of-gamut values stay visible. */\nexport function oklabToLinear({ L, a, b }: Oklab): LinearRgb {\n const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;\n const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;\n const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;\n return {\n r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n };\n}\n\n/** Parses a hex colour straight to OKLab. */\nexport function hexToOklab(hex: string): Oklab {\n return linearToOklab(rgbToLinear(hexToRgb(hex)));\n}\n\n/** Formats OKLab as `#rrggbb`, clamping anything out of gamut. */\nexport function oklabToHex(lab: Oklab): string {\n return rgbToHex(linearToRgb(oklabToLinear(lab)));\n}\n\n/** Converts OKLab to OKLCH, with hue in degrees from 0 up to 360. */\nexport function oklabToOklch({ L, a, b }: Oklab): Oklch {\n const degrees = (Math.atan2(b, a) * DEGREES_PER_HALF_TURN) / Math.PI;\n return { L, C: Math.hypot(a, b), h: (degrees + DEGREES_PER_TURN) % DEGREES_PER_TURN };\n}\n\n/** Converts OKLCH, hue in degrees, to OKLab. */\nexport function oklchToOklab({ L, C, h }: Oklch): Oklab {\n const radians = (h * Math.PI) / DEGREES_PER_HALF_TURN;\n return { L, a: C * Math.cos(radians), b: C * Math.sin(radians) };\n}\n\n/** True when the colour lands inside sRGB, within a tiny rounding allowance. */\nexport function isInSrgbGamut(lab: Oklab): boolean {\n const { r, g, b } = oklabToLinear(lab);\n return isUnitChannel(r) && isUnitChannel(g) && isUnitChannel(b);\n}\n\n/** Lowers chroma until the colour fits sRGB; lightness and hue are kept. */\nexport function fitChromaToSrgb(lch: Oklch): Oklch {\n if (isInSrgbGamut(oklchToOklab(lch))) {\n return lch;\n }\n let low = 0;\n let high = lch.C;\n for (let step = 0; step < CHROMA_SEARCH_STEPS; step += 1) {\n const middle = (low + high) * HALF;\n if (isInSrgbGamut(oklchToOklab({ ...lch, C: middle }))) {\n low = middle;\n } else {\n high = middle;\n }\n }\n return { ...lch, C: low };\n}\n","export const enum CvdType {\n Protanopia = 'protanopia',\n Deuteranopia = 'deuteranopia',\n Tritanopia = 'tritanopia',\n}\n","import type { Oklab } from './types';\n\nfunction squaredDistance(x: Oklab, y: Oklab): number {\n const dL = x.L - y.L;\n const da = x.a - y.a;\n const db = x.b - y.b;\n return dL * dL + da * da + db * db;\n}\n\n/** Euclidean distance in OKLab, the CSS Color 4 deltaEOK. About 0.02 is just noticeable. */\nexport function deltaEOk(x: Oklab, y: Oklab): number {\n return Math.sqrt(squaredDistance(x, y));\n}\n\n/** Index of the palette entry nearest the target; first wins a tie, -1 when empty. */\nexport function nearestInPalette(target: Oklab, palette: readonly Oklab[]): number {\n let bestIndex = -1;\n let bestDistance = Infinity;\n palette.forEach((entry, index) => {\n const distance = squaredDistance(target, entry);\n if (distance < bestDistance) {\n bestDistance = distance;\n bestIndex = index;\n }\n });\n return bestIndex;\n}\n","import { linearToOklab, linearToRgb, rgbToLinear } from './convert';\nimport { CvdType } from './CvdType';\nimport { deltaEOk } from './deltaE';\nimport type { LinearRgb, Oklab, Rgb } from './types';\n\ntype MatrixRow = readonly [number, number, number];\ntype Matrix = readonly [MatrixRow, MatrixRow, MatrixRow];\n\nconst MACHADO_2009_FULL_SEVERITY: Readonly<Record<CvdType, Matrix>> = {\n [CvdType.Protanopia]: [\n [0.152286, 1.052583, -0.204868],\n [0.114503, 0.786281, 0.099216],\n [-0.003882, -0.048116, 1.051998],\n ],\n [CvdType.Deuteranopia]: [\n [0.367322, 0.860646, -0.227968],\n [0.280085, 0.672501, 0.047413],\n [-0.01182, 0.04294, 0.968881],\n ],\n [CvdType.Tritanopia]: [\n [1.255528, -0.076749, -0.178779],\n [-0.078411, 0.930809, 0.147602],\n [0.004733, 0.691367, 0.3039],\n ],\n};\n\nconst ALL_CVD_TYPES = [CvdType.Protanopia, CvdType.Deuteranopia, CvdType.Tritanopia];\n\nfunction applyRow([x, y, z]: MatrixRow, { r, g, b }: LinearRgb): number {\n return x * r + y * g + z * b;\n}\n\nfunction toLab(rgb: Rgb): Oklab {\n return linearToOklab(rgbToLinear(rgb));\n}\n\n/** What a viewer with full dichromacy of this type sees (Machado 2009, severity 1). */\nexport function simulateCvd(rgb: Rgb, type: CvdType): Rgb {\n const linear = rgbToLinear(rgb);\n const [red, green, blue] = MACHADO_2009_FULL_SEVERITY[type];\n return linearToRgb({\n r: applyRow(red, linear),\n g: applyRow(green, linear),\n b: applyRow(blue, linear),\n });\n}\n\n/** Smallest deltaEOk between two colours across normal vision and all three CVD types. */\nexport function visionDistance(x: Rgb, y: Rgb): number {\n const normalVision = deltaEOk(toLab(x), toLab(y));\n return ALL_CVD_TYPES.reduce(\n (smallest, type) =>\n Math.min(smallest, deltaEOk(toLab(simulateCvd(x, type)), toLab(simulateCvd(y, type)))),\n normalVision,\n );\n}\n","import {\n fitChromaToSrgb,\n hexToOklab,\n hexToRgb,\n linearToRgb,\n oklabToLinear,\n oklabToOklch,\n oklchToOklab,\n rgbToHex,\n} from './convert';\nimport { visionDistance } from './cvd';\nimport type { Oklch, Rgb } from './types';\n\nexport type HarmonyCount = 2 | 3 | 4;\n\n/** Every pair in a harmony is at least this far apart in every vision type. */\nexport const MIN_HARMONY_DISTANCE = 0.1;\n\nconst HUE_OFFSETS: Readonly<Record<HarmonyCount, readonly number[]>> = {\n 2: [180],\n 3: [120, 240],\n 4: [90, 180, 270],\n};\n\nconst LIGHTNESS_STEPS = 100;\n\ninterface Candidate {\n readonly rgb: Rgb;\n readonly clearance: number;\n readonly lightnessShift: number;\n}\n\nfunction buildCandidate(lch: Oklch, baseLightness: number, chosen: readonly Rgb[]): Candidate {\n const rgb = linearToRgb(oklabToLinear(oklchToOklab(fitChromaToSrgb(lch))));\n const distances = chosen.map((other) => visionDistance(rgb, other));\n return {\n rgb,\n clearance: Math.min(MIN_HARMONY_DISTANCE, ...distances),\n lightnessShift: Math.abs(lch.L - baseLightness),\n };\n}\n\nfunction pickBetter(best: Candidate, next: Candidate): Candidate {\n if (next.clearance !== best.clearance) {\n return next.clearance > best.clearance ? next : best;\n }\n return next.lightnessShift < best.lightnessShift ? next : best;\n}\n\nfunction pickAtHue(base: Oklch, hueOffset: number, chosen: readonly Rgb[]): Rgb {\n const candidates = Array.from({ length: LIGHTNESS_STEPS + 1 }, (_, step) =>\n buildCandidate(\n { L: step / LIGHTNESS_STEPS, C: base.C, h: base.h + hueOffset },\n base.L,\n chosen,\n ),\n );\n return candidates.reduce(pickBetter).rgb;\n}\n\n/**\n * Base colour first, then the others at fixed OKLCH hue offsets: 2 = 180, 3 = 120, 4 = 90.\n * Lightness moves as little as it can to clear MIN_HARMONY_DISTANCE; chroma only drops to fit sRGB.\n * @returns `count` lowercase `#rrggbb` values.\n */\nexport function harmony(baseHex: string, count: HarmonyCount): string[] {\n const base = oklabToOklch(hexToOklab(baseHex));\n const colours = HUE_OFFSETS[count].reduce<readonly Rgb[]>(\n (chosen, hueOffset) => [...chosen, pickAtHue(base, hueOffset, chosen)],\n [hexToRgb(baseHex)],\n );\n return colours.map(rgbToHex);\n}\n"]}
1
+ {"version":3,"sources":["../src/convert.ts","../src/CvdType.ts","../src/deltaE.ts","../src/cvd.ts","../src/backdrop.ts","../src/harmony.ts"],"names":["CvdType","HALF"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { LinearRgb, Oklab, Oklch, Rgb } from './types';\n\nconst BYTE_MAX = 255;\nconst HEX_RADIX = 16;\nconst HEX_BYTE_WIDTH = 2;\nconst SHORT_HEX_LENGTH = 3;\nconst RED_SHIFT = 16;\nconst GREEN_SHIFT = 8;\nconst HEX_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\n\nconst SRGB_ENCODED_CUTOFF = 0.04045;\nconst SRGB_LINEAR_CUTOFF = 0.0031308;\nconst SRGB_LINEAR_SLOPE = 12.92;\nconst SRGB_OFFSET = 0.055;\nconst SRGB_SCALE = 1.055;\nconst SRGB_GAMMA = 2.4;\n\nconst DEGREES_PER_TURN = 360;\nconst DEGREES_PER_HALF_TURN = 180;\nconst GAMUT_EPSILON = 1e-4;\nconst CHROMA_SEARCH_STEPS = 20;\nconst HALF = 0.5;\n\nfunction clampByte(value: number): number {\n return Math.min(BYTE_MAX, Math.max(0, Math.round(value)));\n}\n\nfunction toHexByte(value: number): string {\n return clampByte(value).toString(HEX_RADIX).padStart(HEX_BYTE_WIDTH, '0');\n}\n\nfunction byteToLinear(byte: number): number {\n const encoded = byte / BYTE_MAX;\n return encoded <= SRGB_ENCODED_CUTOFF\n ? encoded / SRGB_LINEAR_SLOPE\n : ((encoded + SRGB_OFFSET) / SRGB_SCALE) ** SRGB_GAMMA;\n}\n\nfunction linearToByte(linear: number): number {\n const encoded =\n linear <= SRGB_LINEAR_CUTOFF\n ? linear * SRGB_LINEAR_SLOPE\n : SRGB_SCALE * linear ** (1 / SRGB_GAMMA) - SRGB_OFFSET;\n return clampByte(encoded * BYTE_MAX);\n}\n\nfunction isUnitChannel(channel: number): boolean {\n return channel >= -GAMUT_EPSILON && channel <= 1 + GAMUT_EPSILON;\n}\n\n/** Parses `#rgb` or `#rrggbb`, any case. Throws RangeError on anything else. */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX_PATTERN.test(hex)) {\n throw new RangeError(`Not a hex colour: ${hex}`);\n }\n const digits = hex.slice(1);\n const full = digits.length === SHORT_HEX_LENGTH ? digits.replace(/./g, '$&$&') : digits;\n const value = parseInt(full, HEX_RADIX);\n return {\n r: (value >> RED_SHIFT) & BYTE_MAX,\n g: (value >> GREEN_SHIFT) & BYTE_MAX,\n b: value & BYTE_MAX,\n };\n}\n\n/** Formats as lowercase `#rrggbb`, rounding and clamping each channel. */\nexport function rgbToHex(rgb: Rgb): string {\n return `#${toHexByte(rgb.r)}${toHexByte(rgb.g)}${toHexByte(rgb.b)}`;\n}\n\n/** Decodes sRGB bytes to linear light, 0 to 1. */\nexport function rgbToLinear(rgb: Rgb): LinearRgb {\n return { r: byteToLinear(rgb.r), g: byteToLinear(rgb.g), b: byteToLinear(rgb.b) };\n}\n\n/** Encodes linear light to sRGB bytes, clamping anything out of gamut. */\nexport function linearToRgb(linear: LinearRgb): Rgb {\n return { r: linearToByte(linear.r), g: linearToByte(linear.g), b: linearToByte(linear.b) };\n}\n\n/** Converts linear sRGB to OKLab (Ottosson 2020 matrices). */\nexport function linearToOklab({ r, g, b }: LinearRgb): Oklab {\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n return {\n L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n };\n}\n\n/** Converts OKLab to linear sRGB, unclamped, so out-of-gamut values stay visible. */\nexport function oklabToLinear({ L, a, b }: Oklab): LinearRgb {\n const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;\n const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;\n const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;\n return {\n r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n };\n}\n\n/** Parses a hex colour straight to OKLab. */\nexport function hexToOklab(hex: string): Oklab {\n return linearToOklab(rgbToLinear(hexToRgb(hex)));\n}\n\n/** Formats OKLab as `#rrggbb`, clamping anything out of gamut. */\nexport function oklabToHex(lab: Oklab): string {\n return rgbToHex(linearToRgb(oklabToLinear(lab)));\n}\n\n/** Converts OKLab to OKLCH, with hue in degrees from 0 up to 360. */\nexport function oklabToOklch({ L, a, b }: Oklab): Oklch {\n const degrees = (Math.atan2(b, a) * DEGREES_PER_HALF_TURN) / Math.PI;\n return { L, C: Math.hypot(a, b), h: (degrees + DEGREES_PER_TURN) % DEGREES_PER_TURN };\n}\n\n/** Converts OKLCH, hue in degrees, to OKLab. */\nexport function oklchToOklab({ L, C, h }: Oklch): Oklab {\n const radians = (h * Math.PI) / DEGREES_PER_HALF_TURN;\n return { L, a: C * Math.cos(radians), b: C * Math.sin(radians) };\n}\n\n/** True when the colour lands inside sRGB, within a tiny rounding allowance. */\nexport function isInSrgbGamut(lab: Oklab): boolean {\n const { r, g, b } = oklabToLinear(lab);\n return isUnitChannel(r) && isUnitChannel(g) && isUnitChannel(b);\n}\n\n/** Lowers chroma until the colour fits sRGB; lightness and hue are kept. */\nexport function fitChromaToSrgb(lch: Oklch): Oklch {\n if (isInSrgbGamut(oklchToOklab(lch))) {\n return lch;\n }\n let low = 0;\n let high = lch.C;\n for (let step = 0; step < CHROMA_SEARCH_STEPS; step += 1) {\n const middle = (low + high) * HALF;\n if (isInSrgbGamut(oklchToOklab({ ...lch, C: middle }))) {\n low = middle;\n } else {\n high = middle;\n }\n }\n return { ...lch, C: low };\n}\n","export const enum CvdType {\n Protanopia = 'protanopia',\n Deuteranopia = 'deuteranopia',\n Tritanopia = 'tritanopia',\n}\n","import type { Oklab } from './types';\n\nfunction squaredDistance(x: Oklab, y: Oklab): number {\n const dL = x.L - y.L;\n const da = x.a - y.a;\n const db = x.b - y.b;\n return dL * dL + da * da + db * db;\n}\n\n/** Euclidean distance in OKLab, the CSS Color 4 deltaEOK. About 0.02 is just noticeable. */\nexport function deltaEOk(x: Oklab, y: Oklab): number {\n return Math.sqrt(squaredDistance(x, y));\n}\n\n/** Index of the palette entry nearest the target; first wins a tie, -1 when empty. */\nexport function nearestInPalette(target: Oklab, palette: readonly Oklab[]): number {\n let bestIndex = -1;\n let bestDistance = Infinity;\n palette.forEach((entry, index) => {\n const distance = squaredDistance(target, entry);\n if (distance < bestDistance) {\n bestDistance = distance;\n bestIndex = index;\n }\n });\n return bestIndex;\n}\n","import { linearToOklab, linearToRgb, rgbToLinear } from './convert';\nimport { CvdType } from './CvdType';\nimport { deltaEOk } from './deltaE';\nimport type { LinearRgb, Oklab, Rgb } from './types';\n\ntype MatrixRow = readonly [number, number, number];\ntype Matrix = readonly [MatrixRow, MatrixRow, MatrixRow];\n\nconst MACHADO_2009_FULL_SEVERITY: Readonly<Record<CvdType, Matrix>> = {\n [CvdType.Protanopia]: [\n [0.152286, 1.052583, -0.204868],\n [0.114503, 0.786281, 0.099216],\n [-0.003882, -0.048116, 1.051998],\n ],\n [CvdType.Deuteranopia]: [\n [0.367322, 0.860646, -0.227968],\n [0.280085, 0.672501, 0.047413],\n [-0.01182, 0.04294, 0.968881],\n ],\n [CvdType.Tritanopia]: [\n [1.255528, -0.076749, -0.178779],\n [-0.078411, 0.930809, 0.147602],\n [0.004733, 0.691367, 0.3039],\n ],\n};\n\nconst ALL_CVD_TYPES = [CvdType.Protanopia, CvdType.Deuteranopia, CvdType.Tritanopia];\n\nfunction applyRow([x, y, z]: MatrixRow, { r, g, b }: LinearRgb): number {\n return x * r + y * g + z * b;\n}\n\nfunction toLab(rgb: Rgb): Oklab {\n return linearToOklab(rgbToLinear(rgb));\n}\n\n/** What a viewer with full dichromacy of this type sees (Machado 2009, severity 1). */\nexport function simulateCvd(rgb: Rgb, type: CvdType): Rgb {\n const linear = rgbToLinear(rgb);\n const [red, green, blue] = MACHADO_2009_FULL_SEVERITY[type];\n return linearToRgb({\n r: applyRow(red, linear),\n g: applyRow(green, linear),\n b: applyRow(blue, linear),\n });\n}\n\n/** Smallest deltaEOk between two colours across normal vision and all three CVD types. */\nexport function visionDistance(x: Rgb, y: Rgb): number {\n const normalVision = deltaEOk(toLab(x), toLab(y));\n return ALL_CVD_TYPES.reduce(\n (smallest, type) =>\n Math.min(smallest, deltaEOk(toLab(simulateCvd(x, type)), toLab(simulateCvd(y, type)))),\n normalVision,\n );\n}\n","import { hexToOklab, hexToRgb, rgbToLinear } from './convert';\nimport { deltaEOk } from './deltaE';\n\nconst RED_WEIGHT = 0.2126;\nconst GREEN_WEIGHT = 0.7152;\nconst BLUE_WEIGHT = 0.0722;\nconst FLARE = 0.05;\nconst HALF = 0.5;\n\n/** WCAG 2 non-text contrast floor a bead colour must reach against what sits behind it. */\nexport const MIN_BEAD_CONTRAST = 3;\n\n/** Under-3:1 shares this close count as equal, and delta-E decides. */\nexport const BACKDROP_TIE_MARGIN = 0.01;\n\nexport interface BeadCount {\n readonly hex: string;\n readonly count: number;\n}\n\nexport interface BackdropScore {\n readonly hex: string;\n readonly underShare: number;\n readonly medianDeltaE: number;\n}\n\nfunction luminance(hex: string): number {\n const { r, g, b } = rgbToLinear(hexToRgb(hex));\n return RED_WEIGHT * r + GREEN_WEIGHT * g + BLUE_WEIGHT * b;\n}\n\nfunction weightedMedian(entries: readonly { value: number; count: number }[], total: number): number {\n const sorted = [...entries].sort((x, y) => x.value - y.value);\n let seen = 0;\n for (const entry of sorted) {\n seen += entry.count;\n if (seen >= total * HALF) {return entry.value;}\n }\n return 0;\n}\n\n/** WCAG 2 contrast ratio of two hex colours, 1 to 21. */\nexport function contrastRatio(x: string, y: string): number {\n const lumX = luminance(x);\n const lumY = luminance(y);\n return (Math.max(lumX, lumY) + FLARE) / (Math.min(lumX, lumY) + FLARE);\n}\n\n/** Measures one backdrop against a pattern's beads, weighted by how many of each bead it uses. */\nexport function scoreBackdrop(beads: readonly BeadCount[], backdropHex: string): BackdropScore {\n const counted = beads.filter((bead) => bead.count > 0);\n const total = counted.reduce((sum, bead) => sum + bead.count, 0);\n if (total === 0) {return { hex: backdropHex, underShare: 0, medianDeltaE: 0 };}\n const backdrop = hexToOklab(backdropHex);\n const under = counted\n .filter((bead) => contrastRatio(bead.hex, backdropHex) < MIN_BEAD_CONTRAST)\n .reduce((sum, bead) => sum + bead.count, 0);\n const distances = counted.map((bead) => ({ value: deltaEOk(hexToOklab(bead.hex), backdrop), count: bead.count }));\n return { hex: backdropHex, underShare: under / total, medianDeltaE: weightedMedian(distances, total) };\n}\n\n/** Lowest under-3:1 share wins; inside the margin the higher median delta-E wins; first wins a tie; null when empty. */\nexport function pickBackdrop(\n beads: readonly BeadCount[],\n backdrops: readonly string[],\n margin: number = BACKDROP_TIE_MARGIN,\n): BackdropScore | null {\n const scores = backdrops.map((hex) => scoreBackdrop(beads, hex));\n if (scores.length === 0) {return null;}\n const lowest = Math.min(...scores.map((score) => score.underShare));\n return scores\n .filter((score) => score.underShare <= lowest + margin)\n .reduce((best, score) => (score.medianDeltaE > best.medianDeltaE ? score : best));\n}\n","import {\n fitChromaToSrgb,\n hexToOklab,\n hexToRgb,\n linearToRgb,\n oklabToLinear,\n oklabToOklch,\n oklchToOklab,\n rgbToHex,\n} from './convert';\nimport { visionDistance } from './cvd';\nimport type { Oklch, Rgb } from './types';\n\nexport type HarmonyCount = 2 | 3 | 4;\n\n/** Every pair in a harmony is at least this far apart in every vision type. */\nexport const MIN_HARMONY_DISTANCE = 0.1;\n\nconst HUE_OFFSETS: Readonly<Record<HarmonyCount, readonly number[]>> = {\n 2: [180],\n 3: [120, 240],\n 4: [90, 180, 270],\n};\n\nconst LIGHTNESS_STEPS = 100;\n\ninterface Candidate {\n readonly rgb: Rgb;\n readonly clearance: number;\n readonly lightnessShift: number;\n}\n\nfunction buildCandidate(lch: Oklch, baseLightness: number, chosen: readonly Rgb[]): Candidate {\n const rgb = linearToRgb(oklabToLinear(oklchToOklab(fitChromaToSrgb(lch))));\n const distances = chosen.map((other) => visionDistance(rgb, other));\n return {\n rgb,\n clearance: Math.min(MIN_HARMONY_DISTANCE, ...distances),\n lightnessShift: Math.abs(lch.L - baseLightness),\n };\n}\n\nfunction pickBetter(best: Candidate, next: Candidate): Candidate {\n if (next.clearance !== best.clearance) {\n return next.clearance > best.clearance ? next : best;\n }\n return next.lightnessShift < best.lightnessShift ? next : best;\n}\n\nfunction pickAtHue(base: Oklch, hueOffset: number, chosen: readonly Rgb[]): Rgb {\n const candidates = Array.from({ length: LIGHTNESS_STEPS + 1 }, (_, step) =>\n buildCandidate(\n { L: step / LIGHTNESS_STEPS, C: base.C, h: base.h + hueOffset },\n base.L,\n chosen,\n ),\n );\n return candidates.reduce(pickBetter).rgb;\n}\n\n/**\n * Base colour first, then the others at fixed OKLCH hue offsets: 2 = 180, 3 = 120, 4 = 90.\n * Lightness moves as little as it can to clear MIN_HARMONY_DISTANCE; chroma only drops to fit sRGB.\n * @returns `count` lowercase `#rrggbb` values.\n */\nexport function harmony(baseHex: string, count: HarmonyCount): string[] {\n const base = oklabToOklch(hexToOklab(baseHex));\n const colours = HUE_OFFSETS[count].reduce<readonly Rgb[]>(\n (chosen, hueOffset) => [...chosen, pickAtHue(base, hueOffset, chosen)],\n [hexToRgb(baseHex)],\n );\n return colours.map(rgbToHex);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dloizides/color-science",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pure colour maths: OKLab and OKLCH conversion, deltaE, nearest-in-palette, colour-vision-deficiency simulation and 2, 3, 4-colour harmonies that stay distinct under CVD. No DOM, no React, no dependencies.",
5
5
  "keywords": [
6
6
  "color",