After tinkering with the Pimoroni Galactic Unicorn LED panel for almost a year, I decided to put together a few different techniques into an animated display for Hallowe'en. I've combined custom sprite animation with scrolling, bordered text, fading between colours and use of bounding boxes to only target one part of the screen.
The setup
First of all, flash the Pico with the Pimoroni custom build of MicroPython, which has built-in support for controlling all of the features of the device as well as the PicoGraphics and Bitbank PNGdec libraries.
The main file imports the standard libraries, initiates the classes needed for the display, creates constants for the display width and height and defines some pen colours:
from galactic import GalacticUnicorn
from picographics import PicoGraphics, DISPLAY_GALACTIC_UNICORN
gu = GalacticUnicorn()
display = PicoGraphics(display=DISPLAY_GALACTIC_UNICORN)
WIDTH = GalacticUnicorn.WIDTH
HEIGHT = GalacticUnicorn.HEIGHT
# Define colours
BLACK = display.create_pen(0, 0, 0)
SKY = display.create_pen(20, 13, 237)
RED = display.create_pen(96, 0, 0)
YELLOW = display.create_pen(127, 80, 0)
Sprite class
I've used the PNG sprite class I created in my article on the Pimoroni Display Pack, which has a similar API to the built-in PicoGraphics sprite but uses PNGs directly rather than converting them, which may cause memory problems for larger images, but with the images I used I had no such issues on the Pico 2 version of the Unicorn.
Here is the class, which I put in its own file:
# pngsprite.py
from pngdec import PNG
class PNGSprite:
def __init__(self, display, png_file, width, height):
self.png_file = png_file
self.width = width
self.height = height
self.display = display
def get_sprite(self, sx, sy, dx, dy, scale=1):
png = PNG(self.display)
png.open_file(self.png_file)
png.decode(dx, dy, scale, source=(sx * self.width, sy * self.height, self.width, self.height))
A spritesheet should be a PNG file in which each sprite image has the same width and the same height. They should be side-by-side on the PNG image with no gaps between them, but there can also be multiple rows.
The three spritesheets used here are defined like this:
from pngsprite import PNGSprite
ghost_sprites = PNGSprite(display, "pacman_ghosts.png", 11, 11)
bat_sprite = PNGSprite(display, "bat3.png", 31, 13)
pumpkin_sprite = PNGSprite(display, "pumpkins.png", 13, 11)
The display variable is passed in, initiated from the PicoGraphics library, then the name of the image file as a string, then the width of each image in the spritesheet in pixels, then the height.
To grab a single image from the spritesheet and display it, you can use the get_sprite() method like so:
spritesheet.get_sprite(sx, sy, dx. dy, scale)
Where sx is the number of the sprite counting from the left and starting from zero on the spritesheet, sy is the number counting down, dx is the number of pixels from the left of the screen where it should be placed (again from zero) and dy the number of pixels from the top.
scale is optional and defaults to 1. It allows scaling up of the image only and only in whole numbers.
Utility functions
There are a few functions that perform useful tasks defined in the main file.
clear_to_colour(), as the name suggests, clears the screen to a particular colour, taking a Pen object, and immediately updates the display.
def clear_to_colour(colour):
display.set_pen(colour)
display.clear()
gu.update(display)
fade_colour() will take two colours and fade between them, taking as individual parameters the R, G and B values of the colours to move from and to, the number of steps to use between them and the delay between each step (optional, defaults to 0.1 seconds).
from utime import sleep
def fade_colour(startR, startG, startB, endR, endG, endB, steps, delay = 0.1):
for i in range(steps):
r = startR + (endR - startR) * i // steps
g = startG + (endG - startG) * i // steps
b = startB + (endB - startB) * i // steps
clear_to_colour(display.create_pen(r, g, b))
sleep(delay)
Sequence player
The final function plays a sequence defined in a separate variable. I put these sequences in a separate file and imported them:
from sequences import ghosts, bats, pumpkin, pumpkin2
Here is the sequence player:
def play_sequence(sequence, spritesheet, background):
for frame in sequence:
display.set_pen(background)
display.clear()
for sprite in frame["frame"]:
scale = frame["scale"] if "scale" in frame else 1
spritesheet.get_sprite(*sprite, scale)
gu.update(display)
sleep(frame["pause"])
It takes a list of frames in the sequence, a PNGSprite object and a background colour as a Pen object.
The frames in the sequence list are dictionaries that must have "frame" and "pause" entries and can also have an optional "scale".
frame is a list consisting of a tuple for each object that should be on screen for that frame. There should be four integers in that tuple: the number across in the spritesheet of the desired sprite, the number down, the position across the screen from the left where it should be and the position down.
The pause is the time that this frame lingers on the screen before moving to the next frame.
scale is an integer allowing the image to be scaled up on that frame, defaulting to 1
For example:
ghosts = [
{"frame": [(2, 0, -10, 0)], "pause": 0.1},
{"frame": [(3, 0, -9, 0)], "pause": 0.1},
{"frame": [(2, 0, -8, 0)], "pause": 0.1},
{"frame": [(3, 0, -7, 0)], "pause": 0.1},
...
]
This system works well if all sprites are from the spritesheet but falls down if you need to mix images from different sheets.
The sequences
The different sequences are played in a while True infinite loop.
Ghosts
The ghost sequence is pretty straightforward, if a little tedious to programme in individual steps. The spritesheet has Pacman-style ghosts in three different colours on three rows, each with two images of them going left and two going right and a slight animation between the two.
play_sequence(ghosts, ghost_sprites, BLACK)
Bat
This uses the fade to colour function then utilises a spritesheet of a single image that flies past a couple of times then uses scaling to make it seem like it flies at the screen.
fade_colour(0, 0, 0, 20, 13, 237, 10)
play_sequence(bats, bat_sprite, SKY)
clear_to_colour(BLACK)
Pumpkin
This one is a bit more complicated. Firstly a sequence is played of the pumpkin appearing and animating its mouth:
play_sequence(pumpkin, pumpkin_sprite, BLACK)
The blank area next to the pumpkin is defined as a clipping area so the text animation will not bleed into the pumpkin.
win_width = 41
display.set_clip(14, 0, win_width, 11)
The parameters for the scrolling text are defined:
text = "Happy Hallowe'en"
win_x = 14
win_y = 4
scale = 0.4
display.set_font("serif")
msg_width = display.measure_text(text, scale)
In a loop, the text is drawn with a shadow appearing from the right and gradually scrolling off to the left.
for z in range (msg_width + win_width + 2):
x = win_x - z + win_width
y = win_y
clear_to_colour(BLACK)
# Draw shadow
display.set_pen(YELLOW)
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
display.text(text, x + dx, y + dy, -1, scale)
# Draw text
display.set_pen(RED)
display.text(text, x, y, -1, scale)
gu.update(display)
sleep(0.1)
The clipping bounds are removed and the pumpkin moves through a different sequence, crossing the screen, before the whole loop goes back to the beginning.
display.remove_clip()
play_sequence(pumpkin2, pumpkin_sprite, BLACK)
clear_to_colour(BLACK)
sleep(1)
The full listing together with my images can be found on Github at https://github.com/djchadderton/halloween-unicorn. It is currently playing in my front window.
I hope you have fun playing around with some of the ideas. I was wondering about turning the display to the vertical for fireworks for 5 November (Bonfire Night here in the UK), then there's Christmas...



Top comments (0)