EyeLights_Fire.ino
1 // SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 /* 6 FIRE EFFECT for Adafruit EyeLights (LED Glasses + Driver). 7 A demoscene classic that produces a cool analog-esque look with 8 modest means, iteratively scrolling and blurring raster data. 9 */ 10 11 #include <Adafruit_IS31FL3741.h> // For LED driver 12 13 Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation 14 15 // The raster data is intentionally one row taller than the LED matrix. 16 // Each frame, random noise is put in the bottom (off matrix) row. There's 17 // also an extra column on either side, to avoid needing edge clipping when 18 // neighboring pixels (left, center, right) are averaged later. 19 float data[6][20]; // 2D array where elements are accessed as data[y][x] 20 21 // Each element in the raster is a single value representing brightness. 22 // A pre-computed lookup table maps these to RGB colors. This one happens 23 // to have 32 elements, but as we're not on an actual paletted hardware 24 // framebuffer it could be any size really (with suitable changes throughout). 25 uint32_t colormap[32]; 26 #define GAMMA 2.6 27 28 // Crude error handler, prints message to Serial console, flashes LED 29 void err(char *str, uint8_t hz) { 30 Serial.println(str); 31 pinMode(LED_BUILTIN, OUTPUT); 32 for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1); 33 } 34 35 void setup() { // Runs once at program start... 36 37 // Initialize hardware 38 Serial.begin(115200); 39 if (! glasses.begin()) err("IS3741 not found", 2); 40 41 // Configure glasses for reduced brightness, enable output 42 glasses.setLEDscaling(0xFF); 43 glasses.setGlobalCurrent(20); 44 glasses.enable(true); 45 46 memset(data, 0, sizeof data); 47 48 for(uint8_t i=0; i<32; i++) { 49 float n = i * 3.0 / 31.0; // 0.0 <= n <= 3.0 from start to end of map 50 float r, g, b; 51 if (n <= 1) { // 0.0 <= n <= 1.0 : black to red 52 r = n; // r,g,b are initially calculated 0 to 1 range 53 g = b = 0.0; 54 } else if (n <= 2) { // 1.0 <= n <= 2.0 : red to yellow 55 r = 1.0; 56 g = n - 1.0; 57 b = 0.0; 58 } else { // 2.0 <= n <= 3.0 : yellow to white 59 r = g = 1.0; 60 b = n - 2.0; 61 } 62 // Gamma correction linearizes perceived brightness, then scale to 63 // 0-255 for LEDs and store as a 'packed' RGB color. 64 colormap[i] = (uint32_t(pow(r, GAMMA) * 255.0) << 16) | 65 (uint32_t(pow(g, GAMMA) * 255.0) << 8) | 66 uint32_t(pow(b, GAMMA) * 255.0); 67 } 68 } 69 70 // Linearly interpolate a range of brightnesses between two LEDs of 71 // one eyeglass ring, mapping through the global color table. LED range 72 // is non-inclusive; the first and last LEDs (which overlap matrix pixels) 73 // are not set. led2 MUST be > led1. LED indices may be >= 24 to 'wrap 74 // around' the seam at the top of the ring. 75 void interp(bool isRight, int led1, int led2, float level1, float level2) { 76 int span = led2 - led1 + 1; // Number of LEDs 77 float delta = level2 - level1; // Difference in brightness 78 for (int led = led1 + 1; led < led2; led++) { // For each LED in-between, 79 float ratio = (float)(led - led1) / span; // interpolate brightness level 80 uint32_t color = colormap[min(31, int(level1 + delta * ratio))]; 81 if (isRight) glasses.right_ring.setPixelColor(led % 24, color); 82 else glasses.left_ring.setPixelColor(led % 24, color); 83 } 84 } 85 86 void loop() { // Repeat forever... 87 // At the start of each frame, fill the bottom (off matrix) row 88 // with random noise. To make things less strobey, old data from the 89 // prior frame still has about 1/3 'weight' here. There's no special 90 // real-world significance to the 85, it's just an empirically- 91 // derived fudge factor that happens to work well with the size of 92 // the color map. 93 for (uint8_t x=1; x<19; x++) { 94 data[5][x] = 0.33 * data[5][x] + 0.67 * ((float)random(1000) / 1000.0) * 85.0; 95 } 96 // If this were actual SRS BZNS 31337 D3M0SC3N3 code, great care 97 // would be taken to avoid floating-point math. But with few pixels, 98 // and so this code might be less obtuse, a casual approach is taken. 99 100 // Each row (except last) is then processed, top-to-bottom. This 101 // order is important because it's an iterative algorithm...the 102 // output of each frame serves as input to the next, and the steps 103 // below (looking at the pixels below each row) are what makes the 104 // "flames" appear to move "up." 105 for (uint8_t y=0; y<5; y++) { // Current row of pixels 106 float *y1 = &data[y + 1][0]; // One row down 107 for (uint8_t x = 1; x < 19; x++) { // Skip left, right columns in data 108 // Each pixel is sort of the average of the three pixels 109 // under it (below left, below center, below right), but not 110 // exactly. The below center pixel has more 'weight' than the 111 // others, and the result is scaled to intentionally land 112 // short, making each row bit darker as they move up. 113 data[y][x] = (y1[x] + ((y1[x - 1] + y1[x + 1]) * 0.33)) * 0.35; 114 glasses.drawPixel(x - 1, y, glasses.color565(colormap[min(31, int(data[y][x]))])); 115 // Remember that the LED matrix uses GFX-style "565" colors, 116 // hence the round trip through color565() here, whereas the LED 117 // rings (referenced in interp()) use NeoPixel-style 24-bit colors 118 // (those can reference colormap[] directly). 119 } 120 } 121 122 // That's all well and good for the matrix, but what about the extra 123 // LEDs in the rings? Since these don't align to the pixel grid, 124 // rather than trying to extend the raster data and filter it in 125 // somehow, we'll fill those arcs with colors interpolated from the 126 // endpoints where rings and matrix intersect. Maybe not perfect, 127 // but looks okay enough! 128 interp(false, 7, 17, data[4][8], data[4][1]); // Left ring bottom 129 interp(false, 21, 29, data[0][2], data[1][8]); // Left ring top 130 interp(true, 7, 17, data[4][18], data[4][11]); // Right ring bottom 131 interp(true, 19, 27, data[1][11], data[0][17]); // Right ring top 132 133 glasses.show(); 134 delay(25); 135 }