Skip to content

Commit 99b9dc7

Browse files
authored
Merge pull request #1290 from jedgarpark/window-skull
first commit
2 parents 9bc64ca + eaf9f19 commit 99b9dc7

File tree

6 files changed

+246
-0
lines changed

6 files changed

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

0 commit comments

Comments
 (0)