Created
December 13, 2013 16:16
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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