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 }); }