Make sure you've set up the Matrix Portal with Circuit Python and the necessary libraries as shown on the Code the Pixel Art Display page of this guide. This code uses the same libraries.
Code
Click the Download: Project Zip File link below in the code window to get a zip file with all the files needed for the project. Copy code.py from the zip file and place it on the CIRCUITPY drive.
You'll also need to copy the following files to the CIRCUITPY drive. See the graphic at the top of the page as to filenames and where they go):
- /bmps directory, which contains the sprite sheet .bmp files.
Sprite Sheet Specs
Make sure your sprite sheets are
- .bmp files
- 64 pixels wide
- multiples of 32 pixels high, depending on how many frames there are
- Export as vertical sprite sheets with no border padding. The code will use these dimensions to display the tiles
# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries # # SPDX-License-Identifier: MIT import time import os import board import displayio from digitalio import DigitalInOut, Pull from adafruit_matrixportal.matrix import Matrix from adafruit_debouncer import Debouncer SPRITESHEET_FOLDER = "/bmps" DEFAULT_FRAME_DURATION = 0.1 # 100ms AUTO_ADVANCE_LOOPS = 3 FRAME_DURATION_OVERRIDES = { "three_rings1-sheet.bmp": 0.15, "hop1-sheet.bmp": 0.05, "firework1-sheet.bmp": 0.03, } # --- Display setup --- matrix = Matrix(bit_depth=4) sprite_group = displayio.Group() matrix.display.root_group = sprite_group # --- Button setup --- pin_down = DigitalInOut(board.BUTTON_DOWN) pin_down.switch_to_input(pull=Pull.UP) button_down = Debouncer(pin_down) pin_up = DigitalInOut(board.BUTTON_UP) pin_up.switch_to_input(pull=Pull.UP) button_up = Debouncer(pin_up) auto_advance = True file_list = sorted( [ f for f in os.listdir(SPRITESHEET_FOLDER) if (f.endswith(".bmp") and not f.startswith(".")) ] ) if len(file_list) == 0: raise RuntimeError("No images found") current_image = None current_frame = 0 current_loop = 0 frame_count = 0 frame_duration = DEFAULT_FRAME_DURATION def load_image(): """ Load an image as a sprite """ # pylint: disable=global-statement global current_frame, current_loop, frame_count, frame_duration while sprite_group: sprite_group.pop() filename = SPRITESHEET_FOLDER + "/" + file_list[current_image] # CircuitPython 6 & 7 compatible bitmap = displayio.OnDiskBitmap(open(filename, "rb")) sprite = displayio.TileGrid( bitmap, pixel_shader=getattr(bitmap, 'pixel_shader', displayio.ColorConverter()), tile_width=bitmap.width, tile_height=matrix.display.height, ) # # CircuitPython 7+ compatible # bitmap = displayio.OnDiskBitmap(filename) # sprite = displayio.TileGrid( # bitmap, # pixel_shader=bitmap.pixel_shader, # tile_width=bitmap.width, # tile_height=matrix.display.height, # ) sprite_group.append(sprite) current_frame = 0 current_loop = 0 frame_count = int(bitmap.height / matrix.display.height) frame_duration = DEFAULT_FRAME_DURATION if file_list[current_image] in FRAME_DURATION_OVERRIDES: frame_duration = FRAME_DURATION_OVERRIDES[file_list[current_image]] def advance_image(): """ Advance to the next image in the list and loop back at the end """ # pylint: disable=global-statement global current_image if current_image is not None: current_image += 1 if current_image is None or current_image >= len(file_list): current_image = 0 load_image() def advance_frame(): """ Advance to the next frame and loop back at the end """ # pylint: disable=global-statement global current_frame, current_loop current_frame = current_frame + 1 if current_frame >= frame_count: current_frame = 0 current_loop = current_loop + 1 sprite_group[0][0] = current_frame advance_image() while True: if auto_advance and current_loop >= AUTO_ADVANCE_LOOPS: advance_image() button_down.update() button_up.update() if button_up.fell: auto_advance = not auto_advance if button_down.fell: advance_image() advance_frame() time.sleep(frame_duration)
How it Works
Libraries
Here's how the code works. First we import necessary libraries including dislpayio
and adafruit_matrixportal
to handle the TileGrid display.
Variables
We use a few variables that are user adjustable to fine tune the way the playback works. DEFAULT_FRAME_DURATION = 0.1
sets the frame-rate to 10fps, a good starting point, as it mimics the default playback rate in many pixel animation programs.
You can also set your own framerate overrides per sprite sheet as shown here:
FRAME_DURATION_OVERRIDES = { "three_rings1-sheet.bmp": 0.15, "hop1-sheet.bmp": 0.05, "firework1-sheet.bmp": 0.03, }
The AUTO_ADVANCE_LOOPS = 3
variable specifies how many times to run through each animation before advancing to the next one.
Setup
The display and button setups are next, followed by some variables used to track the state of the playback later.
# --- Display setup --- matrix = Matrix(bit_depth=4) sprite_group = displayio.Group(max_size=1) matrix.display.root_group = sprite_group # --- Button setup --- pin_down = DigitalInOut(board.BUTTON_DOWN) pin_down.switch_to_input(pull=Pull.UP) button_down = Debouncer(pin_down) pin_up = DigitalInOut(board.BUTTON_UP) pin_up.switch_to_input(pull=Pull.UP) button_up = Debouncer(pin_up) auto_advance = True file_list = sorted( [ f for f in os.listdir(SPRITESHEET_FOLDER) if (f.endswith(".bmp") and not f.startswith(".")) ] ) if len(file_list) == 0: raise RuntimeError("No images found") current_image = None current_frame = 0 current_loop = 0 frame_count = 0 frame_duration = DEFAULT_FRAME_DURATION
Image Loading Fuction
The load_image()
function is where the first part of the sprite sheet magic happens! The key moment is where the displayio.TileGrid is defined to set the tile_height = matrix.display.heightÂ
which in the case of our dislpay is 32 pixels. This effectively slices up the sprite sheet into the individual frames for display.
def load_image(): """ Load an image as a sprite """ # pylint: disable=global-statement global current_frame, current_loop, frame_count, frame_duration while sprite_group: sprite_group.pop() bitmap = displayio.OnDiskBitmap( open(SPRITESHEET_FOLDER + "/" + file_list[current_image], "rb") ) frame_count = int(bitmap.height / matrix.display.height) frame_duration = DEFAULT_FRAME_DURATION if file_list[current_image] in FRAME_DURATION_OVERRIDES: frame_duration = FRAME_DURATION_OVERRIDES[file_list[current_image]] sprite = displayio.TileGrid( bitmap, pixel_shader=displayio.ColorConverter(), width=1, height=1, tile_width=bitmap.width, tile_height=matrix.display.height, ) sprite_group.append(sprite) current_frame = 0 current_loop = 0
The advance_image() function is used to select the next sprite sheet when it is time.
def advance_image(): """ Advance to the next image in the list and loop back at the end """ # pylint: disable=global-statement global current_image if current_image is not None: current_image += 1 if current_image is None or current_image >= len(file_list): current_image = 0 load_image()
And, the final part of setup is the creation of the advance_frame() function, which allows the sprite sheet to move from "frame" to "frame" as it works it's way down the sprite sheet.
def advance_frame(): """ Advance to the next frame and loop back at the end """ # pylint: disable=global-statement global current_frame, current_loop current_frame = current_frame + 1 if current_frame >= frame_count: current_frame = 0 current_loop = current_loop + 1 sprite_group[0][0] = current_frame
Main Loop
The main loop of the program runs the advance image and advance frame functions, while also checking for button_down and button_up events.
The Up button stops the auto advance from animation to animations, constantly looping just one animation.
The Down button advances manually to the next animation.
while True: if auto_advance and current_loop >= AUTO_ADVANCE_LOOPS: advance_image() button_down.update() button_up.update() if button_up.fell: auto_advance = not auto_advance if button_down.fell: advance_image() advance_frame() time.sleep(frame_duration)
Page last edited January 22, 2025
Text editor powered by tinymce.