960565482f
Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
"""Generate PWA / favicon assets under static/icons."""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
BASE = os.path.join(os.path.dirname(__file__), "..", "static", "icons")
|
|
BASE = os.path.abspath(BASE)
|
|
|
|
BG = (11, 13, 20, 255)
|
|
PANEL = (18, 24, 40, 255)
|
|
ACCENT = (66, 133, 244, 255)
|
|
ACCENT2 = (123, 66, 255, 255)
|
|
WHITE = (255, 255, 255, 255)
|
|
|
|
|
|
def thick_line(draw: ImageDraw.ImageDraw, p1, p2, t: float, color) -> None:
|
|
x1, y1 = p1
|
|
x2, y2 = p2
|
|
dx, dy = x2 - x1, y2 - y1
|
|
length = math.hypot(dx, dy) or 1.0
|
|
ux, uy = -dy / length, dx / length
|
|
hx, hy = ux * t / 2, uy * t / 2
|
|
draw.polygon(
|
|
[
|
|
(x1 + hx, y1 + hy),
|
|
(x2 + hx, y2 + hy),
|
|
(x2 - hx, y2 - hy),
|
|
(x1 - hx, y1 - hy),
|
|
],
|
|
fill=color,
|
|
)
|
|
|
|
|
|
def make_icon(size: int, *, maskable: bool = False) -> Image.Image:
|
|
im = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(im)
|
|
pad = int(size * (0.08 if maskable else 0.0))
|
|
outer = [pad, pad, size - 1 - pad, size - 1 - pad]
|
|
r = int((size - 2 * pad) * 0.22)
|
|
d.rounded_rectangle(outer, radius=r, fill=BG)
|
|
inset = int(size * 0.07)
|
|
inner = [pad + inset, pad + inset, size - 1 - pad - inset, size - 1 - pad - inset]
|
|
r2 = max(8, int((size - 2 * pad - 2 * inset) * 0.18))
|
|
d.rounded_rectangle(inner, radius=r2, fill=PANEL, outline=WHITE, width=max(2, size // 64))
|
|
cx = cy = size // 2
|
|
ring_r = int(size * 0.22)
|
|
stroke = max(3, size // 28)
|
|
bbox = [
|
|
cx - ring_r,
|
|
cy - ring_r - int(size * 0.02),
|
|
cx + ring_r,
|
|
cy + ring_r - int(size * 0.02),
|
|
]
|
|
d.ellipse(bbox, outline=ACCENT, width=stroke)
|
|
arm = int(size * 0.11)
|
|
thick = max(3, size // 26)
|
|
ox = cx
|
|
oy = cy - int(size * 0.02)
|
|
thick_line(d, (ox - arm, oy - arm), (ox + arm, oy + arm), thick, WHITE)
|
|
thick_line(d, (ox + arm, oy - arm), (ox - arm, oy + arm), thick, WHITE)
|
|
bar_h = max(3, size // 28)
|
|
bar_w = int(size * 0.28)
|
|
by = int(size * (0.74 if maskable else 0.78))
|
|
d.rounded_rectangle(
|
|
[cx - bar_w // 2, by, cx + bar_w // 2, by + bar_h],
|
|
radius=max(1, bar_h // 2),
|
|
fill=ACCENT2,
|
|
)
|
|
return im
|
|
|
|
|
|
def main() -> None:
|
|
os.makedirs(BASE, exist_ok=True)
|
|
mapping = {
|
|
"icon-16.png": 16,
|
|
"icon-32.png": 32,
|
|
"apple-touch-icon.png": 180,
|
|
"icon-192.png": 192,
|
|
"icon-512.png": 512,
|
|
}
|
|
for name, sz in mapping.items():
|
|
path = os.path.join(BASE, name)
|
|
make_icon(sz).save(path, format="PNG", optimize=True)
|
|
print("wrote", name, os.path.getsize(path))
|
|
|
|
for name, sz in (("icon-192-maskable.png", 192), ("icon-512-maskable.png", 512)):
|
|
path = os.path.join(BASE, name)
|
|
make_icon(sz, maskable=True).save(path, format="PNG", optimize=True)
|
|
print("wrote", name, os.path.getsize(path))
|
|
|
|
ico_sizes = [16, 32, 48]
|
|
frames = [make_icon(s) for s in ico_sizes]
|
|
ico_path = os.path.join(BASE, "favicon.ico")
|
|
frames[0].save(
|
|
ico_path,
|
|
format="ICO",
|
|
sizes=[(s, s) for s in ico_sizes],
|
|
append_images=frames[1:],
|
|
)
|
|
print("wrote favicon.ico", os.path.getsize(ico_path))
|
|
|
|
svg = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
<rect width="512" height="512" rx="112" fill="#0b0d14"/>
|
|
<rect x="36" y="36" width="440" height="440" rx="88" fill="#121828" stroke="#ffffff" stroke-width="10"/>
|
|
<circle cx="256" cy="236" r="108" fill="none" stroke="#4285f4" stroke-width="22"/>
|
|
<path d="M196 176 L316 296 M316 176 L196 296" stroke="#ffffff" stroke-width="28" stroke-linecap="round"/>
|
|
<rect x="186" y="392" width="140" height="18" rx="9" fill="#7b42ff"/>
|
|
</svg>
|
|
"""
|
|
with open(os.path.join(BASE, "icon.svg"), "w", encoding="utf-8") as f:
|
|
f.write(svg)
|
|
print("wrote icon.svg")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|