code.py
  1  # SPDX-FileCopyrightText: 2020 Phillip Burgess for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  """
  6  RASTER EYES for Adafruit Matrix Portal: animated spooky eyes.
  7  """
  8  
  9  # pylint: disable=import-error
 10  import math
 11  import random
 12  import time
 13  import displayio
 14  import adafruit_imageload
 15  from adafruit_matrixportal.matrix import Matrix
 16  
 17  # TO LOAD DIFFERENT EYE DESIGNS: change the middle word here (between
 18  # 'eyes.' and '.data') to one of the folder names inside the 'eyes' folder:
 19  from eyes.werewolf.data import EYE_DATA
 20  #from eyes.cyclops.data import EYE_DATA
 21  #from eyes.kobold.data import EYE_DATA
 22  #from eyes.adabot.data import EYE_DATA
 23  #from eyes.skull.data import EYE_DATA
 24  
 25  # UTILITY FUNCTIONS AND CLASSES --------------------------------------------
 26  
 27  # pylint: disable=too-few-public-methods
 28  class Sprite(displayio.TileGrid):
 29      """Single-tile-with-bitmap TileGrid subclass, adds a height element
 30         because TileGrid doesn't appear to have a way to poll that later,
 31         object still functions in a displayio.Group.
 32      """
 33      def __init__(self, filename, transparent=None):
 34          """Create Sprite object from color-paletted BMP file, optionally
 35             set one color to transparent (pass as RGB tuple or list to locate
 36             nearest color, or integer to use a known specific color index).
 37          """
 38          bitmap, palette = adafruit_imageload.load(
 39              filename, bitmap=displayio.Bitmap, palette=displayio.Palette)
 40          if isinstance(transparent, (tuple, list)): # Find closest RGB match
 41              closest_distance = 0x1000000           # Force first match
 42              for color_index, color in enumerate(palette): # Compare each...
 43                  delta = (transparent[0] - ((color >> 16) & 0xFF),
 44                           transparent[1] - ((color >> 8) & 0xFF),
 45                           transparent[2] - (color & 0xFF))
 46                  rgb_distance = (delta[0] * delta[0] +
 47                                  delta[1] * delta[1] +
 48                                  delta[2] * delta[2]) # Actually dist^2
 49                  if rgb_distance < closest_distance:  # but adequate for
 50                      closest_distance = rgb_distance  # compare purposes,
 51                      closest_index = color_index      # no sqrt needed
 52              palette.make_transparent(closest_index)
 53          elif isinstance(transparent, int):
 54              palette.make_transparent(transparent)
 55          super(Sprite, self).__init__(bitmap, pixel_shader=palette)
 56  
 57  
 58  # ONE-TIME INITIALIZATION --------------------------------------------------
 59  
 60  MATRIX = Matrix(bit_depth=6)
 61  DISPLAY = MATRIX.display
 62  
 63  # Order in which sprites are added determines the 'stacking order' and
 64  # visual priority. Lower lid is added before the upper lid so that if they
 65  # overlap, the upper lid is 'on top' (e.g. if it has eyelashes or such).
 66  SPRITES = displayio.Group()
 67  SPRITES.append(Sprite(EYE_DATA['eye_image'])) # Base image is opaque
 68  SPRITES.append(Sprite(EYE_DATA['lower_lid_image'], EYE_DATA['transparent']))
 69  SPRITES.append(Sprite(EYE_DATA['upper_lid_image'], EYE_DATA['transparent']))
 70  SPRITES.append(Sprite(EYE_DATA['stencil_image'], EYE_DATA['transparent']))
 71  DISPLAY.show(SPRITES)
 72  
 73  EYE_CENTER = ((EYE_DATA['eye_move_min'][0] +           # Pixel coords of eye
 74                 EYE_DATA['eye_move_max'][0]) / 2,       # image when centered
 75                (EYE_DATA['eye_move_min'][1] +           # ('neutral' position)
 76                 EYE_DATA['eye_move_max'][1]) / 2)
 77  EYE_RANGE = (abs(EYE_DATA['eye_move_max'][0] -         # Max eye image motion
 78                   EYE_DATA['eye_move_min'][0]) / 2,     # delta from center
 79               abs(EYE_DATA['eye_move_max'][1] -
 80                   EYE_DATA['eye_move_min'][1]) / 2)
 81  UPPER_LID_MIN = (min(EYE_DATA['upper_lid_open'][0],    # Motion bounds of
 82                       EYE_DATA['upper_lid_closed'][0]), # upper and lower
 83                   min(EYE_DATA['upper_lid_open'][1],    # eyelids
 84                       EYE_DATA['upper_lid_closed'][1]))
 85  UPPER_LID_MAX = (max(EYE_DATA['upper_lid_open'][0],
 86                       EYE_DATA['upper_lid_closed'][0]),
 87                   max(EYE_DATA['upper_lid_open'][1],
 88                       EYE_DATA['upper_lid_closed'][1]))
 89  LOWER_LID_MIN = (min(EYE_DATA['lower_lid_open'][0],
 90                       EYE_DATA['lower_lid_closed'][0]),
 91                   min(EYE_DATA['lower_lid_open'][1],
 92                       EYE_DATA['lower_lid_closed'][1]))
 93  LOWER_LID_MAX = (max(EYE_DATA['lower_lid_open'][0],
 94                       EYE_DATA['lower_lid_closed'][0]),
 95                   max(EYE_DATA['lower_lid_open'][1],
 96                       EYE_DATA['lower_lid_closed'][1]))
 97  EYE_PREV = (0, 0)
 98  EYE_NEXT = (0, 0)
 99  MOVE_STATE = False                                     # Initially stationary
100  MOVE_EVENT_DURATION = random.uniform(0.1, 3)           # Time to first move
101  BLINK_STATE = 2                                        # Start eyes closed
102  BLINK_EVENT_DURATION = random.uniform(0.25, 0.5)       # Time for eyes to open
103  TIME_OF_LAST_MOVE_EVENT = TIME_OF_LAST_BLINK_EVENT = time.monotonic()
104  
105  
106  # MAIN LOOP ----------------------------------------------------------------
107  
108  while True:
109      NOW = time.monotonic()
110  
111      # Eye movement ---------------------------------------------------------
112  
113      if NOW - TIME_OF_LAST_MOVE_EVENT > MOVE_EVENT_DURATION:
114          TIME_OF_LAST_MOVE_EVENT = NOW # Start new move or pause
115          MOVE_STATE = not MOVE_STATE   # Toggle between moving & stationary
116          if MOVE_STATE:                # Starting a new move?
117              MOVE_EVENT_DURATION = random.uniform(0.08, 0.17) # Move time
118              ANGLE = random.uniform(0, math.pi * 2)
119              EYE_NEXT = (math.cos(ANGLE) * EYE_RANGE[0], # (0,0) in center,
120                          math.sin(ANGLE) * EYE_RANGE[1]) # NOT pixel coords
121          else:                         # Starting a new pause
122              MOVE_EVENT_DURATION = random.uniform(0.04, 3)    # Hold time
123              EYE_PREV = EYE_NEXT
124  
125      # Fraction of move elapsed (0.0 to 1.0), then ease in/out 3*e^2-2*e^3
126      RATIO = (NOW - TIME_OF_LAST_MOVE_EVENT) / MOVE_EVENT_DURATION
127      RATIO = 3 * RATIO * RATIO - 2 * RATIO * RATIO * RATIO
128      EYE_POS = (EYE_PREV[0] + RATIO * (EYE_NEXT[0] - EYE_PREV[0]),
129                 EYE_PREV[1] + RATIO * (EYE_NEXT[1] - EYE_PREV[1]))
130  
131      # Blinking -------------------------------------------------------------
132  
133      if NOW - TIME_OF_LAST_BLINK_EVENT > BLINK_EVENT_DURATION:
134          TIME_OF_LAST_BLINK_EVENT = NOW # Start change in blink
135          BLINK_STATE += 1               # Cycle paused/closing/opening
136          if BLINK_STATE == 1:           # Starting a new blink (closing)
137              BLINK_EVENT_DURATION = random.uniform(0.03, 0.07)
138          elif BLINK_STATE == 2:         # Starting de-blink (opening)
139              BLINK_EVENT_DURATION *= 2
140          else:                          # Blink ended,
141              BLINK_STATE = 0            # paused
142              BLINK_EVENT_DURATION = random.uniform(BLINK_EVENT_DURATION * 3, 4)
143  
144      if BLINK_STATE: # Currently in a blink?
145          # Fraction of closing or opening elapsed (0.0 to 1.0)
146          RATIO = (NOW - TIME_OF_LAST_BLINK_EVENT) / BLINK_EVENT_DURATION
147          if BLINK_STATE == 2:    # Opening
148              RATIO = 1.0 - RATIO # Flip ratio so eye opens instead of closes
149      else:           # Not blinking
150          RATIO = 0
151  
152      # Eyelid tracking ------------------------------------------------------
153  
154      # Initial estimate of 'tracked' eyelid positions
155      UPPER_LID_POS = (EYE_DATA['upper_lid_center'][0] + EYE_POS[0],
156                       EYE_DATA['upper_lid_center'][1] + EYE_POS[1])
157      LOWER_LID_POS = (EYE_DATA['lower_lid_center'][0] + EYE_POS[0],
158                       EYE_DATA['lower_lid_center'][1] + EYE_POS[1])
159      # Then constrain these to the upper/lower lid motion bounds
160      UPPER_LID_POS = (min(max(UPPER_LID_POS[0],
161                               UPPER_LID_MIN[0]), UPPER_LID_MAX[0]),
162                       min(max(UPPER_LID_POS[1],
163                               UPPER_LID_MIN[1]), UPPER_LID_MAX[1]))
164      LOWER_LID_POS = (min(max(LOWER_LID_POS[0],
165                               LOWER_LID_MIN[0]), LOWER_LID_MAX[0]),
166                       min(max(LOWER_LID_POS[1],
167                               LOWER_LID_MIN[1]), LOWER_LID_MAX[1]))
168      # Then interpolate between bounded tracked position to closed position
169      UPPER_LID_POS = (UPPER_LID_POS[0] + RATIO *
170                       (EYE_DATA['upper_lid_closed'][0] - UPPER_LID_POS[0]),
171                       UPPER_LID_POS[1] + RATIO *
172                       (EYE_DATA['upper_lid_closed'][1] - UPPER_LID_POS[1]))
173      LOWER_LID_POS = (LOWER_LID_POS[0] + RATIO *
174                       (EYE_DATA['lower_lid_closed'][0] - LOWER_LID_POS[0]),
175                       LOWER_LID_POS[1] + RATIO *
176                       (EYE_DATA['lower_lid_closed'][1] - LOWER_LID_POS[1]))
177  
178      # Move eye sprites -----------------------------------------------------
179  
180      SPRITES[0].x, SPRITES[0].y = (int(EYE_CENTER[0] + EYE_POS[0] + 0.5),
181                                    int(EYE_CENTER[1] + EYE_POS[1] + 0.5))
182      SPRITES[2].x, SPRITES[2].y = (int(UPPER_LID_POS[0] + 0.5),
183                                    int(UPPER_LID_POS[1] + 0.5))
184      SPRITES[1].x, SPRITES[1].y = (int(LOWER_LID_POS[0] + 0.5),
185                                    int(LOWER_LID_POS[1] + 0.5))