/ EyeLights_Fire / EyeLights_Fire / EyeLights_Fire.ino
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  }