This commit is contained in:
2026-08-03 21:48:39 -04:00
parent 22df5b3a57
commit 392ff3100f
2 changed files with 34 additions and 75 deletions
+34 -74
View File
@@ -1,8 +1,10 @@
import os
import tempfile
import requests
from PIL import Image
from PIL import Image, ImageDraw, ImageFont, BdfFontFile
from time import time
from rgbmatrix import graphics
from vars import PANEL_HEIGHT, PANEL_WIDTH, GAME_WIDTH, DIVIDER_COLOR, Colors, font, font_small, font_super_small
from vars import PANEL_HEIGHT, PANEL_WIDTH, GAME_WIDTH, DIVIDER_COLOR, Colors, font, font_small, font_super_small, ASSET_DIR
players = {
"team1": [],
@@ -19,11 +21,30 @@ SCROLL_REGION_X = STATIC_CARD_WIDTH
SCROLL_REGION_WIDTH = PANEL_WIDTH - STATIC_CARD_WIDTH
# BDF fonts here are fixed-width bitmap fonts named "<char_width>x<char_height>.bdf"
NAME_CHAR_WIDTH = 7 # 7x13.bdf
SMALL_CHAR_WIDTH = 5 # 5x7.bdf
SUPER_SMALL_CHAR_WIDTH = 4 # 4x6.bdf
STATIC_CARD_MARGIN = 4
_pil_font_cache = {}
def _pil_font(bdf_filename):
"""Load a .bdf bitmap font as a PIL ImageFont, so player text can be
drawn directly into the same virtual PIL canvas used for logos. That way
it goes through the exact same blit_slice() pixel copy/clip as the logos,
instead of being drawn separately onto the live matrix canvas."""
if bdf_filename not in _pil_font_cache:
bdf_path = os.path.join(ASSET_DIR, "fonts", bdf_filename)
cache_base = os.path.join(tempfile.gettempdir(), f"scoreboard_pilfont_{bdf_filename}")
if not os.path.exists(cache_base + ".pil"):
with open(bdf_path, "rb") as f:
BdfFontFile.BdfFontFile(f).save(cache_base)
_pil_font_cache[bdf_filename] = ImageFont.load(cache_base + ".pil")
return _pil_font_cache[bdf_filename]
NAME_PIL_FONT = _pil_font("7x13.bdf")
SMALL_PIL_FONT = _pil_font("5x7.bdf")
SUPER_SMALL_PIL_FONT = _pil_font("4x6.bdf")
scroll_x = 0
scroll_speed = 1
frames_per_tick = 1
@@ -120,83 +141,23 @@ def draw_static_card(canvas, team):
for row in range(PANEL_HEIGHT):
canvas.SetPixel(STATIC_CARD_WIDTH - 1, row, r, b, g)
def _clip_left(text, x, char_width, boundary):
"""Trim whole characters off the left of text that fall entirely before
boundary, so a string scrolling left erases character-by-character at the
boundary instead of vanishing all at once. Returns (text, x) to draw, or
(None, x) if nothing is left to show."""
if x >= boundary:
return text, x
if not text:
return None, x
hidden = -(-(boundary - x) // char_width) # ceil division
if hidden >= len(text):
return None, x
return text[hidden:], x + hidden * char_width
def draw_text_overlay(canvas, players, scroll_x, dest_x=0, width=PANEL_WIDTH):
total_width = GAME_WIDTH * len(players)
for i, player in enumerate(players):
base_x = (i * GAME_WIDTH) - scroll_x
# draw twice to handle the wrap-around copy
for wrap in [0, total_width]:
x = base_x + wrap
# cull slots fully off the visible region
if x + GAME_WIDTH < 0 or x >= width:
continue
x += dest_x
name_text, name_x = _clip_left(player['abbr_name'], x + 35, NAME_CHAR_WIDTH, dest_x)
if name_text:
graphics.DrawText(canvas, font, name_x, 15,
rbg(Colors.WHITE.value), name_text)
pos_text, pos_x = _clip_left(player['position'], x + 35, SMALL_CHAR_WIDTH, dest_x)
if pos_text:
graphics.DrawText(canvas, font_small, pos_x, 25,
rbg(Colors.WHITE.value), pos_text)
if player['injury_status'] and player['injury_body_part']:
status_text, status_x = _clip_left(
f"{str(player['injury_status'])} - ", x + 50, SMALL_CHAR_WIDTH, dest_x)
if status_text:
graphics.DrawText(canvas, font_small, status_x, 25,
rbg(Colors.RED.value), status_text)
part_text, part_x = _clip_left(
str(player["injury_body_part"]), x + 70, SUPER_SMALL_CHAR_WIDTH, dest_x)
if part_text:
graphics.DrawText(canvas, font_super_small, part_x, 25,
rbg(Colors.RED.value), part_text)
# # if the time is shown it should be split between lines
# # if not, just display the status
# if "AM" in game["status"] or "PM" in game["status"]:
# game_status_split = game["status"].split("-")
# date = game_status_split[0].strip()
# time = game_status_split[1].strip()
# graphics.DrawText(canvas, font_small, x + 60, 10,
# rbg(Colors.YELLOW.value), date)
# graphics.DrawText(canvas, font_small, x + 60, 20,
# rbg(Colors.YELLOW.value), time)
# graphics.DrawText(canvas, font_small, x + 60, 30,
# rbg(Colors.YELLOW.value), game["venue"])
# else:
# graphics.DrawText(canvas, font_small, x + 65, 20,
# rbg(Colors.YELLOW.value), game["status"])
def render_player(matrix, img, player, x_offset):
league = "nfl"
draw = ImageDraw.Draw(img)
team_logo = matrix.load_logo(league, player['team'])
if team_logo:
img.paste(team_logo.resize((28, 28)), (x_offset, 0))
# player text is baked directly into the virtual canvas (same as the logo
# above) so it gets clipped identically by blit_slice() when scrolled
draw.text((x_offset + 35, 3), player['abbr_name'], font=NAME_PIL_FONT, fill=Colors.WHITE.value)
draw.text((x_offset + 35, 18), player['position'], font=SMALL_PIL_FONT, fill=Colors.WHITE.value)
if player['injury_status'] and player['injury_body_part']:
draw.text((x_offset + 50, 18), f"{player['injury_status']} - ", font=SMALL_PIL_FONT, fill=Colors.RED.value)
draw.text((x_offset + 70, 19), str(player['injury_body_part']), font=SUPER_SMALL_PIL_FONT, fill=Colors.RED.value)
# divider on right edge (except last slot handled by wrapping)
for row in range(PANEL_HEIGHT):
img.putpixel((x_offset + GAME_WIDTH - 1, row), DIVIDER_COLOR)
@@ -254,7 +215,6 @@ def draw_frame(matrix):
total_scroll_width = GAME_WIDTH * len(players[current_team])
if virtual_canvas:
blit_slice(matrix.canvas, virtual_canvas, scroll_x, dest_x=SCROLL_REGION_X, width=SCROLL_REGION_WIDTH)
draw_text_overlay(matrix.canvas, players[current_team], scroll_x, dest_x=SCROLL_REGION_X, width=SCROLL_REGION_WIDTH)
tick += 1
if tick >= frames_per_tick: