Let's have some fun with RGB LEDs! I love RGB LEDs, there's a huge amount of possibilities you can have with them. It can be used for different scenarios, including decorations and aesthetics. I also love JavaScript, and it's been rather perplexing to make smooth colour transitions with RGB LEDs in JavaScript, even with Johnny-Five. However, I think I have a simple solution to allow smooth colour transitions using Johnny-Five, so let's dig in!
The Current ProblemJohnny Five has RGB LED support, and can even pulse and fade similar to regular LEDs. However, the problem with RGB LEDs in JavaScript is there's no built-in function to transition from one colour to another. There are examples using jumps from one colour to the next, but it isn't smooth. Instead, it goes from one colour to the next without a transition.
Maybe I'm complicating things, but I like to make my colours go from one to the other smoothly. It's quite relaxing to see the colour go from one specific colour to the other, while showing all the colours in between! It should be possible with JavaScript, right?
const int redPin = 11;
const int greenPin = 10;
const int bluePin = 9;
void setup() {
// Start off with the LED off.
setColourRgb(0,0,0);
}
void loop() {
unsigned int rgbColour[3];
// Start off with red.
rgbColour[0] = 255;
rgbColour[1] = 0;
rgbColour[2] = 0;
// Choose the colours to increment and decrement.
for (int decColour = 0; decColour < 3; decColour += 1) {
int incColour = decColour == 2 ? 0 : decColour + 1;
// cross-fade the two colours.
for(int i = 0; i < 255; i += 1) {
rgbColour[decColour] -= 1;
rgbColour[incColour] += 1;
setColourRgb(rgbColour[0], rgbColour[1], rgbColour[2]);
delay(5);
}
}
}
void setColourRgb(unsigned int red, unsigned int green, unsigned int blue) {
analogWrite(redPin, red);
analogWrite(greenPin, green);
analogWrite(bluePin, blue);
}
It is possible to achieve this with the above C++ code, however with JavaScript it's a bit more complicated due to how Johnny-Five works with RGB LEDs and working with transitioning RGB values using JavaScript.
I had a thought about this, and because of finding the process of transitioning RGB values while trying to cause a delay a bit headache-inducing, I then thought...why not just transition usingHue,SaturationandLightness?
How Hue, Saturation and Lightness WorkBefore we begin, let's talk Hue, Saturation, and Lightness, or HSL. HSL is one colour space, alongside RGB, CMYK, and others. Colours defined by HSL are based on three components:
- Hue defines the degree of colour in the spectrum, from 0 to 360 degrees. To put it simply, 0 is Red, 120 is Green, 240 is Blue, and 360 is back to red. It's a full wheel of colour.
- Saturation defines the intensity of colour, from 0 to 100. 0 is a lack of colour, while 100 is the colour at its highest value.
- Lightness defines how much light is in the colour, from 0 to 100. In this case, 0 is black, 50 is full colour, and 100 is white.
Keeping the saturation at 100 and the lightness at 50, we can have the RGB LED at a pure, saturated colour. All we need to do is manipulate the Hue and it can go to any colour we like!
Johnny-Five's RGB LED color()
function come with its limits, as it will only accept colours set as the following:
- A CSS colour name, such as
'red'
,'green'
,'blue'
,'purple'
,'aqua'
,'cyan'
, and more. You can find a full list of CSS colour names here.
- A hexadecimal colour string, such as
'ff0000'
, or'f00'
for red.
- A hexadecimal colour string prefixed with a
#
symbol. Like the above, but using'#ff0000'
or'#f00'
for red.
- An array of 8-bit bytes, such as
[0x00, 0xFF, 0x00]
for green.
- An object of 8-bit bytes. Similar to the above, but using a JavaScript object instead, an example being
{r: 0x00, g: 0x00, b: 0xFF }
for blue.
So, we can't just go in and use led.colour()
with just a hue. However, we can take the HSL value using color-convert
, convert that to a hexadecimal value, and assign the LED colour to that hex value!
Then, using this method, we can change the hue, in steps, then apply the colour to the LED in each step, making a smooth transition between colours! Neato!
Making a New Johnny-Five ProjectLet's start by making a new Johnny-Five project. We're going to keep it nice and simple, no super complex circuits or anything. First, we need to make our circuit. For this, I'm using an Arduino UNO, and a common anode RGB LED.
A note about Common Anode and Common Cathode LEDs: If you're unsure about the difference between common cathode and common anode LEDs, it's simple. Common cathode LEDs connect to ground on the long leg, and the RGB diode pins supply current to the LED to sets the colour. However, common anodes are the opposite — they are connected to power on the long leg, so it's continuously on, and the RGB diode pins take current away to set the colour instead!
In this circuit, I've set up pins 9, 10, and 11 to control the RGB pins since they're PWM (Pulse-Width Modulation) pins with 330 Ohm resistors, and 3.3V to the LED since it's a common anode. If you're using a common cathode, don't panic! Just connect the long leg to ground instead of 3.3V!
Now onto making our code — let's make a new folder and initialise it with Yarn, which we will be using to manage our dependencies. Simply run the following command in your project directory using a CLI:
yarn init -y
Using the -y flag allows us to skip the questions about our project and just rapidly sets up our package.json file. Next, we need to add the following dependencies to our project using Yarn.
yarn add johnny-five color-convert temporal
Obviously, we need to add Johnny-Five, but let's talk about color-convert and Temporal for a second. color-covert is a JavaScript library that allows color values to be changed into another color space, i.e. RGB to CMYK. This is great as we'll need this to convert our HSL colour values to Hex.
Temporal is a library by Rick Waldron, the creator of Johnny-Five. Temporal is capable of performing loops, sequences of animations, and more. Temporal is to be used with Johnny-Five as a non-blocking alternative to setTimeout
and setInterval
, allowing other parts of the programming to work seamlessly while a timed sequence within the robot's script is happening.
A quick note about Temporal: while Temporal is a non-blocking method of timed sequences in JavaScript, please bear in mind it is prone to taking up memory! Don't leave Temporal sequences on too long, otherwise it will take up more and more of your device's memory!
Much like in the JavaScript with Hardware series I've been writing, let's add some dependencies to help make our code clean and tidy. Let's add ESLint and Flow to check our code. First of all, we add eslint-config-airbnb-base to our development dependencies.
npm info eslint-config-airbnb-base@latest peerDependencies --json | command sed 's/[\{\},]//g ; s/: /@/g' | xargs yarn add --dev eslint-config-airbnb-base@latest
Since we're also going to be using Flow, we need to add babel-eslint. This is a bit of a nitpick but it's an apparent requirement.
yarn add --dev babel-eslint
With our ESLint dependencies installed, we can now use the ESLint Airbnb rules, as well as add our own rules in eslintrc.json
. We'll need to add some of our own so we won't get errors for specific things we need in our code!
{
"extends": [
"airbnb-base"
],
"rules": {
"func-names": "off",
"space-before-function-paren": "off",
"padded-blocks": "off",
"no-console": "off"
}
}
Next, we need to add Flow. To do this, we'll add Flow dependencies using Yarn.
yarn add --dev eslint-plugin-import flow-bin
With this, we need to update our eslintrc.json file to use Flow.
{
"extends": [
"airbnb-base",
"plugin:flowtype/recommended"
],
"plugins": [
"flowtype"
],
"rules": {
"func-names": "off",
"space-before-function-paren": "off",
"padded-blocks": "off",
"no-console": "off"
}
}
Finally, we just need to add a .flowconfig
file, and we should be good to go!
Now that we have our dependencies, we need to make a script file called index.js, and we can start writing the basics of our Johnny-Five's scripting.
var five = require('johnny-five');
var temporal = require('temporal');
var colorConvert = require('color-convert');
// Set time delay in milliseconds.
let delay = 20;
// Starting hue for the LED - turquoise.
let initialHue = 160;
// Boolean to control the color transition loop
let loopActive = false;
// Make a new Johnny-Five board instance.
const board = new five.Board();
board.on('ready', function() {
// Store the RGB LED in a constant.
// Make sure they're all connected to PWM pins.
const light = new five.Led.RGB({
pins: [9, 10, 11],
// This example uses a Common Anode.
// Don't set this if you're using Common Cathode LEDs!
isAnode: true,
});
let currentHue = initialHue;
/**
*
* Exit Event
* When disconnecting, make sure components are off.
*
*/
this.on('exit', () => {
// When the board disconnects, turn the LED off.
light.stop().off();
console.log('[johnny-five] Board closing - bye bye!');
});
});
First, we require all three dependencies in our file. Next, we write variables for how long our Temporal loops are to be delayed for. Then, we store a default Hue in a variable, which is what the light's colour will be when it comes on. The hue can only be between 0 to 360. We also have a loopActive
variable for this example, which we will use to stop the loop if it's running via REPL.
We then make a new Johnny-Five Board()
instance, and when it's ready, we make a new Led.RGB()
instance inside the board's 'ready'
event. Since mine is a common anode, I have set isAnode
to true
in the object's properties. Note, however, if you're using a common cathode, you don't need to set this!
Note also there's a currentHue
variable set in the board's scope, and starts with the initialHue
variable's value. Finally, there's an 'exit'
event wherein the LED should stop its current colour as the board is disconnected.
Now that we've got the base code set up, we can now start making functions for making our RGB LED work with HSL values!
Making a HSL to Hex Colour FunctionBefore we can set the light to the initialHue, we need to make a function that takes a specified hue, then coverts the hue, along with saturation and lightness values, and coverts it into a hexadecimal colour. Then afterwards, it'll set the RGB LED's colour to that hexadecimal value.
board.on('ready', function() {
// [...] Our previous code lives here...
/**
*
* setLightColor() Function
*
* Set the RGB LED to the specific hue passed in the function.
* Requires conversion from HSL to Hex.
*
* @param {Number} hue - the hue to set the LED color.
*/
const setLightColor = (hue) => {
/**
*
* Store the current hue as a hexadecimal value.
* Set it to full saturation and medium lightness.
* Note on lightness: 0 is black, 100 is white. Bear this
* in mind.
*
*/
const hueToHex = colorConvert.hsl.hex(hue, 100, 50);
// Set RGB LED to current hexidecimal value
light.color(hueToHex);
};
setLightColor(currentHue);
// [...] Our exit code lives lives here...
});
With the code above, the function takes in a parameter for a colour hue, then is set as a HSL value, with 100 being at full colour while the lightness is set at 50, so it's not too white or black, and then sets the LED to the current value. We can then get this to use the starting hue once the LED is ready!
Next, we need to make functions that can smoothly change the LED colour. Using the currentHue
variable, we can use this to store any new hues specified during transitions.
Now we go onto a tricky part, which is making the RGB LED transition through all the colours of the spectrum. What we want to make is a function that will go through all 360 degrees of the colour wheel, or at least until we tell it to stop. For a nice touch, we'll also make it loop from the last hue, and continue from there until it stops.
board.on('ready', function() {
// [...] Our previous code lives here...
/**
*
* colorLoop() function
*
* When fired, loopActive boolean is set to true.
* Using a temporal loop set to the specified delay, keep looping until
* the loopActive boolean is set back to false. The loop increments the
* currentHue variable every 100 milliseconds, and then set the RGB LED color
* to the new value in currentHue.
*
*/
const colorLoop = () => {
loopActive = true;
temporal.loop(delay, (loop) => {
// If the hue has reached 360, set it back to zero
if (currentHue === 360) {
currentHue = 0;
}
currentHue += 1;
// Set the LED color to the current hue.
setLightColor(currentHue);
// If the loopActive boolean is set to false during a step, stop the loop.
if (!loopActive) {
loop.stop();
}
});
};
// [...] Our remaining code lives lives here...
});
The code above does the following things: when the function is fired, it will update our loopActive
boolean to true
, and then it will make a loop using Temporal. We'll be using the delay variable we wrote earlier, and this will be how long it takes to go to the next step of the loop.
The loop will check on the current angle on the colour wheel, and if it's at 160 degrees, set it to 0. Next, it will increment the degree by 1 on each step, and apply the new RGB colour to the current hue. If the loopActive boolean becomes false, the loop will stop and stay at the current hue.
Now that we've written a loop for the colour wheel, this means we can continuously transition smoothly!
Transitioning to a Specific HueLet's do something a bit more specific: what if we want the LED to transition from the current hue to a new hue? Using a similar method to the colour loop, we can write a function that takes the current hue and transition to a specific number in the colour wheel.
board.on('ready', function() {
// [...] Our previous code lives here...
/**
*
* colorTransition() Function
*
* Allows the RGB LED to transition from one hue to another.
* Depending on the value of the hue, it will transition forward or backwards
* and do it in steps.
*
* @param {Number} hue - the color to change to.
*/
const colorTransition = (hue) => {
if (hue !== currentHue) {
temporal.loop(delay, (loop) => {
// Check if the new hue is higher or lower than the current one.
if (hue > currentHue) {
currentHue += 1;
} else {
currentHue -= 1;
}
// Set the light color to the hue
setLightColor(currentHue);
if (currentHue === hue) {
loop.stop();
}
});
}
};
// [...] Our remaning code lives down here
});
If the number placed in the function is not the current hue, then it will make a new Temporal loop, using the delay we specified. Also, we need to check if the new hue specified is higher in the colour wheel than the current one. If it is, the RGB LED will go further up the wheel in increments, otherwise it will go backwards.
Once the current hue is the same value as the specified hue, it will stop. Neato, huh?
Writing a Fading Hue and Light TransitionWe've got two methods of colour transitions now, but let's add something else that's a little bit different. How about a function where the colours change, but the light dims from the current colour and fades up to a new colour? This is a bit complicated, but here we're not going through the colour wheel to change the colour in increments, we're darkening the RGB colour in steps, then lightening it with the new hue.
board.on('ready', function() {
// [...] Our previous code lives here...
/**
*
* dimTransition() function
*
* Instead of changinng through the color wheel, change the colour by
* fading down the lightness of the light, then when the lightness is at
* zero, raise the lightness to the new color hue.
*
*
* @param {Number} hue - the color to change to.
*/
const dimTransition = (hue) => {
// Store Lightness in a variable
let lightness = 50; // Not too light, not too dark. Just right.
// Only run if the hue paramter isn't the current one!
if (hue !== currentHue) {
temporal.loop(delay, (loop) => {
/**
*
* If the lightness is at 50 or less, but not at 0,
* then decrease the lightness.
* When it's at 0 or more, but less than 50, and the
* currentHue is set to the new hue, and increase the lightness.
*
* In both conditions, convert the respected color and lightness to
* hexadecimal colors.
*
*/
if (hue !== currentHue && lightness <= 50 && lightness > 0) {
lightness -= 1; // Lower lightness
} else if (currentHue === hue && lightness >= 0 && lightness < 50) {
lightness += 1; // Increase lightness
}
if (lightness === 0) {
currentHue = hue; // Set currentHue to the new hue
}
const lightnessToHex = colorConvert.hsl.hex(currentHue, 100, lightness);
light.color(lightnessToHex);
/**
*
* If the current hue matches the new one, and the lightness has
* reached up to 50 again, stop the loop.
*
*/
if (currentHue === hue && lightness === 50) {
loop.stop();
}
});
}
};
// [...] Our remaining code lives here...
});
There's quite a chunk of code here, so let's go through it bit by bit:
- Much like
colortransition()
, we need to check if the hue specified is not the current hue. If it isn't, make a new Temporal loop.
- We've also got a lightness variable set to 50. 0 is dark, 100 is light, so we need to keep it at 50 since the any higher or lower would make the RGB colour to dark or too light, 50 is just right.
- If the current hue is not the new one specified, and the lightness is equal or lower to 50 but is greater than 0, darken the LED in increments of one.
- If the current hue is the new one specified, and the lightness is greater than or equal to 0, but less than 50, set the current hue to the new one, and fade up the LED in increments.
- Also, when the lightness is at zero, set the current hue to the new one.
- Once the current hue is the new hue and the lightness is at 50 again, stop the loop.
Using this method, we can transition the RGB LED from one colour to the next by fading down and then fading up!
We've got all our functions ready and written, now we just need to test them out using REPL. Using REPL, we can interact directly with the Arduino and the RGB LED using the CLI.
board.on('ready', function() {
// [...] Our previous code lives here...
/**
*
* REPL functions
*
* Allows to interact with the created functions for testing.
* - startLoop() begins the colorLoop() function - this also sets
* `loopActive` to true.
* - stopLoop() sets `loopActive` to false, and in turn stops the
* temporal loop in `colorLoop()`.
* - changeColor() activates the colorTransition function, from one
* hue to the next.
* - dimTransition() activates the `dimTransition()` function, changing
* the color by fading out and back in.
*
*/
this.repl.inject({
changeColor: (hue) => {
loopActive = false;
colorTransition(hue);
},
startLoop: () => {
colorLoop();
},
stopLoop: () => {
loopActive = false;
},
dimTransition: (hue) => {
loopActive = false;
dimTransition(hue);
},
});
// [...] Our exit code lives here...
});
This allows to use the following functions in the CLI:
changeColor()
— specify a colour between 0 and 360 to transition the RGB LED to.
startLoop()
— start the colour loop. This setsloopActive
totrue
.
stopLoop()
— Sets theloopActive
boolean tofalse
and it turn stops the colour loop if it's running.
dimTransition()
— specify a colour from 0 to 360 to dim down the LED and fade up with a new colour.
With this set, we can now run node index.js
in the CLI, and this will make our Arduino run using Johnny-Five. We can then use the CLI to fire our new RGB LED functions, and see our RGB LED smoothly transition!
A little tidbit about making sure our code is consistent and cleanly written — remember when we installed our ESLint and and Flow dependencies? Now we can put them to use to check our code.
In our package.json file, we can make a new test
script to execute ESlint and Flow in our project.
"scripts": {
"test": "eslint ./ && flow"
},
Next, we can run yarn test
— since we've written consistent code, we should get no errors!
And there we have it! Using HSL values to control RGB LED colours and transitions is fairly easy to write, as well as control RGB colours using a different method. There's a lot more that can be done with this, even using it in other colour spaces.
If you have more methods and have additional ideas for colour transitions and animations using JavaScript, let me know! I'd love to see them.
Did you enjoy this protip?If you enjoyed this project and helped you learn more about using JavaScript with your projects, feel free to pledge to my Patreon and receive some exclusive perks! Alternatively, you can donate to my PayPal so I can make more awesome stuff. You can also follow me on Twitter, like me on Facebook, and subscribe to my mailing list so you stay up to date!
Comments