Skip to content

Commit 5b0cf73

Browse files
More eye WIP, tweak movement and rasterization
1 parent a604198 commit 5b0cf73

2 files changed

Lines changed: 40 additions & 49 deletions

File tree

EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ MOVE-AND-BLINK EYES for Adafruit EyeLights (LED Glasses + Driver).
88
I'd written a very cool squash-and-stretch effect for the eye movement,
99
but unfortunately the resolution is such that the pupils just look like
1010
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.
11+
because this WILL look great later on a bigger matrix or a TFT/OLED,
12+
and this way the hard parts won't require a re-write at such time.
1313
*/
1414

1515
#include <Adafruit_IS31FL3741.h> // For LED driver
@@ -24,6 +24,13 @@ TwoWire *i2c = &Wire; // e.g. change this to &Wire1 for QT Py RP2040
2424
Adafruit_EyeLights_buffered glasses(true); // Buffered + 3X canvas
2525
GFXcanvas16 *canvas; // Pointer to canvas object
2626

27+
// Reading through the code, you'll see a lot of references to this "3X"
28+
// space. This is referring to the glasses' optional "offscreen" drawing
29+
// canvas that's 3 times the resolution of the LED matrix (i.e. 15 pixels
30+
// tall instead of 5), which gets scaled down to provide some degree of
31+
// antialiasing. It's why the pupils have soft edges and can make
32+
// fractional-pixel motions.
33+
2734
uint32_t frames = 0;
2835
uint32_t start_time;
2936

@@ -84,21 +91,17 @@ void setup() { // Runs once at program start...
8491

8592
// Initialize hardware
8693
Serial.begin(115200);
87-
while(!Serial);
88-
Serial.println("HEY!"); yield();
8994
if (! glasses.begin(IS3741_ADDR_DEFAULT, i2c)) err("IS3741 not found", 2);
9095

9196
canvas = glasses.getCanvas();
9297
if (!canvas) err("Can't allocate canvas", 5);
9398

94-
Serial.println("A");
9599
i2c->setClock(1000000);
96100

97101
// Configure glasses for reduced brightness, enable output
98102
glasses.setLEDscaling(0xFF);
99103
glasses.setGlobalCurrent(20);
100104
glasses.enable(true);
101-
Serial.println("B"); yield();
102105

103106
// INITIALIZE TABLES & OTHER GLOBALS ----
104107

@@ -110,7 +113,6 @@ Serial.println("B"); yield();
110113
}
111114

112115
ring_open_color_packed = gammify(ring_open_color);
113-
Serial.println("C"); yield();
114116

115117
start_time = millis();
116118
}
@@ -152,16 +154,18 @@ void rasterize(float point1[2], float point2[2], int rect[4]) {
152154

153155
// Like I'm sure there's a way to rasterize this by spans rather than
154156
// 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
157+
for (int y=rect[1]; y<rect[3]; y++) { // For each row...
158+
float y5 = (float)y + 0.5; // Pixel center
159+
float dy1 = y5 - point1[1]; // Y distance from pixel to first point
160+
float dy2 = y5 - point2[1]; // " to second
161+
dy1 *= dy1; // Y1^2
162+
dy2 *= dy2; // Y2^2
163+
for (int x=rect[0]; x<rect[2]; x++) { // For each column...
164+
float x5 = (float)x + 0.5; // Pixel center
165+
float dx1 = x5 - point1[0]; // X distance from pixel to first point
166+
float dx2 = x5 - point2[0]; // " to second
167+
float d1 = sqrt(dx1 * dx1 + dy1); // 2D distance to first point
168+
float d2 = sqrt(dx2 * dx2 + dy2); // " to second
165169
if ((d1 + d2 + d) <= perimeter) {
166170
canvas->drawPixel(x, y, eye_color); // Point is inside ellipse
167171
}
@@ -170,11 +174,8 @@ void rasterize(float point1[2], float point2[2], int rect[4]) {
170174
}
171175

172176
void loop() { // Repeat forever...
173-
yield();
174-
Serial.println("1"); yield();
175177
canvas->fillScreen(0);
176178

177-
178179
// The eye animation logic is a carry-over from like a billion
179180
// prior eye projects, so this might be comment-light.
180181
uint32_t now = micros(); // 'Snapshot' the time once per frame
@@ -202,7 +203,6 @@ Serial.println("1"); yield();
202203
upper = ratio * 15.0 - 4.0; // Upper eyelid pos. in 3X space
203204
lower = 23.0 - ratio * 8.0; // Lower eyelid pos. in 3X space
204205
}
205-
Serial.println("2"); yield();
206206

207207
// Eye movement logic. Two points, 'p1' and 'p2', are the foci of an
208208
// ellipse. p1 moves from current to next position a little faster
@@ -224,23 +224,21 @@ Serial.println("2"); yield();
224224
delta[0] = next_pos[0] - cur_pos[0];
225225
delta[1] = next_pos[1] - cur_pos[1];
226226
ratio = (float)elapsed / (float)move_duration;
227-
if (ratio < 0.7) { // First 70% of move time
228-
// p1 is in motion
227+
if (ratio < 0.6) { // First 60% of move time, p1 is in motion
229228
// Easing function: 3*e^2-2*e^3 0.0 to 1.0
230-
float e = ratio / 0.7; // 0.0 to 1.0
229+
float e = ratio / 0.6; // 0.0 to 1.0
231230
e = 3 * e * e - 2 * e * e * e;
232231
p1[0] = cur_pos[0] + delta[0] * e;
233232
p1[1] = cur_pos[1] + delta[1] * e;
234-
} else { // Last 30% of move time
233+
} else { // Last 40% of move time
235234
memcpy(&p1, &next_pos, sizeof next_pos); // p1 has reached end position
236235
}
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
236+
if (ratio > 0.3) { // Last 70% of move time, p2 is in motion
237+
float e = (ratio - 0.3) / 0.7; // 0.0 to 1.0
240238
e = 3 * e * e - 2 * e * e * e; // Easing func.
241239
p2[0] = cur_pos[0] + delta[0] * e;
242240
p2[1] = cur_pos[1] + delta[1] * e;
243-
} else { // First 20% of move time
241+
} else { // First 30% of move time
244242
memcpy(&p2, &cur_pos, sizeof cur_pos); // p2 waits at start position
245243
}
246244
}
@@ -257,7 +255,6 @@ Serial.println("2"); yield();
257255
next_pos[1] = 7.5 + sin(angle) * dist * 0.8;
258256
}
259257
}
260-
Serial.println("3"); yield();
261258

262259
// Draw the raster part of each eye...
263260
for (uint8_t e=0; e<2; e++) {
@@ -297,8 +294,6 @@ bounds[3] = 5 * 3;
297294
rasterize(p1a, p2a, bounds); // Render ellipse into buffer
298295
}
299296

300-
Serial.println("4"); yield();
301-
302297
// If the eye is currently blinking, and if the top edge of the
303298
// eyelid overlaps the bitmap, draw a scanline across the bitmap
304299
// and update the bounds rect so the whole width of the bitmap
@@ -307,10 +302,8 @@ Serial.println("4"); yield();
307302
canvas->fillRect(0, 0, canvas->width(), (int)upper + 1, 0x0004);
308303
}
309304

310-
Serial.println("5"); yield();
311305

312306
glasses.scale();
313-
Serial.println("6"); yield();
314307

315308
// Matrix and rings share a few pixels. To make the rings take
316309
// precedence, they're drawn later. So blink state is revisited now...
@@ -333,11 +326,7 @@ Serial.println("6"); yield();
333326
glasses.right_ring.fill(ring_open_color_packed);
334327
}
335328

336-
Serial.println("7"); yield();
337-
338-
339329
glasses.show();
340-
Serial.println("8"); yield();
341330

342331
frames += 1;
343332
elapsed = millis() - start_time;

EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
# Reading through the code, you'll see a lot of references to this "3X"
3535
# space. What it's referring to is a bitmap that's 3 times the resolution
36-
# of the LED matrix (e.g. 15 pixels tall instead of 5), which gets scaled
36+
# of the LED matrix (i.e. 15 pixels tall instead of 5), which gets scaled
3737
# down to provide some degree of antialiasing. It's why the pupils have
3838
# soft edges and can make fractional-pixel motions.
3939
# Because of the way the downsampling is done, the eyelid edge when drawn
@@ -127,13 +127,15 @@ def rasterize(data, point1, point2, rect):
127127
# Like I'm sure there's a way to rasterize this by spans rather than
128128
# all these square roots on every pixel, but for now...
129129
for y in range(rect[1], rect[3]): # For each row...
130-
dy1 = y - point1[1] # Y distance from pixel to first point
131-
dy2 = y - point2[1] # " to second
130+
y5 = y + 0.5 # Pixel center
131+
dy1 = y5 - point1[1] # Y distance from pixel to first point
132+
dy2 = y5 - point2[1] # " to second
132133
dy1 *= dy1 # Y1^2
133134
dy2 *= dy2 # Y2^2
134135
for x in range(rect[0], rect[2]): # For each column...
135-
dx1 = x - point1[0] # X distance from pixel to first point
136-
dx2 = x - point2[0] # " to second
136+
x5 = x + 0.5 # Pixel center
137+
dx1 = x5 - point1[0] # X distance from pixel to first point
138+
dx2 = x5 - point2[0] # " to second
137139
d1 = (dx1 * dx1 + dy1) ** 0.5 # 2D distance to first point
138140
d2 = (dx2 * dx2 + dy2) ** 0.5 # " to second
139141
if (d1 + d2 + d) <= perimeter:
@@ -255,20 +257,20 @@ def interp(color1, color2, blend):
255257
# Determine p1, p2 position in time
256258
delta = (next_pos[0] - cur_pos[0], next_pos[1] - cur_pos[1])
257259
ratio = elapsed / move_duration
258-
if ratio < 0.7: # First 70% of move time
260+
if ratio < 0.6: # First 60% of move time
259261
# p1 is in motion
260262
# Easing function: 3*e^2-2*e^3 0.0 to 1.0
261-
e = ratio / 0.7 # 0.0 to 1.0
263+
e = ratio / 0.6 # 0.0 to 1.0
262264
e = 3 * e * e - 2 * e * e * e
263265
p1 = (cur_pos[0] + delta[0] * e, cur_pos[1] + delta[1] * e)
264-
else: # Last 30% of move time
266+
else: # Last 40% of move time
265267
p1 = next_pos # p1 has reached end position
266-
if ratio > 0.2: # Last 80% of move time
268+
if ratio > 0.3: # Last 60% of move time
267269
# p2 is in motion
268-
e = (ratio - 0.2) / 0.8 # 0.0 to 1.0
270+
e = (ratio - 0.3) / 0.7 # 0.0 to 1.0
269271
e = 3 * e * e - 2 * e * e * e # Easing func.
270272
p2 = (cur_pos[0] + delta[0] * e, cur_pos[1] + delta[1] * e)
271-
else: # First 20% of move time
273+
else: # First 40% of move time
272274
p2 = cur_pos # p2 waits at start position
273275
else: # Eye is stopped
274276
p1 = p2 = cur_pos # Both foci at current eye position

0 commit comments

Comments
 (0)