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