Skip to the content

Capsid menu

app/lib/contrast.mjs

61 lines and 8 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of contrast.mjs
FunctionLinesComplexityCoveredCRAPMutation
channels5 to 203No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:11>11 to 111No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
luminance23 to 291No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
linear27 to 272No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
contrast32 to 361No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
apca44 to 665No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
Y46 to 491No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
clampBlack51 to 512No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports no other module of this repo. No module of this repo imports it.

Source

/**
 * @param {string} hex
 * @returns {[number, number, number]}
 */
export function channels(hex) {
  const h = hex.trim().replace("#", "");
  const full =
    h.length === 3
      ? h
          .split("")
          .map((c) => c + c)
          .join("")
      : h;
  if (!/^[0-9a-fA-F]{6}$/.test(full)) throw new Error(`not a hex color: ${hex}`);
  return [
    parseInt(full.slice(0, 2), 16) / 255,
    parseInt(full.slice(2, 4), 16) / 255,
    parseInt(full.slice(4, 6), 16) / 255,
  ];
}

/** @param {string} hex */
function luminance(hex) {
  // Destructured before the transform: `.map` over a tuple loses its length for the type checker.
  const [r, g, b] = channels(hex);
  /** @param {number} c */
  const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
  return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
}

/** @param {string} a @param {string} b */
export function contrast(a, b) {
  const la = luminance(a);
  const lb = luminance(b);
  return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
}

/**
 * APCA, W3C draft 0.1.9 constants. Sign carries polarity; magnitude is what is compared.
 *
 * @param {string} textHex
 * @param {string} bgHex
 */
export function apca(textHex, bgHex) {
  /** @param {string} hex */
  const Y = (hex) => {
    const [r, g, b] = channels(hex);
    return 0.2126729 * r ** 2.4 + 0.7151522 * g ** 2.4 + 0.072175 * b ** 2.4;
  };
  /** @param {number} y */
  const clampBlack = (y) => (y < 0.022 ? y + (0.022 - y) ** 1.414 : y);

  const Ytxt = clampBlack(Y(textHex));
  const Ybg = clampBlack(Y(bgHex));
  if (Math.abs(Ybg - Ytxt) < 0.0005) return 0;

  let out;
  if (Ybg > Ytxt) {
    const sapc = (Ybg ** 0.56 - Ytxt ** 0.57) * 1.14;
    out = sapc < 0.1 ? 0 : sapc - 0.027;
  } else {
    const sapc = (Ybg ** 0.65 - Ytxt ** 0.62) * 1.14;
    out = sapc > -0.1 ? 0 : sapc + 0.027;
  }
  return out * 100;
}