Like lots of folks on the internet, I saw the DIODER and felt that it could be improved. And what better way than to make it internet-controlled? Given that I had a Nanode lying around looking for a use it seemed like providence.
So the plan was this:
- Instead of just cycling between some gaudy colours, lets pick some classier ones
- Let’s allow the colours to be updated via the web, saving them to EEPROM
- Using 1D Perlin noise to blend between a pair of colours will allow for pleasing fades and far more interest than just transitioning from A->B (so much interest that it turned out 2 colours is all it needs … for now)
- Use HSL colour space for super-classy fading
// based on http://www.dipzo.com/wordpress/?p=50 void hslToRgb( int hue, byte sat, byte light, byte* lpOutRgb ) { if( sat == 0 ) { lpOutRgb[0] = lpOutRgb[1] = lpOutRgb[2] = light; return; } float nhue = (float)hue * (1.0f / 360.0f); float nsat = (float)sat * (1.0f / 255.0f); float nlight = (float)light * (1.0f / 255.0f); float m2; if( light < 128 ) m2 = nlight * ( 1.0f + nsat ); else m2 = ( nlight + nsat ) - ( nsat * nlight ); float m1 = ( 2.0f * nlight ) - m2; lpOutRgb[0] = hueToChannel( m1, m2, nhue + (1.0f / 3.0f) ); lpOutRgb[1] = hueToChannel( m1, m2, nhue ); lpOutRgb[2] = hueToChannel( m1, m2, nhue - (1.0f / 3.0f) ); } byte hueToChannel( float m1, float m2, float h ) { float channel = hueToChannelInternal( m1, m2, h ); byte uchan = (byte)(255.0f * channel); return uchan; } float hueToChannelInternal( float m1, float m2, float h ) { if( h < 0.0f ) h += 1.0f; if( h > 1.0f ) h -= 1.0f; if( ( 6.0f * h ) < 1.0f ) return ( m1 + ( m2 - m1 ) * 6.0f * h ); if( ( 2.0f * h ) < 1.0f ) return m2; if( ( 3.0f * h ) < 2.0f ) return ( m1 + ( m2 - m1 ) * ((2.0f/3.0f) - h) * 6.0f ); return m1; }
I also experimented with adding a gamma curve to the output, but while it definitely improved colour matching, it made fading a lot more jerky, so I didn’t go with it in the end.
Once I had reliable HSL->RGB working, I needed a good HSL interpolate function. That took some effort. I found you can use a straight linear interpolate on the S & L channels, but you need a custom one for the Hue channel as it needs to pick the shortest way to go around the circle. I also wrote it all to treat the interpolation parameter as a value from 0 to 1 in 0.8 fixed point format. This is how it looks:
void lerpHsl( const int* a, const int* b, byte t, int* lpOutHsl ) { lpOutHsl[0] = lerpHueFp8( a[0], b[0], t ); lpOutHsl[1] = lerpFp8( a[1], b[1], t ); lpOutHsl[2] = lerpFp8( a[2], b[2], t ); } int lerpFp8( int a, int b, byte t ) { long t0 = ( b - a ) * t; return ( a + ( t0 >> 8 ) ); } int lerpHueFp8( int a, int b, byte t ) { // adjust inputs so we take the shortest route around the circle int cwdist = b - a; if( abs(cwdist) > 180 ) { if( b > a ) a += 360; else b += 360; } long t0 = ( b - a ) * t; int l = a + (t0 >> 8); return ( l % 360 ); }
The last colour-related piece was to get a good 1D Perlin noise-like function. After some googling failed to turn up a nice implementation for Arduino, I built this, based on Hugo Elias’ great page here.
#define kPerlinOctaves 3 static const float kaPerlinOctaveAmplitude[kPerlinOctaves] = { 0.75f, 0.4f, 0.1f, }; float perlinNoise1( long x ) { x = ( x<<13 ) ^ x; return ( 1.0f - ( (x * (x * x * 15731 + 789221L) + 1376312589L) & 0x7fffffff) * (1.0f / 1073741824.0f) ); } float perlinSmoothedNoise1( long x ) { return perlinNoise1(x)*0.5f + perlinNoise1(x-1)*0.25f + perlinNoise1(x+1)*0.25f; } float perlinLerpedNoise1( float x ) { long xInteger = long(x); float xFraction = x - xInteger; float v1 = perlinSmoothedNoise1( xInteger ); float v2 = perlinSmoothedNoise1( xInteger + 1 ); return lerpFloat( v1, v2, xFraction ); } float perlinNoise1D( float x ) { float total = 0.0f; for( int octave = 0; octave < kPerlinOctaves; octave++ ) { int frequency = 1 << octave; float amplitude = kaPerlinOctaveAmplitude[octave]; total += perlinLerpedNoise1( x * frequency ) * amplitude; } // map from [-1,1] to [0,1] and constrain return constrain( (total + 1.0f) * 0.5f, 0.0f, 1.0f ); }
This generates the following output, which looks pretty sweet when used as the interpolation parameter
With that, all of the RGB LED controlling was done 🙂
For the web interface, I used the excellent EtherCard library, which made it super-easy.
Job done*!
*actually, I still need to build a nice jQuery’d webpage to make setting colours easier. Currently I need to go to http://192.168.1.25/hsl?i=0&h=135&s=90&l=50, which isn’t exactly as futuristic as I’d like 🙂