Skip to content

Commit a604198

Browse files
EyeLights blinky eyes (Arduino) WIP, not finished
1 parent bfa9bba commit a604198

1 file changed

Lines changed: 320 additions & 2 deletions

File tree

EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino

Lines changed: 320 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,351 @@
44

55
/*
66
MOVE-AND-BLINK EYES for Adafruit EyeLights (LED Glasses + Driver).
7+
8+
I'd written a very cool squash-and-stretch effect for the eye movement,
9+
but unfortunately the resolution is such that the pupils just look like
10+
circles regardless. I'm keeping it in despite the added complexity,
11+
because LED matrix densities WILL improve, and this way the code won't
12+
require a re-write at such a later time.
713
*/
814

915
#include <Adafruit_IS31FL3741.h> // For LED driver
1016

11-
Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation
17+
// CONFIGURABLES ------------------------
18+
19+
#define RADIUS 3.4 // Size of pupil (3X because of downsampling later)
20+
21+
// Some boards have just one I2C interface, but some have more...
22+
TwoWire *i2c = &Wire; // e.g. change this to &Wire1 for QT Py RP2040
23+
24+
Adafruit_EyeLights_buffered glasses(true); // Buffered + 3X canvas
25+
GFXcanvas16 *canvas; // Pointer to canvas object
26+
27+
uint32_t frames = 0;
28+
uint32_t start_time;
1229

1330
#define GAMMA 2.6
1431

32+
float y_pos[13];
33+
// Initialize eye position and move/blink animation timekeeping
34+
float cur_pos[2] = { 9.0, 7.5 }; // Current position of eye in canvas space
35+
float next_pos[2] = { 9.0, 7.5 }; // Next position "
36+
bool in_motion = false; // true = eyes moving, False = eyes paused
37+
uint8_t blink_state = 0; // 0, 1, 2 = unblinking, closing, opening
38+
uint32_t move_start_time = 0;
39+
uint32_t move_duration = 0;
40+
uint32_t blink_start_time = 0;
41+
uint32_t blink_duration = 0;
42+
43+
float x_offset[2] = { 5.0, 31.0 };
44+
45+
uint16_t eye_color = glasses.color565(255, 128, 0);
46+
//uint8_t eye_color[3] = { 255, 128, 0 }; // Amber pupils
47+
uint8_t ring_open_color[3] = { 75, 75, 75 }; // Color of LED rings when eyes open
48+
uint8_t ring_blink_color[3] = { 50, 25, 0 }; // Color of LED ring "eyelid" when blinking
49+
50+
// Pre-compute color of LED ring in fully open (unblinking) state
51+
uint32_t ring_open_color_packed;
52+
53+
1554
// Crude error handler, prints message to Serial console, flashes LED
1655
void err(char *str, uint8_t hz) {
1756
Serial.println(str);
1857
pinMode(LED_BUILTIN, OUTPUT);
1958
for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1);
2059
}
2160

61+
// Given an [R,G,B] color, apply gamma correction, return packed RGB integer.
62+
uint32_t gammify(uint8_t color[3]) {
63+
uint32_t rgb[3];
64+
for (uint8_t i=0; i<3; i++) {
65+
rgb[i] = uint32_t(pow((float)color[i] / 255.0, GAMMA) * 255 + 0.5);
66+
}
67+
return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
68+
}
69+
70+
// Given two [R,G,B] colors and a blend ratio (0.0 to 1.0), interpolate between
71+
// the two colors and return a gamma-corrected in-between color as a packed RGB
72+
// integer. No bounds clamping is performed on blend value, be nice.
73+
uint32_t interp(uint8_t color1[3], uint8_t color2[3], float blend) {
74+
float inv = 1.0 - blend; // Weighting of second color
75+
uint8_t rgb[3];
76+
for(uint8_t i=0; i<3; i++) {
77+
rgb[i] = (int)((float)color1[i] * blend + (float)color2[i] * inv);
78+
}
79+
return gammify(rgb);
80+
}
81+
82+
2283
void setup() { // Runs once at program start...
2384

2485
// Initialize hardware
2586
Serial.begin(115200);
26-
if (! glasses.begin()) err("IS3741 not found", 2);
87+
while(!Serial);
88+
Serial.println("HEY!"); yield();
89+
if (! glasses.begin(IS3741_ADDR_DEFAULT, i2c)) err("IS3741 not found", 2);
90+
91+
canvas = glasses.getCanvas();
92+
if (!canvas) err("Can't allocate canvas", 5);
93+
94+
Serial.println("A");
95+
i2c->setClock(1000000);
2796

2897
// Configure glasses for reduced brightness, enable output
2998
glasses.setLEDscaling(0xFF);
3099
glasses.setGlobalCurrent(20);
31100
glasses.enable(true);
101+
Serial.println("B"); yield();
102+
103+
// INITIALIZE TABLES & OTHER GLOBALS ----
104+
105+
// Pre-compute the Y position of 1/2 of the LEDs in a ring, relative
106+
// to the 3X canvas resolution, so ring & matrix animation can be aligned.
107+
for (uint8_t i=0; i<13; i++) {
108+
float angle = (float)i / 24.0 * M_PI * 2.0;
109+
y_pos[i] = 10.0 - cos(angle) * 12.0;
110+
}
111+
112+
ring_open_color_packed = gammify(ring_open_color);
113+
Serial.println("C"); yield();
114+
115+
start_time = millis();
116+
}
117+
118+
// Rasterize an arbitrary ellipse into the offscreen 3X canvas, given
119+
// foci point1 and point2 and with area determined by global RADIUS
120+
// (when foci are same point; a circle). Foci and radius are all
121+
// floating point values, which adds to the buttery impression. 'rect'
122+
// is a bounding rect of which pixels are likely affected. Canvas is
123+
// assumed cleared before arriving here.
124+
void rasterize(float point1[2], float point2[2], int rect[4]) {
125+
float perimeter, d;
126+
float dx = point2[0] - point1[0];
127+
float dy = point2[1] - point1[1];
128+
float d2 = dx * dx + dy * dy; // Dist between foci, squared
129+
if (d2 <= 0.0) {
130+
// Foci are in same spot - it's a circle
131+
perimeter = 2.0 * RADIUS;
132+
d = 0.0;
133+
} else {
134+
// Foci are separated - it's an ellipse.
135+
d = sqrt(d2); // Distance between foci
136+
float c = d * 0.5; // Center-to-foci distance
137+
// This is an utterly brute-force way of ellipse-filling based on
138+
// the "two nails and a string" metaphor...we have the foci points
139+
// and just need the string length (triangle perimeter) to yield
140+
// an ellipse with area equal to a circle of 'radius'.
141+
// c^2 = a^2 - b^2 <- ellipse formula
142+
// a = r^2 / b <- substitute
143+
// c^2 = (r^2 / b)^2 - b^2
144+
// b = sqrt(((c^2) + sqrt((c^4) + 4 * r^4)) / 2) <- solve for b
145+
float c2 = c * c;
146+
float b2 = (c2 + sqrt((c2 * c2) + 4 * (RADIUS * RADIUS * RADIUS * RADIUS))) * 0.5;
147+
// By my math, perimeter SHOULD be...
148+
// perimeter = d + 2 * sqrt(b2 + c2);
149+
// ...but for whatever reason, working approach here is really...
150+
perimeter = d + 2 * sqrt(b2);
151+
}
152+
153+
// Like I'm sure there's a way to rasterize this by spans rather than
154+
// all these square roots on every pixel, but for now...
155+
for (int y=rect[1]; y<rect[3]; y++) { // For each row...
156+
float dy1 = (float)y - point1[1]; // Y distance from pixel to first point
157+
float dy2 = (float)y - point2[1]; // " to second
158+
dy1 *= dy1; // Y1^2
159+
dy2 *= dy2; // Y2^2
160+
for (int x=rect[0]; x<rect[2]; x++) { // For each column...
161+
float dx1 = (float)x - point1[0]; // X distance from pixel to first point
162+
float dx2 = (float)x - point2[0]; // " to second
163+
float d1 = sqrt(dx1 * dx1 + dy1); // 2D distance to first point
164+
float d2 = sqrt(dx2 * dx2 + dy2); // " to second
165+
if ((d1 + d2 + d) <= perimeter) {
166+
canvas->drawPixel(x, y, eye_color); // Point is inside ellipse
167+
}
168+
}
169+
}
32170
}
33171

34172
void loop() { // Repeat forever...
173+
yield();
174+
Serial.println("1"); yield();
175+
canvas->fillScreen(0);
176+
177+
178+
// The eye animation logic is a carry-over from like a billion
179+
// prior eye projects, so this might be comment-light.
180+
uint32_t now = micros(); // 'Snapshot' the time once per frame
181+
182+
float upper, lower, ratio;
183+
184+
// Blink logic
185+
uint32_t elapsed = now - blink_start_time; // Time since start of blink event
186+
if (elapsed > blink_duration) { // All done with event?
187+
blink_start_time = now; // A new one starts right now
188+
elapsed = 0;
189+
blink_state++; // Cycle closing/opening/paused
190+
if (blink_state == 1) { // Starting new blink...
191+
blink_duration = random(60000, 120000);
192+
} else if (blink_state == 2) { // Switching closing to opening...
193+
blink_duration *= 2; // Opens at half the speed
194+
} else { // Switching to pause in blink
195+
blink_state = 0;
196+
blink_duration = random(500000, 4000000);
197+
}
198+
}
199+
if (blink_state) { // If currently in a blink...
200+
float ratio = (float)elapsed / (float)blink_duration; // 0.0-1.0 as it closes
201+
if (blink_state == 2) ratio = 1.0 - ratio; // 1.0-0.0 as it opens
202+
upper = ratio * 15.0 - 4.0; // Upper eyelid pos. in 3X space
203+
lower = 23.0 - ratio * 8.0; // Lower eyelid pos. in 3X space
204+
}
205+
Serial.println("2"); yield();
206+
207+
// Eye movement logic. Two points, 'p1' and 'p2', are the foci of an
208+
// ellipse. p1 moves from current to next position a little faster
209+
// than p2, creating a "squash and stretch" effect (frame rate and
210+
// resolution permitting). When motion is stopped, the two points
211+
// are at the same position.
212+
float p1[2], p2[2];
213+
elapsed = now - move_start_time; // Time since start of move event
214+
if (in_motion) { // Currently moving?
215+
if (elapsed > move_duration) { // If end of motion reached,
216+
in_motion = false; // Stop motion and
217+
memcpy(&p1, &next_pos, sizeof next_pos); // set everything to new position
218+
memcpy(&p2, &next_pos, sizeof next_pos);
219+
memcpy(&cur_pos, &next_pos, sizeof next_pos);
220+
move_duration = random(500000, 1500000); // Wait this long
221+
} else { // Still moving
222+
// Determine p1, p2 position in time
223+
float delta[2];
224+
delta[0] = next_pos[0] - cur_pos[0];
225+
delta[1] = next_pos[1] - cur_pos[1];
226+
ratio = (float)elapsed / (float)move_duration;
227+
if (ratio < 0.7) { // First 70% of move time
228+
// p1 is in motion
229+
// Easing function: 3*e^2-2*e^3 0.0 to 1.0
230+
float e = ratio / 0.7; // 0.0 to 1.0
231+
e = 3 * e * e - 2 * e * e * e;
232+
p1[0] = cur_pos[0] + delta[0] * e;
233+
p1[1] = cur_pos[1] + delta[1] * e;
234+
} else { // Last 30% of move time
235+
memcpy(&p1, &next_pos, sizeof next_pos); // p1 has reached end position
236+
}
237+
if (ratio > 0.2) { // Last 80% of move time
238+
// p2 is in motion
239+
float e = (ratio - 0.2) / 0.8; // 0.0 to 1.0
240+
e = 3 * e * e - 2 * e * e * e; // Easing func.
241+
p2[0] = cur_pos[0] + delta[0] * e;
242+
p2[1] = cur_pos[1] + delta[1] * e;
243+
} else { // First 20% of move time
244+
memcpy(&p2, &cur_pos, sizeof cur_pos); // p2 waits at start position
245+
}
246+
}
247+
} else { // Eye is stopped
248+
memcpy(&p1, &cur_pos, sizeof cur_pos); // Both foci at current eye position
249+
memcpy(&p2, &cur_pos, sizeof cur_pos);
250+
if (elapsed > move_duration) { // Pause time expired?
251+
in_motion = true; // Start up new motion!
252+
move_start_time = now;
253+
move_duration = random(150000, 250000);
254+
float angle = (float)random(1000) / 1000.0 * M_PI * 2.0;
255+
float dist = (float)random(750) / 100.0;
256+
next_pos[0] = 9.0 + cos(angle) * dist;
257+
next_pos[1] = 7.5 + sin(angle) * dist * 0.8;
258+
}
259+
}
260+
Serial.println("3"); yield();
261+
262+
// Draw the raster part of each eye...
263+
for (uint8_t e=0; e<2; e++) {
264+
// Each eye's foci are offset slightly, to fixate toward center
265+
#if 0
266+
float p1a[2] = { p1[0] + x_offset[e], p1[1] };
267+
float p2a[2] = { p2[0] + x_offset[e], p2[1] };
268+
#else
269+
float p1a[2], p2a[2];
270+
p1a[0] = p1[0] + x_offset[e];
271+
p2a[0] = p2[0] + x_offset[e];
272+
p1a[1] = p2a[1] = p1[1];
273+
#endif
274+
// Compute bounding rectangle (in 3X space) of ellipse
275+
// (min X, min Y, max X, max Y). Like the ellipse rasterizer,
276+
// this isn't optimal, but will suffice.
277+
int bounds[4];
278+
bounds[0] = max(int(min(p1a[0], p2a[0]) - RADIUS), 0);
279+
bounds[1] = max(int(min(p1a[1], p2a[1]) - RADIUS), 0);
280+
// bounds[1] = max(bounds[1], (int)upper);
281+
bounds[2] = min(int(max(p1a[0], p2a[0]) + RADIUS + 1), 18);
282+
bounds[3] = min(int(max(p1a[1], p2a[1]) + RADIUS + 1), 15);
283+
// bounds[2] = min(bounds[3], (int)lower);
284+
bounds[0] = 0;
285+
bounds[1] = 0;
286+
bounds[2] = 18 * 3;
287+
bounds[3] = 5 * 3;
288+
289+
#if 0
290+
bounds = (
291+
max(int(min(p1a[0], p2a[0]) - radius), 0),
292+
max(int(min(p1a[1], p2a[1]) - radius), 0, int(upper)),
293+
min(int(max(p1a[0], p2a[0]) + radius + 1), 18),
294+
min(int(max(p1a[1], p2a[1]) + radius + 1), 15, int(lower) + 1),
295+
)
296+
#endif
297+
rasterize(p1a, p2a, bounds); // Render ellipse into buffer
298+
}
299+
300+
Serial.println("4"); yield();
301+
302+
// If the eye is currently blinking, and if the top edge of the
303+
// eyelid overlaps the bitmap, draw a scanline across the bitmap
304+
// and update the bounds rect so the whole width of the bitmap
305+
// is scaled.
306+
if (blink_state and upper >= 0.0) {
307+
canvas->fillRect(0, 0, canvas->width(), (int)upper + 1, 0x0004);
308+
}
309+
310+
Serial.println("5"); yield();
311+
312+
glasses.scale();
313+
Serial.println("6"); yield();
314+
315+
// Matrix and rings share a few pixels. To make the rings take
316+
// precedence, they're drawn later. So blink state is revisited now...
317+
if (blink_state) { // In mid-blink?
318+
for (uint8_t i=0; i<13; i++) { // Half an LED ring, top-to-bottom...
319+
float a = min(max(y_pos[i] - upper + 1.0, 0.0), 3.0);
320+
float b = min(max(lower - y_pos[i] + 1.0, 0.0), 3.0);
321+
ratio = a * b / 9.0; // Proximity of LED to eyelid edges
322+
uint32_t packed = interp(ring_open_color, ring_blink_color, ratio);
323+
glasses.left_ring.setPixelColor(i, packed);
324+
glasses.right_ring.setPixelColor(i, packed);
325+
if ((i > 0) && (i < 12)) {
326+
uint8_t j = 24 - i; // Mirror half-ring to other side
327+
glasses.left_ring.setPixelColor(j, packed);
328+
glasses.right_ring.setPixelColor(j, packed);
329+
}
330+
}
331+
} else {
332+
glasses.left_ring.fill(ring_open_color_packed);
333+
glasses.right_ring.fill(ring_open_color_packed);
334+
}
335+
336+
Serial.println("7"); yield();
337+
338+
35339
glasses.show();
340+
Serial.println("8"); yield();
341+
342+
frames += 1;
343+
elapsed = millis() - start_time;
344+
Serial.println(frames * 1000 / elapsed);
36345
}
346+
347+
348+
#if 0
349+
# Two eye objects. The first starts at column 1 of the matrix with its
350+
# pupil offset by +2 (in 3X space), second at column 11 with -2 offset.
351+
# The offsets make the pupils fixate slightly (converge on a point), so
352+
# the two pupils aren't always aligned the same on the pixel grid, which
353+
# would be conspicuously pixel-y.
354+
#endif // 0

0 commit comments

Comments
 (0)