colorizr
Version:
Manipulate colors like a boss
61 lines (49 loc) • 1.85 kB
text/typescript
import { oklch2oklab } from '~/converters';
import formatCSS from '~/format-css';
import { GAMUT_EPSILON, MESSAGES } from '~/modules/constants';
import { invariant } from '~/modules/invariant';
import { isInGamut, oklabToLinearSRGB } from '~/modules/linear-rgb';
import { resolveColor } from '~/modules/parsed-color';
import { isString } from '~/modules/validators';
import { ColorType } from '~/types';
/**
* Map a color into the sRGB gamut by reducing chroma in OkLCH space.
* Colors already in gamut are returned unchanged.
*
* @param input - The input color string.
* @param format - Optional output color format.
* @returns The gamut-mapped color string.
*/
export default function toGamut(input: string, format?: ColorType): string {
invariant(isString(input), MESSAGES.inputString);
const parsed = resolveColor(input);
const lch = parsed.oklch;
const output = format ?? parsed.type;
const alpha = parsed.alpha < 1 ? parsed.alpha : undefined;
// Edge cases: extreme lightness
if (lch.l <= 0) {
return formatCSS({ r: 0, g: 0, b: 0 }, { format: output, alpha });
}
if (lch.l >= 1) {
return formatCSS({ r: 255, g: 255, b: 255 }, { format: output, alpha });
}
// Already in gamut?
const lab = oklch2oklab(lch, 16);
if (isInGamut(oklabToLinearSRGB(lab.l, lab.a, lab.b))) {
return formatCSS(lch, { format: output, alpha });
}
// Binary search: reduce chroma until in sRGB gamut
const epsilon = GAMUT_EPSILON;
let low = 0;
let high = lch.c;
while (high - low > epsilon) {
const mid = (low + high) / 2;
const midLab = oklch2oklab({ l: lch.l, c: mid, h: lch.h }, 16);
if (isInGamut(oklabToLinearSRGB(midLab.l, midLab.a, midLab.b))) {
low = mid;
} else {
high = mid;
}
}
return formatCSS({ l: lch.l, c: low, h: lch.h }, { format: output, alpha });
}