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