You might’ve noticed the prior examples were all a bit code-heavy. If you’d just like to draw the animation, we’ve got a trick for that. There’s a tiny bit of CircuitPython code but it’s really quite simple. The rest is just BMP images like you can produce with most image editing software.

The ideas have been used in other projects already, so rather than reiterate we’ll simply point you to these other tutorials: this guide on “sprite sheet” animation is how the matrix animation works, and this one on bitmap-to-NeoPixel animation for the rings (the EyeLights LEDs aren’t NeoPixels, but the idea is the same). The images sizes here are different, that’s really the only change: 18x5 pixels per frame for the matrix, and 48 pixels tall for the rings.

Yes, the “time axis” for each is different. That was done so we could continue using the guides above for reference, and not need to explain the same principles in a new format.

Matrix Layout:

18x5 pixels, each frame stacked vertically, any height (RAM permitting).

The image should be saved as an indexed color BMP image, 4 or 8 bits per pixel.

LED Ring Layout:

48 pixels tall, any width (RAM permitting). The image should be saved as an indexed color BMP, 4 or 8 bits per pixel.

The matrix and rings share a few pixels in common. The rings are normally drawn “on top” and take precedence over the matrix.

If you just want one or the other (matrix or rings, not both), specify None for the corresponding filename in the source.

The matrix and rings do not need to be the same number of frames. The two parts work independently and it’s okay if the durations don’t match (or do, it’s all good).

Source Code and Example Images

Here’s the project source if you’d like to skim it. 

To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.

Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory EyeLights_BMP_Animation/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.

Your CIRCUITPY drive should now look similar to the following image:

CIRCUITPY
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
EyeLightsAnim example for Adafruit EyeLights (LED Glasses + Driver).
The accompanying eyelights_anim.py provides pre-drawn frame-by-frame
animation from BMP images. Sort of a catch-all for modest projects that may
want to implement some animation without having to express that animation
entirely in code. The idea is based upon two prior projects:

https://learn.adafruit.com/32x32-square-pixel-display/overview
learn.adafruit.com/circuit-playground-neoanim-using-bitmaps-to-animate-neopixels

The 18x5 matrix and the LED rings are regarded as distinct things, fed from
two separate BMPs (or can use just one or the other). The former guide above
uses the vertical axis for time (like a strip of movie film), while the
latter uses the horizontal axis for time (as in audio or video editing).
Despite this contrast, the same conventions are maintained here to avoid
conflicting explanations...what worked in those guides is what works here,
only the resolutions are different. See also the example BMPs.
"""

import time
import board
from busio import I2C
import adafruit_is31fl3741
from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses
from eyelights_anim import EyeLightsAnim


# HARDWARE SETUP -----------------------

i2c = I2C(board.SCL, board.SDA, frequency=1000000)

# Initialize the IS31 LED driver, buffered for smoother animation
glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
glasses.show()  # Clear any residue on startup
glasses.global_current = 20  # Just middlin' bright, please


# ANIMATION SETUP ----------------------

# Two indexed-color BMP filenames are specified: first is for the LED matrix
# portion, second is for the LED rings -- or pass None for one or the other
# if not animating that part. The two elements, matrix and rings, share a
# few LEDs in common...by default the rings appear "on top" of the matrix,
# or you can optionally pass a third argument of False to have the rings
# underneath. There's that one odd unaligned pixel between the two though,
# so this may only rarely be desirable.
anim = EyeLightsAnim(glasses, "matrix.bmp", "rings.bmp")


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

# This example just runs through a repeating cycle. If you need something
# else, like ping-pong animation, or frames based on a specific time, the
# anim.frame() function can optionally accept two arguments: an index for
# the matrix animation, and an index for the rings.

while True:
    anim.frame()  #     Advance matrix and rings by 1 frame and wrap around
    glasses.show()  #   Update LED matrix
    time.sleep(0.02)  # Pause briefly
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
EyeLightsAnim provides EyeLights LED glasses with pre-drawn frame-by-frame
animation from BMP images. Sort of a catch-all for modest projects that may
want to implement some animation without having to express that animation
entirely in code. The idea is based upon two prior projects:

https://learn.adafruit.com/32x32-square-pixel-display/overview
learn.adafruit.com/circuit-playground-neoanim-using-bitmaps-to-animate-neopixels

The 18x5 matrix and the LED rings are regarded as distinct things, fed from
two separate BMPs (or can use just one or the other). The former guide above
uses the vertical axis for time (like a strip of movie film), while the
latter uses the horizontal axis for time (as in audio or video editing).
Despite this contrast, the same conventions are maintained here to avoid
conflicting explanations...what worked in those guides is what works here,
only the resolutions are different."""

import displayio
import adafruit_imageload


def gamma_adjust(palette):
    """Given a color palette that was returned by adafruit_imageload, apply
    gamma correction and place results back in original palette. This makes
    LED brightness and colors more perceptually linear, to better match how
    the source BMP might've appeared on screen."""

    for index, entry in enumerate(palette):
        palette[index] = sum(
            [
                int(((((entry >> shift) & 0xFF) / 255) ** 2.6) * 255 + 0.5) << shift
                for shift in range(16, -1, -8)
            ]
        )


class EyeLightsAnim:
    """Class encapsulating BMP image-based frame animation for the matrix
    and rings of an LED_Glasses object."""

    def __init__(self, glasses, matrix_filename, ring_filename, rings_on_top=True):
        """Constructor for EyeLightsAnim. Accepts an LED_Glasses object and
        filenames for two indexed-color BMP images: first is a "sprite
        sheet" for animating on the matrix portion of the glasses, second is
        a pixels-over-time graph for the rings portion. Either filename may
        be None if not used. Because the matrix and rings share some pixels
        in common, the last argument determines the "stacking order" - which
        of the two bitmaps is drawn later or "on top." Default of True
        places the rings over the matrix, False gives the matrix priority.
        It's possible to use transparent palette indices but that may be
        more trouble than it's worth."""

        self.glasses = glasses
        self.matrix_bitmap = self.ring_bitmap = None
        self.rings_on_top = rings_on_top

        if matrix_filename:
            self.matrix_bitmap, self.matrix_palette = adafruit_imageload.load(
                matrix_filename, bitmap=displayio.Bitmap, palette=displayio.Palette
            )
            if (self.matrix_bitmap.width < glasses.width) or (
                self.matrix_bitmap.height < glasses.height
            ):
                raise ValueError("Matrix bitmap must be at least 18x5 pixels")
            gamma_adjust(self.matrix_palette)
            self.tiles_across = self.matrix_bitmap.width // glasses.width
            self.tiles_down = self.matrix_bitmap.height // glasses.height
            self.matrix_frames = self.tiles_across * self.tiles_down
            self.matrix_frame = self.matrix_frames - 1

        if ring_filename:
            self.ring_bitmap, self.ring_palette = adafruit_imageload.load(
                ring_filename, bitmap=displayio.Bitmap, palette=displayio.Palette
            )
            if self.ring_bitmap.height < 48:
                raise ValueError("Ring bitmap must be at least 48 pixels tall")
            gamma_adjust(self.ring_palette)
            self.ring_frames = self.ring_bitmap.width
            self.ring_frame = self.ring_frames - 1

    def draw_matrix(self, matrix_frame=None):
        """Draw the matrix portion of EyeLights from one frame of the matrix
        bitmap "sprite sheet." Can either request a specific frame index
        (starting from 0), or pass None (or no arguments) to advance by one
        frame, "wrapping around" to beginning if needed. For internal use by
        library; user code should call frame(), not this function."""

        if matrix_frame:  # Go to specific frame
            self.matrix_frame = matrix_frame
        else:  # Advance one frame forward
            self.matrix_frame += 1
        self.matrix_frame %= self.matrix_frames  # Wrap to valid range

        xoffset = self.matrix_frame % self.tiles_across * self.glasses.width
        yoffset = self.matrix_frame // self.tiles_across * self.glasses.height

        for y in range(self.glasses.height):
            y1 = y + yoffset
            for x in range(self.glasses.width):
                idx = self.matrix_bitmap[x + xoffset, y1]
                if not self.matrix_palette.is_transparent(idx):
                    self.glasses.pixel(x, y, self.matrix_palette[idx])

    def draw_rings(self, ring_frame=None):
        """Draw the rings portion of EyeLights from one frame of the rings
        bitmap graph. Can either request a specific frame index (starting
        from 0), or pass None (or no arguments) to advance by one frame,
        'wrapping around' to beginning if needed. For internal use by
        library; user code should call frame(), not this function."""

        if ring_frame:  # Go to specific frame
            self.ring_frame = ring_frame
        else:  # Advance one frame forward
            self.ring_frame += 1
        self.ring_frame %= self.ring_frames  # Wrap to valid range

        for y in range(24):
            idx = self.ring_bitmap[self.ring_frame, y]
            if not self.ring_palette.is_transparent(idx):
                self.glasses.left_ring[y] = self.ring_palette[idx]
            idx = self.ring_bitmap[self.ring_frame, y + 24]
            if not self.ring_palette.is_transparent(idx):
                self.glasses.right_ring[y] = self.ring_palette[idx]

    def frame(self, matrix_frame=None, ring_frame=None):
        """Draw one frame of animation to the matrix and/or rings portions
        of EyeLights. Frame index (starting from 0) for matrix and rings
        respectively can be passed as arguments, or either/both may be None
        to advance by one frame, 'wrapping around' to beginning if needed.
        Because some pixels are shared in common between matrix and rings,
        the "stacking order" -- which of the two appears "on top", is
        specified as an argument to the constructor."""

        if self.matrix_bitmap and self.rings_on_top:
            self.draw_matrix(matrix_frame)

        if self.ring_bitmap:
            self.draw_rings(ring_frame)

        if self.matrix_bitmap and not self.rings_on_top:
            self.draw_matrix(matrix_frame)

This guide was first published on Oct 12, 2021. It was last updated on Mar 19, 2024.

This page (BMP Animation) was last updated on Mar 18, 2024.

Text editor powered by tinymce.