|
4 | 4 |
|
5 | 5 | /* |
6 | 6 | 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. |
7 | 13 | */ |
8 | 14 |
|
9 | 15 | #include <Adafruit_IS31FL3741.h> // For LED driver |
10 | 16 |
|
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; |
12 | 29 |
|
13 | 30 | #define GAMMA 2.6 |
14 | 31 |
|
| 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 | + |
15 | 54 | // Crude error handler, prints message to Serial console, flashes LED |
16 | 55 | void err(char *str, uint8_t hz) { |
17 | 56 | Serial.println(str); |
18 | 57 | pinMode(LED_BUILTIN, OUTPUT); |
19 | 58 | for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1); |
20 | 59 | } |
21 | 60 |
|
| 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 | + |
22 | 83 | void setup() { // Runs once at program start... |
23 | 84 |
|
24 | 85 | // Initialize hardware |
25 | 86 | 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); |
27 | 96 |
|
28 | 97 | // Configure glasses for reduced brightness, enable output |
29 | 98 | glasses.setLEDscaling(0xFF); |
30 | 99 | glasses.setGlobalCurrent(20); |
31 | 100 | 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 | + } |
32 | 170 | } |
33 | 171 |
|
34 | 172 | 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 | + |
35 | 339 | glasses.show(); |
| 340 | +Serial.println("8"); yield(); |
| 341 | + |
| 342 | + frames += 1; |
| 343 | + elapsed = millis() - start_time; |
| 344 | + Serial.println(frames * 1000 / elapsed); |
36 | 345 | } |
| 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