From d856a4ea405e7281e692d2916104be4edd2cab6c Mon Sep 17 00:00:00 2001 From: Alex Frantz Date: Wed, 17 Jun 2026 23:11:29 -0400 Subject: [PATCH] organize --- .gitignore | 3 +- goal_celebrations/sabres.py | 113 ++++++++++++ scoreboard.py | 183 ++----------------- download_logos.py => utils/download_logos.py | 0 govee.py => utils/govee.py | 0 utils/image_utils.py | 42 +++++ 6 files changed, 172 insertions(+), 169 deletions(-) create mode 100644 goal_celebrations/sabres.py rename download_logos.py => utils/download_logos.py (100%) rename govee.py => utils/govee.py (100%) create mode 100644 utils/image_utils.py diff --git a/.gitignore b/.gitignore index d889806..ce9cad6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ .env -logos \ No newline at end of file +logos +.ruff_cache \ No newline at end of file diff --git a/goal_celebrations/sabres.py b/goal_celebrations/sabres.py new file mode 100644 index 0000000..7055e43 --- /dev/null +++ b/goal_celebrations/sabres.py @@ -0,0 +1,113 @@ +from PIL import Image, ImageDraw, ImageFont + +def render_goal_frame(text, text_scale, bg_color, text_color): + big_h = max(8, int(32 * text_scale)) + big_img = Image.new("RGB", (1024, 128), bg_color) + big_draw = ImageDraw.Draw(big_img) + + # rpi specific, fall back to default font if not existing + try: + pil_font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h + ) + except: + pil_font = ImageFont.load_default() + + bbox = big_draw.textbbox((0, 0), text, font=pil_font) + tw = bbox[2] - bbox[0] + th = bbox[3] - bbox[1] + + tx = (1024 - tw) // 2 + ty = (128 - th) // 2 - bbox[1] + big_draw.text((tx, ty), text, font=pil_font, fill=text_color) + + scaled = big_img.resize((256, 32), Image.LANCZOS) + + # paste sabres logo on left and right + logo_path = os.path.join(LOGO_DIR, "nhl_BUF.png") + if os.path.exists(logo_path): + try: + logo = Image.open(logo_path).convert("RGBA") + logo = logo.resize((28, 28), Image.LANCZOS) + + # swap G and B channels for RBG order + r, g, b, a = logo.split() + logo_rbg = Image.merge("RGBA", (r, b, g, a)) + + # make near-black pixels transparent + pixels = logo_rbg.load() + for px in range(logo_rbg.width): + for py in range(logo_rbg.height): + rv, gv, bv, av = pixels[px, py] + if rv < 30 and gv < 30 and bv < 30: + pixels[px, py] = (rv, gv, bv, 0) + + # paste with transparency onto bg-colored canvas + bg_left = Image.new("RGBA", (28, 28), bg_color + (255,)) + bg_left.paste(logo_rbg, mask=logo_rbg.split()[3]) + scaled.paste(bg_left.convert("RGB"), (2, 2)) + + bg_right = Image.new("RGBA", (28, 28), bg_color + (255,)) + bg_right.paste(logo_rbg, mask=logo_rbg.split()[3]) + scaled.paste(bg_right.convert("RGB"), (226, 2)) + + except Exception as e: + print(f"Logo paste error: {e}") + + return scaled + +def play_goal_celebration(text, color1, color2): + global canvas + + # Phase 1: zoom in from tiny to full, alternating bg color + zoom_steps = [0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 1.1, 1.0] + for _ in range(5): + for i, scale in enumerate(zoom_steps): + bg = color1 if i % 2 == 0 else color2 + fg = color2 if i % 2 == 0 else color1 + frame = render_goal_frame(text, scale, bg, fg) + canvas.Clear() + draw_pil_image(canvas, frame) + canvas = matrix.SwapOnVSync(canvas) + sleep(0.05) + + # Phase 2: rapid flashing at full size + for i in range(10): + bg = color1 if i % 2 == 0 else color2 + fg = color2 if i % 2 == 0 else color1 + frame = render_goal_frame(text, 1.0, bg, fg) + canvas.Clear() + draw_pil_image(canvas, frame) + canvas = matrix.SwapOnVSync(canvas) + sleep(0.12) + + # Phase 3: zoom back out and fade to white flash + zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4] + for i, scale in enumerate(zoom_out): + bg = color2 if i % 2 == 0 else color1 + fg = color1 if i % 2 == 0 else color2 + frame = render_goal_frame(text, scale, bg, fg) + canvas.Clear() + draw_pil_image(canvas, frame) + canvas = matrix.SwapOnVSync(canvas) + sleep(0.08) + + # Phase 4: white flash to end + for _ in range(3): + canvas.Clear() + frame = render_goal_frame( + text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value + ) + draw_pil_image(canvas, frame) + canvas = matrix.SwapOnVSync(canvas) + sleep(0.1) + + # clear board + canvas = canvas.Clear() + canvas = matrix.SwapOnVSync(canvas) + + # stop music if playing + # pygame.mixer.music.stop() + + # Hold for a moment then return to scoreboard + sleep(0.5) \ No newline at end of file diff --git a/scoreboard.py b/scoreboard.py index cdfa68b..ec39ad1 100644 --- a/scoreboard.py +++ b/scoreboard.py @@ -1,22 +1,18 @@ import requests import os -import govee -import math -import pygame +import utils.govee as govee +import utils.image_utils as image_utils from enum import Enum from time import sleep, time from pathlib import Path -from PIL import Image, ImageDraw, ImageFont from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics from dotenv import load_dotenv -# --- Current file path for file resolution -SCRIPT_DIR = Path(__file__).parent.resolve() - # --- Load environment vars --- load_dotenv() # --- Default vars --- +SCRIPT_DIR = Path(__file__).parent.resolve() ASSET_DIR = os.path.join(SCRIPT_DIR, "assets") LOGO_DIR = os.path.join(ASSET_DIR, "logos") @@ -58,159 +54,10 @@ if os.environ['GOVEE_API_KEY']: govee_api = govee.GoveeApi(key=os.environ["GOVEE_API_KEY"]) # --- PyGame Audio --- -pygame.mixer.init() - -# --- Goal celebrations --- -def render_goal_frame(text, text_scale, bg_color, text_color): - big_h = max(8, int(32 * text_scale)) - big_img = Image.new("RGB", (1024, 128), bg_color) - big_draw = ImageDraw.Draw(big_img) - - # rpi specific, fall back to default font if not existing - try: - pil_font = ImageFont.truetype( - "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h - ) - except: - pil_font = ImageFont.load_default() - - bbox = big_draw.textbbox((0, 0), text, font=pil_font) - tw = bbox[2] - bbox[0] - th = bbox[3] - bbox[1] - - tx = (1024 - tw) // 2 - ty = (128 - th) // 2 - bbox[1] - big_draw.text((tx, ty), text, font=pil_font, fill=text_color) - - scaled = big_img.resize((256, 32), Image.LANCZOS) - - # paste sabres logo on left and right - logo_path = os.path.join(LOGO_DIR, "nhl_BUF.png") - if os.path.exists(logo_path): - try: - logo = Image.open(logo_path).convert("RGBA") - logo = logo.resize((28, 28), Image.LANCZOS) - - # swap G and B channels for RBG order - r, g, b, a = logo.split() - logo_rbg = Image.merge("RGBA", (r, b, g, a)) - - # make near-black pixels transparent - pixels = logo_rbg.load() - for px in range(logo_rbg.width): - for py in range(logo_rbg.height): - rv, gv, bv, av = pixels[px, py] - if rv < 30 and gv < 30 and bv < 30: - pixels[px, py] = (rv, gv, bv, 0) - - # paste with transparency onto bg-colored canvas - bg_left = Image.new("RGBA", (28, 28), bg_color + (255,)) - bg_left.paste(logo_rbg, mask=logo_rbg.split()[3]) - scaled.paste(bg_left.convert("RGB"), (2, 2)) - - bg_right = Image.new("RGBA", (28, 28), bg_color + (255,)) - bg_right.paste(logo_rbg, mask=logo_rbg.split()[3]) - scaled.paste(bg_right.convert("RGB"), (226, 2)) - - except Exception as e: - print(f"Logo paste error: {e}") - - return scaled - -def play_goal_celebration(text, color1, color2): - global canvas - - # Phase 1: zoom in from tiny to full, alternating bg color - zoom_steps = [0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 1.1, 1.0] - for _ in range(5): - for i, scale in enumerate(zoom_steps): - bg = color1 if i % 2 == 0 else color2 - fg = color2 if i % 2 == 0 else color1 - frame = render_goal_frame(text, scale, bg, fg) - canvas.Clear() - draw_pil_image(canvas, frame) - canvas = matrix.SwapOnVSync(canvas) - sleep(0.05) - - # Phase 2: rapid flashing at full size - for i in range(10): - bg = color1 if i % 2 == 0 else color2 - fg = color2 if i % 2 == 0 else color1 - frame = render_goal_frame(text, 1.0, bg, fg) - canvas.Clear() - draw_pil_image(canvas, frame) - canvas = matrix.SwapOnVSync(canvas) - sleep(0.12) - - # Phase 3: zoom back out and fade to white flash - zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4] - for i, scale in enumerate(zoom_out): - bg = color2 if i % 2 == 0 else color1 - fg = color1 if i % 2 == 0 else color2 - frame = render_goal_frame(text, scale, bg, fg) - canvas.Clear() - draw_pil_image(canvas, frame) - canvas = matrix.SwapOnVSync(canvas) - sleep(0.08) - - # Phase 4: white flash to end - for _ in range(3): - canvas.Clear() - frame = render_goal_frame( - text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value - ) - draw_pil_image(canvas, frame) - canvas = matrix.SwapOnVSync(canvas) - sleep(0.1) - - # clear board - canvas = canvas.Clear() - canvas = matrix.SwapOnVSync(canvas) - - # stop music if playing - pygame.mixer.music.stop() - - # Hold for a moment then return to scoreboard - sleep(0.5) - -def play_audio(filename): - pygame.mixer.music.load(os.path.join(ASSET_DIR, filename)) - pygame.mixer.music.play() - -# --- Utilities --- -def draw_pil_image(canvas, img): - for x in range(img.width): - for y in range(img.height): - r, g, b = img.getpixel((x, y)) - canvas.SetPixel(x, y, b, g, r) # bgr panels - -def load_logo(league, abbr): - key = f"{league}_{abbr}" - if key in logo_cache: - return logo_cache[key] - - path = os.path.join(LOGO_DIR, f"{key}.png") - if not os.path.exists(path): - print(f"Logo not found: {path}") - logo_cache[key] = None - return None - - try: - img = Image.open(path).convert("RGB") - logo_cache[key] = img - return img - except Exception as e: - print(f"Error loading logo {key}: {e}") - logo_cache[key] = None - return None - -def draw_logo(canvas, img, x, y): - if img is None: - return - for px in range(img.width): - for py in range(img.height): - r, g, b = img.getpixel((px, py)) - canvas.SetPixel(x + px, y + py, r, b, g) # bgr panels +# pygame.mixer.init() +# def play_audio(filename): +# pygame.mixer.music.load(os.path.join(ASSET_DIR, filename)) +# pygame.mixer.music.play() # --- Fetch scores --- def get_scores(sport, league): @@ -258,11 +105,11 @@ def draw_all_games(canvas, games, start_index): offset = i * 64 league = game["league"] - away_logo = load_logo(league, game["away"]) - home_logo = load_logo(league, game["home"]) + away_logo = image_utils.load_logo(league, game["away"]) + home_logo = image_utils.load_logo(league, game["home"]) - draw_logo(canvas, away_logo, offset + 0, 0) - draw_logo(canvas, home_logo, offset + 0, 16) + image_utils.draw_logo(canvas, away_logo, offset + 0, 0) + image_utils.draw_logo(canvas, home_logo, offset + 0, 16) graphics.DrawText( canvas, @@ -304,11 +151,11 @@ def draw_all_games(canvas, games, start_index): def draw_single_game(canvas, game): league = game["league"] - home_logo = load_logo(league, game["home"]) - away_logo = load_logo(league, game["away"]) + home_logo = image_utils.load_logo(league, game["home"]) + away_logo = image_utils.load_logo(league, game["away"]) - draw_logo(canvas, away_logo, 0, 0) - draw_logo(canvas, home_logo, 0, 16) + image_utils.draw_logo(canvas, away_logo, 0, 0) + image_utils.draw_logo(canvas, home_logo, 0, 16) graphics.DrawText( canvas, diff --git a/download_logos.py b/utils/download_logos.py similarity index 100% rename from download_logos.py rename to utils/download_logos.py diff --git a/govee.py b/utils/govee.py similarity index 100% rename from govee.py rename to utils/govee.py diff --git a/utils/image_utils.py b/utils/image_utils.py new file mode 100644 index 0000000..3565581 --- /dev/null +++ b/utils/image_utils.py @@ -0,0 +1,42 @@ +import os +from pathlib import Path +from PIL import Image + +logo_cache = {} +SCRIPT_DIR = Path(__file__).parent.resolve() +ASSET_DIR = os.path.join(SCRIPT_DIR, "assets") +LOGO_DIR = os.path.join(ASSET_DIR, "logos") + +def draw_pil_image(canvas, img): + for x in range(img.width): + for y in range(img.height): + r, g, b = img.getpixel((x, y)) + canvas.SetPixel(x, y, b, g, r) # bgr panels + +def load_logo(league, abbr): + key = f"{league}_{abbr}" + if key in logo_cache: + return logo_cache[key] + + path = os.path.join(LOGO_DIR, f"{key}.png") + if not os.path.exists(path): + print(f"Logo not found: {path}") + logo_cache[key] = None + return None + + try: + img = Image.open(path).convert("RGB") + logo_cache[key] = img + return img + except Exception as e: + print(f"Error loading logo {key}: {e}") + logo_cache[key] = None + return None + +def draw_logo(canvas, img, x, y): + if img is None: + return + for px in range(img.width): + for py in range(img.height): + r, g, b = img.getpixel((px, py)) + canvas.SetPixel(x + px, y + py, r, b, g) # bgr panels \ No newline at end of file