/ OozeMaster3000 / OozeMaster3000.ino
OozeMaster3000.ino
  1  // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  // OOZE MASTER 3000: NeoPixel simulated liquid physics. Up to 7 NeoPixel
  6  // strands dribble light, while an 8th strand "catches the drips."
  7  // Designed for the Adafruit Feather M0 or M4 with matching version of
  8  // NeoPXL8 FeatherWing. This can be adapted for other M0 or M4 boards but
  9  // you will need to do your own "pin sudoku" and level shifting
 10  // (e.g. NeoPXL8 Friend breakout or similar).
 11  // See here: https://learn.adafruit.com/adafruit-neopxl8-featherwing-and-library
 12  // Requires Adafruit_NeoPixel, Adafruit_NeoPXL8 and Adafruit_ZeroDMA libraries.
 13  
 14  #include <Adafruit_NeoPXL8.h>
 15  
 16  uint8_t dripColor[] = { 0, 255, 0 }; // Bright green ectoplasm
 17  #define PIXEL_PITCH (1.0 / 150.0)    // 150 pixels/m
 18  #define ICE_BRIGHTNESS 0             // Icycle effect Brightness (0 to <100%)
 19  
 20  #define GAMMA   2.6
 21  #define G_CONST 9.806 // Standard acceleration due to gravity
 22  // While the above G_CONST is correct for "real time" drips, you can dial it back
 23  // for a more theatric effect / to slow down the drips like they've still got a
 24  // syrupy "drool string" attached (try much lower values like 2.0 to 3.0).
 25  
 26  // NeoPXL8 pin numbers (these are default connections on NeoPXL8 M0 FeatherWing)
 27  int8_t pins[8] = { PIN_SERIAL1_RX, PIN_SERIAL1_TX, MISO, 13, 5, SDA, A4, A3 };
 28  
 29  // If using an M4 Feather & NeoPXL8 FeatherWing, use these values instead:
 30  //int8_t pins[8] = { 13, 12, 11, 10, SCK, 5, 9, 6 };
 31  
 32  
 33  typedef enum {
 34    MODE_IDLE,
 35    MODE_OOZING,
 36    MODE_DRIBBLING_1,
 37    MODE_DRIBBLING_2,
 38    MODE_DRIPPING
 39  } dropState;
 40  
 41  struct {
 42    uint16_t  length;            // Length of NeoPixel strip IN PIXELS
 43    uint16_t  dribblePixel;      // Index of pixel where dribble pauses before drop (0 to length-1)
 44    float     height;            // Height IN METERS of dribblePixel above ground
 45    dropState mode;              // One of the above states (MODE_IDLE, etc.)
 46    uint32_t  eventStartUsec;    // Starting time of current event
 47    uint32_t  eventDurationUsec; // Duration of current event, in microseconds
 48    float     eventDurationReal; // Duration of current event, in seconds (float)
 49    uint32_t  splatStartUsec;    // Starting time of most recent "splat"
 50    uint32_t  splatDurationUsec; // Fade duration of splat
 51    float     pos;               // Position of drip on prior frame
 52  } drip[] = {
 53    // THIS TABLE CONTAINS INFO FOR UP TO 8 NEOPIXEL DRIPS
 54    { 16,  7, 0.157 }, // NeoPXL8 output 0: 16 pixels long, drip pauses at index 7, 0.157 meters above ground
 55    { 19,  6, 0.174 }, // NeoPXL8 output 1: 19 pixels long, pause at index 6, 0.174 meters up
 56    { 18,  5, 0.195 }, // NeoPXL8 output 2: etc.
 57    { 17,  6, 0.16  }, // NeoPXL8 output 3
 58    { 16,  1, 0.21  }, // NeoPXL8 output 4
 59    { 16,  1, 0.21  }, // NeoPXL8 output 5
 60    { 21, 10, 0.143 }, // NeoPXL8 output 6
 61    // NeoPXL8 output 7 is normally reserved for ground splats
 62    // You CAN add an eighth drip here, but then will not get splats
 63  };
 64  
 65  #define N_DRIPS   (sizeof drip / sizeof drip[0])
 66  int               longestStrand = (N_DRIPS < 8) ? N_DRIPS : 0;
 67  Adafruit_NeoPXL8 *pixels;
 68  
 69  void setup() {
 70    Serial.begin(9600);
 71    randomSeed(analogRead(A0) + analogRead(A5));
 72  
 73    for(int i=0; i<N_DRIPS; i++) {
 74      drip[i].mode              = MODE_IDLE; // Start all drips in idle mode
 75      drip[i].eventStartUsec    = 0;
 76      drip[i].eventDurationUsec = random(500000, 2500000); // Initial idle 0.5-2.5 sec
 77      drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0;
 78      drip[i].splatStartUsec    = 0;
 79      drip[i].splatDurationUsec = 0;
 80      if(drip[i].length > longestStrand) longestStrand = drip[i].length;
 81    }
 82  
 83    pixels = new Adafruit_NeoPXL8(longestStrand, pins, NEO_GRB);
 84    pixels->begin();
 85  }
 86  
 87  void loop() {
 88    uint32_t t = micros(); // Current time, in microseconds
 89  
 90    float x; // multipurpose interim result
 91    pixels->clear();
 92  
 93    for(int i=0; i<N_DRIPS; i++) {
 94      uint32_t dtUsec = t - drip[i].eventStartUsec; // Elapsed time, in microseconds, since start of current event
 95      float    dtReal = (float)dtUsec / 1000000.0;  // Elapsed time, in seconds
 96  
 97      // Handle transitions between drip states (oozing, dribbling, dripping, etc.)
 98      if(dtUsec >= drip[i].eventDurationUsec) {              // Are we past end of current event?
 99        drip[i].eventStartUsec += drip[i].eventDurationUsec; // Yes, next event starts here
100        dtUsec                 -= drip[i].eventDurationUsec; // We're already this far into next event
101        dtReal                  = (float)dtUsec / 1000000.0;
102        switch(drip[i].mode) { // Current mode...about to switch to next mode...
103          case MODE_IDLE:
104            drip[i].mode              = MODE_OOZING; // Idle to oozing transition
105            drip[i].eventDurationUsec = random(800000, 1200000); // 0.8 to 1.2 sec ooze
106            drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0;
107            break;
108          case MODE_OOZING:
109            if(drip[i].dribblePixel) { // If dribblePixel is nonzero...
110              drip[i].mode              = MODE_DRIBBLING_1; // Oozing to dribbling transition
111              drip[i].pos               = (float)drip[i].dribblePixel;
112              drip[i].eventDurationUsec = 250000 + drip[i].dribblePixel * random(30000, 40000);
113              drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0;
114            } else { // No dribblePixel...
115              drip[i].pos               = (float)drip[i].dribblePixel; // Oozing to dripping transition
116              drip[i].mode              = MODE_DRIPPING;
117              drip[i].eventDurationReal = sqrt(drip[i].height * 2.0 / G_CONST); // SCIENCE
118              drip[i].eventDurationUsec = (uint32_t)(drip[i].eventDurationReal * 1000000.0);
119            }
120            break;
121          case MODE_DRIBBLING_1:
122            drip[i].mode              = MODE_DRIBBLING_2; // Dripping 1st half to 2nd half transition
123            drip[i].eventDurationUsec = drip[i].eventDurationUsec * 3 / 2; // Second half is 1/3 slower
124            drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0;
125            break;
126          case MODE_DRIBBLING_2:
127            drip[i].mode              = MODE_DRIPPING; // Dribbling 2nd half to dripping transition
128            drip[i].pos               = (float)drip[i].dribblePixel;
129            drip[i].eventDurationReal = sqrt(drip[i].height * 2.0 / G_CONST); // SCIENCE
130            drip[i].eventDurationUsec = (uint32_t)(drip[i].eventDurationReal * 1000000.0);
131            break;
132          case MODE_DRIPPING:
133            drip[i].mode              = MODE_IDLE; // Dripping to idle transition
134            drip[i].eventDurationUsec = random(500000, 1200000); // Idle for 0.5 to 1.2 seconds
135            drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0;
136            drip[i].splatStartUsec    = drip[i].eventStartUsec; // Splat starts now!
137            drip[i].splatDurationUsec = random(900000, 1100000);
138            break;
139        }
140      }
141  
142      // Render drip state to NeoPixels...
143  #if ICE_BRIGHTNESS > 0
144        // Draw icycles if ICE_BRIGHTNESS is set
145        x = pow((float)ICE_BRIGHTNESS * 0.01, GAMMA);
146        for(int d=0; d<=drip[i].dribblePixel; d++) {
147          set(i, d, x);
148        }
149  #endif
150      switch(drip[i].mode) {
151        case MODE_IDLE:
152          // Do nothing
153          break;
154        case MODE_OOZING:
155          x = dtReal / drip[i].eventDurationReal; // 0.0 to 1.0 over ooze interval
156          x = sqrt(x); // Perceived area increases linearly
157  #if ICE_BRIGHTNESS > 0
158          x = ((float)ICE_BRIGHTNESS * 0.01) +
159              x * (float)(100 - ICE_BRIGHTNESS) * 0.01;
160  #endif
161          x = pow(x, GAMMA);
162          set(i, 0, x);
163          break;
164        case MODE_DRIBBLING_1:
165          // Point b moves from first to second pixel over event time
166          x = dtReal / drip[i].eventDurationReal; // 0.0 to 1.0 during move
167          x = 3 * x * x - 2 * x * x * x; // Easing function: 3*x^2-2*x^3 0.0 to 1.0
168          dripDraw(i, 0.0, x * drip[i].dribblePixel, false);
169          break;
170        case MODE_DRIBBLING_2:
171          // Point a moves from first to second pixel over event time
172          x = dtReal / drip[i].eventDurationReal; // 0.0 to 1.0 during move
173          x = 3 * x * x - 2 * x * x * x; // Easing function: 3*x^2-2*x^3 0.0 to 1.0
174          dripDraw(i, x * drip[i].dribblePixel, drip[i].dribblePixel, false);
175          break;
176        case MODE_DRIPPING:
177          x = 0.5 * G_CONST * dtReal * dtReal; // Position in meters
178          x = drip[i].dribblePixel + x / PIXEL_PITCH; // Position in pixels
179          dripDraw(i, drip[i].pos, x, true);
180          drip[i].pos = x;
181          break;
182      }
183  
184      if(N_DRIPS < 8) { // Do splats unless there's an 8th drip defined
185        dtUsec = t - drip[i].splatStartUsec; // Elapsed time, in microseconds, since start of splat
186        if(dtUsec < drip[i].splatDurationUsec) {
187          x = 1.0 - sqrt((float)dtUsec / (float)drip[i].splatDurationUsec);
188          x = pow(x, GAMMA);
189          set(7, i, x);
190        }
191      }
192    }
193  
194    pixels->show();
195  }
196  
197  // This "draws" a drip in the NeoPixel buffer...zero to peak brightness
198  // at center and back to zero. Peak brightness diminishes with length,
199  // and drawn dimmer as pixels approach strand length.
200  void dripDraw(uint8_t dNum, float a, float b, bool fade) {
201    if(a > b) { // Sort a,b inputs if needed so a<=b
202      float t = a;
203      a = b;
204      b = t;
205    }
206    // Find range of pixels to draw. If first pixel is off end of strand,
207    // nothing to draw. If last pixel is off end of strand, clip to strand length.
208    int firstPixel = (int)a;
209    if(firstPixel >= drip[dNum].length) return;
210    int lastPixel  = (int)b + 1;
211    if(lastPixel >= drip[dNum].length) lastPixel = drip[dNum].length - 1;
212  
213    float center   = (a + b) * 0.5;    // Midpoint of a-to-b
214    float range    = center - a + 1.0; // Distance from center to a, plus 1 pixel
215    for(int i=firstPixel; i<= lastPixel; i++) {
216      float x = fabs(center - (float)i); // Pixel distance from center point
217      if(x < range) {                    // Inside drip
218        x = (range - x) / range;         // 0.0 (edge) to 1.0 (center)
219        if(fade) {
220          int dLen   = drip[dNum].length - drip[dNum].dribblePixel; // Length of drip
221          if(dLen > 0) { // Scale x by 1.0 at top to 1/3 at bottom of drip
222            int dPixel = i - drip[dNum].dribblePixel; // Pixel position along drip
223            x *= 1.0 - ((float)dPixel / (float)dLen * 0.66);
224          }
225        }
226      } else {
227        x = 0.0;
228      }
229  #if ICE_BRIGHTNESS > 0
230      // Upper pixels may be partially lit for an icycle effect
231      if(i <= drip[dNum].dribblePixel) {
232        // Math because preprocessor doesn't allow float constant in #if.
233        // Optimizer will reduce the math to float constants, it's fine.
234        x = ((float)ICE_BRIGHTNESS * 0.01) +
235            x * (float)(100 - ICE_BRIGHTNESS) * 0.01;
236      }
237  #endif
238      x = pow(x, GAMMA);
239      set(dNum, i, x);
240    }
241  }
242  
243  // Set one pixel to a given brightness level (0.0 to 1.0)
244  void set(uint8_t strand, uint8_t pixel, float brightness) {
245    pixels->setPixelColor(pixel + strand * longestStrand,
246      (int)((float)dripColor[0] * brightness + 0.5),
247      (int)((float)dripColor[1] * brightness + 0.5),
248      (int)((float)dripColor[2] * brightness + 0.5));
249  }