Spaces:
Sleeping
Sleeping
Claude commited on
Commit ·
082c232
1
Parent(s): 3e42d8b
Add hoopshype-official premium theme (#51) with bg image and custom font support
Browse files- New theme with dark carbon fiber mesh background, Futura Today font,
inside-bar labels, rectangle headshots, rounded bars, uppercase date
- Add bg_image, font_custom_dir, date_uppercase fields to Theme dataclass
- Implement background image loading (cover resize) in render.py
- Implement custom font directory resolution in FrameRenderer
- Skip code vignette when bg_image provides its own
- Set hoopshype-official as default theme in config.py and customizer
- Asset placeholders: fonts in assets/fonts/, mesh3.jpg in assets/backgrounds/
- Mirror all changes to hf_space/bar_race/
https://claude.ai/code/session_01PzSuVpJ1KAsAPLLEw75nZS
- assets/backgrounds/mesh3.jpg +3 -0
- assets/fonts/Futura_Today_Bold.otf +0 -0
- assets/fonts/Futura_Today_DemiBold.otf +0 -0
- assets/fonts/Futura_Today_Light.otf +0 -0
- assets/fonts/Futura_Today_Normal.otf +0 -0
- bar_race/config.py +1 -1
- bar_race/render.py +63 -3
- bar_race/themes.py +42 -0
assets/backgrounds/mesh3.jpg
ADDED
|
Git LFS Details
|
assets/fonts/Futura_Today_Bold.otf
ADDED
|
File without changes
|
assets/fonts/Futura_Today_DemiBold.otf
ADDED
|
File without changes
|
assets/fonts/Futura_Today_Light.otf
ADDED
|
File without changes
|
assets/fonts/Futura_Today_Normal.otf
ADDED
|
File without changes
|
bar_race/config.py
CHANGED
|
@@ -159,7 +159,7 @@ class Config:
|
|
| 159 |
watermark: str = ""
|
| 160 |
|
| 161 |
# --- theme -------------------------------------------------------------
|
| 162 |
-
theme: str = "
|
| 163 |
|
| 164 |
# --- visual tweaks -----------------------------------------------------
|
| 165 |
bg_gradient: tuple[str, str] = ("#0f0c29", "#302b63")
|
|
|
|
| 159 |
watermark: str = ""
|
| 160 |
|
| 161 |
# --- theme -------------------------------------------------------------
|
| 162 |
+
theme: str = "hoopshype-official"
|
| 163 |
|
| 164 |
# --- visual tweaks -----------------------------------------------------
|
| 165 |
bg_gradient: tuple[str, str] = ("#0f0c29", "#302b63")
|
bar_race/render.py
CHANGED
|
@@ -233,7 +233,32 @@ def _render_split_bg(
|
|
| 233 |
return img
|
| 234 |
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
def _build_background(theme: Theme, width: int, height: int) -> Image.Image:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
colors = [_hex_to_rgb(c) for c in theme.bg_colors]
|
| 238 |
c1 = colors[0]
|
| 239 |
c2 = colors[1] if len(colors) > 1 else c1
|
|
@@ -814,8 +839,40 @@ class FrameRenderer:
|
|
| 814 |
regular_path = cfg.font_regular
|
| 815 |
light_path = cfg.font_light
|
| 816 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
# Override with font_family if config used defaults (auto-resolved).
|
| 818 |
-
|
| 819 |
bold_path = _resolve_font(family, "bold")
|
| 820 |
medium_path = _resolve_font(family, "medium")
|
| 821 |
regular_path = _resolve_font(family, "regular")
|
|
@@ -835,7 +892,7 @@ class FrameRenderer:
|
|
| 835 |
|
| 836 |
# Precompute background.
|
| 837 |
self._bg = _build_background(th, self.W, self.H)
|
| 838 |
-
if th.vignette:
|
| 839 |
self._bg = _apply_vignette(self._bg)
|
| 840 |
|
| 841 |
# Draw static decorative elements on background.
|
|
@@ -1225,8 +1282,11 @@ class FrameRenderer:
|
|
| 1225 |
date_alpha = int(255 * th.date_opacity)
|
| 1226 |
date_xy = (self.W - self._margin_right - 10,
|
| 1227 |
int(self.H * 0.93))
|
|
|
|
|
|
|
|
|
|
| 1228 |
draw.text(
|
| 1229 |
-
date_xy,
|
| 1230 |
fill=(*date_c, date_alpha),
|
| 1231 |
font=self.font_date, anchor="rt",
|
| 1232 |
)
|
|
|
|
| 233 |
return img
|
| 234 |
|
| 235 |
|
| 236 |
+
def _load_bg_image(path: str, width: int, height: int) -> Image.Image:
|
| 237 |
+
"""Load a background image, resize to cover the target dimensions."""
|
| 238 |
+
img = Image.open(path).convert("RGBA")
|
| 239 |
+
# Cover: scale so smallest dimension fills, then center-crop.
|
| 240 |
+
src_w, src_h = img.size
|
| 241 |
+
scale = max(width / src_w, height / src_h)
|
| 242 |
+
new_w = int(src_w * scale)
|
| 243 |
+
new_h = int(src_h * scale)
|
| 244 |
+
img = img.resize((new_w, new_h), Image.LANCZOS)
|
| 245 |
+
# Center crop.
|
| 246 |
+
left = (new_w - width) // 2
|
| 247 |
+
top = (new_h - height) // 2
|
| 248 |
+
return img.crop((left, top, left + width, top + height))
|
| 249 |
+
|
| 250 |
+
|
| 251 |
def _build_background(theme: Theme, width: int, height: int) -> Image.Image:
|
| 252 |
+
# If a background image is set, use it instead of gradient.
|
| 253 |
+
if theme.bg_image:
|
| 254 |
+
bg_path = Path(theme.bg_image)
|
| 255 |
+
if not bg_path.is_absolute():
|
| 256 |
+
# Resolve relative to package root (two levels up from this file).
|
| 257 |
+
pkg_root = Path(__file__).resolve().parent.parent.parent
|
| 258 |
+
bg_path = pkg_root / bg_path
|
| 259 |
+
if bg_path.is_file():
|
| 260 |
+
return _load_bg_image(str(bg_path), width, height)
|
| 261 |
+
|
| 262 |
colors = [_hex_to_rgb(c) for c in theme.bg_colors]
|
| 263 |
c1 = colors[0]
|
| 264 |
c2 = colors[1] if len(colors) > 1 else c1
|
|
|
|
| 839 |
regular_path = cfg.font_regular
|
| 840 |
light_path = cfg.font_light
|
| 841 |
|
| 842 |
+
# Custom font directory: map weights to specific font files.
|
| 843 |
+
if th.font_custom_dir:
|
| 844 |
+
custom_dir = Path(th.font_custom_dir)
|
| 845 |
+
if not custom_dir.is_absolute():
|
| 846 |
+
pkg_root = Path(__file__).resolve().parent.parent.parent
|
| 847 |
+
custom_dir = pkg_root / custom_dir
|
| 848 |
+
_custom_map = {
|
| 849 |
+
"bold": "Futura_Today_Bold.otf",
|
| 850 |
+
"medium": "Futura_Today_DemiBold.otf",
|
| 851 |
+
"regular": "Futura_Today_Normal.otf",
|
| 852 |
+
"light": "Futura_Today_Light.otf",
|
| 853 |
+
}
|
| 854 |
+
all_found = True
|
| 855 |
+
for weight, filename in _custom_map.items():
|
| 856 |
+
candidate = custom_dir / filename
|
| 857 |
+
if candidate.is_file():
|
| 858 |
+
if weight == "bold":
|
| 859 |
+
bold_path = str(candidate)
|
| 860 |
+
elif weight == "medium":
|
| 861 |
+
medium_path = str(candidate)
|
| 862 |
+
elif weight == "regular":
|
| 863 |
+
regular_path = str(candidate)
|
| 864 |
+
elif weight == "light":
|
| 865 |
+
light_path = str(candidate)
|
| 866 |
+
else:
|
| 867 |
+
all_found = False
|
| 868 |
+
# If not all custom fonts found, fall back to standard resolution.
|
| 869 |
+
if not all_found:
|
| 870 |
+
bold_path = cfg.font_bold
|
| 871 |
+
medium_path = cfg.font_medium
|
| 872 |
+
regular_path = cfg.font_regular
|
| 873 |
+
light_path = cfg.font_light
|
| 874 |
# Override with font_family if config used defaults (auto-resolved).
|
| 875 |
+
elif family != "sans":
|
| 876 |
bold_path = _resolve_font(family, "bold")
|
| 877 |
medium_path = _resolve_font(family, "medium")
|
| 878 |
regular_path = _resolve_font(family, "regular")
|
|
|
|
| 892 |
|
| 893 |
# Precompute background.
|
| 894 |
self._bg = _build_background(th, self.W, self.H)
|
| 895 |
+
if th.vignette and not th.bg_image:
|
| 896 |
self._bg = _apply_vignette(self._bg)
|
| 897 |
|
| 898 |
# Draw static decorative elements on background.
|
|
|
|
| 1282 |
date_alpha = int(255 * th.date_opacity)
|
| 1283 |
date_xy = (self.W - self._margin_right - 10,
|
| 1284 |
int(self.H * 0.93))
|
| 1285 |
+
date_text = state.date_label
|
| 1286 |
+
if th.date_uppercase:
|
| 1287 |
+
date_text = date_text.upper()
|
| 1288 |
draw.text(
|
| 1289 |
+
date_xy, date_text,
|
| 1290 |
fill=(*date_c, date_alpha),
|
| 1291 |
font=self.font_date, anchor="rt",
|
| 1292 |
)
|
bar_race/themes.py
CHANGED
|
@@ -89,6 +89,15 @@ class Theme:
|
|
| 89 |
noise: bool = True
|
| 90 |
bar_shadow: bool = True
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
# ---------------------------------------------------------------------------
|
| 94 |
# Theme definitions
|
|
@@ -943,6 +952,39 @@ _register(
|
|
| 943 |
show_grid_lines=True,
|
| 944 |
border_frame="left-accent",
|
| 945 |
vignette=False, noise=False, bar_shadow=False),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 946 |
)
|
| 947 |
|
| 948 |
|
|
|
|
| 89 |
noise: bool = True
|
| 90 |
bar_shadow: bool = True
|
| 91 |
|
| 92 |
+
# Background image (optional — overrides gradient when set)
|
| 93 |
+
bg_image: str = "" # path to background image file
|
| 94 |
+
|
| 95 |
+
# Custom font directory (optional — overrides font_family when set)
|
| 96 |
+
font_custom_dir: str = "" # directory containing custom .otf/.ttf files
|
| 97 |
+
|
| 98 |
+
# Date formatting
|
| 99 |
+
date_uppercase: bool = False # uppercase month abbreviation (JAN vs Jan)
|
| 100 |
+
|
| 101 |
|
| 102 |
# ---------------------------------------------------------------------------
|
| 103 |
# Theme definitions
|
|
|
|
| 952 |
show_grid_lines=True,
|
| 953 |
border_frame="left-accent",
|
| 954 |
vignette=False, noise=False, bar_shadow=False),
|
| 955 |
+
|
| 956 |
+
# 51
|
| 957 |
+
_t("hoopshype-official", "HoopsHype Official",
|
| 958 |
+
"Premium broadcast look. Dark carbon fiber mesh background with Futura Today font.",
|
| 959 |
+
bg_type="solid", bg_colors=["#1a1a1a"],
|
| 960 |
+
bg_image="assets/backgrounds/mesh3.jpg",
|
| 961 |
+
font_family="sans",
|
| 962 |
+
font_custom_dir="assets/fonts",
|
| 963 |
+
accent_color="#ffffff", accent_secondary="#888888",
|
| 964 |
+
bar_radius=6, bar_opacity=1.0,
|
| 965 |
+
bar_border=True, bar_border_width=1,
|
| 966 |
+
bar_gradient=False, bar_team_stripe=False,
|
| 967 |
+
show_highlight_strip=True, show_shadow_strip=False,
|
| 968 |
+
leader_glow=False, leader_outline=False,
|
| 969 |
+
leader_underline=False, leader_bg_highlight=False,
|
| 970 |
+
show_rank_numbers=False, rank_giant_watermark=False,
|
| 971 |
+
text_color="#ffffff", text_secondary_color="#cccccc",
|
| 972 |
+
label_case="normal",
|
| 973 |
+
label_position="inside",
|
| 974 |
+
value_suffix="",
|
| 975 |
+
title_color="#ffffff",
|
| 976 |
+
title_position="top-left",
|
| 977 |
+
date_color="#ffffff", date_opacity=0.9,
|
| 978 |
+
date_uppercase=True,
|
| 979 |
+
headshot_shape="rounded", headshot_style="rectangle",
|
| 980 |
+
headshot_border=False,
|
| 981 |
+
headshot_position="in-bar",
|
| 982 |
+
show_branding_tag=False,
|
| 983 |
+
show_accent_line=False, show_diagonal_slash=False,
|
| 984 |
+
show_court_lines=False,
|
| 985 |
+
show_background_circle=False, show_grid_lines=False,
|
| 986 |
+
border_frame="none",
|
| 987 |
+
vignette=False, noise=False, bar_shadow=False),
|
| 988 |
)
|
| 989 |
|
| 990 |
|