MSG_MOTOC / scripts /make_icons.py
Samuel ADONE
first commit
5e0d7d2
Raw
History Blame Contribute Delete
5.64 kB
"""Génère les icônes PWA (PNG) sans dépendance externe.
Rendu par supersampling 4x : dégradé de fond, bulle de discussion cyan et
éclair orange — la signature graphique reprise de l'affiche Motocultor.
"""
from __future__ import annotations
import struct
import zlib
from pathlib import Path
OUT = Path(__file__).resolve().parent.parent / "static" / "icons"
SS = 4 # facteur de supersampling
# Palette (voir static/styles.css)
NIGHT_TOP = (13, 40, 49)
NIGHT_BOTTOM = (6, 20, 25)
CYAN = (72, 201, 214)
BUBBLE_FILL = (15, 58, 69)
EMBER = (238, 139, 61)
LIGHTNING = (247, 221, 84)
def lerp(a, b, t):
return tuple(round(x + (y - x) * t) for x, y in zip(a, b))
def rounded_rect_sdf(px, py, x0, y0, x1, y1, r):
"""Distance signée à un rectangle arrondi (négative à l'intérieur)."""
cx = max(x0 + r, min(px, x1 - r))
cy = max(y0 + r, min(py, y1 - r))
dx, dy = px - cx, py - cy
return (dx * dx + dy * dy) ** 0.5 - r
def point_in_poly(px, py, poly):
inside = False
n = len(poly)
for i in range(n):
x0, y0 = poly[i]
x1, y1 = poly[(i + 1) % n]
if (y0 > py) != (y1 > py):
xin = x0 + (py - y0) * (x1 - x0) / (y1 - y0)
if px < xin:
inside = not inside
return inside
def over(dst, src, alpha):
return tuple(round(s * alpha + d * (1 - alpha)) for s, d in zip(src, dst))
def render(size: int, maskable: bool = False) -> bytes:
"""Renvoie les octets RGB (size x size)."""
big = size * SS
# Sur une icône « maskable » Android rogne jusqu'à 20 % : on rétrécit le motif.
pad = 0.28 if maskable else 0.16
radius = big * (0.5 if maskable else 0.22)
# Bulle de discussion
bx0, by0 = big * (pad + 0.02), big * (pad + 0.06)
bx1, by1 = big * (1 - pad - 0.02), big * (1 - pad - 0.16)
br = (bx1 - bx0) * 0.24
stroke = max(1.0, big * 0.018)
# Queue de la bulle
tail = [
(bx0 + (bx1 - bx0) * 0.22, by1 - stroke),
(bx0 + (bx1 - bx0) * 0.44, by1 - stroke),
(bx0 + (bx1 - bx0) * 0.26, by1 + (by1 - by0) * 0.26),
]
# Éclair, centré dans la bulle
cx, cy = (bx0 + bx1) / 2, (by0 + by1) / 2
w, h = (bx1 - bx0) * 0.42, (by1 - by0) * 0.56
bolt = [
(cx + w * 0.16, cy - h * 0.50),
(cx - w * 0.50, cy + h * 0.10),
(cx - w * 0.08, cy + h * 0.10),
(cx - w * 0.20, cy + h * 0.50),
(cx + w * 0.50, cy - h * 0.14),
(cx + w * 0.04, cy - h * 0.14),
]
rows = []
for y in range(size):
row = bytearray()
for x in range(size):
racc = gacc = bacc = 0
for sy in range(SS):
for sx in range(SS):
px = x * SS + sx + 0.5
py = y * SS + sy + 0.5
# Fond : dégradé vertical, arrondi aux coins
col = lerp(NIGHT_TOP, NIGHT_BOTTOM, py / big)
d_bg = rounded_rect_sdf(px, py, 0, 0, big, big, radius)
bg_a = max(0.0, min(1.0, 0.5 - d_bg))
col = over((0, 0, 0), col, bg_a)
# Lueur de braise en bas
gy = max(0.0, (py / big - 0.55) / 0.45)
gx = 1.0 - abs(px / big - 0.5) * 1.6
glow = max(0.0, gy * gx) * 0.30 * bg_a
if glow > 0:
col = over(col, EMBER, glow)
# Bulle : remplissage puis contour cyan
d = rounded_rect_sdf(px, py, bx0, by0, bx1, by1, br)
in_tail = point_in_poly(px, py, tail)
fill_a = max(0.0, min(1.0, 0.5 - d))
if in_tail:
fill_a = 1.0
if fill_a > 0:
col = over(col, BUBBLE_FILL, fill_a)
edge = abs(d) - stroke / 2
edge_a = max(0.0, min(1.0, 0.5 - edge))
if in_tail:
edge_a = max(edge_a, 0.0)
if edge_a > 0:
col = over(col, CYAN, edge_a)
# Éclair
if point_in_poly(px, py, bolt):
col = LIGHTNING
elif point_in_poly(px + stroke * 0.6, py + stroke * 0.6, bolt):
col = EMBER
racc += col[0]
gacc += col[1]
bacc += col[2]
n = SS * SS
row += bytes((racc // n, gacc // n, bacc // n))
rows.append(bytes(row))
return b"".join(b"\x00" + r for r in rows)
def write_png(path: Path, size: int, raw: bytes) -> None:
def chunk(tag: bytes, data: bytes) -> bytes:
body = tag + data
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body))
header = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # 8 bits, RGB
png = (
b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", header)
+ chunk(b"IDAT", zlib.compress(raw, 9))
+ chunk(b"IEND", b"")
)
path.write_bytes(png)
print(f" {path.name} ({len(png) / 1024:.1f} Ko)")
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
print("Génération des icônes :")
for size in (192, 512):
write_png(OUT / f"icon-{size}.png", size, render(size))
for size in (192, 512):
write_png(OUT / f"maskable-{size}.png", size, render(size, maskable=True))
write_png(OUT / "favicon-64.png", 64, render(64))
write_png(OUT / "apple-touch-icon.png", 180, render(180, maskable=True))
if __name__ == "__main__":
main()