/ Matrix_Portal_Eyes / code.py
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))