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.


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!

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *