organize
This commit is contained in:
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
.env
|
.env
|
||||||
logos
|
logos
|
||||||
|
.ruff_cache
|
||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user