Instantly share code, notes, and snippets.

Created July 8, 2024 08:59
Show Gist options
• Save mattdesl/5612a41e5d69246f2b807ca1f272807d to your computer and use it in GitHub Desktop.

## Fast OKLab Gamut Mapping in JavaScript

Using OKLCH and sRGB. This also works for Display P3, however you'll have to pass the correct coefficients to the cusp & intersection utilities, and instead of using linear sRGB, you would convert to linear Display P3.

If you wish to support both sRGB and Display P3 gamut mapping, it is possible with a single function and a matrix parameter, as they both use the same coordinate range (0..1) and gamma transfer function.

```import { OKLCH_to_linear_sRGB } from './convert.js';
import { rgb_in_gamut, linear_to_gamma } from './util.js';
import { find_cusp, find_gamut_intersection } from './oklab-cusp.js';

let lch = [ 0.5, 0.2, 270 ];
let lrgb = OKLCH_to_linear_sRGB(lch);

// if not exactly in gamut
if (!rgb_in_gamut(lrgb, 0)) {
// do gamut mapping
const LC = mapToGamutLcusp(lch, cusp);
// get new LCH
lch = [LC[0], LC[1], H];
// convert again to lRGB
lrgb = OKLCH_to_linear_sRGB(lch);
}

// note: after gamut mapping, it may be on the edge, technically outside of sRGB gamut
// but it is so close that we just clip it, as it is far below
// the "JND" in terms of DeltaEOK
const [ R, G, B ] = lrgb.map(n => clamp(linear_to_gamma(n), 0, 1));

// gamma sRGB coords in 0 ... 1
console.log(R,G,B);

function mapToGamutLcusp(
LCH,
cusp
) {
const [L, C, H] = LCH;
const aNorm = Math.cos(hueAngle);
const bNorm = Math.sin(hueAngle);
cusp = cusp || find_cusp(aNorm, bNorm);

// Cusp luminance as target
const LTarget = cusp[0];

// Other strategies
// const LTarget = clamp(LCH[0], 0, 1);
// const LTarget = 0.5;

const t = find_gamut_intersection(
aNorm,
bNorm,
L,
C,
LTarget,
cusp
);
const L_clipped = LTarget * (1 - t) + t * L;
const C_clipped = t * C;
return [L_clipped, C_clipped, H];
}

function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}```

## Credits

The matrices and conversion code has been mostly pulled from Colorjs and Coloraide. It exists mostly for reference, it is probably wiser to use the Colorjs library rather than relying on my conversion code here with no tests/coverage.

Code and reference for sRGB gamut clipping is from Björn Ottosson's blog on the subject here and here.