Buttery Smooth Fades With The Power Of HSV

In firmware-land we usually refer to colors using RGB. This is intuitively pleasing with a little background on color theory and an understanding of how multicolor LEDs work. Most of the colorful LEDs we are use not actually a single diode. They are red, green, and blue diodes shoved together in tight quarters. (Though interestingly very high end LEDs use even more colors than that, but that’s a topic for another article.) When all three light up at once the emitted light munges together into a single color which your brain perceives. Appropriately the schematic symbol for an RGB LED without an onboard controller typically depicts three discrete LEDs all together. So it’s clear why representing an RGB LED in code as three individual values {R, G, B} makes sense. But binding our representation of color in firmware to the physical system we accidentally limit ourselves.

The inside of an RGB LED

Last time we talked about color spaces, we learned about different ways to represent color spatially. The key insight was that these models called color spaces could be used to represent the same colors using different groups of values. And in fact that the grouped values themselves could be used to describe multidimensional spacial coordinates. But that post was missing the punchline. “So what if you can represent colors in a cylinder!” I hear you cry. “Why do I care?” Well, it turns out that using colorspace can make some common firmware tasks easier. Follow on to learn how!

Our friend the HSV Cylinder by [SharkD]

For the rest of this post we’re going to work in the HSV color space. HSV represents single colors as combinations of hue, saturation, and value. Hue is measured in degrees (0°-359°) of rotation and sets the color. Saturation sets the intensity of the color; removing saturation moves towards white, while adding it moves closer to the set hue. And value sets how much lightness there is; a value of 0 is black, whereas maximum value is the lightest the most intense the color can be. This is all a little difficult to describe textually, but take a look at the illustration to the left to see what I mean.

So back again to “why do I care?” Making the butteriest smooth constant brightness color fades is easy with HSV. Trivial. Want to know how to do it? Increment your hue. That’s it. Just increment the hue and the HSV -> RGB math will take care of the rest. If you want to fade to black, adjust your saturation. If you want to perceive true constant brightness or get better dynamic range from your LEDs, that’s another topic. But for creating a simple color fade all you need is HSV and a single variable.

Avoid Strange Fades

A linear interpolation from green to pink

“But RGB color fades are easy!” you say. “All I need to do is fade R and G and B and it works out!” Well actually, they aren’t quite as simple as that makes them appear. The naive way to fade between RGB colors would be exactly what was described, a linear interpolation (a LERP). Take your start and end colors, calculate the difference in each channel, slice those differences into as many frames as you want your animation to last, and done. During each frame add or subtract the appropriate slice and your color changes. But let’s think back to the color cube. Often a simple LERP like this will work fine, but depending on the start and end points you can end up with pretty dismal colors in the middle of the fade. Check out this linear fade between bright green and hot pink. In the middle there is… gray. Gray!?

RGB CubeSo what causes those strange colors to show up? Think back to the RGB cube. By adjusting red, green, and blue at once we’re traversing the space inside the cube between two points in space. In the case of the example green/pink fade the interpolation takes us directly through the center of the cube where grey lives. If every point inside the cube represents a unique mixture of red, green, and blue we’re going to get, well, every color. Some of that space has colors that you probably don’t want to show up on your 40 meter light strip. Somewhere in that cube is murky brown.

But this can be avoided! All you have to do is traverse the colorspace intelligently. In RGB that probably means adjusting channels one or two at a time and trying to avoid going through the mid-cube badlands. For the sample green to pink fade we can break it into two pieces; a fade from green to blue, then a fade from blue to pink. Check out the split LERP on the right to see how it looks. Not too bad, right? At least there is no grey anymore. But that was a pretty complex way to get a boring fade to work. Fortunately we already know about the better way to do it.

A LERP in HSV

How does this fade look in HSV? Well there’s only one channel to interpolate – hue. If we convert the two sample RGB values into HSV we get bright green at {120°, 100%, 100%} for the start and pink at {300°, 100%, 100%} for the end. Do we add or subtract to go between them? It doesn’t actually matter, though often you may want to interpolate as quickly as possible (in which case you want to traverse the shortest distance). It’s worth noting that 0° and 359° are adjacent, so it’s safe to overflow or underflow the degree counter to travel the shortest absolute distance. In the case of green/pink it is equally fast to count up from 120° to 300° as it is to count down from 120° to 300° (passing through 0°). Assuming we count upwards it looks like the figure on the left. Nice, right? Those bland grays have been replaced by perky shade of blue.

There are a couple other nice side effects of using HSV like this. One is that, as long as you don’t care about changing brightness, some animations can be very memory efficient. You only need one byte per pixel! Though that does prevent you from showing black and white, so you’d need an extra byte or two for those (not every colorspace is perfect). Changing a single parameter also makes it easy to experiment with non-linear easing to adjust how you approach a color setpoint, which can lead to some nice effects.

If you want to experiment with HSV, here are a couple files I’ve used in the past. No guarantees about efficiency or accuracy, but I’ve built hundreds of devices that used them and things seem to work ok.

There’s one more addendum here, and that’s that color is nothing if not an extremely complex topic. This post is just the barest poke into one corner of color theory and does not address a range of concerns about gamma/CIE correction, apparent brightness of individual colors, and more. This was what I needed to improve my RGB blinkenlights, not invent a new Pantone. If accurate color is an interesting topic to you, dig in and tell us what you learn!

20 thoughts on “Buttery Smooth Fades With The Power Of HSV

  1. I wonder if there’s a way to use L*a*b color for a similar effect. I’m guessing it’s mostly the separation of hue from brightness and saturation that makes this idea work. Maybe use linear algebra? Normalize and get the magnitude of the *a*b components of source and destination, figure out the rotation and scaling being performed, divide those up and make a little matrix that does an incremental rotate and scale and ditch the matrix when it gets close to the destination.

    1. Sounds a bit similar to how CORDIC was worked out. With that said, maybe there’s a way to extend it so that traversing a color space can be done by using CORDIC. If so, it might reduce the computation and memory necessary to crossfade colors.

    1. Just to clarify my own comment, my suggestion is as follows:
      1) If you want the shortest/smoothest path perceptually across colors, such that you’re not introducing unnecessary intermediate hues (at the cost of creating intermediate saturation changes), LERPing across LAB is more or less ideal.
      2) If you want the shortest/smoothest path perceptually across colors, such that you’re avoiding unnecessary change in saturation (at the cost of introducing other intermediate hues), LERPing across LCH is more or less ideal.

      It’s worth noting that LAB and LCH both preserve perceived luminosity than RGB or HSV.

    2. I like this a lot better. HSV goes through too many random unrelated colors that the designer probably didn’t want. Thank you! I think I’m going to try to add LAB fading to my FOSS lighting control app now!

      1. Unfortunately don’t have a favorite library for such conversion.Times I’ve implemented color space conversion in the past, I’ve just manually implemented it from formulas. This page is a good resource for that:
        http://www.brucelindbloom.com/index.html?Math.html

        There are certainly a few libraries out there though such as:
        https://github.com/ibireme/yy_color_convertor
        https://github.com/dmilos/color
        https://github.com/berendeanicolae/ColorSpace
        https://github.com/colorjs/color-space

        One other thing of note for the case of driving LEDs, is to consider the gamma curve of your output. PWM driven LEDs more or less have a linear relationship between duty cycle and light intensity on each channel, as long as the capacitance or PWM frequency is not too high. In this case, when converting back from interpolated LAB/LCH to RGB, be sure to go with linear RGB instead of some gamma-corrected color space like sRGB. sRGB is the standard/default assumption for most computer graphics.
        If one is using current to control RGB channels or the PWM frequency is high relative to the capacitance, then one may want to apply some gamma correction for that for best results.

        It can also be worth considering that not all LAB/LCH colors will map perfectly to a target RGB color space. In these cases it is preferable if possible to determine the perceptually nearest color to the desired point in LAB color space, that is valid and not clipped in the target RGB color space. Often won’t run into this much if you’re interpolating between colors valid in the target color space in LAB, but if you do interpolation in LCH it’s much more likely. Unfortunately, most color space conversion implementations I’ve seen simply clip the channel in the target color space to be limited to [0, 1] instead of actually properly determining the nearest valid color in the target space (nearest as measured by euclidean distance in LAB), but this is possible to do.

  2. LEDs are linear in PWM/PCM control, but humans are log in perception. So doing your fade in linear light is maybe not ideal (things stay too bright). So consider doing your fade in perceptual space instead (e.g., log or gamma).

  3. There’s a mistake in the statement, “If you want to fade to black, adjust your saturation.” I believe you mean, “If you want to fade to black, adjust your value.”

  4. Very interesting. But it would have been nice if the author stuck with just two colors, since that is the problem he described, adding blue in the middle make not be marketing’s ideal compromise. Coming from the printing field, customer are pretty picking about what they want to see happening.

    1. Grey in the middle is actually the correct transition, because pink/magenta is the absence of green. There is no intermediate color between it and any other color because pink isn’t actually a (single) color or wavelenght of light.

      A split spectrum is a special case for human vision, and the only intermediate between two peaks and one peak is a flat spectrum in between – grey.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.