Decorate your home with a back-lit skull that has animated matrix eyes and a moving jaw.

Use the Matrix Portal and an RGB LED matrix display to create spooky eyes in a silhouetted window skull! Plus, the Matrix Portal drives a servo motor to wag the skull's jaw around!


Folks love our wide selection of RGB matrices and accessories, for making custom colorful LED displays... and our RGB Matrix Shields...
In Stock
Bring a little bit of Times Square into your home with this sweet 64 x 32 square RGB LED matrix panel. These panels are normally used to make video walls, here in New York we see them...
Out of Stock
 nice whoppin' rectangular slab of some lovely black acrylic to add some extra diffusion to your LED Matrix project. This material is 2.6mm (0.1") thick and is made of...
In Stock
Our all-in-one 5V 2.5 Amp + MicroUSB cable power adapter is the perfect choice for powering single-board computers like Raspberry Pi, BeagleBone, or anything else that's...
In Stock
As technology changes and adapts, so does Adafruit, and speaking of adapting, this adapter has a Micro B USB jack and a USB C...
In Stock
This here is your standard A to micro-B USB cable, for USB 1.1 or 2.0. Perfect for connecting a PC to your Metro, Feather, Raspberry Pi or other dev-board or...
In Stock
This high-torque standard servo can rotate approximately 180 degrees (90 in each direction). You can use any servo code, hardware, or library to control these servos. Good for...
In Stock
Socket (female) header is like the duct tape of electronics. Its great for connecting things together, soldering to perf-boards or PCBs to allow 22AWG wire or male header to connect, etc....
In Stock
Breakaway header is like the duct tape of electronics, and this header is one better with extra long pins on both sides. This makes it great for connecting things together that...
In Stock

Materials and Tools

You'll also need:

  • Corrugated medium-sized cardboard box
  • Pencil
  • Hobby knife & cutting surface
  • Filament or cord for hanging
  • Soldering iron and solder


These are nice to have, but not essential:

As technology changes and adapts, so does Adafruit, and speaking of adapting, this right angle adapter is USB C...
Out of Stock
Are you still looking for that perfect PCB holder? The low profile PCB Stickvise might be just the thing you need!In performing the simple task...
In Stock
Stickvise PartLift holds thru-hole parts in place to free up your hands while you solder the legs. A simple yet useful tool to go along with your Stickvise or other...
In Stock

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

Set up CircuitPython Quick Start!

Follow this quick step-by-step for super-fast Python power :)

Further Information

For more detailed info on installing CircuitPython, check out Installing CircuitPython.

Click the link above and download the latest UF2 file.

Download and save it to your desktop (or wherever is handy).

Plug your MatrixPortal M4 into your computer using a known-good USB cable.

A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.

Double-click the Reset button (indicated by the green arrow) on your board, and you will see the NeoPixel RGB LED (indicated by the magenta arrow) turn green. If it turns red, check the USB cable, try another USB port, etc.

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

You will see a new disk drive appear called MATRIXBOOT.


Drag the adafruit_circuitpython_etc.uf2 file to MATRIXBOOT.

The LED will flash. Then, the MATRIXBOOT drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

To use all the amazing features of your MatrixPortal M4 with CircuitPython, you must first install a number of libraries. This page covers that process.

Adafruit CircuitPython Bundle

Download the Adafruit CircuitPython Library Bundle. You can find the latest release here:

Download the adafruit-circuitpython-bundle-version-mpy-*.zip bundle zip file, and unzip a folder of the same name. Inside you'll find a lib folder. The entire collection of libraries is too large to fit on the CIRCUITPY drive. Instead, add each library as you need it, this will reduce the space usage but you'll need to put in a little more effort.

At a minimum we recommend the following libraries, in fact we more than recommend. They're basically required. So grab them and install them into CIRCUITPY/lib now!

  • adafruit_matrixportal - this library is the main library used with the MatrixPortal.
  • adafruit_portalbase - This is the base library that adafruit_matrixportal is built on top of.
  • adafruit_esp32spi - this is the library that gives you internet access via the ESP32 using (you guessed it!) SPI transport. You need this for anything Internet
  • neopixel - for controlling the onboard neopixel
  • adafruit_bus_device - low level support for I2C/SPI
  • adafruit_requests - this library allows us to perform HTTP requests and get responses back from servers. GET/POST/PUT/PATCH - they're all in here!
  • adafruit_fakerequests.mpy  - This library allows you to create fake HTTP requests by using local files.
  • adafruit_io - this library helps connect the PyPortal to our free data logging and viewing service
  • adafruit_bitmap_font - we have fancy font support, and it's easy to make new fonts. This library reads and parses font files.
  • adafruit_display_text - not surprisingly, it displays text on the screen
  • adafruit_lis3dh - this library is used for the onboard accelerometer to detect the orientation of the MatrixPortal

Power Prep

The MatrixPortal supplies power to the matrix display panel via two standoffs. These come with protective tape applied (part of our manufacturing process) which MUST BE REMOVED!

Use some tweezers or a fingernail to remove the two amber circles.

Power Terminals

Next, screw in the spade connectors to the corresponding standoff.

  • red wire goes to +5V 
  • black wire goes to GND

Panel Power

Plug either one of the four-conductor power plugs into the power connector pins on the panel. The plug can only go in one way, and that way is marked on the board's silkscreen.

Board Connection

Now, plug the board into the left side shrouded 8x2 connector as shown. The orientation matters, so take a moment to confirm that the white indicator arrow on the matrix panel is oriented pointing up and right as seen here and the MatrixPortal overhangs the edge of the panel when connected. This allows you to use the edge buttons from the front side.


Check nothing is impeding the board from plugging in firmly. If there's a plastic nub on the matrix that's keeping the Portal from sitting flat, cut it off with diagonal cutters

For info on adding LED diffusion acrylic, see the page LED Matrix Diffuser.

LED Diffusion Acrylic

You can add an LED diffusion acrylic faceplate to the your LED matrix display. (Pictured here with the ON AIR project)

This can help protect the LEDs as well as enhance the look of the sign both indoors and out by reducing glare and specular highlights of the plastic matrix grid.

Measure and Cut the Plastic

You can use the sign to measure and mark cut lines on the paper backing of the acrylic sheet.

Then, use a tablesaw or bandsaw with a fine toothed blade and a guide or sled to make the cuts.

Note: it is possible to score and snap acrylic, but it can be very tricky to get an even snap without proper clamping.

Peel away the paper backing from both sides and set the acrylic onto your matrix display.

Uglu Dashes

The best method we've found for adhering acrylic to the matrix display is to use Uglu Dashes clear adhesive rectangles from Pro Tapes. They are incredibly strong (although can be removed if necessary), easy to apply, and are invisible once attached.

Use one at each corner and one each at the halfway point of the long edges, then press the acrylic and matrix panel together for about 20 seconds.

Here you can see the impact of using the diffusion acrylic. (Pictured here with the ON AIR sign project)


A very simple and attractive way to display your matrix is with the adjustable bent-wire stand.

Alternately, you can use a frame, 3D printed brackets, tape, glue, or even large binder clips to secure the acrylic to the sign and then mount it on on a wall, shelf, or display cabinet.

These mini-magnet feet can be used to stick the sign to a ferrous surface.

We'll add a set of header sockets to Matrix Portal in order to easily plug in the servo motor. A 90º header makes for a nice, compact-yet-accessible setup.

Solder Headers

There are eleven GPIO pins exposed on the Matrix Portal's header row. Trim a section of socket headers to fit as shown. 

Place the header and solder it in place.

You could also use right angle header pins here instead of sockets, its just a matter of personal preference.
Wear appropriate protective eyewear and be careful cutting header apart as pieces often fly which could hurt someone.

Connect Servo

Use small pliers or diagonal cutters to cut off a three-pin section of extra-long header pins and use them to insert the servo motor connector as shown here

  • Brown to GND
  • Orange to 3V
  • Yellow to A4

This project uses the Matrix Portal Creature Eyes code, so familiarize yourself with it here first. We'll be using custom graphics based on the ready-made skull, plus a bit of extra code for the servo motor jaw waggle.

Once you've followed the steps from the Creature Eyes project and have the set of eyes working, you can download the Project Zip from the code embed link below.

Copy the skull_bigger folder from the zip file's /eyes folder and paste it into the /eyes folder on you Matrix Portal CIRCUITPY drive.


These are the libraries you'll need to have on the board:

  • adafruit_bus_device
  • adafruit_debouncer.mpy
  • adafruit_esp32spi
  • adafruit_imageload
  • adafruit_matrixportal
  • adafruit_motor
  • adafruit_requests.mpy
  • neopixel.mpy

These graphics have been designed to be seen through the cardboard skull's eye cutouts, using the same methods shown here.

Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Copy from the zip file and place on the CIRCUITPY drive.

# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries
# SPDX-License-Identifier: MIT

WINDOW SKULL for Adafruit Matrix Portal: animated spooky eyes and servomotor jaw

# pylint: disable=import-error
import math
import random
import time
import board
import pwmio
import displayio
from adafruit_motor import servo
import adafruit_imageload
from adafruit_matrixportal.matrix import Matrix

pwm = pwmio.PWMOut(board.A4, duty_cycle=2 ** 15, frequency=50)
jaw_servo = servo.Servo(pwm)

def jaw_wag():
    for angle in range(90, 70, -2):  # start angle, end angle, degree step size
        jaw_servo.angle = angle
    for angle in range(70, 90, 2):
        jaw_servo.angle = angle
    for angle in range(90, 110, 2):
        jaw_servo.angle = angle
    for angle in range(110, 90, -2):
        jaw_servo.angle = angle

# 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 import EYE_DATA
# from import EYE_DATA
# from import EYE_DATA
# from import EYE_DATA
# from import EYE_DATA
# pylint: disable=wrong-import-position
from 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
        elif isinstance(transparent, int):
        super(Sprite, self).__init__(bitmap, pixel_shader=palette)
        self.height = bitmap.height

# ONE-TIME INITIALIZATION --------------------------------------------------

MATRIX = Matrix(bit_depth=6)

# 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()
SPRITES.append(Sprite(EYE_DATA["eye_image"]))  # Base image is opaque
SPRITES.append(Sprite(EYE_DATA["lower_lid_image"], EYE_DATA["transparent"]))
SPRITES.append(Sprite(EYE_DATA["upper_lid_image"], EYE_DATA["transparent"]))
SPRITES.append(Sprite(EYE_DATA["stencil_image"], EYE_DATA["transparent"]))

    (EYE_DATA["eye_move_min"][0] + EYE_DATA["eye_move_max"][0])  # Pixel coords of eye
    / 2,  # image when centered
    (EYE_DATA["eye_move_min"][1] + EYE_DATA["eye_move_max"][1])  # ('neutral' position)
    / 2,
        - EYE_DATA["eye_move_min"][0]  # Max eye image motion
    / 2,  # delta from center
    abs(EYE_DATA["eye_move_max"][1] - EYE_DATA["eye_move_min"][1]) / 2,
        EYE_DATA["upper_lid_open"][0],  # Motion bounds of
    ),  # upper and lower
    min(EYE_DATA["upper_lid_open"][1], EYE_DATA["upper_lid_closed"][1]),  # eyelids
    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]),
    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]),
    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 = (0, 0)
EYE_NEXT = (0, 0)
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

# MAIN LOOP ----------------------------------------------------------------

while True:
    NOW = time.monotonic()
    # Eye movement ---------------------------------------------------------

        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
    EYE_POS = (
        EYE_PREV[0] + RATIO * (EYE_NEXT[0] - EYE_PREV[0]),
        EYE_PREV[1] + RATIO * (EYE_NEXT[1] - EYE_PREV[1]),

    # Blinking -------------------------------------------------------------

        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)
        if BLINK_STATE == 2:  # Opening
            RATIO = 1.0 - RATIO  # Flip ratio so eye opens instead of closes
    else:  # Not blinking
        RATIO = 0

    # Eyelid tracking ------------------------------------------------------

    # Initial estimate of 'tracked' eyelid positions
        EYE_DATA["upper_lid_center"][0] + EYE_POS[0],
        EYE_DATA["upper_lid_center"][1] + EYE_POS[1],
        EYE_DATA["lower_lid_center"][0] + EYE_POS[0],
        EYE_DATA["lower_lid_center"][1] + EYE_POS[1],
    # Then constrain these to the upper/lower lid motion bounds
        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]),
        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]),
    # Then interpolate between bounded tracked position to closed position
        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]),
        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 -----------------------------------------------------

    SPRITES[0].x, SPRITES[0].y = (
        int(EYE_CENTER[0] + EYE_POS[0] + 0.5),
        int(EYE_CENTER[1] + EYE_POS[1] + 0.5),
    SPRITES[2].x, SPRITES[2].y = (
        int(UPPER_LID_POS[0] + 0.5),
        int(UPPER_LID_POS[1] + 0.5),
    SPRITES[1].x, SPRITES[1].y = (
        int(LOWER_LID_POS[0] + 0.5),
        int(LOWER_LID_POS[1] + 0.5),

The code works the same as the Matrix Eyes project, with one small addition, the servo motor for the jaw. Here's how that is set up.

First, we'll import the servo code from the adafruit_motor library.

from adafruit_motor import servo


Then, we set up pulse width modulation on pin A4 and create the servo object.

pwm = pulseio.PWMOut(board.A4, duty_cycle=2 ** 15, frequency=50)
jaw_servo = servo.Servo(pwm)

Jaw Wag

We'll define a jaw_wag() function that we can call whenever we want to wiggle the jaw back and forth.

def jaw_wag():
    for angle in range(90, 70, -2):  # start angle, end angle, degree step size
        jaw_servo.angle = angle
    for angle in range(70, 90, 2):
        jaw_servo.angle = angle
    for angle in range(90, 110, 2):
        jaw_servo.angle = angle
    for angle in range(110, 90, -2):
        jaw_servo.angle = angle

Blink and Jaw

Finally, whenever there is a blink event in the code, we'll add in a jaw wag:

# Blinking -------------------------------------------------------------

        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)

This project is a nice reuse for a cardboard box -- I made mine from the AdaBox 016 shipping box, but any medium corrugated cardboard box will work.

Box Prep

To begin, flatten out your box.

Then, make sure that the LED Matrix display you're using will fit properly.

Sketch out a skull shape on the cardboard, keeping in mind the positioning of the eyes.

Eye Tracing

To get the positioning of the eyes, plug in the Matrix Portal so you can see the eyes.

Use some tracing paper to trace the sockets, then cut out the sockets and use the paper as a template.

With the hobby knife, carefully cut out the eye sockets from the cardboard.

Add Jaw

Create a lower jaw piece with an additional piece of cardboard. This is what the servo will wiggle back and forth.

Poke a hole in the skull's chin for the servo shaft, then screw on a servo horn. This is the part that will rotate, with the separate jaw connected to it.

Cut out some of the skull's lower jaw, leaving behind a few spooky teeth, and cut out some teeth from the lower jaw as well! This will allow light to shine through the silhouette in your window.

Use some double stick adhesive, such as Uglu Squares, to connect the lower jaw to the servo horn.

Hang the Skull

Screw a couple of M3 screws into the threaded inserts on the matrix display and knot some filament or cord to make a hanger for the skull.

You can then hang it from the window casing with a small hook or nail.

Plug it in and enjoy the show!

This guide was first published on Nov 09, 2020. It was last updated on Nov 09, 2020.