Skip to content

Commit 4633068

Browse files
Merge pull request #1247 from adafruit/pb-moon-clock
Add Matrix Portal Eyes project
2 parents 00b5088 + cf8e513 commit 4633068

16 files changed

+231
-0
lines changed

Matrix_Portal_Eyes/code.py

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

0 commit comments

Comments
 (0)