Use per-exchange PWA names and logos for instance app installs.
Show Binance/OKX/Gate 交易系统 with distinct exchange icons in manifests, apple titles, and login branding. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+159
-12
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成品牌 PNG/ICO(Pillow),供 Chrome 快捷方式与 manifest 使用."""
|
||||
"""生成品牌 PNG/ICO(Pillow),供 Chrome 快捷方式与 manifest 使用.
|
||||
|
||||
中控用通用监控图标;三所各自用交易所标识色+字标.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -14,6 +17,27 @@ CYAN = (34, 211, 238, 255)
|
||||
GREEN = (52, 211, 153, 255)
|
||||
RED = (248, 113, 113, 255)
|
||||
|
||||
EXCHANGES = {
|
||||
"binance": {
|
||||
"label": "B",
|
||||
"accent": (240, 185, 11, 255),
|
||||
"panel": (26, 22, 10, 255),
|
||||
"svg_fill": "#F0B90B",
|
||||
},
|
||||
"okx": {
|
||||
"label": "OKX",
|
||||
"accent": (255, 255, 255, 255),
|
||||
"panel": (18, 18, 18, 255),
|
||||
"svg_fill": "#FFFFFF",
|
||||
},
|
||||
"gate": {
|
||||
"label": "G",
|
||||
"accent": (23, 230, 161, 255),
|
||||
"panel": (10, 28, 24, 255),
|
||||
"svg_fill": "#17E6A1",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _lerp(c1: tuple[int, ...], c2: tuple[int, ...], t: float) -> tuple[int, int, int, int]:
|
||||
t = max(0.0, min(1.0, t))
|
||||
@@ -24,6 +48,25 @@ def _rounded_rect(draw, box, radius: int, fill) -> None:
|
||||
draw.rounded_rectangle(box, radius=radius, fill=fill)
|
||||
|
||||
|
||||
def _font(size: int):
|
||||
from PIL import ImageFont
|
||||
|
||||
candidates = [
|
||||
os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "arialbd.ttf"),
|
||||
os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "segoeuib.ttf"),
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
if path and os.path.isfile(path):
|
||||
try:
|
||||
return ImageFont.truetype(path, size=size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def render_icon(size: int):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
@@ -35,7 +78,6 @@ def render_icon(size: int):
|
||||
inner = m + max(2, size // 28)
|
||||
_rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), PANEL)
|
||||
|
||||
# 渐变描边(四角采样)
|
||||
border = max(2, size // 42)
|
||||
for i in range(border):
|
||||
t0 = i / max(1, border - 1)
|
||||
@@ -56,7 +98,6 @@ def render_icon(size: int):
|
||||
def sy(v: float) -> int:
|
||||
return int(v * size / 512)
|
||||
|
||||
# 趋势线
|
||||
pts = [(120, 320), (200, 248), (280, 272), (392, 168)]
|
||||
scaled = [(sx(x), sy(y)) for x, y in pts]
|
||||
draw.line(scaled, fill=CYAN, width=max(2, size // 26), joint="curve")
|
||||
@@ -66,7 +107,6 @@ def render_icon(size: int):
|
||||
fill=GREEN,
|
||||
)
|
||||
|
||||
# 蜡烛
|
||||
def candle(cx, top, bottom, body_top, body_bottom, color):
|
||||
w = max(1, size // 64)
|
||||
bh = max(2, size // 32)
|
||||
@@ -83,29 +123,136 @@ def render_icon(size: int):
|
||||
return img
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def render_exchange_icon(size: int, key: str):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
cfg = EXCHANGES[key]
|
||||
accent = cfg["accent"]
|
||||
panel = cfg["panel"]
|
||||
label = cfg["label"]
|
||||
|
||||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
m = max(6, size // 12)
|
||||
r = max(8, size // 6)
|
||||
_rounded_rect(draw, (m, m, size - m, size - m), r, BG)
|
||||
inner = m + max(2, size // 28)
|
||||
_rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), panel)
|
||||
|
||||
if size >= 32:
|
||||
border = max(1, size // 48)
|
||||
for i in range(border):
|
||||
x0 = inner + i
|
||||
y0 = inner + i
|
||||
x1 = size - inner - 1 - i
|
||||
y1 = size - inner - 1 - i
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
break
|
||||
draw.rounded_rectangle(
|
||||
(x0, y0, x1, y1),
|
||||
radius=max(2, r - 4 - i),
|
||||
outline=accent,
|
||||
)
|
||||
|
||||
if key == "binance":
|
||||
# 币安菱形标识
|
||||
cx = cy = size // 2
|
||||
s = max(3, int(size * 0.22))
|
||||
diamond = [(cx, cy - s), (cx + s, cy), (cx, cy + s), (cx - s, cy)]
|
||||
draw.polygon(diamond, fill=accent)
|
||||
s2 = max(1, int(s * 0.42))
|
||||
if s2 < s:
|
||||
inner_d = [(cx, cy - s2), (cx + s2, cy), (cx, cy + s2), (cx - s2, cy)]
|
||||
draw.polygon(inner_d, fill=panel)
|
||||
elif key == "okx":
|
||||
# OKX 四格方块风格(右下留空)
|
||||
gap = max(1, size // 48)
|
||||
cell = max(2, int(size * 0.16))
|
||||
cx = cy = size // 2
|
||||
coords = [
|
||||
(cx - cell - gap // 2, cy - cell - gap // 2),
|
||||
(cx + gap // 2, cy - cell - gap // 2),
|
||||
(cx - cell - gap // 2, cy + gap // 2),
|
||||
]
|
||||
for x0, y0 in coords:
|
||||
draw.rectangle((x0, y0, x0 + cell, y0 + cell), fill=accent)
|
||||
else:
|
||||
# Gate: 大字 G
|
||||
font_size = max(10, int(size * 0.42))
|
||||
font = _font(font_size)
|
||||
bbox = draw.textbbox((0, 0), label, font=font)
|
||||
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
x = (size - tw) // 2 - bbox[0]
|
||||
y = (size - th) // 2 - bbox[1] - max(0, size // 64)
|
||||
draw.text((x, y), label, font=font, fill=accent)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def write_exchange_svg(key: str, dest_dir: str) -> None:
|
||||
cfg = EXCHANGES[key]
|
||||
fill = cfg["svg_fill"]
|
||||
if key == "binance":
|
||||
mark = (
|
||||
f'<polygon points="256,150 362,256 256,362 150,256" fill="{fill}"/>'
|
||||
f'<polygon points="256,210 302,256 256,302 210,256" fill="#1a160a"/>'
|
||||
)
|
||||
elif key == "okx":
|
||||
mark = (
|
||||
f'<rect x="168" y="168" width="72" height="72" rx="10" fill="{fill}"/>'
|
||||
f'<rect x="272" y="168" width="72" height="72" rx="10" fill="{fill}"/>'
|
||||
f'<rect x="168" y="272" width="72" height="72" rx="10" fill="{fill}"/>'
|
||||
)
|
||||
else:
|
||||
mark = (
|
||||
f'<text x="256" y="310" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" '
|
||||
f'font-size="220" font-weight="700" fill="{fill}">G</text>'
|
||||
)
|
||||
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="108" fill="#0c1019"/>
|
||||
<rect x="36" y="36" width="440" height="440" rx="88" fill="#141b2d"/>
|
||||
<rect x="36" y="36" width="440" height="440" rx="88" fill="none" stroke="{fill}" stroke-width="12"/>
|
||||
{mark}
|
||||
</svg>
|
||||
"""
|
||||
with open(os.path.join(dest_dir, "icon.svg"), "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(svg)
|
||||
|
||||
|
||||
def _save_set(out_dir: str, render_fn) -> None:
|
||||
from PIL import Image
|
||||
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
shutil.copy2(os.path.join(REPO, "brand", "icon.svg"), os.path.join(OUT, "icon.svg"))
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
sizes = [16, 32, 48, 180, 192, 512]
|
||||
images: dict[int, Image.Image] = {}
|
||||
for sz in sizes:
|
||||
im = render_icon(sz)
|
||||
im = render_fn(sz)
|
||||
images[sz] = im
|
||||
name = "apple-touch-icon.png" if sz == 180 else f"icon-{sz}.png"
|
||||
im.save(os.path.join(OUT, name), format="PNG", optimize=True)
|
||||
im.save(os.path.join(out_dir, name), format="PNG", optimize=True)
|
||||
|
||||
ico_sizes = [16, 32, 48]
|
||||
ico_imgs = [images[s] for s in ico_sizes]
|
||||
ico_imgs[0].save(
|
||||
os.path.join(OUT, "favicon.ico"),
|
||||
os.path.join(out_dir, "favicon.ico"),
|
||||
format="ICO",
|
||||
sizes=[(s, s) for s in ico_sizes],
|
||||
append_images=ico_imgs[1:],
|
||||
)
|
||||
print(f"DONE {OUT}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
shutil.copy2(os.path.join(REPO, "brand", "icon.svg"), os.path.join(OUT, "icon.svg"))
|
||||
_save_set(OUT, render_icon)
|
||||
print(f"DONE hub {OUT}")
|
||||
|
||||
for key in EXCHANGES:
|
||||
dest = os.path.join(OUT, key)
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
write_exchange_svg(key, dest)
|
||||
_save_set(dest, lambda sz, k=key: render_exchange_icon(sz, k))
|
||||
print(f"DONE {key} {dest}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+16
-12
@@ -15,10 +15,10 @@ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SRC = os.path.join(REPO, "brand", "icons")
|
||||
|
||||
HUB_DEST = os.path.join(REPO, "manual_trading_hub", "static", "icons")
|
||||
EXCHANGE_DIRS = (
|
||||
"crypto_monitor_binance",
|
||||
"crypto_monitor_okx",
|
||||
"crypto_monitor_gate",
|
||||
EXCHANGES = (
|
||||
("crypto_monitor_binance", "binance", "manifest.binance.webmanifest"),
|
||||
("crypto_monitor_okx", "okx", "manifest.okx.webmanifest"),
|
||||
("crypto_monitor_gate", "gate", "manifest.gate.webmanifest"),
|
||||
)
|
||||
|
||||
FILES = (
|
||||
@@ -32,12 +32,15 @@ FILES = (
|
||||
)
|
||||
|
||||
|
||||
def sync_dir(dest: str, url_prefix: str, manifest_template: str) -> str:
|
||||
if not os.path.isdir(SRC):
|
||||
return f"SKIP {dest}: 请先运行 python scripts/generate_brand_icons.py"
|
||||
def sync_dir(src_dir: str, dest: str, url_prefix: str, manifest_template: str) -> str:
|
||||
if not os.path.isdir(src_dir):
|
||||
return f"SKIP {dest}: 缺少 {src_dir},请先运行 python scripts/generate_brand_icons.py"
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
for name in FILES:
|
||||
shutil.copy2(os.path.join(SRC, name), os.path.join(dest, name))
|
||||
src = os.path.join(src_dir, name)
|
||||
if not os.path.isfile(src):
|
||||
return f"SKIP {dest}: 缺少 {src}"
|
||||
shutil.copy2(src, os.path.join(dest, name))
|
||||
manifest_src = os.path.join(REPO, "brand", manifest_template)
|
||||
if os.path.isfile(manifest_src):
|
||||
with open(manifest_src, encoding="utf-8") as f:
|
||||
@@ -53,10 +56,11 @@ def sync_dir(dest: str, url_prefix: str, manifest_template: str) -> str:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(sync_dir(HUB_DEST, "/assets/icons", "manifest.webmanifest"))
|
||||
for d in EXCHANGE_DIRS:
|
||||
dest = os.path.join(REPO, d, "static", "icons")
|
||||
print(sync_dir(dest, "/static/icons", "manifest.exchange.webmanifest"))
|
||||
print(sync_dir(SRC, HUB_DEST, "/assets/icons", "manifest.webmanifest"))
|
||||
for folder, key, manifest in EXCHANGES:
|
||||
dest = os.path.join(REPO, folder, "static", "icons")
|
||||
src_dir = os.path.join(SRC, key)
|
||||
print(sync_dir(src_dir, dest, "/static/icons", manifest))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user