Skip to content

Instantly share code, notes, and snippets.

@hughpyle
Created December 13, 2013 16:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hughpyle/7946695 to your computer and use it in GitHub Desktop.
Save hughpyle/7946695 to your computer and use it in GitHub Desktop.
Sparkly patterns for a festive tree decorated with LPD8806 LED striplight. The base is a gently-changing colorwheel that rotates along the strip (downward). The base is modulated a faster sparkly colorwheel rotating upward.
/*
Lights for a tree (3m strip)
2013-12-10 @hughpyle, (cc) https://creativecommons.org/licenses/by/3.0/
Structure is based on the Adafruit "advancedLEDBeltKit.pde" example
Colorwheel and sparkle use the Minsky circle algorithm http://cabezal.com/misc/minsky-circles.html
*/
// LPD8806 library is from https://github.com/adafruit/LPD8806
#include <avr/pgmspace.h>
#include "SPI.h"
#include "LPD8806.h"
// TimerOne library is from http://www.arduino.cc/playground/Code/Timer1
#include "TimerOne.h"
int scale = 128; //255;
// LPD8806-based LED strip for display
// Number of RGB LEDs in strand:
const int numPixels = 160;
// Software SPI, choose any two pins
// LPD8806 strip = LPD8806(nLEDs, 2 /* ledDataPin */, 3 /* ledClockPin */);
// Hardware SPI for faster writes. For "classic" Arduinos (Uno, Duemilanove, etc.), data = pin 11, clock = pin 13
// For Teensy using hardware SPI, using pin B1 (#1) for clock and B2 (#2) for data
LPD8806 strip = LPD8806(numPixels,11,13);
// Two sets of image data
// One for the "front" image (the sparkle)
// Another for the "back" image (the colorwheel)
byte imgData[2][numPixels * 3];
byte backImgIdx = 0; // Index of 'back' image (always 0)
int fxVars[3][8]; // Effect instance variables (explained later)
// function prototypes, leave these be :)
void renderColorwheel(byte idx);
void renderColorwheel2(byte idx);
void renderSparkle(byte idx);
void renderSparkle2(byte idx);
void callback();
byte gamma(byte x);
long hsv2rgb(long h, byte s, byte v);
char fixSin(int angle);
char fixCos(int angle);
// Globals for effects
float valRx = 1, valRy = 0;
float valGx = 1, valGy = 0;
float valBx = 1, valBy = 0;
float valAx = 1, valAy = 0;
// ---------------------------------------------------------------------------
void setup() {
// Start up the LED strip. Note that strip.show() is NOT called here --
// the callback function will be invoked immediately when attached, and
// the first thing the calback does is update the strip.
strip.begin();
// Initialize random number generator from a floating analog input.
randomSeed(analogRead(0));
// Initialize image data
memset(imgData, 0, sizeof(imgData)); // Clear image data
fxVars[backImgIdx][0] = 1; // Mark back image as initialized
// Timer1 is used so the strip will update at a known fixed frame rate.
// Each effect rendering function varies in processing complexity, so
// the timer allows smooth transitions between effects (otherwise the
// effects and transitions would jump around in speed...not attractive).
Timer1.initialize();
Timer1.attachInterrupt(callback, 75000);
}
void loop()
{
delay(35000);
}
// Timer1 interrupt handler. Called at equal intervals; 60 Hz by default.
void callback() {
int i;
byte r, g, b;
uint16_t r1, g1, b1;
byte frontImgIdx = 1 - backImgIdx;
byte *backPtr = &imgData[backImgIdx][0];
byte *frontPtr = &imgData[frontImgIdx][0];
int16_t dither;
int16_t dibs = random(256);
// Write the strip based on the previous render, applying the current brightness
for(i=0; i<numPixels; i++) {
//scale = random(16)*random(16);
r1 = (*backPtr++) * (*frontPtr++); // scale;
g1 = (*backPtr++) * (*frontPtr++); // scale;
b1 = (*backPtr++) * (*frontPtr++); // scale;
dither = (dibs & 0x03)<<9; dibs=dibs>>2;
r = gamma( (r1+dither)>>8 );
dither = (dibs & 0x03)<<9; dibs=dibs>>2;
g = gamma( (g1+dither)>>8 );
dither = (dibs & 0x03)<<9; dibs=dibs>>2;
b = gamma( (b1+dither)>>8 );
strip.setPixelColor(i, r, g, b);
}
// Very first thing here is to issue the strip data generated from the
// *previous* callback. It's done this way on purpose because show() is
// roughly constant-time, so the refresh will always occur on a uniform
// beat with respect to the Timer1 interrupt. The various effects
// rendering and compositing code is not constant-time, and that
// unevenness would be apparent if show() were called at the end.
strip.show();
// Always render back image based on current effect index:
renderColorwheel(backImgIdx);
renderSparkle2(frontImgIdx);
}
// ---------------------------------------------------------------------------
// Image effect rendering functions. Each effect is generated parametrically
// (that is, from a set of numbers, usually randomly seeded). Because both
// back and front images may be rendering the same effect at the same time
// (but with different parameters), a distinct block of parameter memory is
// required for each image. The 'fxVars' array is a two-dimensional array
// of integers, where the major axis is either 0 or 1 to represent the two
// images, while the minor axis holds 50 elements -- this is working scratch
// space for the effect code to preserve its "state." The meaning of each
// element is generally unique to each rendering effect, but the first element
// is most often used as a flag indicating whether the effect parameters have
// been initialized yet. When the back/front image indexes swap at the end of
// each transition, the corresponding set of fxVars, being keyed to the same
// indexes, are automatically carried with them.
// Multiphase sinewaves in r,g,b
void renderColorwheel(byte idx) {
static const float dR = 0.02 * (500+random(100))/500;
static const float dG = 0.0243 * (500+random(100))/500;
static const float dB = 0.031 * (500+random(100))/500;
byte r, g, b;
if(fxVars[idx][0] == 0) {
// Initialize by writing all pixels
byte *ptr = &imgData[idx][0];
for(int i=0; i<numPixels; i++) {
valRx = valRx + (dR * valRy); valRy = valRy - (dR * valRx);
valGx = valGx + (dG * valGy); valGy = valGy - (dG * valGx);
valBx = valBx + (dB * valBy); valBy = valBy - (dB * valBx);
r = 120 * (1+valRx) + 5;
g = 120 * (1+valGx) + 5;
b = 120 * (1+valBx) + 5;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
fxVars[idx][0] = 1; // Effect initialized
}
else
{
// Copy the image data forward
byte *ptp = &imgData[idx][3];
byte *ptr = &imgData[idx][0];
for(int i=1; i<numPixels; i++) {
r = *ptp++; g = *ptp++; b = *ptp++;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
// Calculate one new pixel
ptr = &imgData[idx][3*numPixels-3];
valRx = valRx + (dR * valRy); valRy = valRy - (dR * valRx);
valGx = valGx + (dG * valGy); valGy = valGy - (dG * valGx);
valBx = valBx + (dB * valBy); valBy = valBy - (dB * valBx);
r = 60 * (2+valRx) + 5;
g = 60 * (2+valGx) + 5;
b = 120 * (1+valBx) + 5;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
}
void renderColorwheel2(byte idx) {
static const float dR = 0.02 * (500+random(100))/500;
static const float dG = 0.0243 * (500+random(100))/500;
static const float dB = 0.031 * (500+random(100))/500;
byte r, g, b;
if(fxVars[idx][0] == 0) {
// Initialize by writing all pixels
byte *ptr = &imgData[idx][0];
for(int i=0; i<numPixels; i++) {
valRx = valRx + (dR * valRy); valRy = valRy - (dR * valRx);
valGx = valGx + (dG * valGy); valGy = valGy - (dG * valGx);
valBx = valBx + (dB * valBy); valBy = valBy - (dB * valBx);
r = 120 * (1+valRx) + 5;
g = 120 * (1+valGx) + 5;
b = 120 * (1+valBx) + 5;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
fxVars[idx][0] = 1; // Effect initialized
}
else
{
// Copy the image data backward
byte *ptp = &imgData[idx][3*numPixels-4];
byte *ptr = &imgData[idx][3*numPixels-1];
for(int i=numPixels-1; i>0; i--) {
r = *ptp--; g = *ptp--; b = *ptp--;
*ptr-- = r; *ptr-- = g; *ptr-- = b;
}
// Calculate one new pixel
ptr = &imgData[idx][0];
valRx = valRx + (dR * valRy); valRy = valRy - (dR * valRx);
valGx = valGx + (dG * valGy); valGy = valGy - (dG * valGx);
valBx = valBx + (dB * valBy); valBy = valBy - (dB * valBx);
r = 60 * (2+valRx) + 5;
g = 60 * (2+valGx) + 5;
b = 120 * (1+valBx) + 5;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
}
/*
Sparkle is really just a constant brightness
*/
void renderSparkle(byte idx) {
byte r, g, b;
if(fxVars[idx][0] == 0) {
// Initialize by writing all pixels
byte *ptr = &imgData[idx][0];
for(int i=0; i<numPixels; i++) {
r = 127;
g = 127;
b = 127;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
fxVars[idx][0] = 1; // Effect initialized
}
}
void renderSparkle2(byte idx) {
static const float dA = 0.12 * (500+random(100))/500;
byte r, g, b;
if(fxVars[idx][0] == 0) {
// Initialize by writing all pixels
byte *ptr = &imgData[idx][0];
for(int i=0; i<numPixels; i++) {
valAx = valAx + (dA * valAy); valAy = valAy - (dA * valAx);
r = 120 * (1+valAx) + 5;
g = r;
b = r;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
fxVars[idx][0] = 1; // Effect initialized
}
else
{
// Copy the image data backward
byte *ptp = &imgData[idx][3*numPixels-4];
byte *ptr = &imgData[idx][3*numPixels-1];
for(int i=numPixels-1; i>0; i--) {
r = *ptp--; g = *ptp--; b = *ptp--;
*ptr-- = r; *ptr-- = g; *ptr-- = b;
}
// Calculate one new pixel
ptr = &imgData[idx][0];
valAx = valAx + (dA * valAy); valAy = valAy - (dA * valAx);
// 50 adjust depth-of-sparkle
// 140 adjust absolute brightness
r = random(50 * (1+valAx)) + 140;
g = random(50 * (1+valAx)) + 140;
b = random(80 * (1+valAx)) + 140;
*ptr++ = r; *ptr++ = g; *ptr++ = b;
}
}
// ---------------------------------------------------------------------------
// Assorted fixed-point utilities below this line. Not real interesting.
// Gamma correction compensates for our eyes' nonlinear perception of
// intensity. It's the LAST step before a pixel value is stored, and
// allows intermediate rendering/processing to occur in linear space.
// The table contains 256 elements (8 bit input), though the outputs are
// only 7 bits (0 to 127). This is normal and intentional by design: it
// allows all the rendering code to operate in the more familiar unsigned
// 8-bit colorspace (used in a lot of existing graphics code), and better
// preserves accuracy where repeated color blending operations occur.
// Only the final end product is converted to 7 bits, the native format
// for the LPD8806 LED driver. Gamma correction and 7-bit decimation
// thus occur in a single operation.
PROGMEM prog_uchar gammaTable[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2,
2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4,
4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7,
7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11,
11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15, 16, 16,
16, 17, 17, 17, 18, 18, 18, 19, 19, 20, 20, 21, 21, 21, 22, 22,
23, 23, 24, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30,
30, 31, 32, 32, 33, 33, 34, 34, 35, 35, 36, 37, 37, 38, 38, 39,
40, 40, 41, 41, 42, 43, 43, 44, 45, 45, 46, 47, 47, 48, 49, 50,
50, 51, 52, 52, 53, 54, 55, 55, 56, 57, 58, 58, 59, 60, 61, 62,
62, 63, 64, 65, 66, 67, 67, 68, 69, 70, 71, 72, 73, 74, 74, 75,
76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
92, 93, 94, 95, 96, 97, 98, 99,100,101,102,104,105,106,107,108,
109,110,111,113,114,115,116,117,118,120,121,122,123,125,126,127
};
// This function (which actually gets 'inlined' anywhere it's called)
// exists so that gammaTable can reside out of the way down here in the
// utility code...didn't want that huge table distracting or intimidating
// folks before even getting into the real substance of the program, and
// the compiler permits forward references to functions but not data.
inline byte gamma(byte x) {
return pgm_read_byte(&gammaTable[x]);
}
// Fixed-point colorspace conversion: HSV (hue-saturation-value) to RGB.
// This is a bit like the 'Wheel' function from the original strandtest
// code on steroids. The angular units for the hue parameter may seem a
// bit odd: there are 1536 increments around the full color wheel here --
// not degrees, radians, gradians or any other conventional unit I'm
// aware of. These units make the conversion code simpler/faster, because
// the wheel can be divided into six sections of 256 values each, very
// easy to handle on an 8-bit microcontroller. Math is math, and the
// rendering code elsehwere in this file was written to be aware of these
// units. Saturation and value (brightness) range from 0 to 255.
long hsv2rgb(long h, byte s, byte v) {
byte r, g, b, lo;
int s1;
long v1;
// Hue
h %= 1536; // -1535 to +1535
if(h < 0) h += 1536; // 0 to +1535
lo = h & 255; // Low byte = primary/secondary color mix
switch(h >> 8) { // High byte = sextant of colorwheel
case 0 : r = 255 ; g = lo ; b = 0 ; break; // R to Y
case 1 : r = 255 - lo; g = 255 ; b = 0 ; break; // Y to G
case 2 : r = 0 ; g = 255 ; b = lo ; break; // G to C
case 3 : r = 0 ; g = 255 - lo; b = 255 ; break; // C to B
case 4 : r = lo ; g = 0 ; b = 255 ; break; // B to M
default: r = 255 ; g = 0 ; b = 255 - lo; break; // M to R
}
// Saturation: add 1 so range is 1 to 256, allowig a quick shift operation
// on the result rather than a costly divide, while the type upgrade to int
// avoids repeated type conversions in both directions.
s1 = s + 1;
r = 255 - (((255 - r) * s1) >> 8);
g = 255 - (((255 - g) * s1) >> 8);
b = 255 - (((255 - b) * s1) >> 8);
// Value (brightness) and 24-bit color concat merged: similar to above, add
// 1 to allow shifts, and upgrade to long makes other conversions implicit.
v1 = v + 1;
return (((r * v1) & 0xff00) << 8) |
((g * v1) & 0xff00) |
( (b * v1) >> 8);
}
// The fixed-point sine and cosine functions use marginally more
// conventional units, equal to 1/2 degree (720 units around full circle),
// chosen because this gives a reasonable resolution for the given output
// range (-127 to +127). Sine table intentionally contains 181 (not 180)
// elements: 0 to 180 *inclusive*. This is normal.
PROGMEM prog_char sineTable[181] = {
0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 29, 30, 31, 32, 33, 34,
35, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 77, 78, 79, 80, 81,
82, 83, 83, 84, 85, 86, 87, 88, 88, 89, 90, 91, 92, 92, 93, 94,
95, 95, 96, 97, 97, 98, 99,100,100,101,102,102,103,104,104,105,
105,106,107,107,108,108,109,110,110,111,111,112,112,113,113,114,
114,115,115,116,116,117,117,117,118,118,119,119,120,120,120,121,
121,121,122,122,122,123,123,123,123,124,124,124,124,125,125,125,
125,125,126,126,126,126,126,126,126,127,127,127,127,127,127,127,
127,127,127,127,127
};
char fixSin(int angle) {
angle %= 720; // -719 to +719
if(angle < 0) angle += 720; // 0 to +719
return (angle <= 360) ?
pgm_read_byte(&sineTable[(angle <= 180) ?
angle : // Quadrant 1
(360 - angle)]) : // Quadrant 2
-pgm_read_byte(&sineTable[(angle <= 540) ?
(angle - 360) : // Quadrant 3
(720 - angle)]) ; // Quadrant 4
}
char fixCos(int angle) {
angle %= 720; // -719 to +719
if(angle < 0) angle += 720; // 0 to +719
return (angle <= 360) ?
((angle <= 180) ? pgm_read_byte(&sineTable[180 - angle]) : // Quad 1
-pgm_read_byte(&sineTable[angle - 180])) : // Quad 2
((angle <= 540) ? -pgm_read_byte(&sineTable[540 - angle]) : // Quad 3
pgm_read_byte(&sineTable[angle - 540])) ; // Quad 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment