HUSL is a human-friendly alternative to HSL.

you can see CIELUV, a color space designed for perceptual uniformity based on human experiments. When accessed by polar coordinates, it becomes functionally similar to HSL with a single problem: its chroma component doesn't fit into a specific range.

HUSL extends CIELUV with a new saturation component that allows you to span all the available chroma as a neat percentage.

H
S
L

Implementations

HUSL is implemented as a set of functions to convert colors between RGB, HUSL and HUSLp. Check the individual ports for documentation and licensing. Most of the code is released under the MIT license. The math is available under the public domain.

The reference implementation is in JavaScript. This code is used to generate a snapshot of the color space. These snapshots, serialized in JSON, are used for regression testing. If you want to port HUSL to a different language or finish one of the started ports, you should match your implementation against the snapshot generated by the reference implementation. The snapshots are stored in the repo. Contact the original author by email.

Comparison

HSL CIE LChuv HUSL HUSLp
Lightness Relative Absolute Absolute Absolute
Saturation* Relative Absolute Relative Absolute
Hue uniformity Poor Good Good Good
Component ranges Defined Undefined Defined Defined
Saturated colors Yes Yes Yes No
* Saturation and chroma are actually distinct concepts

HSL

CIE LChuv

The dips in the graph represent impossible colors (such as dark saturated yellow). CIE LChuv doesn't warn you about them, making it unsuitable for generating colors.

HUSL

HUSL preserves the lightness and hue components of CIE LChuv and stretches its chroma so that every color has the same range, defined as a percentage.

HUSLp

HUSLp takes as many colors as it can from CIE LChuv without distorting the chroma. As you can see, the resulting color space is smooth, but only pastel colors can be included.

Chroma

The chroma component of CIE LChuv is absolute. Unlike HSL's saturation you can effectively use it to compare two different colors.

Both HSL and HUSL have a distorted chroma map, this is a trade-off of their convenient shape. Admittedly, HUSL's chroma has more sudden shifts.

Lightness

This is why HSL is useless for working out contrast. The actual lightness varies drastically between hues.

For this demo I am using CIE's definition of lightness. Both CIE LChuv and HUSL use this component, so their picture is an even gray.

Let's try to generate some random background colors:

function randomHue() {
  return Math.floor(Math.random() * 360);
}
$(...).css('background-color', $.husl.toHex(randomHue(), 90, 60));
$(...).css('background-color', hslToHex(randomHue(), 90, 60));
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%
Lightness: 60%

HUSL's uniform hue means random colors will be truly random. Iterating over colors will also produce better results.

$('#rainbow-husl div').each(function(index) {
  $(this).css('background-color', $.husl.toHex(index * 36, 90, 60));
});

$('#rainbow-hsl div').each(function(index) {
  $(this).css('background-color', hslToHex(index * 36, 90, 60));
});

How does HUSL work?

HUSL is defined as a conversion to and from CIE LChuv. For both HUSL and HUSLp, the first step is to find the chroma bounds for a given lightness. These are lines, crossing which will push the color out of the RGB gamut. We define a function, getBounds(L) which returns 6 straight lines in slope-intercept format. Each of them represents one of three RGB channels being pushed either below 0 or above 1.

HUSL:

  1. Given a hue (angle), build a ray starting from (0, 0) and find the point where it first intersects with one of the bounding lines.
  2. The distance from (0, 0) to this point is the maximum chroma for given L and H. Scale saturation to be a percentage of this distance.

HUSLp:

  1. Use perpendiculars to find the shortest distance from (0, 0) to one of the bounding lines.
  2. This distance is the lowest common chroma. For the given L and any H let saturation span this distance.

I used Maxima to build and solve these equations, and a wxMaxima file is bundled with the code explaining how to get the bounding lines. The rest is simple geometry you can see in the reference implementation.