Color Representation and Gradients

Part 2: Gradients

In this part we will begin our discussion of gradients, and how they are produced differently in different color spaces.

Contents

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

What a Gradient Is

A gradient is a path through a color space. Just as a line segment can go from (128, 53, 82) to (28, 117, 87) in 3 dimensional space, so to can a color go between rgb(128, 53, 82) and rgb(28, 117, 87) in RGB space. The way this is accomplished in RGB space is relatively straightforward:

  1. Take the difference in the R, G, and B channels.
  2. Divide each by the number of pixels the gradient goes through.
  3. Add these values to the R, G, and B channels for each subsequent line in the gradient.

It's best to illustrate this with an example. Let's say we have p pixels, then taking the colors mentioned above we would do the following:

  1. (28, 117, 87) - (128, 53, 82) = (-100, 64, 5)
  2. (-100 / p, 64 / p, 5 / p)
  3. Give the nth line of the gradient an RGB value of (128 - 100n / p, 53 + 64n / p, 82 + 5n / p)

Once we have done these steps we can make a gradient like the one above. This gradient is formed by having each horizontal line of pixels be equal to the one before it, but with the R, G, and B values being increased by (-100 / p, 64 / p, 5 / p).

Some will recognize this process as that of finding points on a line in three dimensional space using the vector form of a line. If you are not such a person I hope that it still makes sense as to why it works.

This is only a path in RGB space, however. A path between the same colors in HSL space using the same method would produce the following:

This is accomplished by a practically identital process, the only difference being that in step 1 we take the difference of the H, S, and L values of the colors instead of the R, G, and B values.

If we are tracing a path in RGB space between two colors with the same amount of red, then every color in the gradient will have that amount of red. Likewise, in HSL space, if we're tracing a path between two colors with the same saturation every color in the resulting gradient will have that saturation; this will produce gradients that are uniformly colorful, and avoid the gray band that is present in many RGB gradients. It can be seen why this might be a desirable property for certain gradients in certain contexts.

The process described above, where we appropriate the strategy for making RGB gradients for making HSL gradients, while operational, has several undesirable properties, which we will now explore.

Difficulties with HSL Paths

The two main aspects of HSL that cause difficulties in defining gradients are

  1. The non-unique representations of grayscale colors
  2. The periodic nature of hue

We'll breifly touch on what these mean before going on to how to fix them.

Non-Unique Grayscale Colors

The first aspect affects any gradient that goes either to or from a grayscale color. Even though hsl(0, 0, .5) and hsl(.9, 0, .5) are the same color, computing a gradient from them to hsl(.9, 1, .5) using the method outlined for RGB colors would result in very different gradients: hsl(0, 0, .5) to hsl(.9, 1, .5) would result in the computation of a traversal from 0 to .9 in the H channel, while a similar computation for hsl(.9, 0, .5) to hsl(.9, 1, .5) would maintain the same hue throughout the gradient. We can see the difference below:

hsl(0, 0, .5) to hsl(.9, 1, .5)
hsl(.9, 0, .5) to hsl(.9, 1, .5)

This problem is worse for black and white. While a gradient involving gray is caused by hue not being unique, black and white also have the saturation lacking uniqueness. This means that there can be many gradients from black or white to a given color, some of which will traverse about a wide range of hues and saturations and others that will not change in hue or saturation at all. A gradient from black to white could go through any color whatsoever, depending on the accidentals of how it is represented in HSL.

The Periodicity of Hue

The problem with hue is that it is intended, as in the cylinder model, to be periodic, yet we represent hue on an interval ([0, 1) in our case), which is linear. Thus, a path from a color with hue 0 to a hue of .99 would go all the way around the rainbow of hues, when it would be more desirable for it to wrap around. Consider, for example, the following gradient generated by the procedure described above:

rgb(255, 0, 0) to rgb(255, 0, 1) in HSL

Even though the only difference in the colors was 1 unit of blue, the naive method used needlessly produces every hue there is. This is obviously not a desirable feature, but it is something we can fix.

Fixing HSL Paths

Fixing the problems with grayscale colors is easier, so we will start with that before we go on to solve the problems with hue.

Fixing Grayscale Colors

The problem with grayscale colors is that they don't have unique hues, and in the case of black and white they also lack unique saturations. To solve this all we need is some pre-processing: we set the hue of the gray equal to whatever other color defines our gradient, and then employ the same method as before. For black and white we would also set saturations equal. This provides a unique path from any grayscale color to any other color, with the added benefit that it is the shortest path between them in HSL space.

As an example, we might consider the path between hsl(0, 0, .5) and hsl(.9, 1, .5) that proved difficult for the naive method to handle. With this fix we would simply look at the two colors, see that one of them is gray, and set its hue, 0 in this case, equal to the hue of the other color, .9. This would produce the same gradient as that between hsl(.9, 0, .5) and hsl(.9, 1, .5) illustrated earlier.

Fixing Hue

The problem with hue demands a solution slightly more complicated than the fix for grayscale colors.

First, we can notice that given two colors, the problem with hue only presents itself when the positive difference between the hues of our colors exceeds .5. If this condition is met we then take that difference D' and compute the distance in hue as D = 1 - D'. We then either decrement or add to the hue of our first color D', adding or subtracting 1 when the value for hue leaves [0, 1]. Put succinctly, this procedure would look like this:

  1. Let D' equal the positive difference in the hues of our colors. If D' > .5, then
  2. Let D = 1 - D'.
  3. Let C = D / p, where p is the number of pixels.
  4. If the hue of the first color is less than the hue of the second, increment by C, otherwise, decrement by C
  5. If the computed hue becomes negative, add 1. If it exceeds 1, subtract 1.

This procedure accomplishes our goal of 'wrapping around' the HSL cylinder.

As an illustration of this fix we can consider the path between rgb(255, 0, 0) and rgb(255, 0, 1) mentioned above. With this it would look like this:

Hardly a gradient, but for these two colors that is precisely the desired behavior.

The only undesirable property of HSL paths now is that if the difference in hue goes from .5 to .51 a completely different path will be calculated, but this is unavoidable.

With these issues addressed, we now have a fully functional method for creating paths in HSL space.


You can see the result of these paths yourself in the space below, where a gradient in both RGB and HSL space will appear:

RGB

HSL

In part 3, just as a bit of fun, we will create another way to form gradients in a sort of quasi-HSL space. Be forewarned, however, it will involve a bit of math.