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 ADDED

Git LFS Details

  • SHA256: 04f654c9a7898cf987904b28999436fe6b6e7b03bdd67644a1c33012df4dc36e
  • Pointer size: 131 Bytes
  • Size of remote file: 285 kB
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 = "midnight-premium"
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
- if family != "sans":
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, state.date_label,
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