Once you've finished setting up your MatrixPortal S3 with CircuitPython, you can access the code, fonts, images and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import time from adafruit_matrixportal.matrix import Matrix from messageboard import MessageBoard from messageboard.fontpool import FontPool from messageboard.message import Message matrix = Matrix(width=128, height=16, bit_depth=5) messageboard = MessageBoard(matrix) messageboard.set_background("images/background.bmp") fontpool = FontPool() fontpool.add_font("arial", "fonts/Arial-10.pcf") # Create the message ahead of time message = Message(fontpool.find_font("arial"), mask_color=0xFF00FF, opacity=0.8) message.add_image("images/maskedstar.bmp") message.add_text("Hello World!", color=0xFFFF00, x_offset=2, y_offset=2) while True: # Animate the message messageboard.animate(message, "Scroll", "in_from_right") time.sleep(1) messageboard.animate(message, "Scroll", "out_to_left")
After downloading the Project Bundle, plug your MatrixPortal S3 into the computer's USB port. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the MatrixPortal S3's CIRCUITPY drive.
- lib folder
- fonts folder
- images folder
- code.py
- demo.py
Your MatrixPortal S3 CIRCUITPY drive should look like this after copying the fonts, images, and lib folders and the code.py and demo.py files.

Code Overview
The majority of this code is written as a library. There are a couple of demos, which will be covered in the Usage page, but the overview will go over the library part. The code relies heavily on the CircuitPython bitmaptools
and displayio
modules in order to to the graphics work. The library classes can be broken up into a couple of different categories. You can find more information about displayio
in the CircuitPython Display Support Using displayio guide.
Double Buffering
Double Buffering is handled by the DoubleBuffer
class. This class works by creating 2 sets of displayio
Bitmaps, TileGrids, and Groups. It sets 1 of the bitmaps as the "active" buffer, meaning the buffer that is currently being drawn to. Once it is ready to display, the show()
function is called, which will set the group of the active buffer to be displayed and then swap the buffers. By doing this, it avoids displaying small changes as they are drawn, which can lead to some flickering, and switches everything all at once. Both buffer bitmaps share the same shader, so it is best to keep consistent with the same bit depth of all bitmaps.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import displayio class DoubleBuffer: def __init__(self, display, width, height, shader=None, bit_depth=16): self._buffer_group = (displayio.Group(), displayio.Group()) self._buffer = ( displayio.Bitmap(width, height, 2**bit_depth - 1), displayio.Bitmap(width, height, 2**bit_depth - 1), ) self._x_offset = display.width - width self._y_offset = display.height - height self.display = display self._active_buffer = 0 # The buffer we are updating if shader is None: shader = displayio.ColorConverter() buffer0_sprite = displayio.TileGrid( self._buffer[0], pixel_shader=shader, x=self._x_offset, y=self._y_offset, ) self._buffer_group[0].append(buffer0_sprite) buffer1_sprite = displayio.TileGrid( self._buffer[1], pixel_shader=shader, x=self._x_offset, y=self._y_offset, ) self._buffer_group[1].append(buffer1_sprite) def show(self, swap=True): self.display.root_group = self._buffer_group[self._active_buffer] if swap: self.swap() def swap(self): self._active_buffer = 0 if self._active_buffer else 1 @property def active_buffer(self): return self._buffer[self._active_buffer] @property def shader(self): return self._buffer_group[0][0].pixel_shader @shader.setter def shader(self, shader): self._buffer_group[0][0].pixel_shader = shader self._buffer_group[1][0].pixel_shader = shader
Message
A Message
is simply class that uses a bitmap which contains one or more labels or images placed onto it. When displayed, it acts as the foreground which can be animated on the sign. The message's bitmap buffer is automatically enlarged as content is added to it so that it only takes up as much space as it needs. The mask_color
and opacity
can be set to control how it is blended into the background.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import bitmaptools import displayio import adafruit_imageload from adafruit_display_text import bitmap_label class Message: def __init__( self, font, opacity=1.0, mask_color=0xFF00FF, blendmode=bitmaptools.BlendMode.Normal, ): self._current_font = font self._current_color = 0xFF0000 self._buffer = displayio.Bitmap(0, 0, 65535) self._cursor = [0, 0] self.opacity = opacity self._blendmode = blendmode self._mask_color = 0 self.mask_color = mask_color self._width = 0 self._height = 0 def _enlarge_buffer(self, width, height): """Resize the message buffer to grow as necessary""" new_width = self._width if self._cursor[0] + width >= self._width: new_width = self._cursor[0] + width new_height = self._height if self._cursor[1] + height >= self._height: new_height = self._cursor[1] + height if new_width > self._width or new_height > self._height: new_buffer = displayio.Bitmap(new_width, new_height, 65535) if self._mask_color is not None: bitmaptools.fill_region( new_buffer, 0, 0, new_width, new_height, self._mask_color ) bitmaptools.blit(new_buffer, self._buffer, 0, 0) self._buffer = new_buffer self._width = new_width self._height = new_height def _add_bitmap(self, bitmap, x_offset=0, y_offset=0): new_width, new_height = ( self._cursor[0] + bitmap.width + x_offset, self._cursor[1] + bitmap.height + y_offset, ) # Resize the buffer if necessary self._enlarge_buffer(new_width, new_height) # Blit the image into the buffer source_left, source_top = 0, 0 if self._cursor[0] + x_offset < 0: source_left = 0 - (self._cursor[0] + x_offset) x_offset = 0 if self._cursor[1] + y_offset < 0: source_top = 0 - (self._cursor[1] + y_offset) y_offset = 0 bitmaptools.blit( self._buffer, bitmap, self._cursor[0] + x_offset, self._cursor[1] + y_offset, x1=source_left, y1=source_top, ) # Move the cursor self._cursor[0] += bitmap.width + x_offset def add_text( self, text, color=None, font=None, x_offset=0, y_offset=0, ): if font is None: font = self._current_font if color is None: color = self._current_color color_565value = displayio.ColorConverter().convert(color) # Create a bitmap label and add it to the buffer bmp_label = bitmap_label.Label(font, text=text) color_overlay = displayio.Bitmap( bmp_label.bitmap.width, bmp_label.bitmap.height, 65535 ) color_overlay.fill(color_565value) mask_overlay = displayio.Bitmap( bmp_label.bitmap.width, bmp_label.bitmap.height, 65535 ) mask_overlay.fill(self._mask_color) bitmaptools.blit(color_overlay, bmp_label.bitmap, 0, 0, skip_source_index=1) bitmaptools.blit( color_overlay, mask_overlay, 0, 0, skip_dest_index=color_565value ) bmp_label = None self._add_bitmap(color_overlay, x_offset, y_offset) def add_image(self, image, x_offset=0, y_offset=0): # Load the image with imageload and add it to the buffer bmp_image, _ = adafruit_imageload.load(image) self._add_bitmap(bmp_image, x_offset, y_offset) def clear(self): """Clear the canvas content, but retain all of the style settings""" self._buffer = displayio.Bitmap(0, 0, 65535) self._cursor = [0, 0] self._width = 0 self._height = 0 @property def buffer(self): """Return the current buffer""" if self._width == 0 or self._height == 0: raise RuntimeError("No content in the message") return self._buffer @property def mask_color(self): """Get or Set the mask color""" return self._mask_color @mask_color.setter def mask_color(self, value): self._mask_color = displayio.ColorConverter().convert(value) @property def blendmode(self): """Get or Set the blendmode""" return self._blendmode @blendmode.setter def blendmode(self, value): if value in bitmaptools.BlendMode: self._blendmode = value
Font Pool
The FontPool
class is a simple font loader and dictionary that holds the loaded fonts so that they don't need to be duplicated for multiple messages.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import terminalio from adafruit_bitmap_font import bitmap_font class FontPool: def __init__(self): """Create a pool of fonts for reuse to avoid loading duplicates""" self._fonts = {} self.add_font("terminal") def add_font(self, name, file=None): if name in self._fonts: return if name == "terminal": font = terminalio.FONT else: font = bitmap_font.load_font(file) self._fonts[name] = font def find_font(self, name): if name in self._fonts: return self._fonts[name] return None
MessageBoard
The MessageBoard
class is the main container class and as the name implies, it controls the message board. The message board allows setting what the current background should be and will be used when the animate command is used. The animate()
function is used to dynamically load and run an animation, which will perform an action on the text until it reaches the end of the function. The _draw()
function is a callback that is passed into the animation class when it is instantiated and is the function primarily responsible for drawing each frame of the animation.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import bitmaptools import displayio import adafruit_imageload from .doublebuffer import DoubleBuffer from .message import Message class MessageBoard: def __init__(self, matrix): self.fonts = {} self.display = matrix.display self._buffer_width = self.display.width * 2 self._buffer_height = self.display.height * 2 self._dbl_buf = DoubleBuffer( self.display, self._buffer_width, self._buffer_height ) self._background = None self.set_background() # Set to black self._position = (0, 0) self._shift_count_x = 0 self._shift_count_y = 0 def set_background(self, file_or_color=0x000000): """The background image to a bitmap file.""" if isinstance(file_or_color, str): # its a filenme: background, bg_shader = adafruit_imageload.load(file_or_color) self._dbl_buf.shader = bg_shader self._background = background elif isinstance(file_or_color, int): # Make a background color fill bg_shader = displayio.ColorConverter( input_colorspace=displayio.Colorspace.RGB565 ) background = displayio.Bitmap( self.display.width, self.display.height, 65535 ) background.fill(displayio.ColorConverter().convert(file_or_color)) self._dbl_buf.shader = bg_shader self._background = background else: raise RuntimeError("Unknown type of background") def animate(self, message, animation_class, animation_function, **kwargs): anim_class = __import__( f"{self.__module__}.animations.{animation_class.lower()}" ) anim_class = getattr(anim_class, "animations") anim_class = getattr(anim_class, animation_class.lower()) anim_class = getattr(anim_class, animation_class) animation = anim_class( self.display, self._draw, self._position, (self._shift_count_x, self._shift_count_y) ) # Instantiate the class # Call the animation function and pass kwargs along with the message (positional) anim_func = getattr(animation, animation_function) anim_func(message, **kwargs) def set_message_position(self, x, y): """Set the position of the message on the display""" self._position = (x, y) self._shift_count_x = 0 self._shift_count_y = 0 def _draw( self, image, x, y, opacity=None, mask_color=0xFF00FF, blendmode=bitmaptools.BlendMode.Normal, post_draw_position=None, ): """Draws a message to the buffer taking its current settings into account. It also sets the current position and performs a swap. """ buffer_x_offset = self._buffer_width - self.display.width buffer_y_offset = self._buffer_height - self.display.height # Image can be a message in which case its properties will be used if isinstance(image, Message): if opacity is None: opacity = image.opacity mask_color = image.mask_color blendmode = image.blendmode image = image.buffer if opacity is None: opacity = 1.0 if mask_color > 65535: mask_color = displayio.ColorConverter().convert(mask_color) # New significantly shorter message, so adjust the position while image.width + x < 0: x += self.display.width while image.height + y < 0: y += self.display.height self._position = (x, y) # Blit the background bitmaptools.blit( self._dbl_buf.active_buffer, self._background, buffer_x_offset, buffer_y_offset, ) # If the image is wider than the display buffer, we need to shrink it shift_count = 0 while x + buffer_x_offset < 0: new_image = displayio.Bitmap( image.width - self.display.width, image.height, 65535 ) bitmaptools.blit( new_image, image, 0, 0, x1=self.display.width, y1=0, x2=image.width, y2=image.height, ) x += self.display.width self._position = (x, y) # Update the stored position shift_count += 1 image = new_image self._shift_count_x = shift_count # If the image is taller than the display buffer, we need to shrink it shift_count = 0 while y + buffer_y_offset < 0: new_image = displayio.Bitmap( image.width, image.height - self.display.height, 65535 ) bitmaptools.blit( new_image, image, 0, 0, x1=0, y1=self.display.height, x2=image.width, y2=image.height, ) y += self.display.height self._position = (x, y) # Update the stored position shift_count += 1 image = new_image self._shift_count_y = shift_count # Clear the foreground buffer foreground_buffer = displayio.Bitmap( self._buffer_width, self._buffer_height, 65535 ) foreground_buffer.fill(mask_color) bitmaptools.blit( foreground_buffer, image, x + buffer_x_offset, y + buffer_y_offset ) # Blend the foreground buffer into the main buffer bitmaptools.alphablend( self._dbl_buf.active_buffer, self._dbl_buf.active_buffer, foreground_buffer, displayio.Colorspace.RGB565, 1.0, opacity, blendmode=blendmode, skip_source2_index=mask_color, ) self._dbl_buf.show() # Allow for an override of the position after drawing (needed for split effects) if post_draw_position is not None and isinstance(post_draw_position, tuple): self._position = post_draw_position
Animation Classes
All of the animation classes are built on top of the base Animation
class, which includes any functions that are shared with the different categories.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import time class Animation: def __init__(self, display, draw_callback, starting_position=(0, 0), shift_count=(0, 0)): self._display = display starting_position = ( starting_position[0] - shift_count[0] * self._display.width, starting_position[1] - shift_count[1] * self._display.height ) self._position = starting_position self._draw = draw_callback @staticmethod def _wait(start_time, duration): """Uses time.monotonic() to wait from the start time for a specified duration""" while time.monotonic() < (start_time + duration): pass return time.monotonic() def _get_centered_position(self, message): return int(self._display.width / 2 - message.buffer.width / 2), int( self._display.height / 2 - message.buffer.height / 2 )
Scroll Animations
The Scroll
animation class contains 8 different main animation functions plus 1 generic animation function, which the main functions call. The 8 functions include a scroll in and scroll out function for each of the 4 directions. Functions beginning with out_to cause the content to scroll offscreen and functions beginning with in_from cause the content to scroll towards the center of the screen.
The generic scroll_from_to()
function allows for scrolling from one coordinate to another. It could be used to make text scroll at a diagonal angle.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import time from . import Animation class Scroll(Animation): def scroll_from_to(self, message, duration, start_x, start_y, end_x, end_y): """ Scroll the message from one position to another over a certain period of time. :param message: The message to animate. :param float duration: The period of time to perform the animation over in seconds. :param int start_x: The Starting X Position :param int start_yx: The Starting Y Position :param int end_x: The Ending X Position :param int end_y: The Ending Y Position :type message: Message """ steps = max(abs(end_x - start_x), abs(end_y - start_y)) if not steps: return increment_x = (end_x - start_x) / steps increment_y = (end_y - start_y) / steps for i in range(steps + 1): start_time = time.monotonic() current_x = start_x + round(i * increment_x) current_y = start_y + round(i * increment_y) self._draw(message, current_x, current_y) if i <= steps: self._wait(start_time, duration / steps) def out_to_left(self, message, duration=1): """Scroll a message off the display from its current position towards the left over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :type message: Message """ current_x, current_y = self._position self.scroll_from_to( message, duration, current_x, current_y, 0 - message.buffer.width, current_y ) def in_from_left(self, message, duration=1, x=0): """Scroll a message in from the left side of the display over a certain period of time. The final position is centered. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :param int x: (optional) The amount of x-offset from the center position (default=0) :type message: Message """ center_x, center_y = self._get_centered_position(message) self.scroll_from_to( message, duration, 0 - message.buffer.width, center_y, center_x + x, center_y, ) def in_from_right(self, message, duration=1, x=0): """Scroll a message in from the right side of the display over a certain period of time. The final position is centered. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :param int x: (optional) The amount of x-offset from the center position (default=0) :type message: Message """ center_x, center_y = self._get_centered_position(message) self.scroll_from_to( message, duration, self._display.width - 1, center_y, center_x + x, center_y ) def in_from_top(self, message, duration=1, y=0): """Scroll a message in from the top side of the display over a certain period of time. The final position is centered. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :param int y: (optional) The amount of y-offset from the center position (default=0) :type message: Message """ center_x, center_y = self._get_centered_position(message) self.scroll_from_to( message, duration, center_x, 0 - message.buffer.height, center_x, center_y + y, ) def in_from_bottom(self, message, duration=1, y=0): """Scroll a message in from the bottom side of the display over a certain period of time. The final position is centered. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :param int y: (optional) The amount of y-offset from the center position (default=0) :type message: Message """ center_x, center_y = self._get_centered_position(message) self.scroll_from_to( message, duration, center_x, self._display.height - 1, center_x, center_y + y, ) def out_to_right(self, message, duration=1): """Scroll a message off the display from its current position towards the right over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :type message: Message """ current_x, current_y = self._position self.scroll_from_to( message, duration, current_x, current_y, self._display.width - 1, current_y ) def out_to_top(self, message, duration=1): """Scroll a message off the display from its current position towards the top over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :type message: Message """ current_x, current_y = self._position self.scroll_from_to( message, duration, current_x, current_y, current_x, 0 - message.buffer.height, ) def out_to_bottom(self, message, duration=1): """Scroll a message off the display from its current position towards the bottom over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over in seconds. (default=1) :type message: Message """ current_x, current_y = self._position self.scroll_from_to( message, duration, current_x, current_y, current_x, self._display.height - 1 )
Loop Animations
The Loop
animation class contains functions which are similar to the scroll animations, except they cause the content to wrap back in from the opposite side. There are 4 functions for each of the directions.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import time import displayio import bitmaptools from . import Animation class Loop(Animation): def _create_loop_image(self, image, x_offset, y_offset, mask_color): """Attach a copy of an image by a certain offset so it can be looped.""" if 0 < x_offset < self._display.width: x_offset = self._display.width if 0 < y_offset < self._display.height: y_offset = self._display.height loop_image = displayio.Bitmap( image.width + x_offset, image.height + y_offset, 65535 ) loop_image.fill(mask_color) bitmaptools.blit(loop_image, image, 0, 0) bitmaptools.blit(loop_image, image, x_offset, y_offset) return loop_image def left(self, message, duration=1, count=1): """Loop a message towards the left side of the display over a certain period of time by a certain number of times. The message will re-enter from the right and end up back a the starting position. :param message: The message to animate. :param float count: (optional) The number of times to loop. (default=1) :param float duration: (optional) The period of time to perform the animation over. (default=1) :type message: Message """ current_x, current_y = self._position distance = max(message.buffer.width, self._display.width) loop_image = self._create_loop_image( message.buffer, distance, 0, message.mask_color ) for _ in range(count): for _ in range(distance): start_time = time.monotonic() current_x -= 1 if current_x < 0 - message.buffer.width: current_x += distance self._draw( loop_image, current_x, current_y, message.opacity, ) self._wait(start_time, duration / distance / count) def right(self, message, duration=1, count=1): """Loop a message towards the right side of the display over a certain period of time by a certain number of times. The message will re-enter from the left and end up back a the starting position. :param message: The message to animate. :param float count: (optional) The number of times to loop. (default=1) :param float duration: (optional) The period of time to perform the animation over. (default=1) :type message: Message """ current_x, current_y = self._position distance = max(message.buffer.width, self._display.width) loop_image = self._create_loop_image( message.buffer, distance, 0, message.mask_color ) for _ in range(count): for _ in range(distance): start_time = time.monotonic() current_x += 1 if current_x > 0: current_x -= distance self._draw( loop_image, current_x, current_y, message.opacity, ) self._wait(start_time, duration / distance / count) def up(self, message, duration=0.5, count=1): """Loop a message towards the top side of the display over a certain period of time by a certain number of times. The message will re-enter from the bottom and end up back a the starting position. :param message: The message to animate. :param float count: (optional) The number of times to loop. (default=1) :param float duration: (optional) The period of time to perform the animation over. (default=1) :type message: Message """ current_x, current_y = self._position distance = max(message.buffer.height, self._display.height) loop_image = self._create_loop_image( message.buffer, 0, distance, message.mask_color ) for _ in range(count): for _ in range(distance): start_time = time.monotonic() current_y -= 1 if current_y < 0 - message.buffer.height: current_y += distance self._draw( loop_image, current_x, current_y, message.opacity, ) self._wait(start_time, duration / distance / count) def down(self, message, duration=0.5, count=1): """Loop a message towards the bottom side of the display over a certain period of time by a certain number of times. The message will re-enter from the top and end up back a the starting position. :param message: The message to animate. :param float count: (optional) The number of times to loop. (default=1) :param float duration: (optional) The period of time to perform the animation over. (default=1) :type message: Message """ current_x, current_y = self._position distance = max(message.buffer.height, self._display.height) loop_image = self._create_loop_image( message.buffer, 0, distance, message.mask_color ) for _ in range(count): for _ in range(distance): start_time = time.monotonic() current_y += 1 if current_y > 0: current_y -= distance self._draw( loop_image, current_x, current_y, message.opacity, ) self._wait(start_time, duration / distance / count)
Split Animations
The Split
animation class contains 4 functions as well. There are out functions and in functions, that cause the content to split out and join in respectively, for both the horizontal and vertical directions.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import time import displayio import bitmaptools from . import Animation class Split(Animation): def out_horizontally(self, message, duration=0.5): """Show the effect of a message splitting horizontally over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over. (default=0.5) :type message: Message """ current_x, current_y = self._position image = message.buffer left_image = displayio.Bitmap(image.width // 2, image.height, 65535) bitmaptools.blit( left_image, image, 0, 0, x1=0, y1=0, x2=image.width // 2, y2=image.height ) right_image = displayio.Bitmap(image.width // 2, image.height, 65535) bitmaptools.blit( right_image, image, 0, 0, x1=image.width // 2, y1=0, x2=image.width, y2=image.height, ) distance = self._display.width // 2 for i in range(distance + 1): start_time = time.monotonic() effect_buffer = displayio.Bitmap( self._display.width + image.width, image.height, 65535 ) effect_buffer.fill(message.mask_color) bitmaptools.blit(effect_buffer, left_image, distance - i, 0) bitmaptools.blit( effect_buffer, right_image, distance + image.width // 2 + i, 0 ) self._draw( effect_buffer, current_x - self._display.width // 2, current_y, message.opacity, post_draw_position=(current_x - self._display.width // 2, current_y), ) self._wait(start_time, duration / distance) def out_vertically(self, message, duration=0.5): """Show the effect of a message splitting vertically over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over. (default=0.5) :type message: Message """ current_x, current_y = self._position image = message.buffer top_image = displayio.Bitmap(image.width, image.height // 2, 65535) bitmaptools.blit( top_image, image, 0, 0, x1=0, y1=0, x2=image.width, y2=image.height // 2 ) bottom_image = displayio.Bitmap(image.width, image.height // 2, 65535) bitmaptools.blit( bottom_image, image, 0, 0, x1=0, y1=image.height // 2, x2=image.width, y2=image.height, ) distance = self._display.height // 2 effect_buffer_width = self._display.width if current_x < 0: effect_buffer_width -= current_x for i in range(distance + 1): start_time = time.monotonic() effect_buffer = displayio.Bitmap( effect_buffer_width, self._display.height + image.height, 65535 ) effect_buffer.fill(message.mask_color) bitmaptools.blit(effect_buffer, top_image, 0, distance - i) bitmaptools.blit( effect_buffer, bottom_image, 0, distance + image.height // 2 + i + 1 ) self._draw( effect_buffer, current_x, current_y - self._display.height // 2, message.opacity, post_draw_position=(current_x, current_y - self._display.height // 2), ) self._wait(start_time, duration / distance) def in_horizontally(self, message, duration=0.5): """Show the effect of a split message joining horizontally over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over. (default=0.5) :type message: Message """ current_x = int(self._display.width / 2 - message.buffer.width / 2) current_y = int(self._display.height / 2 - message.buffer.height / 2) image = message.buffer left_image = displayio.Bitmap(image.width // 2, image.height, 65535) bitmaptools.blit( left_image, image, 0, 0, x1=0, y1=0, x2=image.width // 2, y2=image.height ) right_image = displayio.Bitmap(image.width // 2, image.height, 65535) bitmaptools.blit( right_image, image, 0, 0, x1=image.width // 2, y1=0, x2=image.width, y2=image.height, ) distance = self._display.width // 2 effect_buffer = displayio.Bitmap( self._display.width + image.width, image.height, 65535 ) effect_buffer.fill(message.mask_color) for i in range(distance + 1): start_time = time.monotonic() bitmaptools.blit(effect_buffer, left_image, i, 0) bitmaptools.blit( effect_buffer, right_image, self._display.width + image.width // 2 - i + 1, 0, ) self._draw( effect_buffer, current_x - self._display.width // 2, current_y, message.opacity, post_draw_position=(current_x, current_y), ) self._wait(start_time, duration / distance) def in_vertically(self, message, duration=0.5): """Show the effect of a split message joining vertically over a certain period of time. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over. (default=0.5) :type message: Message """ current_x = int(self._display.width / 2 - message.buffer.width / 2) current_y = int(self._display.height / 2 - message.buffer.height / 2) image = message.buffer top_image = displayio.Bitmap(image.width, image.height // 2, 65535) bitmaptools.blit( top_image, image, 0, 0, x1=0, y1=0, x2=image.width, y2=image.height // 2 ) bottom_image = displayio.Bitmap(image.width, image.height // 2, 65535) bitmaptools.blit( bottom_image, image, 0, 0, x1=0, y1=image.height // 2, x2=image.width, y2=image.height, ) distance = self._display.height // 2 effect_buffer_width = self._display.width if current_x < 0: effect_buffer_width -= current_x effect_buffer = displayio.Bitmap( effect_buffer_width, self._display.height + image.height, 65535 ) effect_buffer.fill(message.mask_color) for i in range(distance + 1): start_time = time.monotonic() bitmaptools.blit(effect_buffer, top_image, 0, i + 1) bitmaptools.blit( effect_buffer, bottom_image, 0, self._display.height + image.height // 2 - i + 1, ) self._draw( effect_buffer, current_x, current_y - self._display.height // 2, message.opacity, post_draw_position=(current_x, current_y), ) self._wait(start_time, duration / distance)
The Static
animation class is more of a miscellaneous collection of functions that don't provide any motion. It includes functions to show
, hide
, blink
, flash
, fade_in
, and fade_out
content. The static class will always display the message at the position of the last message to be displayed and default to (0,0).
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries # # SPDX-License-Identifier: MIT import time from . import Animation class Static(Animation): def show(self, message): """Show the message at its current position. :param message: The message to show. :type message: Message """ x, y = self._position self._draw(message, x, y) def hide(self, message): """Hide the message at its current position. :param message: The message to hide. :type message: Message """ x, y = self._position self._draw(message, x, y, opacity=0) def blink(self, message, count=3, duration=1): """Blink the foreground on and off a centain number of times over a certain period of time. :param message: The message to animate. :param float count: (optional) The number of times to blink. (default=3) :param float duration: (optional) The period of time to perform the animation over. (default=1) :type message: Message """ delay = duration / count / 2 for _ in range(count): start_time = time.monotonic() self.hide(message) start_time = self._wait(start_time, delay) self.show(message) self._wait(start_time, delay) def flash(self, message, count=3, duration=1): """Fade the foreground in and out a centain number of times over a certain period of time. :param message: The message to animate. :param float count: (optional) The number of times to flash. (default=3) :param float duration: (optional) The period of time to perform the animation over. (default=1) :type message: Message """ delay = duration / count / 2 steps = 50 // count for _ in range(count): self.fade_out(message, duration=delay, steps=steps) self.fade_in(message, duration=delay, steps=steps) def fade_in(self, message, duration=1, steps=50): """Fade the foreground in over a certain period of time by a certain number of steps. More steps is smoother, but too high of a number may slow down the animation too much. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over. (default=1) :param float steps: (optional) The number of steps to perform the animation. (default=50) :type message: Message """ current_x = int(self._display.width / 2 - message.buffer.width / 2) current_y = int(self._display.height / 2 - message.buffer.height / 2) delay = duration / (steps + 1) for opacity in range(steps + 1): start_time = time.monotonic() self._draw(message, current_x, current_y, opacity=opacity / steps) self._wait(start_time, delay) def fade_out(self, message, duration=1, steps=50): """Fade the foreground out over a certain period of time by a certain number of steps. More steps is smoother, but too high of a number may slow down the animation too much. :param message: The message to animate. :param float duration: (optional) The period of time to perform the animation over. (default=1) :param float steps: (optional) The number of steps to perform the animation. (default=50) :type message: Message """ delay = duration / (steps + 1) for opacity in range(steps + 1): start_time = time.monotonic() self._draw( message, self._position[0], self._position[1], opacity=(steps - opacity) / steps, ) self._wait(start_time, delay)
Page last edited February 25, 2025
Text editor powered by tinymce.