EyeLights_Blinky_Eyes.ino
1 // SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 /* 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 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. 13 It's a really adorable effect with enough pixels. 14 */ 15 16 #include <Adafruit_IS31FL3741.h> // For LED driver 17 18 // CONFIGURABLES ------------------------ 19 20 #define RADIUS 3.4 // Size of pupil (3X because of downsampling later) 21 22 uint8_t eye_color[3] = { 255, 128, 0 }; // Amber pupils 23 uint8_t ring_open_color[3] = { 75, 75, 75 }; // Color of LED rings when eyes open 24 uint8_t ring_blink_color[3] = { 50, 25, 0 }; // Color of LED ring "eyelid" when blinking 25 26 // Some boards have just one I2C interface, but some have more... 27 TwoWire *i2c = &Wire; // e.g. change this to &Wire1 for QT Py RP2040 28 29 // GLOBAL VARIABLES --------------------- 30 31 Adafruit_EyeLights_buffered glasses(true); // Buffered spex + 3X canvas 32 GFXcanvas16 *canvas; // Pointer to canvas object 33 34 // Reading through the code, you'll see a lot of references to this "3X" 35 // space. This is referring to the glasses' optional "offscreen" drawing 36 // canvas that's 3 times the resolution of the LED matrix (i.e. 15 pixels 37 // tall instead of 5), which gets scaled down to provide some degree of 38 // antialiasing. It's why the pupils have soft edges and can make 39 // fractional-pixel motions. 40 41 float cur_pos[2] = { 9.0, 7.5 }; // Current position of eye in canvas space 42 float next_pos[2] = { 9.0, 7.5 }; // Next position " 43 bool in_motion = false; // true = eyes moving, false = eyes paused 44 uint8_t blink_state = 0; // 0, 1, 2 = unblinking, closing, opening 45 uint32_t move_start_time = 0; // For animation timekeeping 46 uint32_t move_duration = 0; 47 uint32_t blink_start_time = 0; 48 uint32_t blink_duration = 0; 49 float y_pos[13]; // Coords of LED ring pixels in canvas space 50 uint32_t ring_open_color_packed; // ring_open_color[] as packed RGB integer 51 uint16_t eye_color565; // eye_color[] as a GFX packed '565' value 52 uint32_t frames = 0; // For frames-per-second calculation 53 uint32_t start_time; 54 55 // These offsets position each pupil on the canvas grid and make them 56 // fixate slightly (converge on a point) so they're not always aligned 57 // the same on the pixel grid, which would be conspicuously pixel-y. 58 float x_offset[2] = { 5.0, 31.0 }; 59 // These help perform x-axis clipping on the rasterized ellipses, 60 // so they don't "bleed" outside the rings and require erasing. 61 int box_x_min[2] = { 3, 33 }; 62 int box_x_max[2] = { 21, 51 }; 63 64 #define GAMMA 2.6 // For color correction, shouldn't need changing 65 66 67 // HELPER FUNCTIONS --------------------- 68 69 // Crude error handler, prints message to Serial console, flashes LED 70 void err(char *str, uint8_t hz) { 71 Serial.println(str); 72 pinMode(LED_BUILTIN, OUTPUT); 73 for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1); 74 } 75 76 // Given an [R,G,B] color, apply gamma correction, return packed RGB integer. 77 uint32_t gammify(uint8_t color[3]) { 78 uint32_t rgb[3]; 79 for (uint8_t i=0; i<3; i++) { 80 rgb[i] = uint32_t(pow((float)color[i] / 255.0, GAMMA) * 255 + 0.5); 81 } 82 return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; 83 } 84 85 // Given two [R,G,B] colors and a blend ratio (0.0 to 1.0), interpolate between 86 // the two colors and return a gamma-corrected in-between color as a packed RGB 87 // integer. No bounds clamping is performed on blend value, be nice. 88 uint32_t interp(uint8_t color1[3], uint8_t color2[3], float blend) { 89 float inv = 1.0 - blend; // Weighting of second color 90 uint8_t rgb[3]; 91 for(uint8_t i=0; i<3; i++) { 92 rgb[i] = (int)((float)color1[i] * blend + (float)color2[i] * inv); 93 } 94 return gammify(rgb); 95 } 96 97 // Rasterize an arbitrary ellipse into the offscreen 3X canvas, given 98 // foci point1 and point2 and with area determined by global RADIUS 99 // (when foci are same point; a circle). Foci and radius are all 100 // floating point values, which adds to the buttery impression. 'rect' 101 // is a bounding rect of which pixels are likely affected. Canvas is 102 // assumed cleared before arriving here. 103 void rasterize(float point1[2], float point2[2], int rect[4]) { 104 float perimeter, d; 105 float dx = point2[0] - point1[0]; 106 float dy = point2[1] - point1[1]; 107 float d2 = dx * dx + dy * dy; // Dist between foci, squared 108 if (d2 <= 0.0) { 109 // Foci are in same spot - it's a circle 110 perimeter = 2.0 * RADIUS; 111 d = 0.0; 112 } else { 113 // Foci are separated - it's an ellipse. 114 d = sqrt(d2); // Distance between foci 115 float c = d * 0.5; // Center-to-foci distance 116 // This is an utterly brute-force way of ellipse-filling based on 117 // the "two nails and a string" metaphor...we have the foci points 118 // and just need the string length (triangle perimeter) to yield 119 // an ellipse with area equal to a circle of 'radius'. 120 // c^2 = a^2 - b^2 <- ellipse formula 121 // a = r^2 / b <- substitute 122 // c^2 = (r^2 / b)^2 - b^2 123 // b = sqrt(((c^2) + sqrt((c^4) + 4 * r^4)) / 2) <- solve for b 124 float c2 = c * c; 125 float b2 = (c2 + sqrt((c2 * c2) + 4 * (RADIUS * RADIUS * RADIUS * RADIUS))) * 0.5; 126 // By my math, perimeter SHOULD be... 127 // perimeter = d + 2 * sqrt(b2 + c2); 128 // ...but for whatever reason, working approach here is really... 129 perimeter = d + 2 * sqrt(b2); 130 } 131 132 // Like I'm sure there's a way to rasterize this by spans rather than 133 // all these square roots on every pixel, but for now... 134 for (int y=rect[1]; y<rect[3]; y++) { // For each row... 135 float y5 = (float)y + 0.5; // Pixel center 136 float dy1 = y5 - point1[1]; // Y distance from pixel to first point 137 float dy2 = y5 - point2[1]; // " to second 138 dy1 *= dy1; // Y1^2 139 dy2 *= dy2; // Y2^2 140 for (int x=rect[0]; x<rect[2]; x++) { // For each column... 141 float x5 = (float)x + 0.5; // Pixel center 142 float dx1 = x5 - point1[0]; // X distance from pixel to first point 143 float dx2 = x5 - point2[0]; // " to second 144 float d1 = sqrt(dx1 * dx1 + dy1); // 2D distance to first point 145 float d2 = sqrt(dx2 * dx2 + dy2); // " to second 146 if ((d1 + d2 + d) <= perimeter) { // Point inside ellipse? 147 canvas->drawPixel(x, y, eye_color565); 148 } 149 } 150 } 151 } 152 153 154 // ONE-TIME INITIALIZATION -------------- 155 156 void setup() { 157 // Initialize hardware 158 Serial.begin(115200); 159 if (! glasses.begin(IS3741_ADDR_DEFAULT, i2c)) err("IS3741 not found", 2); 160 161 canvas = glasses.getCanvas(); 162 if (!canvas) err("Can't allocate canvas", 5); 163 164 i2c->setClock(1000000); // 1 MHz I2C for extra butteriness 165 166 // Configure glasses for reduced brightness, enable output 167 glasses.setLEDscaling(0xFF); 168 glasses.setGlobalCurrent(20); 169 glasses.enable(true); 170 171 // INITIALIZE TABLES & OTHER GLOBALS ---- 172 173 // Pre-compute the Y position of 1/2 of the LEDs in a ring, relative 174 // to the 3X canvas resolution, so ring & matrix animation can be aligned. 175 for (uint8_t i=0; i<13; i++) { 176 float angle = (float)i / 24.0 * M_PI * 2.0; 177 y_pos[i] = 10.0 - cos(angle) * 12.0; 178 } 179 180 // Convert some colors from [R,G,B] (easier to specify) to packed integers 181 ring_open_color_packed = gammify(ring_open_color); 182 eye_color565 = glasses.color565(eye_color[0], eye_color[1], eye_color[2]); 183 184 start_time = millis(); // For frames-per-second math 185 } 186 187 // MAIN LOOP ---------------------------- 188 189 void loop() { 190 canvas->fillScreen(0); 191 192 // The eye animation logic is a carry-over from like a billion 193 // prior eye projects, so this might be comment-light. 194 uint32_t now = micros(); // 'Snapshot' the time once per frame 195 196 float upper, lower, ratio; 197 198 // Blink logic 199 uint32_t elapsed = now - blink_start_time; // Time since start of blink event 200 if (elapsed > blink_duration) { // All done with event? 201 blink_start_time = now; // A new one starts right now 202 elapsed = 0; 203 blink_state++; // Cycle closing/opening/paused 204 if (blink_state == 1) { // Starting new blink... 205 blink_duration = random(60000, 120000); 206 } else if (blink_state == 2) { // Switching closing to opening... 207 blink_duration *= 2; // Opens at half the speed 208 } else { // Switching to pause in blink 209 blink_state = 0; 210 blink_duration = random(500000, 4000000); 211 } 212 } 213 if (blink_state) { // If currently in a blink... 214 float ratio = (float)elapsed / (float)blink_duration; // 0.0-1.0 as it closes 215 if (blink_state == 2) ratio = 1.0 - ratio; // 1.0-0.0 as it opens 216 upper = ratio * 15.0 - 4.0; // Upper eyelid pos. in 3X space 217 lower = 23.0 - ratio * 8.0; // Lower eyelid pos. in 3X space 218 } 219 220 // Eye movement logic. Two points, 'p1' and 'p2', are the foci of an 221 // ellipse. p1 moves from current to next position a little faster 222 // than p2, creating a "squash and stretch" effect (frame rate and 223 // resolution permitting). When motion is stopped, the two points 224 // are at the same position. 225 float p1[2], p2[2]; 226 elapsed = now - move_start_time; // Time since start of move event 227 if (in_motion) { // Currently moving? 228 if (elapsed > move_duration) { // If end of motion reached, 229 in_motion = false; // Stop motion and 230 memcpy(&p1, &next_pos, sizeof next_pos); // set everything to new position 231 memcpy(&p2, &next_pos, sizeof next_pos); 232 memcpy(&cur_pos, &next_pos, sizeof next_pos); 233 move_duration = random(500000, 1500000); // Wait this long 234 } else { // Still moving 235 // Determine p1, p2 position in time 236 float delta[2]; 237 delta[0] = next_pos[0] - cur_pos[0]; 238 delta[1] = next_pos[1] - cur_pos[1]; 239 ratio = (float)elapsed / (float)move_duration; 240 if (ratio < 0.6) { // First 60% of move time, p1 is in motion 241 // Easing function: 3*e^2-2*e^3 0.0 to 1.0 242 float e = ratio / 0.6; // 0.0 to 1.0 243 e = 3 * e * e - 2 * e * e * e; 244 p1[0] = cur_pos[0] + delta[0] * e; 245 p1[1] = cur_pos[1] + delta[1] * e; 246 } else { // Last 40% of move time 247 memcpy(&p1, &next_pos, sizeof next_pos); // p1 has reached end position 248 } 249 if (ratio > 0.3) { // Last 70% of move time, p2 is in motion 250 float e = (ratio - 0.3) / 0.7; // 0.0 to 1.0 251 e = 3 * e * e - 2 * e * e * e; // Easing func. 252 p2[0] = cur_pos[0] + delta[0] * e; 253 p2[1] = cur_pos[1] + delta[1] * e; 254 } else { // First 30% of move time 255 memcpy(&p2, &cur_pos, sizeof cur_pos); // p2 waits at start position 256 } 257 } 258 } else { // Eye is stopped 259 memcpy(&p1, &cur_pos, sizeof cur_pos); // Both foci at current eye position 260 memcpy(&p2, &cur_pos, sizeof cur_pos); 261 if (elapsed > move_duration) { // Pause time expired? 262 in_motion = true; // Start up new motion! 263 move_start_time = now; 264 move_duration = random(150000, 250000); 265 float angle = (float)random(1000) / 1000.0 * M_PI * 2.0; 266 float dist = (float)random(750) / 100.0; 267 next_pos[0] = 9.0 + cos(angle) * dist; 268 next_pos[1] = 7.5 + sin(angle) * dist * 0.8; 269 } 270 } 271 272 // Draw the raster part of each eye... 273 for (uint8_t e=0; e<2; e++) { 274 // Each eye's foci are offset slightly, to fixate toward center 275 float p1a[2], p2a[2]; 276 p1a[0] = p1[0] + x_offset[e]; 277 p2a[0] = p2[0] + x_offset[e]; 278 p1a[1] = p2a[1] = p1[1]; 279 // Compute bounding rectangle (in 3X space) of ellipse 280 // (min X, min Y, max X, max Y). Like the ellipse rasterizer, 281 // this isn't optimal, but will suffice. 282 int bounds[4]; 283 bounds[0] = max(int(min(p1a[0], p2a[0]) - RADIUS), box_x_min[e]); 284 bounds[1] = max(max(int(min(p1a[1], p2a[1]) - RADIUS), 0), (int)upper); 285 bounds[2] = min(int(max(p1a[0], p2a[0]) + RADIUS + 1), box_x_max[e]); 286 bounds[3] = min(int(max(p1a[1], p2a[1]) + RADIUS + 1), 15); 287 rasterize(p1a, p2a, bounds); // Render ellipse into buffer 288 } 289 290 // If the eye is currently blinking, and if the top edge of the eyelid 291 // overlaps the bitmap, draw lines across the bitmap as if eyelids. 292 if (blink_state and upper >= 0.0) { 293 int iu = (int)upper; 294 canvas->drawLine(box_x_min[0], iu, box_x_max[0] - 1, iu, eye_color565); 295 canvas->drawLine(box_x_min[1], iu, box_x_max[1] - 1, iu, eye_color565); 296 } 297 298 glasses.scale(); // Smooth filter 3X canvas to LED grid 299 300 // Matrix and rings share a few pixels. To make the rings take 301 // precedence, they're drawn later. So blink state is revisited now... 302 if (blink_state) { // In mid-blink? 303 for (uint8_t i=0; i<13; i++) { // Half an LED ring, top-to-bottom... 304 float a = min(max(y_pos[i] - upper + 1.0, 0.0), 3.0); 305 float b = min(max(lower - y_pos[i] + 1.0, 0.0), 3.0); 306 ratio = a * b / 9.0; // Proximity of LED to eyelid edges 307 uint32_t packed = interp(ring_open_color, ring_blink_color, ratio); 308 glasses.left_ring.setPixelColor(i, packed); 309 glasses.right_ring.setPixelColor(i, packed); 310 if ((i > 0) && (i < 12)) { 311 uint8_t j = 24 - i; // Mirror half-ring to other side 312 glasses.left_ring.setPixelColor(j, packed); 313 glasses.right_ring.setPixelColor(j, packed); 314 } 315 } 316 } else { 317 glasses.left_ring.fill(ring_open_color_packed); 318 glasses.right_ring.fill(ring_open_color_packed); 319 } 320 321 glasses.show(); 322 323 frames += 1; 324 elapsed = millis() - start_time; 325 Serial.println(frames * 1000 / elapsed); 326 }