From 392ff3100fb859dc9ab3900fe79b2aced9c57770 Mon Sep 17 00:00:00 2001 From: Alex Frantz Date: Mon, 3 Aug 2026 21:48:39 -0400 Subject: [PATCH] claude. --- modes/fantasy.py | 108 +++++++++++++--------------------------- preview_fantasy_card.py | 1 - 2 files changed, 34 insertions(+), 75 deletions(-) diff --git a/modes/fantasy.py b/modes/fantasy.py index 5cf0e85..b8efc5b 100644 --- a/modes/fantasy.py +++ b/modes/fantasy.py @@ -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 "x.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: diff --git a/preview_fantasy_card.py b/preview_fantasy_card.py index 588e874..403bb9a 100644 --- a/preview_fantasy_card.py +++ b/preview_fantasy_card.py @@ -96,7 +96,6 @@ def render_single_card(player, out_path, scale=6): img, _ = fantasy.build_canvas(matrix, "team1") canvas = matrix.canvas fantasy.blit_slice(canvas, img, 0) - fantasy.draw_text_overlay(canvas, [player], 0) card = canvas.img.crop((0, 0, vars.GAME_WIDTH, vars.PANEL_HEIGHT)) card = card.resize((card.width * scale, card.height * scale), Image.NEAREST)