A Single-Line HSV Function
26th May 2026
This is a simple HSV to RGB function that can be expressed in a single line of C. For example, here's a demonstration of the function plotting hue against saturation on an Adafruit 2.0" 320x240 TFT display [1], using my Universal TFT Display Backpack 2:

The Single-Line HSV Function displaying a test chart on an Adfruit 2.0" 320x240 TFT display.
Introduction
The HSV colour space is a convenient way to represent colours in applications such as computer graphics and LED light control, because it allows you to control Hue (colour), Saturation (amount of white), and Value (brightness) independently with independent parameters. However, these applications typically use RGB colour, so you need a function to convert between HSV and RGB.
Existing HSV to RGB programs often seem unnecessarily complicated, but after trying several approaches I've arrived at a simple solution that can be expressed in one line of C.
I've provided two versions; one that uses floating-point arithmetic, and one that uses a 16-bit fixed point format for faster execution on microcontrollers such as AVR that don't have native floating-point handling.
In addition, LED lights often include an additional white channel, to allow a purer white when using unsaturated colours or white. This function can also handle the case where there's an additional white channel.
The desired HSV function
The aim is to write an HSV function in C with the following function header:
float hsv (int k, float h, float s, float v)
where the parameters will be as follows:
- k is the channel we want, an integer from 0 to 2.
- h is the hue, from 0.0 to 1.0.
- s is the saturation, from 0.0 to 1.0.
- v is the value, or brightness, from 0.0 to 1.0. Conventionally b isn't used to avoid confusion with RGB.
The function will return the intensity of that channel, from 0.0 to 1.0. So in a graphics program the hsv() function will be called three times for each pixel to get its red, green, and blue components.
The approach
The triangle function
The first step is to create a tri (triangle) function, defined as follows:
float tri (float m) { return 2.0 * fabsf(fract(m + 0.5) - 0.5); }
where fract() returns the non-negative fractional part of a floating-point number. It can be defined as:
float fract (float a) { return a - floorf(a); }
The tri() function can be pictured as follows:

Adding the curves for each component
Next consider the plots for:
tri(h - k/3.0)
for values of k of 0, 1, and 2. Subtracting k/3.0 from h moves the triangle wave 0.33 to the right:

I have coloured the curves to reflect the fact that k represents the RGB channel.
Scaling and inverting
Now multiply by 3 and subtract from 2. This scales and inverts the image, bringing the points where the curves cross to the 0.0 and 1.0 values:

Clamping to the range 0 to 1
Now apply the clamp() function, which clamps the values to the range 0.0 to 1.0:
float clamp (float v) { return (v<0.0) ? 0.0 : (v>1.0) ? 1.0 : v; }
Here's the result:

This gives the final result for hue, assuming v and s are both 1.0. For example, h = 0.166 gives red = 1.0 and green = 1.0, which is yellow.
Adding saturation and brightness
Finally we need to take into account the effect of saturation and brightness. Brightness scales the maximum of the curves to v rather than 1.0, and saturation adds a white component v*(1-s):

The final hsv() function
This gives the final one-line definition for hsv():
float hsv (int k, float h, float s, float v) {
return clamp(2.0 - (3.0 * tri(h - k/3.0))) * v * s + v * (1 - s);
}
It's convenient to keep v*(1-s) as a separate term to reflect the fact that this is the white component.
The tri() and clamp() functions could be expanded in-line to keep this as a strictly single-line function.
White channel
To handle the case where there's a fourth white channel in addition to the RGB channels, such as in RGBW NeoPixel LEDs, the parameter k can take the additional value 3, and in this case the white component is passed to the white channel:
float hsv (int k, float h, float s, float v) {
if (k == 3) return v * (1 - s);
else return clamp(2.0 - (3.0 * tri(h - k/3.0))) * v * s;
}
Fixed-point version
Here's a fixed-point version, as used on my Light Lab which was based on an AVR processor that doesn't have hardware floating-point.
First we define a type Fixed and some fixed-point constants:
typedef int16_t Fixed; // Fixed point, 8.8 bits
const Fixed half = 0x080;
const Fixed one = 0x100;
For convenience there are optimised functions for fixed-point multiply and divide:
Fixed fmul (Fixed x, Fixed y) {
if (x>181 || x<-181 || y>181 || y<-181) return ((int32_t)x * (int32_t)y) >> 8;
else return (x * y) >> 8;
}
In fmul() when the arguments are both 181 or smaller the product will fit in 16 bits, because 181*181=32761, and we can use faster 16-bit arithmetic; otherwise we use 32-bit arithmetic to avoid overflow.
Fixed fdiv (Fixed x, Fixed y) {
if (x>127 || x<-127) return ((int32_t)x << 8) / y;
else return (x << 8) / y;
}
In fdiv(), when x is 127 or smaller x<<8 will fit in 16 bits, because 127<<8=32512, so we can use faster 16-bit arithmetic; otherwise we use 32-bit arithmetic to avoid overflow.
We also need fixed-point versions of tri(), fract(), and clamp():
Fixed fract (Fixed a) {
if (a >= 0) return a & 0xff; else return 0x100 - (a & 0xff);
}
Fixed clamp (Fixed v) {
return (v<zero) ? zero : (v>one) ? one : v;
}
Fixed tri (Fixed m) { return 2 * abs(fract(m + half) - half); }
Finally, here's the fixed-point version of hsv():
Fixed hsv (int k, Fixed h, Fixed s, Fixed v) {
return fmul(fmul(clamp(two - (3 * tri(h - (k*one)/3))), v), s) + fmul(v, (one-s));
}
Demonstrations
Floating-point version
Here's a demonstration of the floating-point version of hsv() for use with either of my Two TFT Graphics Libraries:
// Fractional part of x
float fract (float a) { return a - floorf(a); }
// Clamp to 0.0 to 1.0
float clamp (float v) { return (v<0.0) ? 0.0 : (v>1.0) ? 1.0 : v; }
// Triangle function
float tri (float m) { return 2.0 * fabsf(fract(m + 0.5) - 0.5); }
float hsv (int k, float h, float s, float v) {
return clamp(2.0 - (3.0 * tri(h - k/3.0))) * v * s + v * (1 - s);
}
void HSVDemo () {
uint8_t rgb[3];
for (int x=0; x<xsize; x++) {
for (int y=0; y<ysize; y++) {
float hue = float(x)/xsize, satn = float(y)/ysize, value = 1.0;
for (int k=0; k<3; k++) {
rgb[k] = 255 * hsv(k, hue, satn, value);
}
fore = Colour(rgb[0], rgb[1], rgb[2]);
PlotPoint(x, y);
}
}
}
It displays the test image shown at the start of the article with hue along the x axis and saturation along the y axis of any size of display.
Fixed-point version
Here's a demonstration of the fixed-point version of hsv(), again for use with either of my Two TFT Graphics Libraries:
typedef int16_t Fixed; // Fixed point, 8.8 bits
const Fixed zero = 0x000;
const Fixed half = 0x080;
const Fixed one = 0x100;
const Fixed two = 0x200;
Fixed fmul (Fixed x, Fixed y) {
if (x>181 || x<-181 || y>181 || y<-181) return ((int32_t)x * (int32_t)y) >> 8;
else return (x * y) >> 8;
}
Fixed fdiv (Fixed x, Fixed y) {
if (x>127 || x<-127) return ((int32_t)x << 8) / y;
else return (x << 8) / y;
}
// Fractional part of x
Fixed fract (Fixed a) {
if (a >= 0) return a & 0xff; else return 0x100 - (a & 0xff);
}
// Clamp to zero to one
Fixed clamp (Fixed v) {
return (v<zero) ? zero : (v>one) ? one : v;
}
// Triangle function
Fixed tri (Fixed m) { return 2 * abs(fract(m + half) - half); }
Fixed hsv (int k, Fixed h, Fixed s, Fixed v) {
return fmul(fmul(clamp(two - (3 * tri(h - (k*one)/3))), v), s) + fmul(v, (one-s));
}
void HSVDemo () {
uint8_t rgb[3];
for (int x=0; x<xsize; x++) {
for (int y=0; y<ysize; y++) {
Fixed hue = (uint32_t)x*one/xsize, satn = (uint32_t)y*one/ysize, value = one;
for (int k=0; k<3; k++) {
rgb[k] = 255 * hsv(k, hue, satn, value) / one;
}
fore = Colour(rgb[0], rgb[1], rgb[2]);
PlotPoint(x, y);
}
}
}
It displays the identical test image shown at the start of the article, but in half the time.
Acknowledgements
I'm grateful to Chris Jordan for suggesting this approach and providing the original functions on which this hsv() routine is based.
Update
27th May 2026: I've added a program to demonstrate the fixed-point version of hsv(), and corrected a couple of mistakes in the fixed-point routines.
- ^ 2.0" 320x240 Color IPS TFT Display on Adafruit.
blog comments powered by Disqus
