// 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(uint8_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(); }