feat: add brand icons for Chrome shortcuts and PWA manifest
Dark cyan-green candlestick icon for hub and four exchanges; generate/sync scripts and docs/shortcut-icon.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 brand/icon.svg 逻辑绘制 PNG/ICO,供 Chrome 快捷方式、PWA manifest 使用。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT = os.path.join(REPO, "brand", "icons")
|
||||
|
||||
|
||||
def _png_chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return (
|
||||
struct.pack(">I", len(data))
|
||||
+ tag
|
||||
+ data
|
||||
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
|
||||
def _write_png(path: str, size: int, rgba: bytes) -> None:
|
||||
raw = b""
|
||||
stride = size * 4
|
||||
for y in range(size):
|
||||
raw += b"\x00" + rgba[y * stride : (y + 1) * stride]
|
||||
ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
|
||||
idat = zlib.compress(raw, 9)
|
||||
png = (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
+ _png_chunk(b"IHDR", ihdr)
|
||||
+ _png_chunk(b"IDAT", idat)
|
||||
+ _png_chunk(b"IEND", b"")
|
||||
)
|
||||
with open(path, "wb") as f:
|
||||
f.write(png)
|
||||
|
||||
|
||||
def _lerp(a: float, b: float, t: float) -> float:
|
||||
return a + (b - a) * t
|
||||
|
||||
|
||||
def _gradient(t: float) -> tuple[int, int, int]:
|
||||
if t < 0.55:
|
||||
u = t / 0.55
|
||||
return (
|
||||
int(_lerp(0, 0, u)),
|
||||
int(_lerp(212, 139, u)),
|
||||
int(_lerp(255, 255, u)),
|
||||
)
|
||||
u = (t - 0.55) / 0.45
|
||||
return (
|
||||
int(_lerp(61, 0, u)),
|
||||
int(_lerp(255, 255, u)),
|
||||
int(_lerp(157, 157, u)),
|
||||
)
|
||||
|
||||
|
||||
def _inside_round_rect(x: int, y: int, size: int, pad: int, radius: int) -> bool:
|
||||
if x < pad or y < pad or x >= size - pad or y >= size - pad:
|
||||
return False
|
||||
r = radius
|
||||
if x < pad + r and y < pad + r:
|
||||
return (x - pad - r) ** 2 + (y - pad - r) ** 2 <= r * r
|
||||
if x >= size - pad - r and y < pad + r:
|
||||
return (x - (size - pad - r)) ** 2 + (y - pad - r) ** 2 <= r * r
|
||||
if x < pad + r and y >= size - pad - r:
|
||||
return (x - pad - r) ** 2 + (y - (size - pad - r)) ** 2 <= r * r
|
||||
if x >= size - pad - r and y >= size - pad - r:
|
||||
return (x - (size - pad - r)) ** 2 + (y - (size - pad - r)) ** 2 <= r * r
|
||||
return True
|
||||
|
||||
|
||||
def _draw_candle(
|
||||
buf: bytearray,
|
||||
size: int,
|
||||
cx: int,
|
||||
top: int,
|
||||
bottom: int,
|
||||
body_top: int,
|
||||
body_bottom: int,
|
||||
rgb: tuple[int, int, int],
|
||||
wick_w: int = 2,
|
||||
body_half: int = 7,
|
||||
) -> None:
|
||||
for y in range(max(0, top), min(size, bottom + 1)):
|
||||
for x in range(cx - wick_w, cx + wick_w + 1):
|
||||
if 0 <= x < size:
|
||||
i = (y * size + x) * 4
|
||||
buf[i : i + 3] = bytes((*rgb, 255))
|
||||
for y in range(max(0, body_top), min(size, body_bottom + 1)):
|
||||
for x in range(cx - body_half, cx + body_half + 1):
|
||||
if 0 <= x < size:
|
||||
i = (y * size + x) * 4
|
||||
buf[i : i + 3] = bytes((*rgb, 255))
|
||||
|
||||
|
||||
def render_icon_rgba(size: int) -> bytes:
|
||||
buf = bytearray(size * size * 4)
|
||||
pad = max(4, size // 14)
|
||||
radius = size // 5
|
||||
inner_pad = pad + max(2, size // 32)
|
||||
ring_w = max(2, size // 48)
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
i = (y * size + x) * 4
|
||||
if not _inside_round_rect(x, y, size, pad, radius):
|
||||
buf[i : i + 4] = b"\x00\x00\x00\x00"
|
||||
continue
|
||||
if _inside_round_rect(x, y, size, inner_pad, radius - 4):
|
||||
buf[i : i + 4] = bytes((18, 24, 42, 255))
|
||||
else:
|
||||
t = (x + y) / (2 * size)
|
||||
r, g, b = _gradient(t)
|
||||
buf[i : i + 4] = bytes((r, g, b, 220))
|
||||
|
||||
s = size / 512.0
|
||||
def sc(v: int) -> int:
|
||||
return int(v * s)
|
||||
|
||||
_draw_candle(buf, size, sc(148), sc(228), sc(332), sc(252), sc(304), (255, 77, 109), sc(8), sc(16))
|
||||
_draw_candle(buf, size, sc(228), sc(196), sc(352), sc(220), sc(308), (0, 255, 157), sc(8), sc(16))
|
||||
_draw_candle(buf, size, sc(308), sc(240), sc(320), sc(264), sc(304), (0, 255, 157), sc(8), sc(14))
|
||||
_draw_candle(buf, size, sc(384), sc(180), sc(300), sc(208), sc(272), (0, 255, 157), sc(8), sc(16))
|
||||
|
||||
ccx, ccy = sc(256), sc(256)
|
||||
cr = sc(52)
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
d = ((x - ccx) ** 2 + (y - ccy) ** 2) ** 0.5
|
||||
if cr - ring_w <= d <= cr:
|
||||
t = (x + y) / (2 * size)
|
||||
r, g, b = _gradient(t)
|
||||
i = (y * size + x) * 4
|
||||
buf[i : i + 4] = bytes((r, g, b, 200))
|
||||
tri = [
|
||||
(sc(236), sc(276)),
|
||||
(sc(256), sc(220)),
|
||||
(sc(276), sc(276)),
|
||||
]
|
||||
|
||||
def _in_tri(px: int, py: int) -> bool:
|
||||
x1, y1 = tri[0]
|
||||
x2, y2 = tri[1]
|
||||
x3, y3 = tri[2]
|
||||
d1 = (px - x2) * (y1 - y2) - (x1 - x2) * (py - y2)
|
||||
d2 = (px - x3) * (y2 - y3) - (x2 - x3) * (py - y3)
|
||||
d3 = (px - x1) * (y3 - y1) - (x3 - x1) * (py - y1)
|
||||
has_neg = d1 < 0 or d2 < 0 or d3 < 0
|
||||
has_pos = d1 > 0 or d2 > 0 or d3 > 0
|
||||
return not (has_neg and has_pos)
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
if _in_tri(x, y):
|
||||
t = (x + y) / (2 * size)
|
||||
r, g, b = _gradient(t)
|
||||
i = (y * size + x) * 4
|
||||
buf[i : i + 4] = bytes((r, g, b, 255))
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def _write_ico(path: str, sizes: list[int]) -> None:
|
||||
images = []
|
||||
for sz in sizes:
|
||||
rgba = render_icon_rgba(sz)
|
||||
# BGRA bottom-up for ICO
|
||||
row = sz * 4
|
||||
pixels = bytearray()
|
||||
for y in range(sz - 1, -1, -1):
|
||||
for x in range(sz):
|
||||
i = (y * sz + x) * 4
|
||||
pixels.extend([rgba[i + 2], rgba[i + 1], rgba[i], rgba[i + 3]])
|
||||
and_row = (sz * 4 + 3) & ~3
|
||||
if and_row > sz * 4:
|
||||
pixels.extend(b"\x00" * (and_row - sz * 4))
|
||||
bmp = b"BM" + struct.pack("<I", 40 + len(pixels)) + b"\x00\x00\x00\x00" + struct.pack(
|
||||
"<I", 40
|
||||
) + struct.pack("<i", sz) + struct.pack("<i", sz * 2) + struct.pack("<H", 1) + struct.pack(
|
||||
"<H", 32
|
||||
) + struct.pack("<I", 0) + struct.pack("<I", len(pixels)) + struct.pack("<i", 0) * 4
|
||||
images.append((sz, bmp + bytes(pixels)))
|
||||
|
||||
offset = 6 + 16 * len(images)
|
||||
parts = [struct.pack("<HHH", 0, 1, len(images))]
|
||||
data_parts = []
|
||||
for sz, data in images:
|
||||
parts.append(
|
||||
struct.pack("<BBBBHHII", 32, 32, 0, 0, 1, 32, len(data), offset)
|
||||
)
|
||||
offset += len(data)
|
||||
data_parts.append(data)
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"".join(parts) + b"".join(data_parts))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
import shutil
|
||||
|
||||
shutil.copy2(
|
||||
os.path.join(REPO, "brand", "icon.svg"),
|
||||
os.path.join(OUT, "icon.svg"),
|
||||
)
|
||||
for sz in (16, 32, 180, 192, 512):
|
||||
_write_png(os.path.join(OUT, f"icon-{sz}.png"), sz, render_icon_rgba(sz))
|
||||
shutil.copy2(os.path.join(OUT, "icon-180.png"), os.path.join(OUT, "apple-touch-icon.png"))
|
||||
_write_ico(os.path.join(OUT, "favicon.ico"), [16, 32, 48])
|
||||
print(f"DONE {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user