@@ -10,6 +10,7 @@ but unfortunately the resolution is such that the pupils just look like
1010circles regardless. I'm keeping it in despite the added complexity,
1111because this WILL look great later on a bigger matrix or a TFT/OLED,
1212and this way the hard parts won't require a re-write at such time.
13+ It's a really adorable effect with enough pixels.
1314*/
1415
1516#include < Adafruit_IS31FL3741.h> // For LED driver
@@ -18,11 +19,17 @@ and this way the hard parts won't require a re-write at such time.
1819
1920#define RADIUS 3.4 // Size of pupil (3X because of downsampling later)
2021
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+
2126// Some boards have just one I2C interface, but some have more...
2227TwoWire *i2c = &Wire; // e.g. change this to &Wire1 for QT Py RP2040
2328
24- Adafruit_EyeLights_buffered glasses (true ); // Buffered + 3X canvas
25- GFXcanvas16 *canvas; // Pointer to canvas object
29+ // GLOBAL VARIABLES ---------------------
30+
31+ Adafruit_EyeLights_buffered glasses (true ); // Buffered spex + 3X canvas
32+ GFXcanvas16 *canvas; // Pointer to canvas object
2633
2734// Reading through the code, you'll see a lot of references to this "3X"
2835// space. This is referring to the glasses' optional "offscreen" drawing
@@ -31,32 +38,33 @@ GFXcanvas16 *canvas; // Pointer to canvas object
3138// antialiasing. It's why the pupils have soft edges and can make
3239// fractional-pixel motions.
3340
34- uint32_t frames = 0 ;
35- uint32_t start_time;
36-
37- #define GAMMA 2.6
38-
39- float y_pos[13 ];
40- // Initialize eye position and move/blink animation timekeeping
4141float cur_pos[2 ] = { 9.0 , 7.5 }; // Current position of eye in canvas space
4242float next_pos[2 ] = { 9.0 , 7.5 }; // Next position "
43- bool in_motion = false ; // true = eyes moving, False = eyes paused
43+ bool in_motion = false ; // true = eyes moving, false = eyes paused
4444uint8_t blink_state = 0 ; // 0, 1, 2 = unblinking, closing, opening
45- uint32_t move_start_time = 0 ;
45+ uint32_t move_start_time = 0 ; // For animation timekeeping
4646uint32_t move_duration = 0 ;
4747uint32_t blink_start_time = 0 ;
4848uint32_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;
4954
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.
5058float 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 };
5163
52- uint16_t eye_color = glasses.color565(255 , 128 , 0 );
53- // uint8_t eye_color[3] = { 255, 128, 0 }; // Amber pupils
54- uint8_t ring_open_color[3 ] = { 75 , 75 , 75 }; // Color of LED rings when eyes open
55- uint8_t ring_blink_color[3 ] = { 50 , 25 , 0 }; // Color of LED ring "eyelid" when blinking
64+ #define GAMMA 2.6 // For color correction, shouldn't need changing
5665
57- // Pre-compute color of LED ring in fully open (unblinking) state
58- uint32_t ring_open_color_packed;
5966
67+ // HELPER FUNCTIONS ---------------------
6068
6169// Crude error handler, prints message to Serial console, flashes LED
6270void err (char *str, uint8_t hz) {
@@ -86,37 +94,6 @@ uint32_t interp(uint8_t color1[3], uint8_t color2[3], float blend) {
8694 return gammify (rgb);
8795}
8896
89-
90- void setup () { // Runs once at program start...
91-
92- // Initialize hardware
93- Serial.begin (115200 );
94- if (! glasses.begin (IS3741_ADDR_DEFAULT, i2c)) err (" IS3741 not found" , 2 );
95-
96- canvas = glasses.getCanvas ();
97- if (!canvas) err (" Can't allocate canvas" , 5 );
98-
99- i2c->setClock (1000000 );
100-
101- // Configure glasses for reduced brightness, enable output
102- glasses.setLEDscaling (0xFF );
103- glasses.setGlobalCurrent (20 );
104- glasses.enable (true );
105-
106- // INITIALIZE TABLES & OTHER GLOBALS ----
107-
108- // Pre-compute the Y position of 1/2 of the LEDs in a ring, relative
109- // to the 3X canvas resolution, so ring & matrix animation can be aligned.
110- for (uint8_t i=0 ; i<13 ; i++) {
111- float angle = (float )i / 24.0 * M_PI * 2.0 ;
112- y_pos[i] = 10.0 - cos (angle) * 12.0 ;
113- }
114-
115- ring_open_color_packed = gammify (ring_open_color);
116-
117- start_time = millis ();
118- }
119-
12097// Rasterize an arbitrary ellipse into the offscreen 3X canvas, given
12198// foci point1 and point2 and with area determined by global RADIUS
12299// (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]) {
154131
155132 // Like I'm sure there's a way to rasterize this by spans rather than
156133 // all these square roots on every pixel, but for now...
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
169- if ((d1 + d2 + d) <= perimeter) {
170- canvas->drawPixel (x, y, eye_color); // Point is inside ellipse
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);
171148 }
172149 }
173150 }
174151}
175152
176- void loop () { // Repeat forever...
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 () {
177190 canvas->fillScreen (0 );
178191
179192 // The eye animation logic is a carry-over from like a billion
@@ -259,51 +272,30 @@ void loop() { // Repeat forever...
259272 // Draw the raster part of each eye...
260273 for (uint8_t e=0 ; e<2 ; e++) {
261274 // Each eye's foci are offset slightly, to fixate toward center
262- #if 0
263- float p1a[2] = { p1[0] + x_offset[e], p1[1] };
264- float p2a[2] = { p2[0] + x_offset[e], p2[1] };
265- #else
266275 float p1a[2 ], p2a[2 ];
267276 p1a[0 ] = p1[0 ] + x_offset[e];
268277 p2a[0 ] = p2[0 ] + x_offset[e];
269278 p1a[1 ] = p2a[1 ] = p1[1 ];
270- #endif
271279 // Compute bounding rectangle (in 3X space) of ellipse
272280 // (min X, min Y, max X, max Y). Like the ellipse rasterizer,
273281 // this isn't optimal, but will suffice.
274282 int bounds[4 ];
275- bounds[0 ] = max (int (min (p1a[0 ], p2a[0 ]) - RADIUS), 0 );
276- bounds[1 ] = max (int (min (p1a[1 ], p2a[1 ]) - RADIUS), 0 );
277- // bounds[1] = max(bounds[1], (int)upper);
278- bounds[2 ] = min (int (max (p1a[0 ], p2a[0 ]) + RADIUS + 1 ), 18 );
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]);
279286 bounds[3 ] = min (int (max (p1a[1 ], p2a[1 ]) + RADIUS + 1 ), 15 );
280- // bounds[2] = min(bounds[3], (int)lower);
281- bounds[0 ] = 0 ;
282- bounds[1 ] = 0 ;
283- bounds[2 ] = 18 * 3 ;
284- bounds[3 ] = 5 * 3 ;
285-
286- #if 0
287- bounds = (
288- max(int(min(p1a[0], p2a[0]) - radius), 0),
289- max(int(min(p1a[1], p2a[1]) - radius), 0, int(upper)),
290- min(int(max(p1a[0], p2a[0]) + radius + 1), 18),
291- min(int(max(p1a[1], p2a[1]) + radius + 1), 15, int(lower) + 1),
292- )
293- #endif
294287 rasterize (p1a, p2a, bounds); // Render ellipse into buffer
295288 }
296289
297- // If the eye is currently blinking, and if the top edge of the
298- // eyelid overlaps the bitmap, draw a scanline across the bitmap
299- // and update the bounds rect so the whole width of the bitmap
300- // is scaled.
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.
301292 if (blink_state and upper >= 0.0 ) {
302- canvas->fillRect (0 , 0 , canvas->width (), (int )upper + 1 , 0x0004 );
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);
303296 }
304297
305-
306- glasses.scale ();
298+ glasses.scale (); // Smooth filter 3X canvas to LED grid
307299
308300 // Matrix and rings share a few pixels. To make the rings take
309301 // precedence, they're drawn later. So blink state is revisited now...
@@ -332,12 +324,3 @@ bounds[3] = 5 * 3;
332324 elapsed = millis () - start_time;
333325 Serial.println (frames * 1000 / elapsed);
334326}
335-
336-
337- #if 0
338- # Two eye objects. The first starts at column 1 of the matrix with its
339- # pupil offset by +2 (in 3X space), second at column 11 with -2 offset.
340- # The offsets make the pupils fixate slightly (converge on a point), so
341- # the two pupils aren't always aligned the same on the pixel grid, which
342- # would be conspicuously pixel-y.
343- #endif // 0
0 commit comments