This is only a preview of the November 2020 issue of Practical Electronics. You can view 0 of the 72 pages in the full issue. Articles in this series:
|
Max’s Cool Beans
By Max the Magnificent
Flashing LEDs and drooling engineers – Part 9
W
ell hello there! On the off chance you are
newly arrived at this party, our current discussions
pertain to one of my hobby projects – a 12×12
array of ping pong balls, each equipped with a WS2818based tricolour LED in the form of an Adafruit NeoPixel.
In the last-but-one Cool Beans column (PE, September
2020), we commenced a set of ‘drip’ experiments. We started
by visualising the array as lying flat on the floor with individual drips of virtual water falling onto it from above. Each
drip lit its associated pixel (ping pong ball) with white light.
The way we implemented our original algorithm, each
drip appeared in a random location, turned hard on, and
then turned hard off again. The duration of each drip was
random, as was the inter-drip delay (ie, the delay between
adjacent drips). Next, we decided to light our drips with
random colours, which we selected from a palette comprising three primary colours (red green and blue), three secondary colours (yellow, cyan, and magenta), and six tertiary colours (flush orange, chartreuse, spring green, azure, electric
indigo, and rose).
In my previous Cool Beans column, (PE, October 2020),
we created some jolly useful functions that allow us to fade
from one colour value to another. If you’ve only just started
subscribing to PE, then you’ll doubtless be delighted to discover that the back issues containing the aforementioned
columns are available for purchase via the magazine’s Digital Downloader page (https://bit.ly/3eC7hjS).
More colour!
So, the current state of play is that each time a random virtual drip drops, as it were, we fade from black to a randomly selected colour, hold for a short time, and then fade back
down again (Fig.1a). Hmmm. On the one hand, this isn’t too
shabby. On the other hand, are we the sort of men and women
(and boys and girls) who can be satiated with a single saturated colour? ‘No!’ I cry, ‘A thousand times no!’
As a first pass, let’s pluck the low-hanging fruit and simply
select a second colour at random. The idea is that we will
fade from black to our first colour, then fade from our first
B la c k
B la c k
B la c k
B la c k
B la c k
B la c k
F a d e
F a d e
F a d e
F a d e
F a d e
F a d e
C o lo u r
C o lo u r 1
C o lo u r 1
C o lo u r 1
C o lo u r 1
C o lo u r 1
F a d e
F a d e
F a d e
F a d e
F a d e
F a d e
B la c k
C o lo u r 2
C o lo u r 2
C o lo u r 2
C o lo u r 2
C o lo u r 2
F a d e
F a d e
F a d e
F a d e
F a d e
B la c k
B la c k
C o lo u r 3
C o lo u r 3
C o lo u r 3
F a d e
F a d e
F a d e
B la c k
B la c k
B la c k
// Get drip colors
iColor = random(0, NUM_WHEEL_COLORS);
dripColorOne = ColorWheel[iColor];
iColor = random(0, NUM_WHEEL_COLORS);
dripColorTwo = ColorWheel[iColor];
Later, after we’ve determined the values of any delays we
wish to use, we add an extra call to our CrossFade() function as follows:
// Fade from black to first drip color, then hold
CrossFade(iNeo, COLOR_BLACK, dripColorOne,
FADE_UP_TIME, dripOnDelay);
// Fade from first drip color to second drip
color, then hold
CrossFade(iNeo, dripColorOne, dripColorTwo,
CROSSFADE_TIME, dripOnDelay);
// Fade from second drip color to black, then wait
CrossFade(iNeo, dripColorTwo, COLOR_BLACK,
FADE_DOWN_TIME, interDripDelay);
For your delectation and delight, I’ve created a video showing this latest incarnation of our drip sketch in action – you
can see it at: https://bit.ly/2G8i2ig
Now, one problem with generating the second colour
(a) Single colour
as a random value is that we could end up with the same
colour as the first, which would be a tad boring. We could,
(b) Second colour
(random)
of course, get around this by tweaking our code to ensure
(c) Second colour
that the second colour is different to the first. Still and
(complementary)
all, as they say, generating the second colour as a random
(d) Second and third colours
value seems to lack a certain creative cunning and exhib(split complementary)
its a scarcity of subtle sophistication. Perhaps we could
(e) Second and third colours
(triadic)
adopt a more scientific approach.
(f) Second and third colours
(analogous)
In all of these examples, this was the initial randomly selected colour
Fig.1. Summary of the effects implemented by our current batch of
ping-pong ball array sketches.
58
colour to our second, then fade from our second back to
black (Fig.1b). As usual, it would probably be a good idea
if you were to download a copy of this sketch (program) in
order to follow along with my cogitations and ruminations
(file CB-Nov20-01.txt – it and the other files associated with
this article, are available on the November 2020 page of the
PE website).
If you compare this program to our original single random
colour version, you will see that the way we’ve built our code
means the modifications are minimal. All these changes occur
in our DropDrip() function. First, we randomly generate
two drip colours rather than one:
Complementary combinations
In the visual arts, the term ‘colour theory’ refers to a body
of practical guidance to colour mixing and the visual effects associated with specific colour combinations. For
example, complementary colours are any two colours that
Practical Electronics | November | 2020
0
1 1
1
2
1 0
9
3
4
8
7
( a ) C o m p le m e n ta r y
( b ) S p lit C o m p le m e n ta r y
( c ) T r ia d ic
Fig.2. Colour theory and colour combinations.
( d ) A n a lo g o u s
6
5
Fig.3. Index numbers used
for colours.
are directly opposite each other on the colour wheel, thereby
providing maximum contrast. In the traditional RYB colour
model, examples of complementary colours would be redgreen, yellow-purple, and blue-orange. Modern colour theory
uses either the CMY subtractive colour model or the RGB
additive colour model, in which case examples of complementary pairs are red-cyan, green-magenta, and blue-yellow
(Fig.2a). The high contrast of complementary colours creates
a lively look, but they can be jarring to the eye, so you might
want to think twice before daubing every room in your house
with complementary colour combinations.
The overall operation of our new sketch is depicted in
Fig.1c, but how are we to determine the complementary
counterpart to our initial randomly generated colour? Well,
as you may recall, when we introduced the concept of the
colour wheel in an earlier column (PE, September 2020), we
annotated each of our colours with a number from 0 to 11
(Fig.3). These are the numbers we are using in our sketches
to index (point to) the colours in our colour wheel.
I don’t know about you, but I think this next bit is really
rather clever (in this case, you can follow along by downloading file CB-Nov20-02.txt). In particular, take a look at the
following lines of code from the DropDrip() function and
compare these to their equivalents in our previous sketch
shown earlier.
Since our colour wheel contains only 12 colours indexed
from 0 to 11, this would be problematic, to say the least. The
solution is to use the % (modulo) operator, which returns the
integer remainder from an integer division. This explains the
% NUM_WHEEL_COLORS (which equates to % 12) portion of
our algorithm, which ensures we only ever end up with index
values between 0 and 11 for our second colour.
Once again, I’ve created an eye-catching video showing
this complementary colour incarnation of our drip sketch
in action (https://bit.ly/3lzUDqa).
// Get drip colors
iColor = random(0, NUM_WHEEL_COLORS);
dripColorOne = ColorWheel[iColor];
// Get drip colors
iColor = random(0, NUM_WHEEL_COLORS);
dripColorOne = ColorWheel[iColor];
iColorTmp = (iColor + (NUM_WHEEL_COLORS / 2)) %
NUM_WHEEL_COLORS;
dripColorTwo = ColorWheel[iColorTmp];
iColorTmp = (iColor + (NUM_WHEEL_COLORS / 2) 1) % NUM_WHEEL_COLORS;
dripColorTwo = ColorWheel[iColorTmp];
Observe that we generate the first colour as a random value as
before. Since we know our colour wheel contains 12 colours
(we defined this in our sketch as NUM_WHEEL_COLORS), we
therefore understand that we need to add half this value (ie,
6) to the index value of our first colour to generate the index
value of its complement, which will be our second colour.
In the case of our current sketches, we could, of course,
simply use (iColor + 6) for this portion of our algorithm.
The reason we used (iColor + (NUM_WHEEL_COLORS /
2)) is to ‘future proof’ our code in case we were to decide
to increase the size of our colour wheel at some stage in the
future. (Yes, I know, a pretty cunning move.)
Now, the (iColor + (NUM_WHEEL_COLORS / 2)) portion of the calculation would work ‘as is’ so long as the index
value of the first colour was between 0 and 5, because this
would result in an index value for the second colour between
6 and 11. But what happens if the index value of the first
colour is between 6 and 11? In this case, we would end up
with an index value for the second colour between 12 and 17.
iColorTmp = (iColor + (NUM_WHEEL_COLORS / 2) +
1) % NUM_WHEEL_COLORS;
dripColorThree = ColorWheel[iColorTmp];
Practical Electronics | November | 2020
A cornucopia of colour combinations
Now we are really going to up our game because we are
going to move to using three colours for each drip. We’ll start
with a split complementary implementation as illustrated in
Fig.1d and Fig.2b. As we see, this is a variation of the original complementary scheme. In this case, in addition to the
base colour, we use the two colours either side of the base
colour’s complement.
Let’s start by considering how we are going to generate
our three colours. As we see below, we commence by generating the first colour as a random value as before, and then
we generate our split complementary values by tweaking
the algorithm we used to generate our complementary value
(you can follow along by downloading file CB-Nov20-03.txt).
The next part is where we fade from colour to colour, which
simply requires us to add one more call to our CrossFade()
function as illustrated below:
// Fade from black to first drip color, then hold
CrossFade(iNeo, COLOR_BLACK, dripColorOne,
FADE_UP_TIME, dripOnDelay);
// Fade from first drip color to second drip
color, then hold
CrossFade(iNeo, dripColorOne, dripColorTwo,
CROSSFADE_TIME, dripOnDelay);
// Fade from second drip color to third drip
color, then hold
59
N o d r ip
D r ip fa d e s o n
D r ip fa d e s o ff
( a ) R u d im e n ta r y d r ip e ffe c t
( a ) O n e d r ip a t a tim e
N o d r ip
D r ip fa d e s o n
S p la s h fa d e s o n
D r ip fa d e s o ff
S p la s h fa d e s o ff
( b ) A u g m e n tin g e a c h d r ip w ith a n a s s o c ia te d s p la s h
( b ) M u ltip le d r ip s a t th e s a m e tim e
Fig.4. Single versus multiple drips.
Fig.5. Basic drip effect compared to a ‘drip plus splash’ effect.
CrossFade(iNeo, dripColorTwo, dripColorThree,
CROSSFADE_TIME, dripOnDelay);
// Fade from third drip color to black, then wait
CrossFade(iNeo, dripColorThree, COLOR_BLACK,
FADE_DOWN_TIME, interDripDelay);
Just for giggles and grins, I also experimented with triadic colour combinations, as depicted in Fig.1e and Fig.2c;
followed by analogous colour combinations, as depicted
in Fig.1f and Fig.2d. Naturally, the code for these can be
downloaded: files CB-Nov20-04.txt and CB-Nov20-05.txt,
respectively. Furthermore, you can view videos of these
split complementary, triadic, and analogous incarnations on
my Cool Beans YouTube Channel (https://bit.ly/3kBgdJD).
As an aside, the reasons for our experiments with the various colour combinations discussed above goes far beyond
our simple drip sketches. There are all sorts of fun things
we might wish to display on our ping-pong ball array in
the future. For example, I’m planning on implementing a
version of Conway’s Game of Life (https://bit.ly/3fQdhqz)
– and knowing how these colour combinations work may
well prove to be invaluable to those efforts.
We demand more drips!
I think it’s fair to say that everything we’ve done thus far has
been pretty exciting, but we could do better. Until now, for
example, we’ve implemented only a single virtual drip at
a time (Fig.4a). Suppose we decide to allow multiple drips
to occur at the same time (Fig.4b)?
Obviously, we could simply pick a random group of pixels
and fade them all on and all off at the same time, and then
repeat this action with another random group of pixels, but
where would the fun be in that? Apart from anything else,
this approach would fall far short of representing randomly
occurring drips. What we want is to allow multiple drips to
be active at the same time, but for their start and end times
to be randomly determined such that they overlap in interesting and unpredictable ways.
We might even decide to augment our drips with a ‘splash’
(or ‘puddle’) effect. For example, neglecting the colour-tocolour fades we discussed earlier, the way in which our
current drips work is that they essentially fade on and then
fade off again (Fig.5a).
By comparison, suppose that, following the initial drip,
our virtual water droplet splashes out into the pixels north,
south, east, and west of our original pixel. There are all
sorts of things we could play with here, such as having the
fading up of the splash pixels overlap the fading down of
60
the initial drip pixel. We might also decide to experiment
with making the colour of the splash pixels analogous to
the colour of their parent drip pixel. I don’t know about
you, but I, for one, can’t wait!
I’m sorry for the delay
Sad to say, the way in which we’ve been implementing
things thus far will prove to be rather limiting. Consider
the following interpretation of the main loop() function
used in the Arduino’s classic ‘Blink’ sketch. In this case,
we are cycling around turning the LED on and off at a frequency of 1Hz (one cycle per second).
void loop()
{
digitalWrite(PinLed, HIGH);
delay(500);
digitalWrite(PinLed, LOW);
delay(500);
}
Observe the use of the delay() function. This is of a type
known as a ‘blocking function,’ because it prevents (or blocks)
anything else from happening while it meanders along through
life twiddling its metaphorical thumbs.
When you come to think about it, all of our existing sketches
have simply been enhanced versions of this Blink approach.
Yes, we’ve implemented random drip lengths and random
times between drips, but these have all been accomplished
by means of the delay() function. And, yes, we’ve added
fades, but these simply involved a series of incremental
colour changes separated by calls to the delay() function.
The thing is, we’ve managed to ‘get by’ when playing with
individual drips, but we would find it extremely difficult (if
not impossible) to handle multiple simultaneous random
drips using our current code architecture.
Next time…
I’ll leave you for the moment to ponder how we might move
to implement things like multiple drips and drips that splash.
In my next column, we will ‘Dump the Delay’ and adopt a
new way of creating our sketches that will allow us to realise
all of these wondrous effects.
…but wait, there’s more!
Hold onto your hats because this is ‘hot off the press’ as I
pen these words. With regard to my Tips and Tricks columns,
one of the most important suggestions I have if you wish to
learn anything is to conceive a project you want to create
Practical Electronics | November | 2020
or implement and then find out how to make it happen. In
the case of learning a computer programming language, for
example, while it’s obviously a jolly good idea to read reference books on that language, working out how to code a
real-world project is an extremely efficacious way to wrap
one’s brain around the nitty-gritty details.
The reason I mention this here is that my chum, Ted Fried,
who is an FPGA designer at Apple, suggested that it would
be a great idea to have a competition. Ted’s proposal was that
if you (yes you) were to create a program to run on my 12 ×
12 ping pong ball array, you could email that program to me
for me to run it on my array and post a video of it running
on YouTube. Eventually, when we have enough videos, we
could all vote to decide which programs provide the coolest and/or most interesting and/or most spectacular effects.
The only problem with Ted’s idea is that you really need
an array of your own to develop your program, which sort of
defeats the object of the exercise. But turn that frown upside
down into a smile because we’ve come up with a cunning
plan (indeed, it is a plan ‘as cunning as a fox what used to
be Professor of Cunning at Oxford University,’ as Edmund
Blackadder once famously said).
In a nutshell, my chums at Alorium Technology (https://
bit.ly/30Za3MA) have created a special simulator for us. This
little beauty allows you to simulate your ping pong ball array
program on your host computer. All you have to do is replace
the #include <Adafruit_NeoPixel.h> statement with
#include <NeoPixel_Simulator.h> – the rest of the program stays ‘as-is’ – pretty cool, eh?
Wnat to build your own amazing array? All the details are in
previous Cool Beans columns, starting in March 2020.
Naturally, I’ve written a column explaining how all of
this works in excruciating detail (https://bit.ly/3h2mvkf).
Also, for your delectation and delight, I’ve created a short
video showing the same program running on the real array
and on the simulator (you can view it
Cool bean Max Maxfield (Hawaiian shirt, on the right) is emperor
here: https://bit.ly/2E4ULgm).
of all he surveys at CliveMaxfield.com – the go-to site for the
So, set to with gusto and abandon (and
latest and greatest in technological geekdom.
aplomb, of course). I cannot wait to see
the cunning creations you come up with.
Comments or questions? Email Max at: max<at>CliveMaxfield.com
Until then, have a good one!
Max’s Cool Beans cunning coding tips and tricks
I
n an earlier Tips and Tricks column (PE, September
2020), we talked about how the integers we use in our
sketches (programs) come in different sizes (8-, 16-, 32-,
64-bits) and flavors (signed and unsigned). In my previous
Tips and Tricks column (PE, October 2020), we discussed
the concept of casting; that is, explicitly promoting a
smaller integer to a larger version, or demoting a larger
integer to a smaller form.
As part of my Cool Beans column in that same issue, we introduced a function called GetBlue(), whose purpose was
to accept a 32-bit unsigned integer as a parameter and return
the least-significant 8 bits as follows:
uint8_t GetBlue (uint32_t tmpColor)
{
return (uint8_t) (tmpColor & 0xFF);
}
Practical Electronics | November | 2020
We also noted the fact that, in this case, both the cast and the
mask operations were unnecessary (the main reason for using
them here is to make our intent clear to anyone who has to
understand and maintain this program in the future), and that
we could actually write this function as follows:
uint8_t GetBlue (uint32_t tmpColor)
{
return tmpColor;
}
I made the comment: ‘You may rest assured that the compiler’s optimisation know-how should enable it to recognise any
operations that are superfluous to requirements and remove
them.’ Well, if you ever find yourself wondering how your C/
C++ code will be converted into assembly language on its way
to machine code, there’s a wonderful free tool called Compiler
61
Explorer you can use. For example, a comparison of the two
versions of our function above shows that they return identical assembly code (https://bit.ly/2P5GemJ).
A cunning algorithm
I must admit that I’m quite proud of this one. In a Tips and
Tricks column from long, long ago, relatively speaking (PE,
May 2020), we were talking about the for() statement, which
we noted could be abstracted as follows:
for (initialization; condition; modification)
{
// More statements go here
}
‘Initialisation’ is where we initialise our control variable(s),
‘condition’ is where we perform a test, and ‘modification’ is
where we increment or decrement our control variable(s).
Note that the way I said this indicates the possibility of having
multiple initialisation and modification variables. Generally
speaking, beginners think of a for() statement as looking
something like the following:
for (i = 0; i < 10; i++)
{
// More statements go here
}
In reality, however, both the initialisation and modification
sections can comprise multiple comma-separated elements
looking something like the following:
for (i = 0, j = 10; i < 10; i++, j--)
{
// More statements go here
}
Observe that we can only have a single condition, although
this could have multiple parts, such as ( (i < 10) && (j
> 0) ), although this would be superfluous to requirements
in this particular example.
The reason I’m waffling on about this here is that I just implemented a cunning algorithm using a for() loop of this
ilk, and I thought it might be instructive to share it with you.
A rainbow of colours
I was starting to get worried with regard to my Cool Beans columns that you might think we were only ever going to light
a single pixel at a time on our 12 × 12 array. Thus, I whipped
up an interesting little sketch based on Adafruit’s classic Rainbow algorithm. As a starting point, we present the dynamically changing rainbow using the 12 columns forming the array.
For each cycle, we first generate the 12 colours and store
them in a 12-element array called RainbowColors[], after
which we call a function called LoadRainbowColors()
to load these colours into the array. The key portion of this
function is as follows:
for (int xInd = 0; xInd < NUM_COLS; xInd++)
{
for (int yInd = 0; yInd < NUM_ROWS; yInd++)
{
iNeo = GetNeoNum(xInd, yInd);
Neos.setPixelColor(iNeo, RainbowColors[xInd]);
}
}
In this case we use two nested for() loops, each of which
employs a single initialisation and modification variable.
62
You can peruse the complete sketch if you wish (file CBNov20-06.txt – available on the November 2020 page of the
PE website) and also watch a video of it in action on the array
(https://bit.ly/310Df5D).
Next, I decided that I wished to present the rainbow on the
array’s diagonals. Let’s think about this for a moment (maybe
you could sketch things out on a piece of paper). If we had
a 2 × 2 array, we would have three diagonals; a 3 × 3 array
would give us five diagonals; a 4 × 4 array would have seven
diagonals, and so on. The bottom line is that the number of
diagonals in a square array is equal to the number of rows
plus the number of columns minus one. Thus, in the case of
our 12 × 12 array, we have 12 + 12 – 1 = 23 diagonals.
This means that, for each cycle, we now need to generate 23 colours and store them in a 23-element array called
RainbowColors[], after which we call a modified version
of our LoadRainbowColors() function to load these colours into the array. The key portion of this modified function is as follows:
for (int xInd = xStart, yInd = yStart;
xInd <= xEnd; xInd++, yInd--)
{
iNeo = GetNeoNum(xInd, yInd);
Neos.setPixelColor(iNeo, RainbowColors[iDiag]);
}
Observe that, in this case, we are using a single for() loop
that employs two initialisation variables and two modification
variables. Once again, you can ponder the complete sketch
for this if you wish (file CB-Nov20-07.txt), and also watch a
video of it in action (https://bit.ly/2DXZsbZ).
Your best bet since MAPLIN
Chock-a-Block with Stock
Visit: www.cricklewoodelectronics.com
O r p h o n e o u r f r i e n d l y kn
o w l e d g e a b l e st a f f o n 0 2 0 8 4 5 2 0 1 6 1
Components • Audio • Video • Connectors • Cables
Arduino • Test Equipment etc, etc
Vi s i t our Sh op , C a ll or B uy on li n e a t :
w w w . c r i c k lew oodelec t r on i c s . c om
0 2 0 8 4 5 2 0 1 6 1
Vi s i t our s h op a t :
4 0 - 4 2 C r i c k lew ood B r oa dw a y
L on don NW 2 3 E T
Practical Electronics | November | 2020
|