More eye WIP, tweak movement and rasterization

This commit is contained in:
Phillip Burgess 2021-10-14 09:14:13 -07:00
parent a6041985cb
commit 5b0cf73c18
2 changed files with 40 additions and 49 deletions

View file

@ -8,8 +8,8 @@ MOVE-AND-BLINK EYES for Adafruit EyeLights (LED Glasses + Driver).
I'd written a very cool squash-and-stretch effect for the eye movement,
but unfortunately the resolution is such that the pupils just look like
circles regardless. I'm keeping it in despite the added complexity,
because LED matrix densities WILL improve, and this way the code won't
require a re-write at such a later time.
because this WILL look great later on a bigger matrix or a TFT/OLED,
and this way the hard parts won't require a re-write at such time.
*/
#include <Adafruit_IS31FL3741.h> // For LED driver
@ -24,6 +24,13 @@ TwoWire *i2c = &Wire; // e.g. change this to &Wire1 for QT Py RP2040
Adafruit_EyeLights_buffered glasses(true); // Buffered + 3X canvas
GFXcanvas16 *canvas; // Pointer to canvas object
// Reading through the code, you'll see a lot of references to this "3X"
// space. This is referring to the glasses' optional "offscreen" drawing
// canvas that's 3 times the resolution of the LED matrix (i.e. 15 pixels
// tall instead of 5), which gets scaled down to provide some degree of
// antialiasing. It's why the pupils have soft edges and can make
// fractional-pixel motions.
uint32_t frames = 0;
uint32_t start_time;
@ -84,21 +91,17 @@ void setup() { // Runs once at program start...
// Initialize hardware
Serial.begin(115200);
while(!Serial);
Serial.println("HEY!"); yield();
if (! glasses.begin(IS3741_ADDR_DEFAULT, i2c)) err("IS3741 not found", 2);
canvas = glasses.getCanvas();
if (!canvas) err("Can't allocate canvas", 5);
Serial.println("A");
i2c->setClock(1000000);
// Configure glasses for reduced brightness, enable output
glasses.setLEDscaling(0xFF);
glasses.setGlobalCurrent(20);
glasses.enable(true);
Serial.println("B"); yield();
// INITIALIZE TABLES & OTHER GLOBALS ----
@ -110,7 +113,6 @@ Serial.println("B"); yield();
}
ring_open_color_packed = gammify(ring_open_color);
Serial.println("C"); yield();
start_time = millis();
}
@ -152,16 +154,18 @@ void rasterize(float point1[2], float point2[2], int rect[4]) {
// Like I'm sure there's a way to rasterize this by spans rather than
// all these square roots on every pixel, but for now...
for (int y=rect[1]; y<rect[3]; y++) { // For each row...
float dy1 = (float)y - point1[1]; // Y distance from pixel to first point
float dy2 = (float)y - point2[1]; // " to second
dy1 *= dy1; // Y1^2
dy2 *= dy2; // Y2^2
for (int x=rect[0]; x<rect[2]; x++) { // For each column...
float dx1 = (float)x - point1[0]; // X distance from pixel to first point
float dx2 = (float)x - point2[0]; // " to second
float d1 = sqrt(dx1 * dx1 + dy1); // 2D distance to first point
float d2 = sqrt(dx2 * dx2 + dy2); // " to second
for (int y=rect[1]; y<rect[3]; y++) { // For each row...
float y5 = (float)y + 0.5; // Pixel center
float dy1 = y5 - point1[1]; // Y distance from pixel to first point
float dy2 = y5 - point2[1]; // " to second
dy1 *= dy1; // Y1^2
dy2 *= dy2; // Y2^2
for (int x=rect[0]; x<rect[2]; x++) { // For each column...
float x5 = (float)x + 0.5; // Pixel center
float dx1 = x5 - point1[0]; // X distance from pixel to first point
float dx2 = x5 - point2[0]; // " to second
float d1 = sqrt(dx1 * dx1 + dy1); // 2D distance to first point
float d2 = sqrt(dx2 * dx2 + dy2); // " to second
if ((d1 + d2 + d) <= perimeter) {
canvas->drawPixel(x, y, eye_color); // Point is inside ellipse
}
@ -170,11 +174,8 @@ void rasterize(float point1[2], float point2[2], int rect[4]) {
}
void loop() { // Repeat forever...
yield();
Serial.println("1"); yield();
canvas->fillScreen(0);
// The eye animation logic is a carry-over from like a billion
// prior eye projects, so this might be comment-light.
uint32_t now = micros(); // 'Snapshot' the time once per frame
@ -202,7 +203,6 @@ Serial.println("1"); yield();
upper = ratio * 15.0 - 4.0; // Upper eyelid pos. in 3X space
lower = 23.0 - ratio * 8.0; // Lower eyelid pos. in 3X space
}
Serial.println("2"); yield();
// Eye movement logic. Two points, 'p1' and 'p2', are the foci of an
// ellipse. p1 moves from current to next position a little faster
@ -224,23 +224,21 @@ Serial.println("2"); yield();
delta[0] = next_pos[0] - cur_pos[0];
delta[1] = next_pos[1] - cur_pos[1];
ratio = (float)elapsed / (float)move_duration;
if (ratio < 0.7) { // First 70% of move time
// p1 is in motion
if (ratio < 0.6) { // First 60% of move time, p1 is in motion
// Easing function: 3*e^2-2*e^3 0.0 to 1.0
float e = ratio / 0.7; // 0.0 to 1.0
float e = ratio / 0.6; // 0.0 to 1.0
e = 3 * e * e - 2 * e * e * e;
p1[0] = cur_pos[0] + delta[0] * e;
p1[1] = cur_pos[1] + delta[1] * e;
} else { // Last 30% of move time
} else { // Last 40% of move time
memcpy(&p1, &next_pos, sizeof next_pos); // p1 has reached end position
}
if (ratio > 0.2) { // Last 80% of move time
// p2 is in motion
float e = (ratio - 0.2) / 0.8; // 0.0 to 1.0
if (ratio > 0.3) { // Last 70% of move time, p2 is in motion
float e = (ratio - 0.3) / 0.7; // 0.0 to 1.0
e = 3 * e * e - 2 * e * e * e; // Easing func.
p2[0] = cur_pos[0] + delta[0] * e;
p2[1] = cur_pos[1] + delta[1] * e;
} else { // First 20% of move time
} else { // First 30% of move time
memcpy(&p2, &cur_pos, sizeof cur_pos); // p2 waits at start position
}
}
@ -257,7 +255,6 @@ Serial.println("2"); yield();
next_pos[1] = 7.5 + sin(angle) * dist * 0.8;
}
}
Serial.println("3"); yield();
// Draw the raster part of each eye...
for (uint8_t e=0; e<2; e++) {
@ -297,8 +294,6 @@ bounds[3] = 5 * 3;
rasterize(p1a, p2a, bounds); // Render ellipse into buffer
}
Serial.println("4"); yield();
// If the eye is currently blinking, and if the top edge of the
// eyelid overlaps the bitmap, draw a scanline across the bitmap
// and update the bounds rect so the whole width of the bitmap
@ -307,10 +302,8 @@ Serial.println("4"); yield();
canvas->fillRect(0, 0, canvas->width(), (int)upper + 1, 0x0004);
}
Serial.println("5"); yield();
glasses.scale();
Serial.println("6"); yield();
// Matrix and rings share a few pixels. To make the rings take
// precedence, they're drawn later. So blink state is revisited now...
@ -333,11 +326,7 @@ Serial.println("6"); yield();
glasses.right_ring.fill(ring_open_color_packed);
}
Serial.println("7"); yield();
glasses.show();
Serial.println("8"); yield();
frames += 1;
elapsed = millis() - start_time;

View file

@ -33,7 +33,7 @@ radius = 3.4 # Size of pupil (3X because of downsampling later)
# Reading through the code, you'll see a lot of references to this "3X"
# space. What it's referring to is a bitmap that's 3 times the resolution
# of the LED matrix (e.g. 15 pixels tall instead of 5), which gets scaled
# of the LED matrix (i.e. 15 pixels tall instead of 5), which gets scaled
# down to provide some degree of antialiasing. It's why the pupils have
# soft edges and can make fractional-pixel motions.
# Because of the way the downsampling is done, the eyelid edge when drawn
@ -127,13 +127,15 @@ def rasterize(data, point1, point2, rect):
# Like I'm sure there's a way to rasterize this by spans rather than
# all these square roots on every pixel, but for now...
for y in range(rect[1], rect[3]): # For each row...
dy1 = y - point1[1] # Y distance from pixel to first point
dy2 = y - point2[1] # " to second
y5 = y + 0.5 # Pixel center
dy1 = y5 - point1[1] # Y distance from pixel to first point
dy2 = y5 - point2[1] # " to second
dy1 *= dy1 # Y1^2
dy2 *= dy2 # Y2^2
for x in range(rect[0], rect[2]): # For each column...
dx1 = x - point1[0] # X distance from pixel to first point
dx2 = x - point2[0] # " to second
x5 = x + 0.5 # Pixel center
dx1 = x5 - point1[0] # X distance from pixel to first point
dx2 = x5 - point2[0] # " to second
d1 = (dx1 * dx1 + dy1) ** 0.5 # 2D distance to first point
d2 = (dx2 * dx2 + dy2) ** 0.5 # " to second
if (d1 + d2 + d) <= perimeter:
@ -255,20 +257,20 @@ while True:
# Determine p1, p2 position in time
delta = (next_pos[0] - cur_pos[0], next_pos[1] - cur_pos[1])
ratio = elapsed / move_duration
if ratio < 0.7: # First 70% of move time
if ratio < 0.6: # First 60% of move time
# p1 is in motion
# Easing function: 3*e^2-2*e^3 0.0 to 1.0
e = ratio / 0.7 # 0.0 to 1.0
e = ratio / 0.6 # 0.0 to 1.0
e = 3 * e * e - 2 * e * e * e
p1 = (cur_pos[0] + delta[0] * e, cur_pos[1] + delta[1] * e)
else: # Last 30% of move time
else: # Last 40% of move time
p1 = next_pos # p1 has reached end position
if ratio > 0.2: # Last 80% of move time
if ratio > 0.3: # Last 60% of move time
# p2 is in motion
e = (ratio - 0.2) / 0.8 # 0.0 to 1.0
e = (ratio - 0.3) / 0.7 # 0.0 to 1.0
e = 3 * e * e - 2 * e * e * e # Easing func.
p2 = (cur_pos[0] + delta[0] * e, cur_pos[1] + delta[1] * e)
else: # First 20% of move time
else: # First 40% of move time
p2 = cur_pos # p2 waits at start position
else: # Eye is stopped
p1 = p2 = cur_pos # Both foci at current eye position