semmixcolor and perception

Hue angle to RGB

The hue angle H is the coordinate shared by HSV and HLS. Turning the angle into RGB is the first step of converting both models back to RGB. You first compute the pure color of that hue, and only then add saturation and brightness.

The color wheel as a hexagon

In HSV and HLS the color wheel is really a hexagon: every 60° there is one corner of the RGB cube. Between corners one component rises or falls linearly while the others stay fixed:

HRGBWhat changes
0°..60°1rises 0 → 10red → yellow
60°..120°falls 1 → 010yellow → green
120°..180°01rises 0 → 1green → cyan
180°..240°0falls 1 → 01cyan → blue
240°..300°rises 0 → 101blue → magenta
300°..360°10falls 1 → 0magenta → red

A formula without branches

The whole table fits into three absolute-value formulas. Let h = H / 60°, and let clamp cut the result to 0..1 (see helper functions):

P_R = clamp(|h - 3| - 1) P_G = clamp(2 - |h - 2|) P_B = clamp(2 - |h - 4|)

For H = 15°: h = 0.25, so P = (clamp(1.75), clamp(0.25), clamp(-1.75)) = (1, 0.25, 0), i.e. RGB (255, 64, 0).

HRGB of the pure color
0°255, 0, 0
30°255, 128, 0
60°255, 255, 0
90°128, 255, 0
120°0, 255, 0
180°0, 255, 255
200°0, 170, 255
240°0, 0, 255
300°255, 0, 255
330°255, 0, 128

Adding saturation and brightness

With the pure color P, any HSV or HSL color follows from one formula:

HSV: RGB = V · (1 - S + S · P) HSL: C = (1 - |2L - 1|) · S RGB = L + C · (P - 0.5)

For HSV (15°, 80%, 78.4%):

RGB = 0.7843 · (0.2 + 0.8 · (1, 0.25, 0)) = 0.7843 · (1, 0.4, 0.2) = (0.7843, 0.3137, 0.1569) → (200, 80, 40)

For HSL (15°, L 47.1%, S 66.7%): C = 0.9412 · 0.6667 = 0.6275, and RGB = 0.4706 + 0.6275 · (0.5, -0.25, -0.5) = (0.7843, 0.3137, 0.1569). The same color.

Code

const clamp = (x) => Math.min(1, Math.max(0, x));

// pure color for hue h in degrees
function hueToRgb(h) {
  const t = (((h % 360) + 360) % 360) / 60;
  return {
    r: clamp(Math.abs(t - 3) - 1),
    g: clamp(2 - Math.abs(t - 2)),
    b: clamp(2 - Math.abs(t - 4)),
  };
}

function hsvToRgb(h, s, v) {
  const p = hueToRgb(h);
  const f = (c) => v * (1 - s + s * c);
  return { r: f(p.r), g: f(p.g), b: f(p.b) };
}

The reverse conversion, from RGB to a hue angle, is on the RGB to hue angle page.