acidportal

- 😈 Worlds smallest Evil Portal on a LilyGo T-QT
git clone git://git.acid.vegas/acidportal.git
Log | Files | Refs | Archive | README | LICENSE

eye_functions.ino (18557B)

      1 //
      2 // Code adapted by Bodmer as an example for TFT_eSPI, this runs on any
      3 // TFT_eSPI compatible processor so ignore the technical limitations
      4 // detailed in the original header below. Assorted changes have been
      5 // made including removal of the display mirror kludge.
      6 
      7 //--------------------------------------------------------------------------
      8 // Uncanny eyes for Adafruit 1.5" OLED (product #1431) or 1.44" TFT LCD
      9 // (#2088).  Works on PJRC Teensy 3.x and on Adafruit M0 and M4 boards
     10 // (Feather, Metro, etc.).  This code uses features specific to these
     11 // boards and WILL NOT work on normal Arduino or other boards!
     12 //
     13 // SEE FILE "config.h" FOR MOST CONFIGURATION (graphics, pins, display type,
     14 // etc).  Probably won't need to edit THIS file unless you're doing some
     15 // extremely custom modifications.
     16 //
     17 // Adafruit invests time and resources providing this open source code,
     18 // please support Adafruit and open-source hardware by purchasing products
     19 // from Adafruit!
     20 //
     21 // Written by Phil Burgess / Paint Your Dragon for Adafruit Industries.
     22 // MIT license.  SPI FIFO insight from Paul Stoffregen's ILI9341_t3 library.
     23 // Inspired by David Boccabella's (Marcwolf) hybrid servo/OLED eye concept.
     24 //--------------------------------------------------------------------------
     25 
     26 #if !defined(LIGHT_PIN) || (LIGHT_PIN < 0)
     27 // Autonomous iris motion uses a fractal behavior to similate both the major
     28 // reaction of the eye plus the continuous smaller adjustments that occur.
     29 uint16_t oldIris = (IRIS_MIN + IRIS_MAX) / 2, newIris;
     30 #endif
     31 
     32 // Initialise eyes ---------------------------------------------------------
     33 void initEyes(void)
     34 {
     35   Serial.println("Initialise eye objects");
     36 
     37   // Initialise eye objects based on eyeInfo list in config.h:
     38   for (uint8_t e = 0; e < NUM_EYES; e++) {
     39     Serial.print("Create display #"); Serial.println(e);
     40 
     41     eye[e].tft_cs      = eyeInfo[e].select;
     42     eye[e].blink.state = NOBLINK;
     43     eye[e].xposition   = eyeInfo[e].xposition;
     44 
     45     pinMode(eye[e].tft_cs, OUTPUT);
     46     digitalWrite(eye[e].tft_cs, LOW);
     47 
     48     // Also set up an individual eye-wink pin if defined:
     49     if (eyeInfo[e].wink >= 0) pinMode(eyeInfo[e].wink, INPUT_PULLUP);
     50   }
     51 
     52 #if defined(BLINK_PIN) && (BLINK_PIN >= 0)
     53   pinMode(BLINK_PIN, INPUT_PULLUP); // Ditto for all-eyes blink pin
     54 #endif
     55 }
     56 
     57 // UPDATE EYE --------------------------------------------------------------
     58 void updateEye (void)
     59 {
     60 #if defined(LIGHT_PIN) && (LIGHT_PIN >= 0) // Interactive iris
     61 
     62   int16_t v = analogRead(LIGHT_PIN);       // Raw dial/photocell reading
     63 #ifdef LIGHT_PIN_FLIP
     64   v = 1023 - v;                            // Reverse reading from sensor
     65 #endif
     66   if (v < LIGHT_MIN)      v = LIGHT_MIN; // Clamp light sensor range
     67   else if (v > LIGHT_MAX) v = LIGHT_MAX;
     68   v -= LIGHT_MIN;  // 0 to (LIGHT_MAX - LIGHT_MIN)
     69 #ifdef LIGHT_CURVE  // Apply gamma curve to sensor input?
     70   v = (int16_t)(pow((double)v / (double)(LIGHT_MAX - LIGHT_MIN),
     71                     LIGHT_CURVE) * (double)(LIGHT_MAX - LIGHT_MIN));
     72 #endif
     73   // And scale to iris range (IRIS_MAX is size at LIGHT_MIN)
     74   v = map(v, 0, (LIGHT_MAX - LIGHT_MIN), IRIS_MAX, IRIS_MIN);
     75 #ifdef IRIS_SMOOTH // Filter input (gradual motion)
     76   static int16_t irisValue = (IRIS_MIN + IRIS_MAX) / 2;
     77   irisValue = ((irisValue * 15) + v) / 16;
     78   frame(irisValue);
     79 #else // Unfiltered (immediate motion)
     80   frame(v);
     81 #endif // IRIS_SMOOTH
     82 
     83 #else  // Autonomous iris scaling -- invoke recursive function
     84 
     85   newIris = random(IRIS_MIN, IRIS_MAX);
     86   split(oldIris, newIris, micros(), 10000000L, IRIS_MAX - IRIS_MIN);
     87   oldIris = newIris;
     88 
     89 #endif // LIGHT_PIN
     90 }
     91 
     92 // EYE-RENDERING FUNCTION --------------------------------------------------
     93 void drawEye( // Renders one eye.  Inputs must be pre-clipped & valid.
     94   // Use native 32 bit variables where possible as this is 10% faster!
     95   uint8_t  e,       // Eye array index; 0 or 1 for left/right
     96   uint32_t iScale,  // Scale factor for iris
     97   uint32_t  scleraX, // First pixel X offset into sclera image
     98   uint32_t  scleraY, // First pixel Y offset into sclera image
     99   uint32_t  uT,      // Upper eyelid threshold value
    100   uint32_t  lT) {    // Lower eyelid threshold value
    101 
    102   uint32_t  screenX, screenY, scleraXsave;
    103   int32_t  irisX, irisY;
    104   uint32_t p, a;
    105   uint32_t d;
    106 
    107   uint32_t pixels = 0;
    108 
    109   // Set up raw pixel dump to entire screen.  Although such writes can wrap
    110   // around automatically from end of rect back to beginning, the region is
    111   // reset on each frame here in case of an SPI glitch.
    112   digitalWrite(eye[e].tft_cs, LOW);
    113   tft.startWrite();
    114   tft.setAddrWindow(eye[e].xposition, 0, 128, 128);
    115 
    116   // Now just issue raw 16-bit values for every pixel...
    117 
    118   scleraXsave = scleraX; // Save initial X value to reset on each line
    119   irisY       = scleraY - (SCLERA_HEIGHT - IRIS_HEIGHT) / 2;
    120 
    121   // Eyelid image is left<>right swapped for two displays
    122   uint16_t lidX = 0;
    123   uint16_t dlidX = -1;
    124   if (e) dlidX = 1;
    125   for (screenY = 0; screenY < SCREEN_HEIGHT; screenY++, scleraY++, irisY++) {
    126     scleraX = scleraXsave;
    127     irisX   = scleraXsave - (SCLERA_WIDTH - IRIS_WIDTH) / 2;
    128     if (e) lidX = 0; else lidX = SCREEN_WIDTH - 1;
    129     for (screenX = 0; screenX < SCREEN_WIDTH; screenX++, scleraX++, irisX++, lidX += dlidX) {
    130       if ((pgm_read_byte(lower + screenY * SCREEN_WIDTH + lidX) <= lT) ||
    131           (pgm_read_byte(upper + screenY * SCREEN_WIDTH + lidX) <= uT)) {              // Covered by eyelid
    132         p = 0;
    133       } else if ((irisY < 0) || (irisY >= IRIS_HEIGHT) ||
    134                  (irisX < 0) || (irisX >= IRIS_WIDTH)) { // In sclera
    135         p = pgm_read_word(sclera + scleraY * SCLERA_WIDTH + scleraX);
    136       } else {                                          // Maybe iris...
    137         p = pgm_read_word(polar + irisY * IRIS_WIDTH + irisX);                        // Polar angle/dist
    138         d = (iScale * (p & 0x7F)) / 128;                // Distance (Y)
    139         if (d < IRIS_MAP_HEIGHT) {                      // Within iris area
    140           a = (IRIS_MAP_WIDTH * (p >> 7)) / 512;        // Angle (X)
    141           p = pgm_read_word(iris + d * IRIS_MAP_WIDTH + a);                           // Pixel = iris
    142         } else {                                        // Not in iris
    143           p = pgm_read_word(sclera + scleraY * SCLERA_WIDTH + scleraX);               // Pixel = sclera
    144         }
    145       }
    146       *(&pbuffer[dmaBuf][0] + pixels++) = p >> 8 | p << 8;
    147 
    148       if (pixels >= BUFFER_SIZE) {
    149         yield();
    150 #ifdef USE_DMA
    151         tft.pushPixelsDMA(&pbuffer[dmaBuf][0], pixels);
    152         dmaBuf  = !dmaBuf;
    153 #else
    154         tft.pushPixels(pbuffer, pixels);
    155 #endif
    156         pixels = 0;
    157       }
    158     }
    159   }
    160 
    161   if (pixels) {
    162 #ifdef USE_DMA
    163     tft.pushPixelsDMA(&pbuffer[dmaBuf][0], pixels);
    164 #else
    165     tft.pushPixels(pbuffer, pixels);
    166 #endif
    167   }
    168   tft.endWrite();
    169   digitalWrite(eye[e].tft_cs, HIGH);
    170 }
    171 
    172 // EYE ANIMATION -----------------------------------------------------------
    173 
    174 const uint8_t ease[] = { // Ease in/out curve for eye movements 3*t^2-2*t^3
    175   0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,  1,  2,  2,  2,  3,   // T
    176   3,  3,  4,  4,  4,  5,  5,  6,  6,  7,  7,  8,  9,  9, 10, 10,   // h
    177   11, 12, 12, 13, 14, 15, 15, 16, 17, 18, 18, 19, 20, 21, 22, 23,   // x
    178   24, 25, 26, 27, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39,   // 2
    179   40, 41, 42, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58,   // A
    180   60, 61, 62, 63, 65, 66, 67, 69, 70, 72, 73, 74, 76, 77, 78, 80,   // l
    181   81, 83, 84, 85, 87, 88, 90, 91, 93, 94, 96, 97, 98, 100, 101, 103, // e
    182   104, 106, 107, 109, 110, 112, 113, 115, 116, 118, 119, 121, 122, 124, 125, 127, // c
    183   128, 130, 131, 133, 134, 136, 137, 139, 140, 142, 143, 145, 146, 148, 149, 151, // J
    184   152, 154, 155, 157, 158, 159, 161, 162, 164, 165, 167, 168, 170, 171, 172, 174, // a
    185   175, 177, 178, 179, 181, 182, 183, 185, 186, 188, 189, 190, 192, 193, 194, 195, // c
    186   197, 198, 199, 201, 202, 203, 204, 205, 207, 208, 209, 210, 211, 213, 214, 215, // o
    187   216, 217, 218, 219, 220, 221, 222, 224, 225, 226, 227, 228, 228, 229, 230, 231, // b
    188   232, 233, 234, 235, 236, 237, 237, 238, 239, 240, 240, 241, 242, 243, 243, 244, // s
    189   245, 245, 246, 246, 247, 248, 248, 249, 249, 250, 250, 251, 251, 251, 252, 252, // o
    190   252, 253, 253, 253, 254, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255
    191 }; // n
    192 
    193 #ifdef AUTOBLINK
    194 uint32_t timeOfLastBlink = 0L, timeToNextBlink = 0L;
    195 #endif
    196 
    197 // Process motion for a single frame of left or right eye
    198 void frame(uint16_t iScale) // Iris scale (0-1023)
    199 {
    200   static uint32_t frames   = 0; // Used in frame rate calculation
    201   static uint8_t  eyeIndex = 0; // eye[] array counter
    202   int16_t         eyeX, eyeY;
    203   uint32_t        t = micros(); // Time at start of function
    204 
    205   if (!(++frames & 255)) { // Every 256 frames...
    206     float elapsed = (millis() - startTime) / 1000.0;
    207     if (elapsed) Serial.println((uint16_t)(frames / elapsed)); // Print FPS
    208   }
    209 
    210   if (++eyeIndex >= NUM_EYES) eyeIndex = 0; // Cycle through eyes, 1 per call
    211 
    212   // X/Y movement
    213 
    214 #if defined(JOYSTICK_X_PIN) && (JOYSTICK_X_PIN >= 0) && \
    215     defined(JOYSTICK_Y_PIN) && (JOYSTICK_Y_PIN >= 0)
    216 
    217   // Read X/Y from joystick, constrain to circle
    218   int16_t dx, dy;
    219   int32_t d;
    220   eyeX = analogRead(JOYSTICK_X_PIN); // Raw (unclipped) X/Y reading
    221   eyeY = analogRead(JOYSTICK_Y_PIN);
    222 #ifdef JOYSTICK_X_FLIP
    223   eyeX = 1023 - eyeX;
    224 #endif
    225 #ifdef JOYSTICK_Y_FLIP
    226   eyeY = 1023 - eyeY;
    227 #endif
    228   dx = (eyeX * 2) - 1023; // A/D exact center is at 511.5.  Scale coords
    229   dy = (eyeY * 2) - 1023; // X2 so range is -1023 to +1023 w/center at 0.
    230   if ((d = (dx * dx + dy * dy)) > (1023 * 1023)) { // Outside circle
    231     d    = (int32_t)sqrt((float)d);               // Distance from center
    232     eyeX = ((dx * 1023 / d) + 1023) / 2;          // Clip to circle edge,
    233     eyeY = ((dy * 1023 / d) + 1023) / 2;          // scale back to 0-1023
    234   }
    235 
    236 #else // Autonomous X/Y eye motion
    237   // Periodically initiates motion to a new random point, random speed,
    238   // holds there for random period until next motion.
    239 
    240   static bool  eyeInMotion      = false;
    241   static int16_t  eyeOldX = 512, eyeOldY = 512, eyeNewX = 512, eyeNewY = 512;
    242   static uint32_t eyeMoveStartTime = 0L;
    243   static int32_t  eyeMoveDuration  = 0L;
    244 
    245   int32_t dt = t - eyeMoveStartTime;      // uS elapsed since last eye event
    246   if (eyeInMotion) {                      // Currently moving?
    247     if (dt >= eyeMoveDuration) {          // Time up?  Destination reached.
    248       eyeInMotion      = false;           // Stop moving
    249       eyeMoveDuration  = random(3000000); // 0-3 sec stop
    250       eyeMoveStartTime = t;               // Save initial time of stop
    251       eyeX = eyeOldX = eyeNewX;           // Save position
    252       eyeY = eyeOldY = eyeNewY;
    253     } else { // Move time's not yet fully elapsed -- interpolate position
    254       int16_t e = ease[255 * dt / eyeMoveDuration] + 1;   // Ease curve
    255       eyeX = eyeOldX + (((eyeNewX - eyeOldX) * e) / 256); // Interp X
    256       eyeY = eyeOldY + (((eyeNewY - eyeOldY) * e) / 256); // and Y
    257     }
    258   } else {                                // Eye stopped
    259     eyeX = eyeOldX;
    260     eyeY = eyeOldY;
    261     if (dt > eyeMoveDuration) {           // Time up?  Begin new move.
    262       int16_t  dx, dy;
    263       uint32_t d;
    264       do {                                // Pick new dest in circle
    265         eyeNewX = random(1024);
    266         eyeNewY = random(1024);
    267         dx      = (eyeNewX * 2) - 1023;
    268         dy      = (eyeNewY * 2) - 1023;
    269       } while ((d = (dx * dx + dy * dy)) > (1023 * 1023)); // Keep trying
    270       eyeMoveDuration  = random(72000, 144000); // ~1/14 - ~1/7 sec
    271       eyeMoveStartTime = t;               // Save initial time of move
    272       eyeInMotion      = true;            // Start move on next frame
    273     }
    274   }
    275 #endif // JOYSTICK_X_PIN etc.
    276 
    277   // Blinking
    278 #ifdef AUTOBLINK
    279   // Similar to the autonomous eye movement above -- blink start times
    280   // and durations are random (within ranges).
    281   if ((t - timeOfLastBlink) >= timeToNextBlink) { // Start new blink?
    282     timeOfLastBlink = t;
    283     uint32_t blinkDuration = random(36000, 72000); // ~1/28 - ~1/14 sec
    284     // Set up durations for both eyes (if not already winking)
    285     for (uint8_t e = 0; e < NUM_EYES; e++) {
    286       if (eye[e].blink.state == NOBLINK) {
    287         eye[e].blink.state     = ENBLINK;
    288         eye[e].blink.startTime = t;
    289         eye[e].blink.duration  = blinkDuration;
    290       }
    291     }
    292     timeToNextBlink = blinkDuration * 3 + random(4000000);
    293   }
    294 #endif
    295 
    296   if (eye[eyeIndex].blink.state) { // Eye currently blinking?
    297     // Check if current blink state time has elapsed
    298     if ((t - eye[eyeIndex].blink.startTime) >= eye[eyeIndex].blink.duration) {
    299       // Yes -- increment blink state, unless...
    300       if ((eye[eyeIndex].blink.state == ENBLINK) && ( // Enblinking and...
    301 #if defined(BLINK_PIN) && (BLINK_PIN >= 0)
    302             (digitalRead(BLINK_PIN) == LOW) ||           // blink or wink held...
    303 #endif
    304             ((eyeInfo[eyeIndex].wink >= 0) &&
    305              digitalRead(eyeInfo[eyeIndex].wink) == LOW) )) {
    306         // Don't advance state yet -- eye is held closed instead
    307       } else { // No buttons, or other state...
    308         if (++eye[eyeIndex].blink.state > DEBLINK) { // Deblinking finished?
    309           eye[eyeIndex].blink.state = NOBLINK;      // No longer blinking
    310         } else { // Advancing from ENBLINK to DEBLINK mode
    311           eye[eyeIndex].blink.duration *= 2; // DEBLINK is 1/2 ENBLINK speed
    312           eye[eyeIndex].blink.startTime = t;
    313         }
    314       }
    315     }
    316   } else { // Not currently blinking...check buttons!
    317 #if defined(BLINK_PIN) && (BLINK_PIN >= 0)
    318     if (digitalRead(BLINK_PIN) == LOW) {
    319       // Manually-initiated blinks have random durations like auto-blink
    320       uint32_t blinkDuration = random(36000, 72000);
    321       for (uint8_t e = 0; e < NUM_EYES; e++) {
    322         if (eye[e].blink.state == NOBLINK) {
    323           eye[e].blink.state     = ENBLINK;
    324           eye[e].blink.startTime = t;
    325           eye[e].blink.duration  = blinkDuration;
    326         }
    327       }
    328     } else
    329 #endif
    330       if ((eyeInfo[eyeIndex].wink >= 0) &&
    331           (digitalRead(eyeInfo[eyeIndex].wink) == LOW)) { // Wink!
    332         eye[eyeIndex].blink.state     = ENBLINK;
    333         eye[eyeIndex].blink.startTime = t;
    334         eye[eyeIndex].blink.duration  = random(45000, 90000);
    335       }
    336   }
    337 
    338   // Process motion, blinking and iris scale into renderable values
    339 
    340   // Scale eye X/Y positions (0-1023) to pixel units used by drawEye()
    341   eyeX = map(eyeX, 0, 1023, 0, SCLERA_WIDTH  - 128);
    342   eyeY = map(eyeY, 0, 1023, 0, SCLERA_HEIGHT - 128);
    343 
    344   // Horizontal position is offset so that eyes are very slightly crossed
    345   // to appear fixated (converged) at a conversational distance.  Number
    346   // here was extracted from my posterior and not mathematically based.
    347   // I suppose one could get all clever with a range sensor, but for now...
    348   if (NUM_EYES > 1) {
    349     if (eyeIndex == 1) eyeX += 4;
    350     else eyeX -= 4;
    351   }
    352   if (eyeX > (SCLERA_WIDTH - 128)) eyeX = (SCLERA_WIDTH - 128);
    353 
    354   // Eyelids are rendered using a brightness threshold image.  This same
    355   // map can be used to simplify another problem: making the upper eyelid
    356   // track the pupil (eyes tend to open only as much as needed -- e.g. look
    357   // down and the upper eyelid drops).  Just sample a point in the upper
    358   // lid map slightly above the pupil to determine the rendering threshold.
    359   static uint8_t uThreshold = 128;
    360   uint8_t        lThreshold, n;
    361 #ifdef TRACKING
    362   int16_t sampleX = SCLERA_WIDTH  / 2 - (eyeX / 2), // Reduce X influence
    363           sampleY = SCLERA_HEIGHT / 2 - (eyeY + IRIS_HEIGHT / 4);
    364   // Eyelid is slightly asymmetrical, so two readings are taken, averaged
    365   if (sampleY < 0) n = 0;
    366   else            n = (pgm_read_byte(upper + sampleY * SCREEN_WIDTH + sampleX) +
    367                          pgm_read_byte(upper + sampleY * SCREEN_WIDTH + (SCREEN_WIDTH - 1 - sampleX))) / 2;
    368   uThreshold = (uThreshold * 3 + n) / 4; // Filter/soften motion
    369   // Lower eyelid doesn't track the same way, but seems to be pulled upward
    370   // by tension from the upper lid.
    371   lThreshold = 254 - uThreshold;
    372 #else // No tracking -- eyelids full open unless blink modifies them
    373   uThreshold = lThreshold = 0;
    374 #endif
    375 
    376   // The upper/lower thresholds are then scaled relative to the current
    377   // blink position so that blinks work together with pupil tracking.
    378   if (eye[eyeIndex].blink.state) { // Eye currently blinking?
    379     uint32_t s = (t - eye[eyeIndex].blink.startTime);
    380     if (s >= eye[eyeIndex].blink.duration) s = 255;  // At or past blink end
    381     else s = 255 * s / eye[eyeIndex].blink.duration; // Mid-blink
    382     s          = (eye[eyeIndex].blink.state == DEBLINK) ? 1 + s : 256 - s;
    383     n          = (uThreshold * s + 254 * (257 - s)) / 256;
    384     lThreshold = (lThreshold * s + 254 * (257 - s)) / 256;
    385   } else {
    386     n          = uThreshold;
    387   }
    388 
    389   // Pass all the derived values to the eye-rendering function:
    390   drawEye(eyeIndex, iScale, eyeX, eyeY, n, lThreshold);
    391 
    392   if (eyeIndex == (NUM_EYES - 1)) {
    393     user_loop(); // Call user code after rendering last eye
    394   }
    395 }
    396 
    397 // AUTONOMOUS IRIS SCALING (if no photocell or dial) -----------------------
    398 
    399 #if !defined(LIGHT_PIN) || (LIGHT_PIN < 0)
    400 
    401 // Autonomous iris motion uses a fractal behavior to similate both the major
    402 // reaction of the eye plus the continuous smaller adjustments that occur.
    403 
    404 void split( // Subdivides motion path into two sub-paths w/randimization
    405   int16_t  startValue, // Iris scale value (IRIS_MIN to IRIS_MAX) at start
    406   int16_t  endValue,   // Iris scale value at end
    407   uint32_t startTime,  // micros() at start
    408   int32_t  duration,   // Start-to-end time, in microseconds
    409   int16_t  range) {    // Allowable scale value variance when subdividing
    410 
    411   if (range >= 8) {    // Limit subdvision count, because recursion
    412     range    /= 2;     // Split range & time in half for subdivision,
    413     duration /= 2;     // then pick random center point within range:
    414     int16_t  midValue = (startValue + endValue - range) / 2 + random(range);
    415     uint32_t midTime  = startTime + duration;
    416     split(startValue, midValue, startTime, duration, range); // First half
    417     split(midValue  , endValue, midTime  , duration, range); // Second half
    418   } else {             // No more subdivisons, do iris motion...
    419     int32_t dt;        // Time (micros) since start of motion
    420     int16_t v;         // Interim value
    421     while ((dt = (micros() - startTime)) < duration) {
    422       v = startValue + (((endValue - startValue) * dt) / duration);
    423       if (v < IRIS_MIN)      v = IRIS_MIN; // Clip just in case
    424       else if (v > IRIS_MAX) v = IRIS_MAX;
    425       frame(v);        // Draw frame w/interim iris scale value
    426     }
    427   }
    428 }
    429 #endif // !LIGHT_PIN