This commit is contained in:
2026-06-17 23:11:29 -04:00
parent 7c1350ac01
commit d856a4ea40
6 changed files with 172 additions and 169 deletions
+1
View File
@@ -1,3 +1,4 @@
__pycache__ __pycache__
.env .env
logos logos
.ruff_cache
+113
View File
@@ -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)
+15 -168
View File
@@ -1,22 +1,18 @@
import requests import requests
import os import os
import govee import utils.govee as govee
import math import utils.image_utils as image_utils
import pygame
from enum import Enum from enum import Enum
from time import sleep, time from time import sleep, time
from pathlib import Path from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
from dotenv import load_dotenv from dotenv import load_dotenv
# --- Current file path for file resolution
SCRIPT_DIR = Path(__file__).parent.resolve()
# --- Load environment vars --- # --- Load environment vars ---
load_dotenv() load_dotenv()
# --- Default vars --- # --- Default vars ---
SCRIPT_DIR = Path(__file__).parent.resolve()
ASSET_DIR = os.path.join(SCRIPT_DIR, "assets") ASSET_DIR = os.path.join(SCRIPT_DIR, "assets")
LOGO_DIR = os.path.join(ASSET_DIR, "logos") 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"]) govee_api = govee.GoveeApi(key=os.environ["GOVEE_API_KEY"])
# --- PyGame Audio --- # --- PyGame Audio ---
pygame.mixer.init() # pygame.mixer.init()
# def play_audio(filename):
# --- Goal celebrations --- # pygame.mixer.music.load(os.path.join(ASSET_DIR, filename))
def render_goal_frame(text, text_scale, bg_color, text_color): # pygame.mixer.music.play()
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
# --- Fetch scores --- # --- Fetch scores ---
def get_scores(sport, league): def get_scores(sport, league):
@@ -258,11 +105,11 @@ def draw_all_games(canvas, games, start_index):
offset = i * 64 offset = i * 64
league = game["league"] league = game["league"]
away_logo = load_logo(league, game["away"]) away_logo = image_utils.load_logo(league, game["away"])
home_logo = load_logo(league, game["home"]) home_logo = image_utils.load_logo(league, game["home"])
draw_logo(canvas, away_logo, offset + 0, 0) image_utils.draw_logo(canvas, away_logo, offset + 0, 0)
draw_logo(canvas, home_logo, offset + 0, 16) image_utils.draw_logo(canvas, home_logo, offset + 0, 16)
graphics.DrawText( graphics.DrawText(
canvas, canvas,
@@ -304,11 +151,11 @@ def draw_all_games(canvas, games, start_index):
def draw_single_game(canvas, game): def draw_single_game(canvas, game):
league = game["league"] league = game["league"]
home_logo = load_logo(league, game["home"]) home_logo = image_utils.load_logo(league, game["home"])
away_logo = load_logo(league, game["away"]) away_logo = image_utils.load_logo(league, game["away"])
draw_logo(canvas, away_logo, 0, 0) image_utils.draw_logo(canvas, away_logo, 0, 0)
draw_logo(canvas, home_logo, 0, 16) image_utils.draw_logo(canvas, home_logo, 0, 16)
graphics.DrawText( graphics.DrawText(
canvas, canvas,
View File
+42
View File
@@ -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