Fading Arduino RGB LED from one color to the other?

28,899

Solution 1

What the other answers omit about this topic is the fact that that human perception of light intensity is logarithmic, not linear. The analogWrite() routines are setting the output pin's PWM duty cycle, and are linear. So by taking the minimum duty cycle (say 0) and maximum duty cycle (say, for the sake of easy math this is 10) and dividing it into equal chunks, you will be controlling the intensitiy linearly which will not give satisfying results.

What you need to do instead is set your intensity exponentially. Let's say your maximum intensity is 255. You can generate this result by treating your intensity as a power to raise some number to. In our case, given that we are dealing with computers that like binary, powers of two are convenient. So,

2^0 =1
2^8=256

so we can have 8 intensity levels. Actually, note that out minimum is now not fully off (it is 1 not 0) and our maximum is out of range (256 not 255). So we modify the formula to be

output = 2 ^ intensity - 1

Or in code

int output = 1<<intensity - 1;

This yields values from 0 to 255 for intensity levels from 0 to 8 (inclusive), so we actually get nine levels of intensity. If you wanted smoother transitions (i.e. more levels of intensity), and still use logarithmic intensity you'll need floating-point math.

If you apply this method of calculating intensity to each channel (R, G, B) then your perception will be in accord with what your code says it should be.


As fars as how to smoothly transition between various colors, the answer depends on how you want to navigate the color space. The simplest thing to do is to think about your color space as a triangle, with R, G, and B, as the verteces:

enter image description here

The question then is how to navigate this triangle: you could go along the sides, from R, to G, to B. This way you will never see white (all channels fully on) or "black" (all fully off). You could think of your color space as a hexagon, with additional purple (R+B), yellow (G+B), and brown (R+G) colors, and also navigate the perimeter (again, no white or black). There are as many fading possibilities as there are ways of navigating insides these, and other figures we might think of.

When I built fading programs like this the color space and the traversal I liked was as follows: think of each channel as a binary bit, so now you have three (R, G, and B). If you think of each color as having some combination of these channels being fully on, you get 7 total colors (excluding black, but including white). Take the first of these colors, fade to it from black and back to black, and then go to the next color. Here's some code that does something like that:

int targetColor = 1;
int nIntensity = 0;
int nDirection = 1;         // When direction is 1 we fade towards the color (fade IN)
                            // when 0 we fade towards black (fade OUT)
#define MAX_INTENSITY 8
#define MIN_INTENSITY 0
#define MAX_TARGETCOLOR 7

void loop() {
    for (;;) {

        // Update the intensity value
        if (nDirection) {
            // Direction is positive, fading towards the color
            if (++nIntensity >= MAX_INTENSITY) {
                // Maximum intensity reached
                nIntensity = MAX_INTENSITY;  // Just in case
                nDirection = 0;             // Now going to fade OUT
            } // else : nothing to do
        } else {
            if (--nIntensity <= MIN_INTENSITY) {
                nIntensity = MIN_INTENSITY; // Just in case
                // When we get back to black, find the next target color
                if (++targetColor>MAX_TARGETCOLOR) 
                    targetColor=1;          // We'll skip fading in and out of black
                nDirection = 1;             // Now going to fade IN
            } // else: nothing to do
        }

        // Compute the colors
        int colors[3];
        for (int i=0;i<3;i++) {
            // If the corresponding bit in targetColor is set, it's part of the target color
            colors[i] = (targetColor & (1<<i)) ? (1<<nIntensity) -1 : 0;
        }

        // Set the color
        setColor(colors[0], colors[1], colors[2]);

        // Wait
        delay(100);     
    }
}

Solution 2

It is indeed possible to fade between different colors. What I'm also usually missing in Arduino books and code on the web is, that it is possible to write C++ classes in Arduino IDE. Therefore, I'm going to show an example that fades between colors using C++ classes.

An issue that should be addressed is on which pins the analogWrite should be done to, because not all pins are capable of Pulse Width Modulation (PWM). On a Arduino device the pins that support PWM are denoted with a tilde '~'. The Arduino UNO has digital pins ~3, ~5, ~6, ~9, ~10 and ~11. And most Arduino use those pins for PWM, but check your device to be sure. You can create PWM on regular digital pins by switching your led on for 1ms and of for 1 ms this mimics 50% power on the LED. Or turn it on 3 ms and of 1 ms this mimics 75% power.

In order to fade a LED you would have to reduce/increase the PWM value and wait a bit. Youl'll have to wait a little while, because otherwise the arduino tries to fade/dim leds thousands of times per second and you won't see a fade effect, although it probably there. So you are looking for a method to gradually reduce/increase the second parameter to analogWrite( ) for three LEDs; For a more thorough explanation see for example chapter 7 of Arduino Cookbook. That book is a good read for Arduino fans anyway!

So I adapted the code from the OP to contain a 'rgb_color' class that is more or less just a container for red, green and blue values. But more importantly is the fader class. When an instance of fader is constructed the proper pins should be in the constructor red, green and blue respectively. Than the fader contains a member function void fade( const rgb_color& const rgb_color&) which will do the fading between the in and out color. By default the function will take 256 steps of 10ms from the input color to the output color. (note here due to integer divisions this doesn't really mean that each step 1/256 th, but perceputally you won't notice it).

/*
 * LedBrightness sketch
 * controls the brightness of LEDs on "analog" (PWM) output ports.
 */

class rgb_color {

  private:
    int my_r;
    int my_g;
    int my_b;
  public:
    rgb_color (int red, int green, int blue)
      :
        my_r(red),
        my_g(green),
        my_b(blue)
    {
    }

    int r() const {return my_r;}
    int b() const {return my_b;}
    int g() const {return my_g;}
};

/*instances of fader can fade between two colors*/
class fader {

  private:
    int r_pin;
    int g_pin;
    int b_pin;

  public:
    /* construct the fader for the pins to manipulate.
     * make sure these are pins that support Pulse
     * width modulation (PWM), these are the digital pins
     * denoted with a tilde(~) common are ~3, ~5, ~6, ~9, ~10 
     * and ~11 but check this on your type of arduino. 
     */ 
    fader( int red_pin, int green_pin, int blue_pin)
      :
        r_pin(red_pin),
        g_pin(green_pin),
        b_pin(blue_pin)
    {
    }

    /*fade from rgb_in to rgb_out*/
    void fade( const rgb_color& in,
               const rgb_color& out,
               unsigned n_steps = 256,  //default take 256 steps
               unsigned time    = 10)   //wait 10 ms per step
    {
      int red_diff   = out.r() - in.r();
      int green_diff = out.g() - in.g();
      int blue_diff  = out.b() - in.b();
      for ( unsigned i = 0; i < n_steps; ++i){
        /* output is the color that is actually written to the pins
         * and output nicely fades from in to out.
         */
        rgb_color output ( in.r() + i * red_diff / n_steps,
                           in.g() + i * green_diff / n_steps,
                           in.b() + i * blue_diff/ n_steps);
        /*put the analog pins to the proper output.*/
        analogWrite( r_pin, output.r() );
        analogWrite( g_pin, output.g() );
        analogWrite( b_pin, output.b() );
        delay(time);
      }
    }

};

void setup()
{
  //pins driven by analogWrite do not need to be declared as outputs
}

void loop()
{
  fader f (3, 5, 6); //note OP uses 9 10 and 11
  /*colors*/
  rgb_color yellow( 250, 105,   0 );
  rgb_color orange( 250,  40,   0 );
  rgb_color red   ( 255,   0,   0 );
  rgb_color blue  (  10,  10, 255 );
  rgb_color pink  ( 255,   0, 100 );
  rgb_color purple( 200,   0, 255 );
  rgb_color green (   0, 255,   0 );
  rgb_color white ( 255, 255, 255 );

  /*fade colors*/
  f.fade( white, yellow);
  f.fade( yellow, orange);
  f.fade( orange, red);
  f.fade( red, blue);
  f.fade( blue, pink);
  f.fade( pink, purple);
  f.fade( purple, green);
  f.fade( green, white);
}

Solution 3

This is probably what you are looking for. Whenever we want to shift color over the spectrum and trasition the colors in a circular and smooth motion, what we are really doing is shifting light using HUE in the HSI/HSV (Hue, Saturation, Intensity/Value) color space.

Take if you will this figure:

enter image description here

We will attach a value from 0-360 for hue because hue has 360 degrees of color. A value of 0.00 - 1.00 for saturation, and a value of 0.00 -1.00 for intensity/value

Here is my circuit on the MEGA 2560: enter image description here

Here is video of this code running:

<iframe width="560" height="315" src="https://www.youtube.com/embed/gGG-GndSKi0" frameborder="0" allowfullscreen></iframe>

So lets build a function that we can pass the hue value and a for loop inside our loop function to call that value 360 times to shift over the full rainbow of color.

//Define the pins we will use with our rgb led
int redPin = 9;
int greenPin = 10;
int bluePin = 11;

//define that we are using common anode leds
#define COMMON_ANODE

void setup()
{
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
}

int rgb[3];
//Arduino has no prebuilt function for hsi to rgb so we make one:
void hsi_to_rgb(float H, float S, float I) {
  int r, g, b;
  if (H > 360) {
    H = H - 360;
  }
  // Serial.println("H: "+String(H));
  H = fmod(H, 360); // cycle H around to 0-360 degrees
  H = 3.14159 * H / (float)180; // Convert to radians.
  S = S > 0 ? (S < 1 ? S : 1) : 0; // clamp S and I to interval [0,1]
  I = I > 0 ? (I < 1 ? I : 1) : 0;
  if (H < 2.09439) {
    r = 255 * I / 3 * (1 + S * cos(H) / cos(1.047196667 - H));
    g = 255 * I / 3 * (1 + S * (1 - cos(H) / cos(1.047196667 - H)));
    b = 255 * I / 3 * (1 - S);
  } else if (H < 4.188787) {
    H = H - 2.09439;
    g = 255 * I / 3 * (1 + S * cos(H) / cos(1.047196667 - H));
    b = 255 * I / 3 * (1 + S * (1 - cos(H) / cos(1.047196667 - H)));
    r = 255 * I / 3 * (1 - S);
  } else {
    H = H - 4.188787;
    b = 255 * I / 3 * (1 + S * cos(H) / cos(1.047196667 - H));
    r = 255 * I / 3 * (1 + S * (1 - cos(H) / cos(1.047196667 - H)));
    g = 255 * I / 3 * (1 - S);
  }
  rgb[0] = r;
  rgb[1] = g;
  rgb[2] = b;

}
void setColor(int red, int green, int blue)
{
  #ifdef COMMON_ANODE
    red = 255 - red;
    green = 255 - green;
    blue = 255 - blue;
  #endif
  analogWrite(redPin, red);
  analogWrite(greenPin, green);
  analogWrite(bluePin, blue);
}


///here we have our main loop and the for loop to shift color
void loop()
{
//the for loop counts to 360 and because its in our control loop it will run forever
// We will use int i to increment the actual desired color 
for (int i=0; i<=360;i++){
  hsi_to_rgb(i,1,1);
  setColor(rgb[0],rgb[1],rgb[2]);
  //Changing the delay() value in milliseconds will change how fast the
  //the light moves over the hue values 
  delay(5);
  }


}

Solution 4

If you want to fade between colours, work in a colourspace which makes it easy and then convert back to RGB at the end.

For example, work in HSL colour space, keep S and L constant (say a fully saturated and bright colour) and then "fade" H around the circle - you'll go from red through green, blue and back to red. Convert back to RGB and then use those values for your LED drives. I used this technique for a "mood lamp" app, and other code for the colour space conversion can be found on SO.

Solution 5

You can simplify your code by using a struct for your color.

struct Color
{
    unsigned char r;
    unsigned char g;
    unsigned char b;
};

Then, it is easy to have a fading function

// the old color will be modified, hence it is not given as reference
void fade(Color old, const Color& newColor)
{
    // get the direction of increment first (count up or down)
    // each of the inc_x will be either 1 or -1
    char inc_r = (newColor.r - old.r)/abs(newColor.r-old.r); // note, that the right hand side will be sign extended to int according to the standard.
    char inc_g = (newColor.g - old.g)/abs(newColor.g-old.g);
    char inc_b = (newColor.g - old.g)/abs(newColor.g-old.g);

    fadeOneColor(old.r, newColor.r, inc_r, old);
    fadeOneColor(old.g, newColor.g, inc_g, old);
    fadeOneColor(old.b, newColor.b, inc_b, old);
}

void fadeOneColor( unsigned char& col_old, 
                   const unsigned char& col_new, 
                   const char inc, 
                   Color& col)
{
    while(col_old != col_new)
    {
        col_old += inc;
        SetColor(col); 
        delay(20);
    }        
}
Share:
28,899
KingPolygon
Author by

KingPolygon

Updated on January 10, 2021

Comments

  • KingPolygon
    KingPolygon over 3 years

    I've currently managed to get my LED to cycle through eight colors that I've selected. Everything is working correctly, except that I want to go for a more natural feel, and would like to fade / transition from one color to the next, instead of having them just replace one another.

    Here's my code so far:

    int redPin = 11;
    int greenPin = 10;
    int bluePin = 9;
    
    void setup()
    {
        pinMode(redPin, OUTPUT);
        pinMode(greenPin, OUTPUT);
        pinMode(bluePin, OUTPUT);
    }
    
    void loop()
    {
        setColor(250, 105, 0);   // Yellow
        delay(1000);
    
        setColor(250, 40, 0);    // Orange
        delay(1000);
    
        setColor(255, 0, 0);     // Red
        delay(1000);
    
        setColor(10, 10, 255);   // Blue
        delay(1000);
    
        setColor(255, 0, 100);   // Pink
        delay(1000);
    
        setColor(200, 0, 255);   // Purple
        delay(1000);
    
        setColor(0, 255, 0);     // Green
        delay(1000);
    
        setColor(255, 255, 255); // White
        delay(1000);
    }
    
    void setColor(int red, int green, int blue)
    {
        analogWrite(redPin, 255-red);
        analogWrite(greenPin, 255-green);
        analogWrite(bluePin, 255-blue);
    }
    
  • Martin Thompson
    Martin Thompson about 11 years
    This doesn't really provide much of an attempt to answer the question of "fading between colours" (IMHO)
  • Martin Thompson
    Martin Thompson about 11 years
    Does this really answer the "fading" question?
  • angelatlarge
    angelatlarge about 11 years
    @MartinThompson: Added some code to actually do this. I think that's what you were looking for.
  • Martin Thompson
    Martin Thompson about 11 years
    thanks, useful code. Although... I still think using a colour-representative colourspace is more helpful - your 'hexagonal route' seems to be an approximation of going around the circle of an HSV-type space.
  • angelatlarge
    angelatlarge about 11 years
    @Martin Thompson I would think one's preferences of how to navigate the color space really depends on the application. I am not 100% clear on what OP is trying to accomplish and I think that really affects how one navigates the color space.
  • Martin Thompson
    Martin Thompson about 11 years
    If you have two colours to fade between, it is my experience that doing the interpolation steps in the HSL domain produces more pleasant (visually) results than doing it in the RGB domain. However, as you say, it does depend on what the end goal is...
  • fulvio
    fulvio almost 10 years
    @ogni42: Usage example?