@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 +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
package/dist/index.mjs.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.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.
|
|
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",
|