Add hash routing and Dota 2 logo to relations preview.
- New web/relations/router.js (parseHash/serializeHash/installRouter): top tabs, hero + detail sub-tab, item, patch version, tag filters and search query all sync to URL (#/heroes/axe/core, ?tags=...&q=...); pushState for discrete picks, replaceState for debounced search. - export_relations_site.py copies router.js so static export deep-links. - Header shows dota2_logo.png + title, served via /ui-icon/ whitelist. - fetch_dota2_logo.py fetches the transparent emblem asset. - Docs: CHANGELOG / README / DESIGN / AGENTS updated.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Fetch the official Dota 2 emblem icon (no wordmark) for the preview header.
|
||||
|
||||
Source (discovered, not guessed) from the dota2.com.cn site HTML:
|
||||
<link rel="Shortcut Icon" href="//www.dota2.com.cn/favicon.ico"/>
|
||||
|
||||
That .ico is a multi-frame container whose largest frames are 256x256 PNGs of
|
||||
the red Dota 2 map-marker emblem with a transparent background — i.e. the
|
||||
brand icon alone, no "DOTA 2" text. We extract the first 256x256 PNG frame,
|
||||
decode with OpenCV, and write it to assets/ui_icons/dota2_logo.png so it rides
|
||||
the existing /ui-icon/ route + static-export glob pipeline.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from common import UI_ICONS
|
||||
from http_utils import http_bytes
|
||||
|
||||
FAVICON_URL = "https://www.dota2.com.cn/favicon.ico"
|
||||
OUT = UI_ICONS / "dota2_logo.png"
|
||||
TARGET_SIZE = 256
|
||||
|
||||
|
||||
def _extract_largest_png_frame(data: bytes) -> bytes | None:
|
||||
"""Return the bytes of the largest PNG-embedded frame in an ICO blob."""
|
||||
if len(data) < 6 or data[:4] != b"\x00\x00\x01\x00":
|
||||
return None
|
||||
count = struct.unpack("<H", data[4:6])[0]
|
||||
best: tuple[int, bytes] | None = None
|
||||
for i in range(count):
|
||||
off = 6 + 16 * i
|
||||
if off + 16 > len(data):
|
||||
break
|
||||
w = data[off]
|
||||
size = struct.unpack("<I", data[off + 8 : off + 12])[0]
|
||||
img_off = struct.unpack("<I", data[off + 12 : off + 16])[0]
|
||||
width = 256 if w == 0 else w
|
||||
blob = data[img_off : img_off + size]
|
||||
if blob[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
continue
|
||||
if best is None or width * width > best[0]:
|
||||
best = (width * width, blob)
|
||||
return best[1] if best else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if OUT.is_file() and OUT.stat().st_size >= 512:
|
||||
print(f"exists: {OUT} ({OUT.stat().st_size} bytes)")
|
||||
return 0
|
||||
data = http_bytes(FAVICON_URL, timeout=30)
|
||||
if not data or len(data) < 16:
|
||||
print("empty favicon download", file=sys.stderr)
|
||||
return 1
|
||||
png = _extract_largest_png_frame(data)
|
||||
if png is None:
|
||||
print("no PNG frame in .ico", file=sys.stderr)
|
||||
return 1
|
||||
arr = cv2.imdecode(np.frombuffer(png, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
|
||||
if arr is None:
|
||||
print("decode failed", file=sys.stderr)
|
||||
return 1
|
||||
UI_ICONS.mkdir(parents=True, exist_ok=True)
|
||||
ok = cv2.imwrite(str(OUT), arr)
|
||||
if not ok:
|
||||
print("write failed", file=sys.stderr)
|
||||
return 1
|
||||
h, w = arr.shape[:2]
|
||||
channels = arr.shape[2] if arr.ndim == 3 else 1
|
||||
transparent = int(np.count_nonzero(arr[:, :, 3] == 0)) if channels == 4 else 0
|
||||
print(f"saved {OUT} ({w}x{h}, {OUT.stat().st_size} bytes, {transparent} px transparent)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user