Animated Message Board Working

This commit is contained in:
Melissa LeBlanc-Williams 2023-08-14 14:05:12 -07:00
parent b5eb25620a
commit 3ca8f9a715
26 changed files with 21497 additions and 0 deletions

View file

@ -0,0 +1,23 @@
# 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")
while True:
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)
messageboard.animate(message, "Scroll", "in_from_right")
time.sleep(1)
messageboard.animate(message, "Scroll", "out_to_left")

View file

@ -0,0 +1,81 @@
# 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")
fontpool.add_font("comic", "fonts/Comic-10.pcf")
fontpool.add_font("dejavu", "fonts/DejaVuSans-10.pcf")
message = Message(fontpool.find_font("terminal"), opacity=0.8)
message.add_image("images/maskedstar.bmp")
message.add_text("Hello World!", color=0xFFFF00, x_offset=2, y_offset=2)
message1 = Message(fontpool.find_font("dejavu"))
message2 = Message(fontpool.find_font("comic"), mask_color=0x00FF00)
print("add blinka")
message2.add_image("images/maskedblinka.bmp")
print("add text")
message2.add_text("CircuitPython", color=0xFFFF00, y_offset=-2)
message3 = Message(fontpool.find_font("dejavu"))
message3.add_text("circuitpython.com", color=0xFF0000)
message4 = Message(fontpool.find_font("arial"))
message4.add_text("Buy Electronics", color=0xFFFFFF)
while True:
message1.clear()
message1.add_text("Scroll Text In", color=0xFF0000)
messageboard.animate(message1, "Scroll", "in_from_left")
time.sleep(1)
message1.clear()
message1.add_text("Change Messages")
messageboard.animate(message1, "Static", "show")
time.sleep(1)
message1.clear()
message1.add_text("And Scroll Out")
messageboard.animate(message1, "Static", "show")
messageboard.animate(message1, "Scroll", "out_to_right")
time.sleep(1)
message1.clear()
message1.add_text("Or more effects like looping ", color=0xFFFF00)
messageboard.animate(
message1, "Split", "in_vertically"
) # Split never completely joins
messageboard.animate(
message1, "Loop", "left"
) # Text too high (probably from split)
messageboard.animate(
message1, "Static", "flash", count=3
) # Flashes in weird positions
messageboard.animate(message1, "Split", "out_vertically")
time.sleep(1)
messageboard.animate(message2, "Static", "fade_in")
time.sleep(1)
messageboard.animate(message2, "Static", "fade_out")
messageboard.set_background(0x00FF00)
messageboard.animate(message3, "Scroll", "in_from_top")
time.sleep(1)
messageboard.animate(message3, "Scroll", "out_to_bottom")
messageboard.set_background("images/background.bmp")
messageboard.animate(message4, "Scroll", "in_from_right")
time.sleep(1)

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View file

@ -0,0 +1,158 @@
# 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)
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
) # 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 _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.
"""
self._position = (x, y)
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)
# 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
if 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
image = new_image
# If the image is taller than the display buffer, we need to shrink it
if 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
image = new_image
# 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

View file

@ -0,0 +1,24 @@
# 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)):
self._display = display
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
)

View file

@ -0,0 +1,146 @@
# 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)

View file

@ -0,0 +1,170 @@
# 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
)

View file

@ -0,0 +1,222 @@
# 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)

View file

@ -0,0 +1,101 @@
# 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)

View file

@ -0,0 +1,58 @@
# 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.show(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

View file

@ -0,0 +1,27 @@
# 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

View file

@ -0,0 +1,144 @@
# 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