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
- L = 0 or L = 1 (black, white): the denominator 1 - |2L - 1| is zero. Then d is zero too, so check d before dividing.
- Very light and very dark colors get high S with barely visible tint, e.g. (255, 250, 250) has S = 100%. See the page on saturation.
- H = 360° must become 0°, or (H / 60°) mod 2 and the sector choice can go wrong.
- Value order: CSS
hsl()takes (H, S, L), while the name HLS suggests (H, L, S). It is easy to swap S and L when moving values around.
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.