|
| 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 | +} |
0 commit comments