480 lines
16 KiB
C
480 lines
16 KiB
C
// TwinkleFOX by Mark Kriegsman: https://gist.github.com/kriegsman/756ea6dcae8e30845b5a
|
|
//
|
|
// TwinkleFOX: Twinkling 'holiday' lights that fade in and out.
|
|
// Colors are chosen from a palette; a few palettes are provided.
|
|
//
|
|
// This December 2015 implementation improves on the December 2014 version
|
|
// in several ways:
|
|
// - smoother fading, compatible with any colors and any palettes
|
|
// - easier control of twinkle speed and twinkle density
|
|
// - supports an optional 'background color'
|
|
// - takes even less RAM: zero RAM overhead per pixel
|
|
// - illustrates a couple of interesting techniques (uh oh...)
|
|
//
|
|
// The idea behind this (new) implementation is that there's one
|
|
// basic, repeating pattern that each pixel follows like a waveform:
|
|
// The brightness rises from 0..255 and then falls back down to 0.
|
|
// The brightness at any given point in time can be determined as
|
|
// as a function of time, for example:
|
|
// brightness = sine( time ); // a sine wave of brightness over time
|
|
//
|
|
// So the way this implementation works is that every pixel follows
|
|
// the exact same wave function over time. In this particular case,
|
|
// I chose a sawtooth triangle wave (triwave8) rather than a sine wave,
|
|
// but the idea is the same: brightness = triwave8( time ).
|
|
//
|
|
// Of course, if all the pixels used the exact same wave form, and
|
|
// if they all used the exact same 'clock' for their 'time base', all
|
|
// the pixels would brighten and dim at once -- which does not look
|
|
// like twinkling at all.
|
|
//
|
|
// So to achieve random-looking twinkling, each pixel is given a
|
|
// slightly different 'clock' signal. Some of the clocks run faster,
|
|
// some run slower, and each 'clock' also has a random offset from zero.
|
|
// The net result is that the 'clocks' for all the pixels are always out
|
|
// of sync from each other, producing a nice random distribution
|
|
// of twinkles.
|
|
//
|
|
// The 'clock speed adjustment' and 'time offset' for each pixel
|
|
// are generated randomly. One (normal) approach to implementing that
|
|
// would be to randomly generate the clock parameters for each pixel
|
|
// at startup, and store them in some arrays. However, that consumes
|
|
// a great deal of precious RAM, and it turns out to be totally
|
|
// unnessary! If the random number generate is 'seeded' with the
|
|
// same starting value every time, it will generate the same sequence
|
|
// of values every time. So the clock adjustment parameters for each
|
|
// pixel are 'stored' in a pseudo-random number generator! The PRNG
|
|
// is reset, and then the first numbers out of it are the clock
|
|
// adjustment parameters for the first pixel, the second numbers out
|
|
// of it are the parameters for the second pixel, and so on.
|
|
// In this way, we can 'store' a stable sequence of thousands of
|
|
// random clock adjustment parameters in literally two bytes of RAM.
|
|
//
|
|
// There's a little bit of fixed-point math involved in applying the
|
|
// clock speed adjustments, which are expressed in eighths. Each pixel's
|
|
// clock speed ranges from 8/8ths of the system clock (i.e. 1x) to
|
|
// 23/8ths of the system clock (i.e. nearly 3x).
|
|
//
|
|
// On a basic Arduino Uno or Leonardo, this code can twinkle 300+ pixels
|
|
// smoothly at over 50 updates per seond.
|
|
//
|
|
// -Mark Kriegsman, December 2015
|
|
|
|
// Overall twinkle speed.
|
|
// 0 (VERY slow) to 8 (VERY fast).
|
|
// 4, 5, and 6 are recommended, default is 4.
|
|
uint8_t twinkleSpeed = 4;
|
|
|
|
// Overall twinkle density.
|
|
// 0 (NONE lit) to 8 (ALL lit at once).
|
|
// Default is 5.
|
|
uint8_t twinkleDensity = 5;
|
|
|
|
// Background color for 'unlit' pixels
|
|
// Can be set to CRGB::Black if desired.
|
|
CRGB gBackgroundColor = CRGB::Black;
|
|
// Example of dim incandescent fairy light background color
|
|
// CRGB gBackgroundColor = CRGB(CRGB::FairyLight).nscale8_video(16);
|
|
|
|
// If AUTO_SELECT_BACKGROUND_COLOR is set to 1,
|
|
// then for any palette where the first two entries
|
|
// are the same, a dimmed version of that color will
|
|
// automatically be used as the background color.
|
|
#define AUTO_SELECT_BACKGROUND_COLOR 0
|
|
|
|
// If COOL_LIKE_INCANDESCENT is set to 1, colors will
|
|
// fade out slighted 'reddened', similar to how
|
|
// incandescent bulbs change color as they get dim down.
|
|
#define COOL_LIKE_INCANDESCENT 1
|
|
|
|
CRGBPalette16 twinkleFoxPalette;
|
|
|
|
// This function is like 'triwave8', which produces a
|
|
// symmetrical up-and-down triangle sawtooth waveform, except that this
|
|
// function produces a triangle wave with a faster attack and a slower decay:
|
|
//
|
|
// / \
|
|
// / \
|
|
// / \
|
|
// / \
|
|
//
|
|
|
|
uint8_t attackDecayWave8(uint8_t i)
|
|
{
|
|
if (i < 86) {
|
|
return i * 3;
|
|
}
|
|
else {
|
|
i -= 86;
|
|
return 255 - (i + (i / 2));
|
|
}
|
|
}
|
|
|
|
// This function takes a pixel, and if its in the 'fading down'
|
|
// part of the cycle, it adjusts the color a little bit like the
|
|
// way that incandescent bulbs fade toward 'red' as they dim.
|
|
void coolLikeIncandescent(CRGB& c, uint8_t phase)
|
|
{
|
|
if (phase < 128) return;
|
|
|
|
uint8_t cooling = (phase - 128) >> 4;
|
|
c.g = qsub8(c.g, cooling);
|
|
c.b = qsub8(c.b, cooling * 2);
|
|
}
|
|
|
|
// This function takes a time in pseudo-milliseconds,
|
|
// figures out brightness = f( time ), and also hue = f( time )
|
|
// The 'low digits' of the millisecond time are used as
|
|
// input to the brightness wave function.
|
|
// The 'high digits' are used to select a color, so that the color
|
|
// does not change over the course of the fade-in, fade-out
|
|
// of one cycle of the brightness wave function.
|
|
// The 'high digits' are also used to determine whether this pixel
|
|
// should light at all during this cycle, based on the twinkleDensity.
|
|
CRGB computeOneTwinkle(uint32_t ms, uint8_t salt)
|
|
{
|
|
uint16_t ticks = ms >> (8 - twinkleSpeed);
|
|
uint8_t fastcycle8 = ticks;
|
|
uint16_t slowcycle16 = (ticks >> 8) + salt;
|
|
slowcycle16 += sin8(slowcycle16);
|
|
slowcycle16 = (slowcycle16 * 2053) + 1384;
|
|
uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8);
|
|
|
|
uint8_t bright = 0;
|
|
if (((slowcycle8 & 0x0E) / 2) < twinkleDensity) {
|
|
bright = attackDecayWave8(fastcycle8);
|
|
}
|
|
|
|
uint8_t hue = slowcycle8 - salt;
|
|
CRGB c;
|
|
if (bright > 0) {
|
|
c = ColorFromPalette(twinkleFoxPalette, hue, bright, NOBLEND);
|
|
if (COOL_LIKE_INCANDESCENT == 1) {
|
|
coolLikeIncandescent(c, fastcycle8);
|
|
}
|
|
}
|
|
else {
|
|
c = CRGB::Black;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// This function loops over each pixel, calculates the
|
|
// adjusted 'clock' that this pixel should use, and calls
|
|
// "CalculateOneTwinkle" on each pixel. It then displays
|
|
// either the twinkle color of the background color,
|
|
// whichever is brighter.
|
|
void drawTwinkles()
|
|
{
|
|
// "PRNG16" is the pseudorandom number generator
|
|
// It MUST be reset to the same starting value each time
|
|
// this function is called, so that the sequence of 'random'
|
|
// numbers that it generates is (paradoxically) stable.
|
|
uint16_t PRNG16 = 11337;
|
|
|
|
uint32_t clock32 = millis();
|
|
|
|
// Set up the background color, "bg".
|
|
// if AUTO_SELECT_BACKGROUND_COLOR == 1, and the first two colors of
|
|
// the current palette are identical, then a deeply faded version of
|
|
// that color is used for the background color
|
|
CRGB bg;
|
|
if ((AUTO_SELECT_BACKGROUND_COLOR == 1) &&
|
|
(twinkleFoxPalette[0] == twinkleFoxPalette[1])) {
|
|
bg = twinkleFoxPalette[0];
|
|
uint8_t bglight = bg.getAverageLight();
|
|
if (bglight > 64) {
|
|
bg.nscale8_video(16); // very bright, so scale to 1/16th
|
|
}
|
|
else if (bglight > 16) {
|
|
bg.nscale8_video(64); // not that bright, so scale to 1/4th
|
|
}
|
|
else {
|
|
bg.nscale8_video(86); // dim, scale to 1/3rd.
|
|
}
|
|
}
|
|
else {
|
|
bg = gBackgroundColor; // just use the explicitly defined background color
|
|
}
|
|
|
|
uint8_t backgroundBrightness = bg.getAverageLight();
|
|
|
|
for (uint16_t i = 0; i < LEAFCOUNT; i++) {
|
|
CRGB& pixel = leds[i*PIXELS_PER_LEAF];
|
|
|
|
PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number
|
|
uint16_t myclockoffset16 = PRNG16; // use that number as clock offset
|
|
PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number
|
|
// use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths)
|
|
uint8_t myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF) >> 4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08;
|
|
uint32_t myclock30 = (uint32_t)((clock32 * myspeedmultiplierQ5_3) >> 3) + myclockoffset16;
|
|
uint8_t myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel
|
|
|
|
// We now have the adjusted 'clock' for this pixel, now we call
|
|
// the function that computes what color the pixel should be based
|
|
// on the "brightness = f( time )" idea.
|
|
CRGB c = computeOneTwinkle(myclock30, myunique8);
|
|
|
|
uint8_t cbright = c.getAverageLight();
|
|
int16_t deltabright = cbright - backgroundBrightness;
|
|
if (deltabright >= 32 || (!bg)) {
|
|
// If the new pixel is significantly brighter than the background color,
|
|
// use the new color.
|
|
fill_solid(leds + i * PIXELS_PER_LEAF, PIXELS_PER_LEAF, c);
|
|
//Serial.printf("rgb: %d, %d, %d\n", c.r,c.g,c.b);
|
|
}
|
|
else if (deltabright > 0) {
|
|
// If the new pixel is just slightly brighter than the background color,
|
|
// mix a blend of the new color and the background color
|
|
fill_solid(leds + i * PIXELS_PER_LEAF, PIXELS_PER_LEAF, blend(bg, c, deltabright * 8));
|
|
//Serial.println("rgb:"+ blend(bg, c, deltabright * 8));
|
|
}
|
|
else {
|
|
// if the new pixel is not at all brighter than the background color,
|
|
// just use the background color.
|
|
fill_solid(leds + i * PIXELS_PER_LEAF, PIXELS_PER_LEAF, bg);
|
|
//Serial.println("rgb:" + bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
/////// #############################BACKUP####################################
|
|
/*
|
|
// This function loops over each pixel, calculates the
|
|
// adjusted 'clock' that this pixel should use, and calls
|
|
// "CalculateOneTwinkle" on each pixel. It then displays
|
|
// either the twinkle color of the background color,
|
|
// whichever is brighter.
|
|
void drawTwinkles()
|
|
{
|
|
// "PRNG16" is the pseudorandom number generator
|
|
// It MUST be reset to the same starting value each time
|
|
// this function is called, so that the sequence of 'random'
|
|
// numbers that it generates is (paradoxically) stable.
|
|
uint16_t PRNG16 = 11337;
|
|
|
|
uint32_t clock32 = millis();
|
|
|
|
// Set up the background color, "bg".
|
|
// if AUTO_SELECT_BACKGROUND_COLOR == 1, and the first two colors of
|
|
// the current palette are identical, then a deeply faded version of
|
|
// that color is used for the background color
|
|
CRGB bg;
|
|
if ((AUTO_SELECT_BACKGROUND_COLOR == 1) &&
|
|
(twinkleFoxPalette[0] == twinkleFoxPalette[1])) {
|
|
bg = twinkleFoxPalette[0];
|
|
uint8_t bglight = bg.getAverageLight();
|
|
if (bglight > 64) {
|
|
bg.nscale8_video(16); // very bright, so scale to 1/16th
|
|
}
|
|
else if (bglight > 16) {
|
|
bg.nscale8_video(64); // not that bright, so scale to 1/4th
|
|
}
|
|
else {
|
|
bg.nscale8_video(86); // dim, scale to 1/3rd.
|
|
}
|
|
}
|
|
else {
|
|
bg = gBackgroundColor; // just use the explicitly defined background color
|
|
}
|
|
|
|
uint8_t backgroundBrightness = bg.getAverageLight();
|
|
|
|
for (uint16_t i = 0; i < NUM_LEDS; i++) {
|
|
CRGB& pixel = leds[i];
|
|
|
|
PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number
|
|
uint16_t myclockoffset16 = PRNG16; // use that number as clock offset
|
|
PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number
|
|
// use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths)
|
|
uint8_t myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF) >> 4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08;
|
|
uint32_t myclock30 = (uint32_t)((clock32 * myspeedmultiplierQ5_3) >> 3) + myclockoffset16;
|
|
uint8_t myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel
|
|
|
|
// We now have the adjusted 'clock' for this pixel, now we call
|
|
// the function that computes what color the pixel should be based
|
|
// on the "brightness = f( time )" idea.
|
|
CRGB c = computeOneTwinkle(myclock30, myunique8);
|
|
|
|
uint8_t cbright = c.getAverageLight();
|
|
int16_t deltabright = cbright - backgroundBrightness;
|
|
if (deltabright >= 32 || (!bg)) {
|
|
// If the new pixel is significantly brighter than the background color,
|
|
// use the new color.
|
|
pixel = c;
|
|
}
|
|
else if (deltabright > 0) {
|
|
// If the new pixel is just slightly brighter than the background color,
|
|
// mix a blend of the new color and the background color
|
|
pixel = blend(bg, c, deltabright * 8);
|
|
}
|
|
else {
|
|
// if the new pixel is not at all brighter than the background color,
|
|
// just use the background color.
|
|
pixel = bg;
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
// A mostly red palette with green accents and white trim.
|
|
// "CRGB::Gray" is used as white to keep the brightness more uniform.
|
|
const TProgmemRGBPalette16 RedGreenWhite_p FL_PROGMEM =
|
|
{ CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red,
|
|
CRGB::Red, CRGB::Red, CRGB::Red, CRGB::Red,
|
|
CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray,
|
|
CRGB::Green, CRGB::Green, CRGB::Green, CRGB::Green };
|
|
|
|
// A mostly (dark) green palette with red berries.
|
|
#define Holly_Green 0x00580c
|
|
#define Holly_Red 0xB00402
|
|
const TProgmemRGBPalette16 Holly_p FL_PROGMEM =
|
|
{ Holly_Green, Holly_Green, Holly_Green, Holly_Green,
|
|
Holly_Green, Holly_Green, Holly_Green, Holly_Green,
|
|
Holly_Green, Holly_Green, Holly_Green, Holly_Green,
|
|
Holly_Green, Holly_Green, Holly_Green, Holly_Red
|
|
};
|
|
|
|
// A red and white striped palette
|
|
// "CRGB::Gray" is used as white to keep the brightness more uniform.
|
|
const TProgmemRGBPalette16 RedWhite_p FL_PROGMEM =
|
|
{ CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray,
|
|
CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray,
|
|
CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray,
|
|
CRGB::Red, CRGB::Red, CRGB::Gray, CRGB::Gray };
|
|
|
|
// A mostly blue palette with white accents.
|
|
// "CRGB::Gray" is used as white to keep the brightness more uniform.
|
|
const TProgmemRGBPalette16 BlueWhite_p FL_PROGMEM =
|
|
{ CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue,
|
|
CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue,
|
|
CRGB::Blue, CRGB::Blue, CRGB::Blue, CRGB::Blue,
|
|
CRGB::Blue, CRGB::Gray, CRGB::Gray, CRGB::Gray };
|
|
|
|
// A pure "fairy light" palette with some brightness variations
|
|
#define HALFFAIRY ((CRGB::FairyLight & 0xFEFEFE) / 2)
|
|
#define QUARTERFAIRY ((CRGB::FairyLight & 0xFCFCFC) / 4)
|
|
const TProgmemRGBPalette16 FairyLight_p FL_PROGMEM =
|
|
{ CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight,
|
|
HALFFAIRY, HALFFAIRY, CRGB::FairyLight, CRGB::FairyLight,
|
|
QUARTERFAIRY, QUARTERFAIRY, CRGB::FairyLight, CRGB::FairyLight,
|
|
CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight };
|
|
|
|
// A palette of soft snowflakes with the occasional bright one
|
|
const TProgmemRGBPalette16 Snow_p FL_PROGMEM =
|
|
{ 0x304048, 0x304048, 0x304048, 0x304048,
|
|
0x304048, 0x304048, 0x304048, 0x304048,
|
|
0x304048, 0x304048, 0x304048, 0x304048,
|
|
0x304048, 0x304048, 0x304048, 0xE0F0FF };
|
|
|
|
// A palette reminiscent of large 'old-school' C9-size tree lights
|
|
// in the five classic colors: red, orange, green, blue, and white.
|
|
#define C9_Red 0xB80400
|
|
#define C9_Orange 0x902C02
|
|
#define C9_Green 0x046002
|
|
#define C9_Blue 0x070758
|
|
#define C9_White 0x606820
|
|
const TProgmemRGBPalette16 RetroC9_p FL_PROGMEM =
|
|
{ C9_Red, C9_Orange, C9_Red, C9_Orange,
|
|
C9_Orange, C9_Red, C9_Orange, C9_Red,
|
|
C9_Green, C9_Green, C9_Green, C9_Green,
|
|
C9_Blue, C9_Blue, C9_Blue,
|
|
C9_White
|
|
};
|
|
|
|
// A cold, icy pale blue palette
|
|
#define Ice_Blue1 0x0C1040
|
|
#define Ice_Blue2 0x182080
|
|
#define Ice_Blue3 0x5080C0
|
|
const TProgmemRGBPalette16 Ice_p FL_PROGMEM =
|
|
{
|
|
Ice_Blue1, Ice_Blue1, Ice_Blue1, Ice_Blue1,
|
|
Ice_Blue1, Ice_Blue1, Ice_Blue1, Ice_Blue1,
|
|
Ice_Blue1, Ice_Blue1, Ice_Blue1, Ice_Blue1,
|
|
Ice_Blue2, Ice_Blue2, Ice_Blue2, Ice_Blue3
|
|
};
|
|
|
|
void redGreenWhiteTwinkles()
|
|
{
|
|
twinkleFoxPalette = RedGreenWhite_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void hollyTwinkles()
|
|
{
|
|
twinkleFoxPalette = Holly_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void redWhiteTwinkles()
|
|
{
|
|
twinkleFoxPalette = RedWhite_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void blueWhiteTwinkles()
|
|
{
|
|
twinkleFoxPalette = BlueWhite_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void fairyLightTwinkles()
|
|
{
|
|
twinkleFoxPalette = FairyLight_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void snow2Twinkles()
|
|
{
|
|
twinkleFoxPalette = Snow_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void iceTwinkles()
|
|
{
|
|
twinkleFoxPalette = Ice_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void retroC9Twinkles()
|
|
{
|
|
twinkleFoxPalette = RetroC9_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void partyTwinkles()
|
|
{
|
|
twinkleFoxPalette = PartyColors_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void forestTwinkles()
|
|
{
|
|
twinkleFoxPalette = ForestColors_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void lavaTwinkles()
|
|
{
|
|
twinkleFoxPalette = LavaColors_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void fireTwinkles()
|
|
{
|
|
twinkleFoxPalette = HeatColors_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void cloud2Twinkles()
|
|
{
|
|
twinkleFoxPalette = CloudColors_p;
|
|
drawTwinkles();
|
|
}
|
|
|
|
void oceanTwinkles()
|
|
{
|
|
twinkleFoxPalette = OceanColors_p;
|
|
drawTwinkles();
|
|
}
|