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