Topics

Games
Sound & Music
Watches & Clocks
Wireless
GPS
Power Supplies
Computers
Graphics
Lighting
Thermometers
Educational
Wearables
Test Equipment
Tutorials
Libraries
PCB-Based Projects

By processor

AVR ATtiny

ATtiny10
ATtiny2313
ATtiny84
ATtiny841
ATtiny85
ATtiny861
ATtiny88

AVR ATmega

ATmega328
ATmega1284

AVR 0, 1, and 2-series

ATmega1608
ATmega4808
ATmega4809
ATtiny1604
ATtiny1614
ATtiny202
ATtiny3216
ATtiny3224
ATtiny3227
ATtiny402
ATtiny404
ATtiny414
ATtiny814

AVR DA/DB/DD-series

AVR128DA28
AVR128DA32
AVR128DA48
AVR128DB28
AVR128DB48
AVR64DD14
AVR32DD28

ARM

ATSAMD21
RP2040
RA4M1

RISC-V

ESP32-P4

About me

  • About me
  • Twitter
  • Mastodon

Feeds

RSS feed

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:

HSVDisplay.jpg

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:

HSV1.gif

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:

HSV2.gif

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:

HSV3.gif

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:

HSV4.gif

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):

HSV5.gif

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.


  1. ^ 2.0" 320x240 Color IPS TFT Display on Adafruit.

blog comments powered by Disqus