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.