Source

spaces/oklab.js

// Oklab and related spaces: OKLCH, OKHSV(sRGB), OKHSL(sRGB)

import { constrainAngle, vec3 } from "../util.js";
import {
  OKHSLToOKLab,
  OKHSVToOKLab,
  OKLabToOKHSL,
  OKLabToOKHSV,
} from "../okhsl.js";

import { sRGBGamut } from "./srgb.js";

// based on colorjs.io, could perhaps use a more specific number than this
const ACHROMATIC_EPSILON = (0.4 - 0.0) / 100000;

/**
 * The OKLab color space.
 * @type {ColorSpace}
 * @category spaces
 */
export const OKLab = {
  id: "oklab",
};

/**
 * The OKLCH color space, with Lightness, Chroma, and Hue components. This is the cylindrical form of the {@link OKLab} color space.
 * @type {ColorSpace}
 * @category spaces
 */
export const OKLCH = {
  id: "oklch",
  base: OKLab,
  toBase: (oklch, out = vec3()) => {
    // Note: newer version of Colorjs.io clamps oklch chroma
    // However, this means that oklch(0.5, -0.36, 90) -> srgb will result in an in-gamut rgb
    // which seems a bit odd; you'd expect it to be out of gamut. So we will leave
    // chroma unclamped for this conversion.
    // const C = Math.max(0, oklch[1]);

    const C = oklch[1];
    const H = oklch[2];
    out[0] = oklch[0]; // L remains the same
    out[1] = C * Math.cos((H * Math.PI) / 180); // a
    out[2] = C * Math.sin((H * Math.PI) / 180); // b
    return out;
  },
  fromBase: (oklab, out = vec3()) => {
    // These methods are used for other polar forms as well, so we can't hardcode the ε
    const a = oklab[1];
    const b = oklab[2];
    let isAchromatic =
      Math.abs(a) < ACHROMATIC_EPSILON && Math.abs(b) < ACHROMATIC_EPSILON;
    let hue = isAchromatic
      ? 0
      : constrainAngle((Math.atan2(b, a) * 180) / Math.PI);
    let C = isAchromatic ? 0 : Math.sqrt(a * a + b * b);
    out[0] = oklab[0]; // L remains the same
    out[1] = C; // Chroma
    out[2] = hue; // Hue, in degrees [0 to 360)
    return out;
  },
};

/**
 * An implementation of the OKHSL color space, fixed to the {@link sRGBGamut}. This is useful for color pickers and other applications where
 * you wish to work with components in a well-defined and enclosed cylindrical form. If you wish to use OKHSL with a different gamut, you'll
 * need to use the {@link OKHSLToOKLab} and {@link OKLabToOKHSL} methods directly, passing your desired gamut.
 * @type {ColorSpace}
 * @category spaces
 */
export const OKHSL = {
  // Note: sRGB gamut only
  // For other gamuts, use okhsl method directly
  id: "okhsl",
  base: OKLab,
  toBase: (okhsl, out = vec3()) => OKHSLToOKLab(okhsl, sRGBGamut, out),
  fromBase: (oklab, out = vec3()) => OKLabToOKHSL(oklab, sRGBGamut, out),
};

/**
 * An implementation of the OKHSV color space, fixed to the {@link sRGBGamut}. This is useful for color pickers and other applications where
 * you wish to work with components in a well-defined and enclosed cylindrical form. If you wish to use OKHSL with a different gamut, you'll
 * need to use the {@link OKHSLToOKLab} and {@link OKLabToOKHSL} methods directly, passing your desired gamut.
 * @type {ColorSpace}
 * @category spaces
 */
export const OKHSV = {
  // Note: sRGB gamut only
  // For other gamuts, use okhsv method directly
  id: "okhsv",
  base: OKLab,
  toBase: (okhsl, out = vec3()) => OKHSVToOKLab(okhsl, sRGBGamut, out),
  fromBase: (oklab, out = vec3()) => OKLabToOKHSV(oklab, sRGBGamut, out),
};