This is just the perfect halloween display I can think of.
Basically took the two examples and mashed them together, and I have to say the effect is quite impressive.
https://learn.adafruit.com/pixel-art-ma ... -animation
https://learn.adafruit.com/matrix-portal-creature-eyes
The images are likely copywritten, so not completely comfortable sharing everything on forums.
The changes are roughly as follows:
*Take the creature eyes project, and load it as is.
*For the new animated eyes, add a sprite sheet rather than a single image
*data.py I delete the code regarding the lid images (later fix references in code)
*data.py add two new entries 'animated':True and 'animation_delay':0.05 or whatever suits
*the code.py changes are pasted below
- Code: Select all | TOGGLE FULL SIZE
"""
RASTER EYES for Adafruit Matrix Portal: animated spooky eyes.
"""
print("Main loop")
# pylint: disable=import-error
import math
import random
import time
import displayio
import adafruit_imageload
from adafruit_matrixportal.matrix import Matrix
# TO LOAD DIFFERENT EYE DESIGNS: change the middle word here (between
# 'eyes.' and '.data') to one of the folder names inside the 'eyes' folder:
#from eyes.werewolf.data import EYE_DATA
#from eyes.cyclops.data import EYE_DATA
#from eyes.kobold.data import EYE_DATA
from eyes.pumpkin.data import EYE_DATA
# UTILITY FUNCTIONS AND CLASSES --------------------------------------------
# pylint: disable=too-few-public-methods
class Sprite(displayio.TileGrid):
"""Single-tile-with-bitmap TileGrid subclass, adds a height element
because TileGrid doesn't appear to have a way to poll that later,
object still functions in a displayio.Group.
"""
def __init__(self, filename, transparent=None):
"""Create Sprite object from color-paletted BMP file, optionally
set one color to transparent (pass as RGB tuple or list to locate
nearest color, or integer to use a known specific color index).
"""
bitmap, palette = adafruit_imageload.load(
filename, bitmap=displayio.Bitmap, palette=displayio.Palette)
if isinstance(transparent, (tuple, list)): # Find closest RGB match
closest_distance = 0x1000000 # Force first match
for color_index, color in enumerate(palette): # Compare each...
delta = (transparent[0] - ((color >> 16) & 0xFF),
transparent[1] - ((color >> 8) & 0xFF),
transparent[2] - (color & 0xFF))
rgb_distance = (delta[0] * delta[0] +
delta[1] * delta[1] +
delta[2] * delta[2]) # Actually dist^2
if rgb_distance < closest_distance: # but adequate for
closest_distance = rgb_distance # compare purposes,
closest_index = color_index # no sqrt needed
palette.make_transparent(closest_index)
elif isinstance(transparent, int):
palette.make_transparent(transparent)
super(Sprite, self).__init__(bitmap, pixel_shader=palette, tile_height=MATRIX.display.height)
self.height = bitmap.height
# ONE-TIME INITIALIZATION --------------------------------------------------
MATRIX = Matrix(bit_depth=6)
DISPLAY = MATRIX.display
# Order in which sprites are added determines the 'stacking order' and
# visual priority. Lower lid is added before the upper lid so that if they
# overlap, the upper lid is 'on top' (e.g. if it has eyelashes or such).
SPRITES = displayio.Group()
EYE_SPRITE = Sprite(EYE_DATA['eye_image']); # Base image is opaque
SPRITES.append(EYE_SPRITE)
LOWER_LID_SPRITE = None
if EYE_DATA['lower_lid_image']:
LOWER_LID_SPRITE = Sprite(EYE_DATA['lower_lid_image'], EYE_DATA['transparent'])
SPRITES.append(LOWER_LID_SPRITE)
UPPER_LID_SPRITE = None
if EYE_DATA['upper_lid_image']:
UPPER_LID_SPRITE = Sprite(EYE_DATA['upper_lid_image'], EYE_DATA['transparent'])
SPRITES.append(LOWER_LID_SPRITE)
STENCIL_SPRITE = Sprite(EYE_DATA['stencil_image'], EYE_DATA['transparent'])
SPRITES.append(STENCIL_SPRITE)
DISPLAY.show(SPRITES)
EYE_CENTER = ((EYE_DATA['eye_move_min'][0] + # Pixel coords of eye
EYE_DATA['eye_move_max'][0]) / 2, # image when centered
(EYE_DATA['eye_move_min'][1] + # ('neutral' position)
EYE_DATA['eye_move_max'][1]) / 2)
EYE_RANGE = (abs(EYE_DATA['eye_move_max'][0] - # Max eye image motion
EYE_DATA['eye_move_min'][0]) / 2, # delta from center
abs(EYE_DATA['eye_move_max'][1] -
EYE_DATA['eye_move_min'][1]) / 2)
if UPPER_LID_SPRITE:
UPPER_LID_MIN = (min(EYE_DATA['upper_lid_open'][0], # Motion bounds of
EYE_DATA['upper_lid_closed'][0]), # upper and lower
min(EYE_DATA['upper_lid_open'][1], # eyelids
EYE_DATA['upper_lid_closed'][1]))
UPPER_LID_MAX = (max(EYE_DATA['upper_lid_open'][0],
EYE_DATA['upper_lid_closed'][0]),
max(EYE_DATA['upper_lid_open'][1],
EYE_DATA['upper_lid_closed'][1]))
if LOWER_LID_SPRITE:
LOWER_LID_MIN = (min(EYE_DATA['lower_lid_open'][0],
EYE_DATA['lower_lid_closed'][0]),
min(EYE_DATA['lower_lid_open'][1],
EYE_DATA['lower_lid_closed'][1]))
LOWER_LID_MAX = (max(EYE_DATA['lower_lid_open'][0],
EYE_DATA['lower_lid_closed'][0]),
max(EYE_DATA['lower_lid_open'][1],
EYE_DATA['lower_lid_closed'][1]))
EYE_PREV = EYE_CENTER
EYE_NEXT = EYE_CENTER
MOVE_STATE = False # Initially stationary
MOVE_EVENT_DURATION = random.uniform(0.1, 3) # Time to first move
BLINK_STATE = 2 # Start eyes closed
BLINK_EVENT_DURATION = random.uniform(0.25, 0.5) # Time for eyes to open
TIME_OF_LAST_MOVE_EVENT = TIME_OF_LAST_BLINK_EVENT = time.monotonic()
CURRENT_FRAME = 0
FRAME_COUNT = 1
LAST_FRAME_TIME = 0
if EYE_DATA['animated']:
FRAME_COUNT = int(EYE_SPRITE.height / MATRIX.display.height)
# MAIN LOOP ----------------------------------------------------------------
while True:
NOW = time.monotonic()
# Eye movement ---------------------------------------------------------
if NOW - TIME_OF_LAST_MOVE_EVENT > MOVE_EVENT_DURATION:
TIME_OF_LAST_MOVE_EVENT = NOW # Start new move or pause
MOVE_STATE = not MOVE_STATE # Toggle between moving & stationary
if MOVE_STATE: # Starting a new move?
MOVE_EVENT_DURATION = random.uniform(0.08, 0.17) # Move time
ANGLE = random.uniform(0, math.pi * 2)
EYE_NEXT = (math.cos(ANGLE) * EYE_RANGE[0], # (0,0) in center,
math.sin(ANGLE) * EYE_RANGE[1]) # NOT pixel coords
else: # Starting a new pause
MOVE_EVENT_DURATION = random.uniform(0.04, 3) # Hold time
EYE_PREV = EYE_NEXT
# Fraction of move elapsed (0.0 to 1.0), then ease in/out 3*e^2-2*e^3
RATIO = (NOW - TIME_OF_LAST_MOVE_EVENT) / MOVE_EVENT_DURATION
RATIO = 3 * RATIO * RATIO - 2 * RATIO * RATIO * RATIO
EYE_POS = (EYE_PREV[0] + RATIO * (EYE_NEXT[0] - EYE_PREV[0]),
EYE_PREV[1] + RATIO * (EYE_NEXT[1] - EYE_PREV[1]))
# Blinking -------------------------------------------------------------
if NOW - TIME_OF_LAST_BLINK_EVENT > BLINK_EVENT_DURATION:
TIME_OF_LAST_BLINK_EVENT = NOW # Start change in blink
BLINK_STATE += 1 # Cycle paused/closing/opening
if BLINK_STATE == 1: # Starting a new blink (closing)
BLINK_EVENT_DURATION = random.uniform(0.03, 0.07)
elif BLINK_STATE == 2: # Starting de-blink (opening)
BLINK_EVENT_DURATION *= 2
else: # Blink ended,
BLINK_STATE = 0 # paused
BLINK_EVENT_DURATION = random.uniform(BLINK_EVENT_DURATION * 3, 4)
if BLINK_STATE: # Currently in a blink?
# Fraction of closing or opening elapsed (0.0 to 1.0)
RATIO = (NOW - TIME_OF_LAST_BLINK_EVENT) / BLINK_EVENT_DURATION
if BLINK_STATE == 2: # Opening
RATIO = 1.0 - RATIO # Flip ratio so eye opens instead of closes
else: # Not blinking
RATIO = 0
# Eyelid tracking ------------------------------------------------------
if UPPER_LID_SPRITE:
# Initial estimate of 'tracked' eyelid positions
UPPER_LID_POS = (EYE_DATA['upper_lid_center'][0] + EYE_POS[0],
EYE_DATA['upper_lid_center'][1] + EYE_POS[1])
# Then constrain these to the upper/lower lid motion bounds
UPPER_LID_POS = (min(max(UPPER_LID_POS[0],
UPPER_LID_MIN[0]), UPPER_LID_MAX[0]),
min(max(UPPER_LID_POS[1],
UPPER_LID_MIN[1]), UPPER_LID_MAX[1]))
# Then interpolate between bounded tracked position to closed position
UPPER_LID_POS = (UPPER_LID_POS[0] + RATIO *
(EYE_DATA['upper_lid_closed'][0] - UPPER_LID_POS[0]),
UPPER_LID_POS[1] + RATIO *
(EYE_DATA['upper_lid_closed'][1] - UPPER_LID_POS[1]))
if LOWER_LID_SPRITE:
LOWER_LID_POS = (EYE_DATA['lower_lid_center'][0] + EYE_POS[0],
EYE_DATA['lower_lid_center'][1] + EYE_POS[1])
LOWER_LID_POS = (min(max(LOWER_LID_POS[0],
LOWER_LID_MIN[0]), LOWER_LID_MAX[0]),
min(max(LOWER_LID_POS[1],
LOWER_LID_MIN[1]), LOWER_LID_MAX[1]))
LOWER_LID_POS = (LOWER_LID_POS[0] + RATIO *
(EYE_DATA['lower_lid_closed'][0] - LOWER_LID_POS[0]),
LOWER_LID_POS[1] + RATIO *
(EYE_DATA['lower_lid_closed'][1] - LOWER_LID_POS[1]))
# Move eye sprites -----------------------------------------------------
EYE_SPRITE.x, EYE_SPRITE.y = (int(EYE_CENTER[0] + EYE_POS[0] + 0.5),
int(EYE_CENTER[1] + EYE_POS[1] + 0.5))
if UPPER_LID_SPRITE:
UPPER_LID_SPRITE.x, UPPER_LID_SPRITE.y = (int(UPPER_LID_POS[0] + 0.5),
int(UPPER_LID_POS[1] + 0.5))
if LOWER_LID_SPRITE:
LOWER_LID_SPRITE.x, LOWER_LID_SPRITE.y = (int(LOWER_LID_POS[0] + 0.5),
int(LOWER_LID_POS[1] + 0.5))
if EYE_DATA['animated'] and time.monotonic() - LAST_FRAME_TIME > EYE_DATA['animation_delay']:
LAST_FRAME_TIME = time.monotonic()
CURRENT_FRAME = (CURRENT_FRAME + 1) % FRAME_COUNT
EYE_SPRITE[0] = CURRENT_FRAME
I could definitely see this being expandable to the eye stencil animating in addition to the eyes themselves.
PS the case is a custom 3d print as well, turned out way better than I would have expected, fits like a glove. Large flat surfaces take forever to print (the back, the front wasn't too bad) The LED diffusion acrylic is super cool. It's two sandwich halves and the internal acrylic and matrix slide in on rails and secure with screws.