animation.cpp
  1  // SPDX-FileCopyrightText: 2019 Anne Barela for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  // Weather animation is rendered procedurally based on a few parameters
  6  // (time of day, cloud cover, rainfall, etc.). Most of the inputs are NOT
  7  // real-world units...see comments for explanation of what's needed.
  8  
  9  // NeoPixel stuff --------------------------------------------------------
 10  
 11  #include <Adafruit_NeoPixel.h>
 12  
 13  #define NEOPIXEL_PIN 14 // NeoPixels are connected to this pin
 14  #define NUM_LEDS    16 // Number of NeoPixels
 15  #define FPS         50 // Animation frame rate (frames per second)
 16  
 17  Adafruit_NeoPixel leds(NUM_LEDS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
 18  
 19  // Animation control stuff -------------------------------------------------
 20  
 21  uint8_t renderBuf[NUM_LEDS][3], // Each frame of animation is assembled here
 22          alphaBuf[NUM_LEDS],     // Alpha mask for compositing each layer
 23          rainBuf[NUM_LEDS],      // Extra mask just for raindrop brightness
 24          rainCounter  = 1,       // Drop-to-drop countdown, in frames
 25          rainInterval = 0,       // Drop-to-drop interval, frames (0=no rain)
 26          windSpeed    = 0,       // Per-frame cloud motion (see comments)
 27          cloudCover   = 0;       // Percent cloud cover
 28  
 29  uint16_t sunCenter   = 0,       // Position of 'sun' in 16-bit sky
 30           sunRadius   = 8192,    // Size of sun (same units)
 31           cloudOffset = 0,       // Position of cloud bitmap 'seam'
 32           timeOfDay   = 32768;   // Fixed-point day/night value (see notes)
 33  
 34  uint8_t lightningBrightness = 0;
 35  uint8_t lightningIntensity  = 0;
 36  uint8_t snowIntensity       = 0;
 37  
 38  uint32_t cloudBits   = 0;       // Bitmask of clouds
 39  #if NUM_LEDS < 32
 40   #define NUM_CLOUD_BITS NUM_LEDS
 41  #else
 42   #define NUM_CLOUD_BITS 32
 43  #endif
 44  
 45  #define N_STARS (3 + (NUM_LEDS / 7))
 46  struct star {
 47    uint8_t pos;
 48    uint8_t brightness;
 49  } star[N_STARS];
 50  
 51  // Flake will "move," then "stop" when it hits the "ground," then fade.
 52  // Kinda like raindrops, but moving first.
 53  #define MAX_FLAKES (3 + (NUM_LEDS / 7))
 54  struct flake {
 55    uint16_t pos;
 56    int16_t  speed;
 57    uint8_t  brightness;
 58    uint8_t  time;
 59  } flake[MAX_FLAKES];
 60  uint8_t nFlakes = 0;
 61  
 62  void randomFlake(void) {
 63    flake[nFlakes].pos        = random(65536);
 64    uint8_t w = windSpeed;
 65    if(w < 20) w = 20;
 66    do {
 67      flake[nFlakes].speed    = random(w / -4, (w * 5) / 4);
 68    } while(!flake[nFlakes].speed);
 69    flake[nFlakes].brightness = random(128, 255);
 70    flake[nFlakes].time       = random(FPS, FPS * 2); // # frames until snowflake "touches ground"
 71    nFlakes++;
 72  }
 73  
 74  
 75  uint16_t lightningCounter = 0;
 76  
 77  extern const uint8_t gamma8[]; // Big table at end of this code
 78  
 79  // One-time initialization - clears NeoPixels & sets up some variables -----
 80  
 81  void animSetup(void) {
 82  
 83    leds.begin();
 84    leds.setBrightness(200);
 85    leds.clear(); // All NeoPixels off ASAP
 86    leds.show();
 87  
 88    randomSeed(analogRead(A0));
 89  
 90    memset(rainBuf, 0, sizeof(rainBuf)); // Clear rain buffer
 91    for(uint8_t i=0; i<N_STARS; i++) {   // Initialize star positions
 92      star[i].pos = random(NUM_LEDS);    // TO DO: make stars not overlap
 93      star[i].brightness = random(15, 45);
 94    }
 95    memset(flake, 0, sizeof(flake));     // Clear snowflakes
 96  }
 97  
 98  // Utility functions -------------------------------------------------------
 99  
100  // Set up animation based on some weather attributes like cloud cover, etc.
101  void animConfig(
102   uint16_t t,   // Time of day in fixed-point 16-bit units, where 0=midnight,
103                 // 32768=noon, 65536=midnight. THIS DOES NOT CORRESPOND TO
104                 // ANY SORT OF REAL-WORLD UNITS LIKE SECONDS, nor does it
105                 // handle things like seasons or Daylight Saving Time, it's
106                 // just an "ish" approximation to give the sky animation some
107                 // vague context. The time of day should be polled from the
108                 // same source that's providing the weather data, DO NOT use
109                 // millis() or micros() to attempt to follow real time, as
110                 // the NeoPixel library is known to mangle these interrupt-
111                 // based functions. TIME OF DAY IS "ISH!"
112   uint8_t  c,   // Cloud cover, as a percentage (0-100).
113   uint8_t  r,   // Rainfall as a "strength" value (0-255) that doesn't really
114                 // correspond to anything except "none" to "max."
115   uint8_t  s,   // Snowfall, similar "strength" value (0-255).
116   uint8_t  l,   // Lightning, ditto.
117   uint8_t  w) { // Wind speed as a "strength" value (0-255) that also doesn't
118                 // correspond to anything real; this is the number of fixed-
119                 // point units that the clouds will move per frame. There are
120                 // 65536 units around the 'sky,' so a value of 255 will take
121                 // about 257 frames to make a full revolution of the LEDs,
122                 // which at 50 FPS would be a little over 5 seconds.
123  
124    timeOfDay          = t;
125    cloudCover         = (c > 100) ? 100 : c;
126    rainInterval       = r ? map(r, 1, 255, 64, 1) : 0;
127    windSpeed          = w;
128    lightningIntensity = l;
129    snowIntensity      = s;
130  
131    // Randomize cloud bitmask based on cloud cover percentage:
132    cloudBits = 0;
133    for(uint8_t i=0; i<NUM_CLOUD_BITS; i++) {
134      cloudBits <<= 1;
135      if(cloudCover > random(150)) cloudBits |= 1;
136    }
137  
138    nFlakes = 0;
139    memset(flake, 0, sizeof(flake));
140    if(s) {
141      uint8_t n = 3 + (snowIntensity * (MAX_FLAKES - 2)) / 256;
142      while(nFlakes < n) {
143        randomFlake();
144      }
145    }
146  }
147  
148  // Interpolate between two 'packed' (32-bit) RGB colors.
149  // Second argument is weighting (0-255) of second color.
150  uint32_t colorInterp(uint32_t color1, uint32_t color2, uint8_t w) {
151    uint8_t  r1 = (color1 >> 16) & 0xFF,
152             g1 = (color1 >>  8) & 0xFF,
153             b1 =  color1        & 0xFF,
154             r2 = (color2 >> 16) & 0xFF,
155             g2 = (color2 >>  8) & 0xFF,
156             b2 =  color2        & 0xFF;
157    uint16_t w2 = (uint16_t)w + 1, // 1-256
158             w1 = 257 - w2;        // 1-256
159    r1 = (r1 * w1 + r2 * w2) >> 8;
160    g1 = (g1 * w1 + g2 * w2) >> 8;
161    b1 = (b1 * w1 + b2 * w2) >> 8;
162    return (((uint32_t)r1 << 16) | ((uint32_t)g1 << 8) | b1);
163  }
164  
165  // Using alphaBuf as a mask, fill an RGB color atop renderBuf
166  void overlay(uint8_t r, uint8_t g, uint8_t b) {
167    uint16_t i, a1, a2;
168    for(i=0; i<NUM_LEDS; i++) {
169      a1 = alphaBuf[i] + 1; // 1-256
170      a2 = 257 - a1;        // 1-256
171      renderBuf[i][0] = (r * a1 + renderBuf[i][0] * a2) >> 8;
172      renderBuf[i][1] = (g * a1 + renderBuf[i][1] * a2) >> 8;
173      renderBuf[i][2] = (b * a1 + renderBuf[i][2] * a2) >> 8;
174    }
175  }
176  
177  // Same as above, for packed 32-bit RGB value
178  void overlay(uint32_t color) {
179    overlay((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF);
180  }
181  
182  void waitForFrame(void) {
183    static uint32_t timeOfLastFrame = 0L;
184    uint32_t t;
185    while(((t = millis()) - timeOfLastFrame) < (1000 / FPS)) yield();
186    timeOfLastFrame = t;
187  }
188  
189  #define NIGHTSKYCLEAR   0x0a1923
190  #define DAYSKYCLEAR     0x28648c
191  #define NIGHTSKYCLOUDBG 0x2c2425
192  #define DAYSKYCLOUDBG   0x5e6065
193  #define NIGHTSKYCLOUDFG 0x515159
194  #define DAYSKYCLOUDFG   0xc2c2c2
195  #define NIGHTSNOW       0xa6b1c0
196  #define DAYSNOW         0xffffff
197  #define SUNCLEAR        0xffff60
198  #define SUNCLOUDY       0x7a7a61
199  
200  void renderFrame(void) {
201    // Display *prior* frame of data at start of function --
202    // this ensures uniform updates, as render time may vary.
203    leds.show();
204  
205    // Then begin processing next frame...
206  
207    int i;
208  
209    // tod: 0-64K, where 0 = midnight, 32K = noon, 64K = midnight
210    // this is an artistic approximation and doesn't take seasons,
211    // etc into consideration. if you need that, can fudge it into
212    // tod rather than here.
213    // Sunrise and sunset are two 90-minute periods centered around
214    // 6am and 6pm (again, not factoring in seasons, daylight savings
215    // time, etc.). Sky and other effects will interpolate between
216    // day and night states for these two things.
217    long y = timeOfDay;
218    uint8_t dayWeight;
219    if(y > 32767) y = 65536 - y;
220    y = y * 256L / 4096 - 896;
221    dayWeight = (y > 255) ? 255 : ((y < 0) ? 0 : y); // 0-255 night/day
222  
223    // Determine sky and cloud color based on % of cloud cover
224    uint32_t
225      clearSkyColor  = colorInterp(NIGHTSKYCLEAR  , DAYSKYCLEAR  , dayWeight),
226      cloudySkyColor = colorInterp(NIGHTSKYCLOUDBG, DAYSKYCLOUDBG, dayWeight),
227      cloudColor     = colorInterp(NIGHTSKYCLOUDFG, DAYSKYCLOUDFG, dayWeight),
228      skyColor       = colorInterp(clearSkyColor, cloudySkyColor, map(cloudCover, 30, 70, 0, 255));
229  
230    for(i=0; i<NUM_LEDS; i++) {
231      renderBuf[i][0] = skyColor >> 16;
232      renderBuf[i][1] = skyColor >>  8;
233      renderBuf[i][2] = skyColor;
234    }
235  
236    // Stars
237    if(dayWeight < 128) { // Dark? Or getting there?
238      uint16_t nightWeight = 257 - dayWeight;
239      memset(alphaBuf, 0, sizeof(alphaBuf));
240      for(i=0; i<N_STARS; i++) {
241        alphaBuf[star[i].pos] = (nightWeight * random(star[i].brightness/2, star[i].brightness)) >> 8;
242      }
243      overlay(255, 255, 255);
244    } else {
245      sunRadius = map(dayWeight, 128, 255, 1, 8192);
246      uint16_t x;
247      int16_t px1, px2, sx1, sx2;
248  
249      // Clear alpha buffer, gonna render 'sun' there...
250      memset(alphaBuf, 0, sizeof(alphaBuf));
251  
252      uint32_t
253        sunColor = colorInterp(SUNCLEAR, SUNCLOUDY, map(cloudCover, 30, 70, 0, 255));
254  
255      // Figure overlap between sun and each pixel...
256      //  uint16_t left, right, dist1, dist2;
257      for(i=0; i<NUM_LEDS; i++) {
258        // Pixel coord in fixed-point space
259        x = (i * 65536L) / NUM_LEDS;
260        int16_t foo = sunCenter - x; // sun center in pixel space
261        sx1 = foo - sunRadius;
262        sx2 = foo + sunRadius;
263        px1 = 0;
264        px2 = 65536 / NUM_LEDS;
265        if((sx1 >= px2) || (sx2 < 0)) continue; // No overlap
266        else if((sx1 <= 0) && (sx2 >= px2)) alphaBuf[i] = 255; // Fully encompassed
267        else {
268          if(sx1 > 0) {
269            if(sx2 < px2) {
270              alphaBuf[i] = 255L * (sx2 - sx1) / (px2 - px1);
271            } else {
272              alphaBuf[i] = 255L * (px2 - sx1) / (px2 - px1);
273            }
274          } else {
275            alphaBuf[i] = 255L * (sx2 - px1) / (px2 - px1);
276          }
277        }
278      }
279      
280      overlay(sunColor); // Composite sun atop sky
281    }
282  
283    if(cloudBits) {
284      // Clear alpha buffer, gonna render clouds there...
285      memset(alphaBuf, 0, sizeof(alphaBuf));
286      uint16_t x, minor;
287      uint8_t  major, l, r;
288      for(i=0; i<NUM_LEDS; i++) {
289        x           = (i * 65536L) / NUM_LEDS - cloudOffset;  // Pixel coord in fixed-point space (0-65535) relative to clouds
290        x           = (x * (NUM_CLOUD_BITS * 256UL)) / 65536; // Scale to cloud pixel space
291        major       = x >> 8;                                 // Left bit number (0 to NUM_CLOUD_BITS-1)
292        minor       = x & 0xFF;                               // Weight (0-255) of next bit over
293        l           = (cloudBits & (1 << major)) ? 220 : 0;   // Left bit opacity
294        if(++major >= NUM_CLOUD_BITS) major = 0;              // Next bit over
295        r           = (cloudBits & (1 << major)) ? 220 : 0;   // Right bit opacity
296        alphaBuf[i] = ((l * (257 - minor)) + (r * (minor + 1))) >> 8; // Blend
297      }
298  
299      uint32_t c = colorInterp(NIGHTSKYCLOUDFG, DAYSKYCLOUDFG, dayWeight);
300      overlay(c); // Composite clouds atop sky
301    }
302  
303    if(rainInterval) {
304      memset(alphaBuf, 0, sizeof(alphaBuf));
305      for(i=0; i<NUM_LEDS; i++) {
306        rainBuf[i] = (rainBuf[i] * (uint16_t)245) >> 8;
307      }
308      // Periodically, randomly, add a drop to rainBuf[]
309      if(!--rainCounter) {
310        i = random(NUM_LEDS); // Which spot?
311        int16_t foo = rainBuf[i] + 255;
312        if(foo > 255) foo = 255;
313        rainBuf[i] = foo;
314        uint8_t r4 = rainInterval / 4;
315        if(r4 < 1) r4 = 1;
316        rainCounter = random(r4, rainInterval);
317      }
318      memcpy(alphaBuf, rainBuf, sizeof(rainBuf));
319      overlay(130, 130, 150);
320    }
321  
322    if(nFlakes) {
323      uint16_t x, minor;
324      uint8_t  major, l, r;
325      memset(alphaBuf, 0, sizeof(alphaBuf));
326      for(i=0; i<nFlakes; i++) {
327        // Render flake here
328        x     = (flake[i].pos * (NUM_LEDS * 256UL)) / 65536;
329        major = x >> 8;   // Left pixel number (0 to NUM_LEDS-1)
330        minor = x & 0xFF; // Weight (0-255) of next pixel over
331        alphaBuf[major] = (alphaBuf[major] * (1   + minor)) + (flake[i].brightness * (257 - minor)) >> 8;
332        if(++major >= NUM_LEDS) major = 0;
333        alphaBuf[major] = (alphaBuf[major] * (257 - minor)) + (flake[i].brightness * (1   + minor)) >> 8;
334        flake[i].pos += flake[i].speed;
335        if(flake[i].time) {
336          flake[i].time--;
337        } else {
338          flake[i].brightness = (flake[i].brightness * 253) >> 8;
339          if(!flake[i].brightness) {
340            memcpy(&flake[i], &flake[nFlakes-1], sizeof(struct flake)); // Move last flake to this pos.
341            i--;           // Flake moved, so don't increment
342            nFlakes--;     // Decrement number of flakes
343            randomFlake(); // And add a new one in last pos.
344          }
345        }
346      }
347      overlay(255, 255, 255);
348    }
349  
350    if(lightningBrightness) {
351      for(i=0; i<NUM_LEDS; i++) alphaBuf[i] = lightningBrightness;
352      overlay(255, 255, 255);
353      lightningBrightness = (lightningBrightness * 220) >> 8;
354    }
355    if(lightningIntensity) {
356      if(!random(50 + (255 - lightningIntensity) * 3)) {
357        i = random(128, 256);
358        if(i > lightningBrightness) lightningBrightness = i;
359      }
360    }
361  
362    sunCenter   += 65536 / 30 / FPS; // 30 sec for 1 revolution
363    cloudOffset -= windSpeed;
364  
365  //  timeOfDay += 65536/60/FPS; // 1 min for day/night cycle
366  
367    // Convert RGB renderbuf to gamma-corrected LED-native color order:
368    for(uint16_t i=0; i<NUM_LEDS; i++) {
369      leds.setPixelColor(i,
370        pgm_read_byte(&gamma8[renderBuf[i][0]]),
371        pgm_read_byte(&gamma8[renderBuf[i][1]]),
372        pgm_read_byte(&gamma8[renderBuf[i][2]]));
373    }
374    // DON'T call leds.show() here! That's done at start of function.
375  }
376  
377  // Gamma correction improves appearance of midrange colors
378  const uint8_t gamma8[] PROGMEM = {
379      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
380      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,
381      1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,
382      2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,
383      5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,
384     10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
385     17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
386     25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
387     37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
388     51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
389     69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
390     90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
391    115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
392    144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
393    177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
394    215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 };
395  
396