Skip to content

Commit de3524e

Browse files
Add OozeMaster3000 Arduino sketch
1 parent beb60b5 commit de3524e

File tree

1 file changed

+216
-0
lines changed

1 file changed

+216
-0
lines changed

OozeMaster3000/OozeMaster3000.ino

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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

Comments
 (0)