From bfa9bba4d9c666045c43883629cafa89650b5b26 Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Wed, 13 Oct 2021 17:46:15 -0700 Subject: [PATCH 1/4] Add EyeLights blinky eyes (CircuitPython) --- .../.ledglasses_nrf52840.test.only | 0 .../EyeLights_Blinky_Eyes.ino | 36 ++ .../code.py | 336 ++++++++++++++++++ 3 files changed, 372 insertions(+) create mode 100644 EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/.ledglasses_nrf52840.test.only create mode 100644 EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino create mode 100755 EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/.ledglasses_nrf52840.test.only b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/.ledglasses_nrf52840.test.only new file mode 100644 index 000000000..e69de29bb diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino new file mode 100644 index 000000000..bd71d5b31 --- /dev/null +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +/* +MOVE-AND-BLINK EYES for Adafruit EyeLights (LED Glasses + Driver). +*/ + +#include // For LED driver + +Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation + +#define GAMMA 2.6 + +// Crude error handler, prints message to Serial console, flashes LED +void err(char *str, uint8_t hz) { + Serial.println(str); + pinMode(LED_BUILTIN, OUTPUT); + for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1); +} + +void setup() { // Runs once at program start... + + // Initialize hardware + Serial.begin(115200); + if (! glasses.begin()) err("IS3741 not found", 2); + + // Configure glasses for reduced brightness, enable output + glasses.setLEDscaling(0xFF); + glasses.setGlobalCurrent(20); + glasses.enable(true); +} + +void loop() { // Repeat forever... + glasses.show(); +} diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py new file mode 100755 index 000000000..dd17a9eeb --- /dev/null +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +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 and frame rate are such that the pupils +just look like circles regardless. I'm keeping it in despite the added +complexity, because CircuitPython devices WILL get faster, LED matrix +densities WILL improve, and this way the code won't require a re-write +at such a later time. +""" + +import math +import random +import time +from supervisor import reload +import board +from busio import I2C +import adafruit_is31fl3741 +from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses + + +# CONFIGURABLES ------------------------ + +eye_color = (255, 128, 0) # Amber pupils +ring_open_color = (75, 75, 75) # Color of LED rings when eyes open +ring_blink_color = (50, 25, 0) # Color of LED ring "eyelid" when blinking + +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 +# 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 +# across the eye will always be the same hue as the pupils, it can't be +# set independently like the ring blink color. + +gamma = 2.6 # For color adjustment. Leave as-is. + + +# CLASSES & FUNCTIONS ------------------ + + +class Eye: + """Holds per-eye positional data; each covers a different area of the + overall LED matrix.""" + + def __init__(self, left, xoff): + self.left = left # Leftmost column on LED matrix + self.x_offset = xoff # Horizontal offset (3X space) to fixate + + def smooth(self, data, rect): + """Scale bitmap (in 'data') to LED array, with smooth 1:3 + downsampling. 'rect' is a 4-tuple rect of which pixels get + filtered (anything outside is cleared to 0), saves a few cycles.""" + # Quantize bounds rect from 3X space to LED matrix space. + rect = ( + rect[0] // 3, # Left + rect[1] // 3, # Top + (rect[2] + 2) // 3, # Right + (rect[3] + 2) // 3, # Bottom + ) + for y in range(rect[1]): # Erase rows above top + for x in range(6): + glasses.pixel(self.left + x, y, 0) + for y in range(rect[1], rect[3]): # Each row, top to bottom... + pixel_sum = bytearray(6) # Initialize row of pixel sums to 0 + for y1 in range(3): # 3 rows of bitmap... + row = data[y * 3 + y1] # Bitmap data for current row + for x in range(rect[0], rect[2]): # Column, left to right + x3 = x * 3 + # Accumulate 3 pixels of bitmap into pixel_sum + pixel_sum[x] += row[x3] + row[x3 + 1] + row[x3 + 2] + # 'pixel_sum' will now contain values from 0-9, indicating the + # number of set pixels in the corresponding section of the 3X + # bitmap. 'colormap' expands the sum to 24-bit RGB space. + for x in range(rect[0]): # Erase any columns to left + glasses.pixel(self.left + x, y, 0) + for x in range(rect[0], rect[2]): # Column, left to right + glasses.pixel(self.left + x, y, colormap[pixel_sum[x]]) + for x in range(rect[2], 6): # Erase columns to right + glasses.pixel(self.left + x, y, 0) + for y in range(rect[3], 5): # Erase rows below bottom + for x in range(6): + glasses.pixel(self.left + x, y, 0) + + +# pylint: disable=too-many-locals +def rasterize(data, point1, point2, rect): + """Rasterize an arbitrary ellipse into the 'data' bitmap (3X pixel + space), given foci point1 and point2 and with area determined by global + 'radius' (when foci are same point; a circle). Foci and radius are all + floating point values, which adds to the buttery impression. 'rect' is + a 4-tuple rect of which pixels are likely affected. Data is assumed 0 + before arriving here; no clearing is performed.""" + + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + d2 = dx * dx + dy * dy # Dist between foci, squared + if d2 <= 0: + # Foci are in same spot - it's a circle + perimeter = 2 * radius + d = 0 + else: + # Foci are separated - it's an ellipse. + d = d2 ** 0.5 # Distance between foci + c = d * 0.5 # Center-to-foci distance + # This is an utterly brute-force way of ellipse-filling based on + # the "two nails and a string" metaphor...we have the foci points + # and just need the string length (triangle perimeter) to yield + # an ellipse with area equal to a circle of 'radius'. + # c^2 = a^2 - b^2 <- ellipse formula + # a = r^2 / b <- substitute + # c^2 = (r^2 / b)^2 - b^2 + # b = sqrt(((c^2) + sqrt((c^4) + 4 * r^4)) / 2) <- solve for b + b2 = ((c ** 2) + (((c ** 4) + 4 * (radius ** 4)) ** 0.5)) * 0.5 + # By my math, perimeter SHOULD be... + # perimeter = d + 2 * ((b2 + (c ** 2)) ** 0.5) + # ...but for whatever reason, working approach here is really... + perimeter = d + 2 * (b2 ** 0.5) + + # 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 + 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 + d1 = (dx1 * dx1 + dy1) ** 0.5 # 2D distance to first point + d2 = (dx2 * dx2 + dy2) ** 0.5 # " to second + if (d1 + d2 + d) <= perimeter: + data[y][x] = 1 # Point is inside ellipse + + +def gammify(color): + """Given an (R,G,B) color tuple, apply gamma correction and return + a packed 24-bit RGB integer.""" + rgb = [int(((color[x] / 255) ** gamma) * 255 + 0.5) for x in range(3)] + return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2] + + +def interp(color1, color2, blend): + """Given two (R,G,B) color tuples and a blend ratio (0.0 to 1.0), + interpolate between the two colors and return a gamma-corrected + in-between color as a packed 24-bit RGB integer. No bounds clamping + is performed on blend value, be nice.""" + inv = 1.0 - blend # Weighting of second color + return gammify([color1[x] * blend + color2[x] * inv for x in range(3)]) + + +# HARDWARE SETUP ----------------------- + +# Manually declare I2C (not board.I2C() directly) to access 1 MHz speed... +i2c = I2C(board.SCL, board.SDA, frequency=1000000) + +# Initialize the IS31 LED driver, buffered for smoother animation +glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER) +glasses.show() # Clear any residue on startup +glasses.global_current = 20 # Just middlin' bright, please + + +# INITIALIZE TABLES & OTHER GLOBALS ---- + +# This table is for mapping 3x3 averaged bitmap values (0-9) to +# RGB colors. Avoids a lot of shift-and-or on every pixel. +colormap = [] +for n in range(10): + colormap.append(gammify([n / 9 * eye_color[x] for x in range(3)])) + +# Pre-compute the Y position of 1/2 of the LEDs in a ring, relative +# to the 3X bitmap resolution, so ring & matrix animation can be aligned. +y_pos = [] +for n in range(13): + angle = n / 24 * math.pi * 2 + y_pos.append(10 - math.cos(angle) * 12) + +# Pre-compute color of LED ring in fully open (unblinking) state +ring_open_color_packed = gammify(ring_open_color) + +# A single pre-computed scanline of "eyelid edge during blink" can be +# stuffed into the 3X raster as needed, avoids setting pixels manually. +eyelid = ( + b"\x01\x01\x00\x01\x01\x00\x01\x01\x00" b"\x01\x01\x00\x01\x01\x00\x01\x01\x00" +) # 2/3 of pixels set + +# Initialize eye position and move/blink animation timekeeping +cur_pos = next_pos = (9, 7.5) # Current, next eye position in 3X space +in_motion = False # True = eyes moving, False = eyes paused +blink_state = 0 # 0, 1, 2 = unblinking, closing, opening +move_start_time = move_duration = blink_start_time = blink_duration = 0 + +# Two eye objects. The first starts at column 1 of the matrix with its +# pupil offset by +2 (in 3X space), second at column 11 with -2 offset. +# The offsets make the pupils fixate slightly (converge on a point), so +# the two pupils aren't always aligned the same on the pixel grid, which +# would be conspicuously pixel-y. +eyes = [Eye(1, 2), Eye(11, -2)] + +frames, start_time = 0, time.monotonic() # For frames/second calculation + + +# MAIN LOOP ---------------------------- + +while True: + # The try/except here is because VERY INFREQUENTLY the I2C bus will + # encounter an error when accessing the LED driver, whether from bumping + # around the wires or sometimes an I2C device just gets wedged. To more + # robustly handle the latter, the code will restart if that happens. + try: + + # The eye animation logic is a carry-over from like a billion + # prior eye projects, so this might be comment-light. + now = time.monotonic() # 'Snapshot' the time once per frame + + # Blink logic + elapsed = now - blink_start_time # Time since start of blink event + if elapsed > blink_duration: # All done with event? + blink_start_time = now # A new one starts right now + elapsed = 0 + blink_state += 1 # Cycle closing/opening/paused + if blink_state == 1: # Starting new blink... + blink_duration = random.uniform(0.06, 0.12) + elif blink_state == 2: # Switching closing to opening... + blink_duration *= 2 # Opens at half the speed + else: # Switching to pause in blink + blink_state = 0 + blink_duration = random.uniform(0.5, 4) + if blink_state: # If currently in a blink... + ratio = elapsed / blink_duration # 0.0-1.0 as it closes + if blink_state == 2: + ratio = 1.0 - ratio # 1.0-0.0 as it opens + upper = ratio * 15 - 4 # Upper eyelid pos. in 3X space + lower = 23 - ratio * 8 # Lower eyelid pos. in 3X space + + # Eye movement logic. Two points, 'p1' and 'p2', are the foci of an + # ellipse. p1 moves from current to next position a little faster + # than p2, creating a "squash and stretch" effect (frame rate and + # resolution permitting). When motion is stopped, the two points + # are at the same position. + elapsed = now - move_start_time # Time since start of move event + if in_motion: # Currently moving? + if elapsed > move_duration: # If end of motion reached, + in_motion = False # Stop motion and + p1 = p2 = cur_pos = next_pos # Set to new position + move_duration = random.uniform(0.5, 1.5) # Wait this long + else: # Still moving + # 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 + # 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 = 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 + p1 = next_pos # p1 has reached end position + if ratio > 0.2: # Last 80% of move time + # p2 is in motion + e = (ratio - 0.2) / 0.8 # 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 + p2 = cur_pos # p2 waits at start position + else: # Eye is stopped + p1 = p2 = cur_pos # Both foci at current eye position + if elapsed > move_duration: # Pause time expired? + in_motion = True # Start up new motion! + move_start_time = now + move_duration = random.uniform(0.15, 0.25) + angle = random.uniform(0, math.pi * 2) + dist = random.uniform(0, 7.5) + next_pos = ( + 9 + math.cos(angle) * dist, + 7.5 + math.sin(angle) * dist * 0.8, + ) + + # Draw the raster part of each eye... + for eye in eyes: + # Allocate/clear the 3X bitmap buffer + bitmap = [bytearray(6 * 3) for _ in range(5 * 3)] + # Each eye's foci are offset slightly, to fixate toward center + p1a = (p1[0] + eye.x_offset, p1[1]) + p2a = (p2[0] + eye.x_offset, p2[1]) + # Compute bounding rectangle (in 3X space) of ellipse + # (min X, min Y, max X, max Y). Like the ellipse rasterizer, + # this isn't optimal, but will suffice. + bounds = ( + max(int(min(p1a[0], p2a[0]) - radius), 0), + max(int(min(p1a[1], p2a[1]) - radius), 0, int(upper)), + min(int(max(p1a[0], p2a[0]) + radius + 1), 18), + min(int(max(p1a[1], p2a[1]) + radius + 1), 15, int(lower) + 1), + ) + rasterize(bitmap, p1a, p2a, bounds) # Render ellipse into buffer + # 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 + # is scaled. + if blink_state and upper >= 0: + bitmap[int(upper)] = eyelid + bounds = (0, int(upper), 18, bounds[3]) + eye.smooth(bitmap, bounds) # 1:3 downsampling for eye + + # Matrix and rings share a few pixels. To make the rings take + # precedence, they're drawn later. So blink state is revisited now... + if blink_state: # In mid-blink? + for i in range(13): # Half an LED ring, top-to-bottom... + a = min(max(y_pos[i] - upper + 1, 0), 3) + b = min(max(lower - y_pos[i] + 1, 0), 3) + ratio = a * b / 9 # Proximity of LED to eyelid edges + packed = interp(ring_open_color, ring_blink_color, ratio) + glasses.left_ring[i] = glasses.right_ring[i] = packed + if 0 < i < 12: + i = 24 - i # Mirror half-ring to other side + glasses.left_ring[i] = glasses.right_ring[i] = packed + else: + glasses.left_ring.fill(ring_open_color_packed) + glasses.right_ring.fill(ring_open_color_packed) + + glasses.show() # Buffered mode MUST use show() to refresh matrix + + except OSError: # See "try" notes above regarding rare I2C errors. + print("Restarting") + reload() + + frames += 1 + elapsed = time.monotonic() - start_time + print(frames / elapsed) From a6041985cbc3349e7774d2e01f13d28c7bf4d672 Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Wed, 13 Oct 2021 22:22:28 -0700 Subject: [PATCH 2/4] EyeLights blinky eyes (Arduino) WIP, not finished --- .../EyeLights_Blinky_Eyes.ino | 322 +++++++++++++++++- 1 file changed, 320 insertions(+), 2 deletions(-) diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino index bd71d5b31..c8778340e 100644 --- a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino @@ -4,14 +4,53 @@ /* 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. */ #include // For LED driver -Adafruit_EyeLights_buffered glasses; // Buffered for smooth animation +// CONFIGURABLES ------------------------ + +#define RADIUS 3.4 // Size of pupil (3X because of downsampling later) + +// Some boards have just one I2C interface, but some have more... +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 + +uint32_t frames = 0; +uint32_t start_time; #define GAMMA 2.6 +float y_pos[13]; +// Initialize eye position and move/blink animation timekeeping +float cur_pos[2] = { 9.0, 7.5 }; // Current position of eye in canvas space +float next_pos[2] = { 9.0, 7.5 }; // Next position " +bool in_motion = false; // true = eyes moving, False = eyes paused +uint8_t blink_state = 0; // 0, 1, 2 = unblinking, closing, opening +uint32_t move_start_time = 0; +uint32_t move_duration = 0; +uint32_t blink_start_time = 0; +uint32_t blink_duration = 0; + +float x_offset[2] = { 5.0, 31.0 }; + +uint16_t eye_color = glasses.color565(255, 128, 0); +//uint8_t eye_color[3] = { 255, 128, 0 }; // Amber pupils +uint8_t ring_open_color[3] = { 75, 75, 75 }; // Color of LED rings when eyes open +uint8_t ring_blink_color[3] = { 50, 25, 0 }; // Color of LED ring "eyelid" when blinking + +// Pre-compute color of LED ring in fully open (unblinking) state +uint32_t ring_open_color_packed; + + // Crude error handler, prints message to Serial console, flashes LED void err(char *str, uint8_t hz) { Serial.println(str); @@ -19,18 +58,297 @@ void err(char *str, uint8_t hz) { for (;;) digitalWrite(LED_BUILTIN, (millis() * hz / 500) & 1); } +// Given an [R,G,B] color, apply gamma correction, return packed RGB integer. +uint32_t gammify(uint8_t color[3]) { + uint32_t rgb[3]; + for (uint8_t i=0; i<3; i++) { + rgb[i] = uint32_t(pow((float)color[i] / 255.0, GAMMA) * 255 + 0.5); + } + return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; +} + +// Given two [R,G,B] colors and a blend ratio (0.0 to 1.0), interpolate between +// the two colors and return a gamma-corrected in-between color as a packed RGB +// integer. No bounds clamping is performed on blend value, be nice. +uint32_t interp(uint8_t color1[3], uint8_t color2[3], float blend) { + float inv = 1.0 - blend; // Weighting of second color + uint8_t rgb[3]; + for(uint8_t i=0; i<3; i++) { + rgb[i] = (int)((float)color1[i] * blend + (float)color2[i] * inv); + } + return gammify(rgb); +} + + void setup() { // Runs once at program start... // Initialize hardware Serial.begin(115200); - if (! glasses.begin()) err("IS3741 not found", 2); + 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 ---- + + // Pre-compute the Y position of 1/2 of the LEDs in a ring, relative + // to the 3X canvas resolution, so ring & matrix animation can be aligned. + for (uint8_t i=0; i<13; i++) { + float angle = (float)i / 24.0 * M_PI * 2.0; + y_pos[i] = 10.0 - cos(angle) * 12.0; + } + + ring_open_color_packed = gammify(ring_open_color); +Serial.println("C"); yield(); + + start_time = millis(); +} + +// Rasterize an arbitrary ellipse into the offscreen 3X canvas, given +// foci point1 and point2 and with area determined by global RADIUS +// (when foci are same point; a circle). Foci and radius are all +// floating point values, which adds to the buttery impression. 'rect' +// is a bounding rect of which pixels are likely affected. Canvas is +// assumed cleared before arriving here. +void rasterize(float point1[2], float point2[2], int rect[4]) { + float perimeter, d; + float dx = point2[0] - point1[0]; + float dy = point2[1] - point1[1]; + float d2 = dx * dx + dy * dy; // Dist between foci, squared + if (d2 <= 0.0) { + // Foci are in same spot - it's a circle + perimeter = 2.0 * RADIUS; + d = 0.0; + } else { + // Foci are separated - it's an ellipse. + d = sqrt(d2); // Distance between foci + float c = d * 0.5; // Center-to-foci distance + // This is an utterly brute-force way of ellipse-filling based on + // the "two nails and a string" metaphor...we have the foci points + // and just need the string length (triangle perimeter) to yield + // an ellipse with area equal to a circle of 'radius'. + // c^2 = a^2 - b^2 <- ellipse formula + // a = r^2 / b <- substitute + // c^2 = (r^2 / b)^2 - b^2 + // b = sqrt(((c^2) + sqrt((c^4) + 4 * r^4)) / 2) <- solve for b + float c2 = c * c; + float b2 = (c2 + sqrt((c2 * c2) + 4 * (RADIUS * RADIUS * RADIUS * RADIUS))) * 0.5; + // By my math, perimeter SHOULD be... + // perimeter = d + 2 * sqrt(b2 + c2); + // ...but for whatever reason, working approach here is really... + perimeter = d + 2 * sqrt(b2); + } + + // 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]; ydrawPixel(x, y, eye_color); // Point is inside ellipse + } + } + } } 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 + + float upper, lower, ratio; + + // Blink logic + uint32_t elapsed = now - blink_start_time; // Time since start of blink event + if (elapsed > blink_duration) { // All done with event? + blink_start_time = now; // A new one starts right now + elapsed = 0; + blink_state++; // Cycle closing/opening/paused + if (blink_state == 1) { // Starting new blink... + blink_duration = random(60000, 120000); + } else if (blink_state == 2) { // Switching closing to opening... + blink_duration *= 2; // Opens at half the speed + } else { // Switching to pause in blink + blink_state = 0; + blink_duration = random(500000, 4000000); + } + } + if (blink_state) { // If currently in a blink... + float ratio = (float)elapsed / (float)blink_duration; // 0.0-1.0 as it closes + if (blink_state == 2) ratio = 1.0 - ratio; // 1.0-0.0 as it opens + 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 + // than p2, creating a "squash and stretch" effect (frame rate and + // resolution permitting). When motion is stopped, the two points + // are at the same position. + float p1[2], p2[2]; + elapsed = now - move_start_time; // Time since start of move event + if (in_motion) { // Currently moving? + if (elapsed > move_duration) { // If end of motion reached, + in_motion = false; // Stop motion and + memcpy(&p1, &next_pos, sizeof next_pos); // set everything to new position + memcpy(&p2, &next_pos, sizeof next_pos); + memcpy(&cur_pos, &next_pos, sizeof next_pos); + move_duration = random(500000, 1500000); // Wait this long + } else { // Still moving + // Determine p1, p2 position in time + float delta[2]; + 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 + // Easing function: 3*e^2-2*e^3 0.0 to 1.0 + float e = ratio / 0.7; // 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 + 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 + 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 + memcpy(&p2, &cur_pos, sizeof cur_pos); // p2 waits at start position + } + } + } else { // Eye is stopped + memcpy(&p1, &cur_pos, sizeof cur_pos); // Both foci at current eye position + memcpy(&p2, &cur_pos, sizeof cur_pos); + if (elapsed > move_duration) { // Pause time expired? + in_motion = true; // Start up new motion! + move_start_time = now; + move_duration = random(150000, 250000); + float angle = (float)random(1000) / 1000.0 * M_PI * 2.0; + float dist = (float)random(750) / 100.0; + next_pos[0] = 9.0 + cos(angle) * dist; + 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++) { + // Each eye's foci are offset slightly, to fixate toward center +#if 0 + float p1a[2] = { p1[0] + x_offset[e], p1[1] }; + float p2a[2] = { p2[0] + x_offset[e], p2[1] }; +#else + float p1a[2], p2a[2]; + p1a[0] = p1[0] + x_offset[e]; + p2a[0] = p2[0] + x_offset[e]; + p1a[1] = p2a[1] = p1[1]; +#endif + // Compute bounding rectangle (in 3X space) of ellipse + // (min X, min Y, max X, max Y). Like the ellipse rasterizer, + // this isn't optimal, but will suffice. + int bounds[4]; + bounds[0] = max(int(min(p1a[0], p2a[0]) - RADIUS), 0); + bounds[1] = max(int(min(p1a[1], p2a[1]) - RADIUS), 0); +// bounds[1] = max(bounds[1], (int)upper); + bounds[2] = min(int(max(p1a[0], p2a[0]) + RADIUS + 1), 18); + bounds[3] = min(int(max(p1a[1], p2a[1]) + RADIUS + 1), 15); +// bounds[2] = min(bounds[3], (int)lower); +bounds[0] = 0; +bounds[1] = 0; +bounds[2] = 18 * 3; +bounds[3] = 5 * 3; + +#if 0 + bounds = ( + max(int(min(p1a[0], p2a[0]) - radius), 0), + max(int(min(p1a[1], p2a[1]) - radius), 0, int(upper)), + min(int(max(p1a[0], p2a[0]) + radius + 1), 18), + min(int(max(p1a[1], p2a[1]) + radius + 1), 15, int(lower) + 1), + ) +#endif + 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 + // is scaled. + if (blink_state and upper >= 0.0) { + 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... + if (blink_state) { // In mid-blink? + for (uint8_t i=0; i<13; i++) { // Half an LED ring, top-to-bottom... + float a = min(max(y_pos[i] - upper + 1.0, 0.0), 3.0); + float b = min(max(lower - y_pos[i] + 1.0, 0.0), 3.0); + ratio = a * b / 9.0; // Proximity of LED to eyelid edges + uint32_t packed = interp(ring_open_color, ring_blink_color, ratio); + glasses.left_ring.setPixelColor(i, packed); + glasses.right_ring.setPixelColor(i, packed); + if ((i > 0) && (i < 12)) { + uint8_t j = 24 - i; // Mirror half-ring to other side + glasses.left_ring.setPixelColor(j, packed); + glasses.right_ring.setPixelColor(j, packed); + } + } + } else { + glasses.left_ring.fill(ring_open_color_packed); + glasses.right_ring.fill(ring_open_color_packed); + } + +Serial.println("7"); yield(); + + glasses.show(); +Serial.println("8"); yield(); + + frames += 1; + elapsed = millis() - start_time; + Serial.println(frames * 1000 / elapsed); } + + +#if 0 +# Two eye objects. The first starts at column 1 of the matrix with its +# pupil offset by +2 (in 3X space), second at column 11 with -2 offset. +# The offsets make the pupils fixate slightly (converge on a point), so +# the two pupils aren't always aligned the same on the pixel grid, which +# would be conspicuously pixel-y. +#endif // 0 From 5b0cf73c1812974f8999cabafa918719c1864f5d Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Thu, 14 Oct 2021 09:14:13 -0700 Subject: [PATCH 3/4] More eye WIP, tweak movement and rasterization --- .../EyeLights_Blinky_Eyes.ino | 65 ++++++++----------- .../code.py | 24 +++---- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino index c8778340e..632f6e470 100644 --- a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino @@ -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 // 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]; ydrawPixel(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; diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py index dd17a9eeb..96cc4322e 100755 --- a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py @@ -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 From c0034b0869bd596da4dcd3ef58a9874ae576341e Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Thu, 14 Oct 2021 10:25:06 -0700 Subject: [PATCH 4/4] EyeLights Arduino blink eyes done, I think --- .../EyeLights_Blinky_Eyes.ino | 187 ++++++++---------- .../code.py | 2 +- 2 files changed, 86 insertions(+), 103 deletions(-) diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino index 632f6e470..f145a0ad0 100644 --- a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes.ino @@ -10,6 +10,7 @@ but unfortunately the resolution is such that the pupils just look like circles regardless. I'm keeping it in despite the added complexity, 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. +It's a really adorable effect with enough pixels. */ #include // For LED driver @@ -18,11 +19,17 @@ and this way the hard parts won't require a re-write at such time. #define RADIUS 3.4 // Size of pupil (3X because of downsampling later) +uint8_t eye_color[3] = { 255, 128, 0 }; // Amber pupils +uint8_t ring_open_color[3] = { 75, 75, 75 }; // Color of LED rings when eyes open +uint8_t ring_blink_color[3] = { 50, 25, 0 }; // Color of LED ring "eyelid" when blinking + // Some boards have just one I2C interface, but some have more... 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 +// GLOBAL VARIABLES --------------------- + +Adafruit_EyeLights_buffered glasses(true); // Buffered spex + 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 @@ -31,32 +38,33 @@ GFXcanvas16 *canvas; // Pointer to canvas object // antialiasing. It's why the pupils have soft edges and can make // fractional-pixel motions. -uint32_t frames = 0; -uint32_t start_time; - -#define GAMMA 2.6 - -float y_pos[13]; -// Initialize eye position and move/blink animation timekeeping float cur_pos[2] = { 9.0, 7.5 }; // Current position of eye in canvas space float next_pos[2] = { 9.0, 7.5 }; // Next position " -bool in_motion = false; // true = eyes moving, False = eyes paused +bool in_motion = false; // true = eyes moving, false = eyes paused uint8_t blink_state = 0; // 0, 1, 2 = unblinking, closing, opening -uint32_t move_start_time = 0; +uint32_t move_start_time = 0; // For animation timekeeping uint32_t move_duration = 0; uint32_t blink_start_time = 0; uint32_t blink_duration = 0; +float y_pos[13]; // Coords of LED ring pixels in canvas space +uint32_t ring_open_color_packed; // ring_open_color[] as packed RGB integer +uint16_t eye_color565; // eye_color[] as a GFX packed '565' value +uint32_t frames = 0; // For frames-per-second calculation +uint32_t start_time; +// These offsets position each pupil on the canvas grid and make them +// fixate slightly (converge on a point) so they're not always aligned +// the same on the pixel grid, which would be conspicuously pixel-y. float x_offset[2] = { 5.0, 31.0 }; +// These help perform x-axis clipping on the rasterized ellipses, +// so they don't "bleed" outside the rings and require erasing. +int box_x_min[2] = { 3, 33 }; +int box_x_max[2] = { 21, 51 }; -uint16_t eye_color = glasses.color565(255, 128, 0); -//uint8_t eye_color[3] = { 255, 128, 0 }; // Amber pupils -uint8_t ring_open_color[3] = { 75, 75, 75 }; // Color of LED rings when eyes open -uint8_t ring_blink_color[3] = { 50, 25, 0 }; // Color of LED ring "eyelid" when blinking +#define GAMMA 2.6 // For color correction, shouldn't need changing -// Pre-compute color of LED ring in fully open (unblinking) state -uint32_t ring_open_color_packed; +// HELPER FUNCTIONS --------------------- // Crude error handler, prints message to Serial console, flashes LED void err(char *str, uint8_t hz) { @@ -86,37 +94,6 @@ uint32_t interp(uint8_t color1[3], uint8_t color2[3], float blend) { return gammify(rgb); } - -void setup() { // Runs once at program start... - - // Initialize hardware - Serial.begin(115200); - if (! glasses.begin(IS3741_ADDR_DEFAULT, i2c)) err("IS3741 not found", 2); - - canvas = glasses.getCanvas(); - if (!canvas) err("Can't allocate canvas", 5); - - i2c->setClock(1000000); - - // Configure glasses for reduced brightness, enable output - glasses.setLEDscaling(0xFF); - glasses.setGlobalCurrent(20); - glasses.enable(true); - - // INITIALIZE TABLES & OTHER GLOBALS ---- - - // Pre-compute the Y position of 1/2 of the LEDs in a ring, relative - // to the 3X canvas resolution, so ring & matrix animation can be aligned. - for (uint8_t i=0; i<13; i++) { - float angle = (float)i / 24.0 * M_PI * 2.0; - y_pos[i] = 10.0 - cos(angle) * 12.0; - } - - ring_open_color_packed = gammify(ring_open_color); - - start_time = millis(); -} - // Rasterize an arbitrary ellipse into the offscreen 3X canvas, given // foci point1 and point2 and with area determined by global RADIUS // (when foci are same point; a circle). Foci and radius are all @@ -154,26 +131,62 @@ 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]; ydrawPixel(x, y, eye_color); // Point is inside ellipse + for (int y=rect[1]; ydrawPixel(x, y, eye_color565); } } } } -void loop() { // Repeat forever... + +// ONE-TIME INITIALIZATION -------------- + +void setup() { + // Initialize hardware + Serial.begin(115200); + if (! glasses.begin(IS3741_ADDR_DEFAULT, i2c)) err("IS3741 not found", 2); + + canvas = glasses.getCanvas(); + if (!canvas) err("Can't allocate canvas", 5); + + i2c->setClock(1000000); // 1 MHz I2C for extra butteriness + + // Configure glasses for reduced brightness, enable output + glasses.setLEDscaling(0xFF); + glasses.setGlobalCurrent(20); + glasses.enable(true); + + // INITIALIZE TABLES & OTHER GLOBALS ---- + + // Pre-compute the Y position of 1/2 of the LEDs in a ring, relative + // to the 3X canvas resolution, so ring & matrix animation can be aligned. + for (uint8_t i=0; i<13; i++) { + float angle = (float)i / 24.0 * M_PI * 2.0; + y_pos[i] = 10.0 - cos(angle) * 12.0; + } + + // Convert some colors from [R,G,B] (easier to specify) to packed integers + ring_open_color_packed = gammify(ring_open_color); + eye_color565 = glasses.color565(eye_color[0], eye_color[1], eye_color[2]); + + start_time = millis(); // For frames-per-second math +} + +// MAIN LOOP ---------------------------- + +void loop() { canvas->fillScreen(0); // The eye animation logic is a carry-over from like a billion @@ -259,51 +272,30 @@ void loop() { // Repeat forever... // Draw the raster part of each eye... for (uint8_t e=0; e<2; e++) { // Each eye's foci are offset slightly, to fixate toward center -#if 0 - float p1a[2] = { p1[0] + x_offset[e], p1[1] }; - float p2a[2] = { p2[0] + x_offset[e], p2[1] }; -#else float p1a[2], p2a[2]; p1a[0] = p1[0] + x_offset[e]; p2a[0] = p2[0] + x_offset[e]; p1a[1] = p2a[1] = p1[1]; -#endif // Compute bounding rectangle (in 3X space) of ellipse // (min X, min Y, max X, max Y). Like the ellipse rasterizer, // this isn't optimal, but will suffice. int bounds[4]; - bounds[0] = max(int(min(p1a[0], p2a[0]) - RADIUS), 0); - bounds[1] = max(int(min(p1a[1], p2a[1]) - RADIUS), 0); -// bounds[1] = max(bounds[1], (int)upper); - bounds[2] = min(int(max(p1a[0], p2a[0]) + RADIUS + 1), 18); + bounds[0] = max(int(min(p1a[0], p2a[0]) - RADIUS), box_x_min[e]); + bounds[1] = max(max(int(min(p1a[1], p2a[1]) - RADIUS), 0), (int)upper); + bounds[2] = min(int(max(p1a[0], p2a[0]) + RADIUS + 1), box_x_max[e]); bounds[3] = min(int(max(p1a[1], p2a[1]) + RADIUS + 1), 15); -// bounds[2] = min(bounds[3], (int)lower); -bounds[0] = 0; -bounds[1] = 0; -bounds[2] = 18 * 3; -bounds[3] = 5 * 3; - -#if 0 - bounds = ( - max(int(min(p1a[0], p2a[0]) - radius), 0), - max(int(min(p1a[1], p2a[1]) - radius), 0, int(upper)), - min(int(max(p1a[0], p2a[0]) + radius + 1), 18), - min(int(max(p1a[1], p2a[1]) + radius + 1), 15, int(lower) + 1), - ) -#endif rasterize(p1a, p2a, bounds); // Render ellipse into buffer } - // 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 - // is scaled. + // If the eye is currently blinking, and if the top edge of the eyelid + // overlaps the bitmap, draw lines across the bitmap as if eyelids. if (blink_state and upper >= 0.0) { - canvas->fillRect(0, 0, canvas->width(), (int)upper + 1, 0x0004); + int iu = (int)upper; + canvas->drawLine(box_x_min[0], iu, box_x_max[0] - 1, iu, eye_color565); + canvas->drawLine(box_x_min[1], iu, box_x_max[1] - 1, iu, eye_color565); } - - glasses.scale(); + glasses.scale(); // Smooth filter 3X canvas to LED grid // Matrix and rings share a few pixels. To make the rings take // precedence, they're drawn later. So blink state is revisited now... @@ -332,12 +324,3 @@ bounds[3] = 5 * 3; elapsed = millis() - start_time; Serial.println(frames * 1000 / elapsed); } - - -#if 0 -# Two eye objects. The first starts at column 1 of the matrix with its -# pupil offset by +2 (in 3X space), second at column 11 with -2 offset. -# The offsets make the pupils fixate slightly (converge on a point), so -# the two pupils aren't always aligned the same on the pixel grid, which -# would be conspicuously pixel-y. -#endif // 0 diff --git a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py index 96cc4322e..1925ef2bb 100755 --- a/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py +++ b/EyeLights_Blinky_Eyes/EyeLights_Blinky_Eyes_CircuitPython/code.py @@ -10,7 +10,7 @@ but unfortunately the resolution and frame rate are such that the pupils just look like circles regardless. I'm keeping it in despite the added complexity, because CircuitPython devices WILL get faster, LED matrix densities WILL improve, and this way the code won't require a re-write -at such a later time. +at such a later time. It's a really adorable effect with enough pixels. """ import math