Understanding the code

There is a lot going on to bring these images to life on the LED matrix, so let's break things down a little bit.

Begin by importing the necessary modules and ensuring that we can create our display:

import math
import time
import random

import adafruit_imageload.bmp
import board
import displayio
import framebufferio
import rgbmatrix
import ulab

displayio.release_displays()

RGBMatrix doesn't have a brightness control, so the Reshader class uses ulab to quickly apply a brightness value from 0.0 to 1.0 to an original palette, updating the palette accordingly. This code works pretty quickly, especially when you make sure your images have a pallette of just a few dozen colors.

class Reshader:
    '''reshader fades the image to mimic brightness control'''
    def __init__(self, palette):
        self.palette = palette
        ulab_palette = ulab.numpy.zeros((len(palette), 3))
        for i in range(len(palette)):
            rgb = int(palette[i])
            ulab_palette[i, 2] = rgb & 0xff
            ulab_palette[i, 1] = (rgb >> 8) & 0xff
            ulab_palette[i, 0] = rgb >> 16
        self.ulab_palette = ulab_palette

    def reshade(self, brightness):
        '''reshader'''
        palette = self.palette
        shaded = ulab.numpy.array(self.ulab_palette * brightness, dtype=ulab.numpy.uint8)
        for i in range(len(palette)):
            palette[i] = tuple(shaded[i])

do_crawl_down makes an image move from the top of the screen to the bottom. Here are what all the parameters mean:  image_file is the filename of an image. Images up to 64x64 pixels work best. speed is the down speed in pixels per second. weave gives the amount the image weaves left and right, 0 for no weave. weave_speed controls how quickly the weave goes. pulse controls how much the brightness changes, values from 0 (no brightness change) to 0.5 work well.

By working with time in integer nanoseconds, we avoid rounding errors that occur when working with time in floating-point seconds. It makes the code a bit more complicated, but avoids the animation becoming "jerky" after running continuously for more than a day or so.

def do_crawl_down(image_file, *,
                  speed=12, weave=4, pulse=.5,
                  weave_speed=1/6, pulse_speed=1/7):
    '''function to scroll the image'''
    the_bitmap, the_palette = adafruit_imageload.load(
        image_file,
        bitmap=displayio.Bitmap,
        palette=displayio.Palette)

    shader = Reshader(the_palette)

    group = displayio.Group()
    tile_grid = displayio.TileGrid(bitmap=the_bitmap, pixel_shader=the_palette)
    group.append(tile_grid)
    display.show(group)

    start_time = time.monotonic_ns()
    start_y = display.height   # High enough to be "off the top"
    end_y = -the_bitmap.height     # Low enough to be "off the bottom"

    # Mix up how the bobs and brightness change on each run
    r1 = random.random() * math.pi
    r2 = random.random() * math.pi

    y = start_y
    while y > end_y:
        now = time.monotonic_ns()
        y = start_y - speed * ((now - start_time) / 1e9)
        group.y = round(y)

        # wave from side to side
        group.x = round(weave * math.cos(y * weave_speed + r1))

        # Change the brightness
        if pulse > 0:
            shader.reshade((1 - pulse) + pulse * math.sin(y * pulse_speed + r2))

        display.refresh(minimum_frames_per_second=0, target_frames_per_second=60)

do_pulse omits the movement part of do_crawl_down, so it's basically a simplified version of do_crawl_down.

def do_pulse(image_file, *, duration=4, pulse_speed=1/8, pulse=.5):
    '''pulse animation'''
    the_bitmap, the_palette = adafruit_imageload.load(
        image_file,
        bitmap=displayio.Bitmap,
        palette=displayio.Palette)

    shader = Reshader(the_palette)

    group = displayio.Group()
    tile_grid = displayio.TileGrid(bitmap=the_bitmap, pixel_shader=the_palette)
    group.append(tile_grid)
    group.x = (display.width - the_bitmap.width) // 2
    group.y = (display.height - the_bitmap.height) // 2
    display.show(group)

    start_time = time.monotonic_ns()
    end_time = start_time + int(duration * 1e9)

    now_ns = time.monotonic_ns()
    while now_ns < end_time:
        now_ns = time.monotonic_ns()
        current_time = (now_ns - start_time) / 1e9

        shader.reshade((1 - pulse) - pulse
                       * math.cos(2*math.pi*current_time*pulse_speed)**2)

        display.refresh(minimum_frames_per_second=0, target_frames_per_second=60)

Create the display object. Check out our dedicated guide for rgbmatrix to find pinouts for other boards besides the Feather M4 Express!

matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=5,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.A5, board.A4, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)

Choose randomly from 5 different effects. By using different images and different combinations of parameters, there's more variety. Why not try picking your own combinations, and see what you like best! Head to the next page for more information about customizing this with your own images.

# Image playlist - set to run randomly. Set pulse from 0 to .5
while True:
    show_next = random.randint(1, 5) #change to reflect how many images you add
    if show_next == 1:
        do_crawl_down("/ray.bmp")
    elif show_next == 2:
        do_crawl_down("/waves1.bmp", speed=7, weave=0, pulse=.35)
    elif show_next == 3:
        do_crawl_down("/waves2.bmp", speed=9, weave=0, pulse=.35)
    elif show_next == 4:
        do_pulse("/heart.bmp", duration=4, pulse=.45)
    elif show_next == 5:
        do_crawl_down("/dark.bmp")

This guide was first published on Aug 12, 2020. It was last updated on Aug 12, 2020.

This page (Code Explanation) was last updated on Aug 12, 2020.

Text editor powered by tinymce.