semmixcolor and perception

Converting HLS and RGB

HLS and HSL are the same model (see HLS and HSI models). Here is the algorithm in both directions, with an example and code, plus the cases implementations most often get wrong.

RGB to HLS

max = max(R, G, B), min = min(R, G, B), d = max - min L = (max + min) / 2 d = 0 → S = 0, H undefined (gray) d > 0 → S = d / (1 - |2L - 1|) H: the hexagonal formula, as in HSV (see RGB to hue angle)

Older descriptions give saturation as two formulas depending on L. It is the same result written differently:

L ≤ 0.5 → S = d / (max + min) L > 0.5 → S = d / (2 - max - min)

HLS to RGB

C = (1 - |2L - 1|) · S (chroma) X = C · (1 - |(H / 60°) mod 2 - 1|) m = L - C / 2 0° ≤ H < 60° → (C, X, 0) 60° ≤ H < 120° → (X, C, 0) 120° ≤ H < 180° → (0, C, X) 180° ≤ H < 240° → (0, X, C) 240° ≤ H < 300° → (X, 0, C) 300° ≤ H < 360° → (C, 0, X) (R, G, B) = table result + m

The only differences from HSV are in two places: C is computed from L rather than V, and the offset m is L - C/2 rather than V - C. The sector table is identical.

Example both ways

RGB (200, 80, 40), #c85028:

R = 0.7843 G = 0.3137 B = 0.1569 max = 0.7843 min = 0.1569 d = 0.6275 L = (0.7843 + 0.1569) / 2 = 0.4706 S = 0.6275 / (1 - |0.9412 - 1|) = 0.6275 / 0.9412 = 0.6667 (or: L ≤ 0.5, so S = 0.6275 / 0.9412, same result) H = 60° · (0.3137 - 0.1569) / 0.6275 = 15° HLS = (15°, 47.1%, 66.7%) back: C = 0.9412 · 0.6667 = 0.6275 X = 0.6275 · (1 - |0.25 - 1|) = 0.1569 m = 0.4706 - 0.3137 = 0.1569 sector 0°..60° → (0.6275, 0.1569, 0) + 0.1569 = (0.7843, 0.3137, 0.1569) RGB = (200, 80, 40)

Edge cases

Code

// r, g, b, s, l in 0..1, h in degrees
function rgbToHsl(r, g, b) {
  const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
  const l = (max + min) / 2;
  if (d < 1e-9) return { h: 0, s: 0, l };
  const s = d / (1 - Math.abs(2 * l - 1));
  let h = max === r ? (g - b) / d : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
  h *= 60;
  return { h: h < 0 ? h + 360 : h, s, l };
}

function hslToRgb(h, s, l) {
  h = ((h % 360) + 360) % 360;
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
  const m = l - c / 2;
  const [r, g, b] =
    h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] :
    h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
  return { r: r + m, g: g + m, b: b + m };
}

Another equally common version of hslToRgb, with a hue2rgb function, is on the helper functions page.