Color Representation and Gradients

Part 1: RGB and HSL

This is a discussion about forming gradients through color spaces, a gradient being a smooth transition between two colors. RGB and HSL will be the focus, but much will be able to be extrapolated to other spaces. In part 1 we define the RGB and HSL color space. In part 2 we discuss what a gradient is and how to define it mathematically. In part 3, as a bit of fun, we will define an alternative way of forming gradients in HSL space.


Part 1: RGB and HSL

  1. Representing Colors
  2. Defining HSL
  3. Deriving HSL

Part 2: Gradients

  1. What a Gradient Is
  2. Difficulties with HSL Paths
  3. Fixing HSL Paths

Part 3: Creating Our Own Gradients

  1. The Idea
  2. The Derivation
  3. Creating Paths
  4. The Implementation

Representing Colors

Colors can be defined in many ways. For computers the most common way to define color is with RGB, which defines every color in terms of its composition of red, green, and blue. Hex codes are merely an alternative respresentation of what is ultimately RGB. For example, #29518d represents rgb(41, 81, 141), as 29, 51, and 8d in hex are 41, 81, and 141 in decimal, respectively.

The next most common representation is HSL, which expresses a color as a combination of hue, saturation, and lightness. One of the immediate problems with HSL is that it relies on aspects of light that are fundamentally subjective; saturation especially is much more about the way humans experience color than any intrinsic properties of color. We will explore this more when we define HSL representation in terms of RGB.

RGB and HSL are both examples of color spaces. Color spaces are so called because they give each color a coordinate, just how each point in a physical space can be given a coordinate. RGB can be thought of as a cube with R, G, and B being the x, y, and z coordinates.

RGB cube

Though there are many other representations of color, such as HSB and CIELAB, here we will focus on RGB and HSL.

Defining HSL

In the HSL color space, each color is given a hue, saturation, and lightness coordinate. Instead of representing this as a rectangle, as in RGB space, HSL is best represented as a cylinder, where hue determines the angle, saturation the distance from the center, and lightness the distance from the bottom.

HSL cylinder

In RGB each of the values has a range of [0, 255], while in most implementations of HSL hue has a range of [0, 360) (being an angle), and saturation and lightness both have a range of [0, 100] (being percents). For our purposes however we will consider hue, saturations, and lightness as all having a range from 0 to 1, as this simplifies the math a bit.

One important thing to notice about HSL is that it, unlike RGB, does not give a unique representation to certain colors. When lightness is 1 the color is white regardless of what hue and saturation are. Likewise, a lightness of 0 always produces black. In addition, whenever saturation is 0 the color will be gray, and only lightness will affect the shade with hue playing no role. This lack of uniqueness will come up again when we try to define gradients involving these colors.

The actual derivations of the hue, saturation, and lightness are complex, but we will go through them and see why they are sensible.

Deriving HSL

We will be deriving hue, saturation, and lightness (denoted H, S, and L) from an RGB color (red, green, and blue will be denoted R, G, and B). We will be using rgb(34, 224, 208) as our example color. Before going into the derivations we will do three things:

  1. We will normalize the RGB colors by dividing each by 255, their max value. For our color this produces the following:
  2. We will define max and min as the maximum and minimum of R, G, and B. For our color this would mean
  3. We will define sum and dif as the sum and difference of max and min. This yields

With that done the derivations will be simpler.


To find hue H we perform the following:

  1. if max = min, then H is undefined. Otherwise,
  2. we define H' according to the following:
    1. if R = max, then H' = 0 + (G - B) / dif.
    2. if G = max, then H' = 2 + (B - R) / dif.
    3. if B = max, then H' = 4 + (R - G) / dif.
  3. if H' < 0, then H = H' / 6 + 1. Otherwise, H = H' / 6.

The reasoning behind step 1 is that if max = min then R = G = B. This would make a shade of gray, for which hue is irrelevant. In many implementations hue is simply set to 0 in this case.

In step 2, note that the fraction used in defining H' will always be less than one. This is because max is missing from the numerator, while being present in the numerator (dif = max - min). Given that the numerator can be positive or negative the range of values of this fraction is [-1, 1]. Because this range has a size of two, we seperate each of the three possibilities by 2 (the 0, 2, and 4 in the equations for red, green, and blue). This makes it so H' has a range of [-1, 1] if R = max, [1, 3] if G = max, and [3, 5] if B = max. Thus, H' has a range of [-1, 5], which has a size of 6. We want our value for H to be between 0 and 1, which is why we divide H' by 6, adding 1 if H' is negative.

Pure red is made to be 0 degrees, so that hsl(0, 1, .5) (100% saturated with 50% brightness) equals rgb(255, 0, 0). This is an arbitrary choice, as any color could have been chosen for 0 degrees, but in this case red was chosen. In addition to this, a pure green has a hue of 1/3 (120 degrees), and pure blue has a hue of 2/3 (240 degrees).

Given this it should make sense as to why, when calculating H', we take the dominant color, max, and calculate the difference between the other two colors as the numerator. In this numerator one color is subtracted from the other. To make sense of which color is the one subtracted it is best to go through an example:

If we have rgb(40, 120, 80), our calculation would be 2 + (80 - 40)/80 = 2.5, which would then get divided by 6 in step 3, yielding a hue of .416. In degrees this hue would be 150, which is 30 degrees closer to pure blue (located at 240 degrees) than is pure green (located at 120 degrees). Because blue has a greater hue than green and red has a smaller hue (in terms of degrees), we add blue and subtract red when green is the dominant color.

Going through these calculations for our example color of rgb(34, 224, 208) results in the following:

  1. We see that max does not equal min, so we go to step 2.
  2. G = max, so H' = 2 + (B - R) / dif = 2 + (.8156863 - .13) / .745098 = 2.9157896.
  3. We see that H' is positive, so H = H' / 6 = 2.9157896 / 6 = .4859649.

So we have determined that the hue for rgb(34, 224, 208) is .4859649.


Saturation S is defined as follows:

  1. If dif = 0, then S = 0. Otherwise,
  2. define D by the follwing:
    1. if sum < 1, then D = sum. Otherwise,
    2. D = 2 - sum.
  3. S = dif / D

Step 1 follows much the same logic as step 1 for hue. If min = max then the color is gray, so there is no saturation. This step would not be necessary (it would be handled by step 3) save for the edge cases where sum = 0 or sum = 2 (in both these cases dif would equal 0), which would cause division by 0.

For step 2, note that sum has a range of [0, 2], so D is the distance of sum to its nearest extreme.

In step 3 we take dif and scale it into the interval [0, 1]. D places an upper bound on dif, and so accomplishes this scaling. Doing this is intended to mirror our notion of what saturation is: how colorful a color is relative to its brightness. dif, being equal to max - min, describes how colorful something is by measuring how far away the maximum and minimum channels of RGB are, as the closer these channels are the more gray a color is. D can be though of as a way to measure brightness.

How well this definition aligns with our perception of saturation is questionable, and the HSL model is generally agreed to have failed in this regard, but nevertheless it acts as a decent approximation. Consider these shades of blue, however:

  1. hsl(.55, .9, .5)
  2. hsl(.66, .8, .5)
  3. hsl(.55, .5, .5)
  4. hsl(.66, .4, .5)

To me, shade 2 seems more saturated than shade 1, but I think we can all agree that 1 and 2 are more saturated than 3 and 4.

This example is meant to show that HSL is not a perfect model, but that it is a working model.

For our example color we see that

  1. dif does not equal 0, so
  2. sum > 1, so D = 2 - sum = 2 - 1.0117647 = .9882353
  3. S = dif / D = .745098 / .9882353 = .7539683.

Thus we have determined that the saturation for rgb(34, 224, 208) is .7539683.


Lightness is defined as simply sum / 2, the average of max and min. This is certainly the most immediately appealing to intuition.

For our example color this would mean that L = 1.0117647 / 2 = .5058824.

And with that we determine that the HSL representation of rgb(34, 224, 208) is hsl(.4859649, .7539683, .5058824).

In CSS the hue value would be multiplied by 360, and the saturation and lightness would be multiplied by 100 with a percent sign affixed, yielding hsl(175, 75%, 51%) (after rounding).

The mathematically inclined will notice that the transformations from RGB to HSL are not linear transformations, and thus straight-line paths in RGB will not be preserved. This will be the motivation behind the construction of gradients in these spaces.