"""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()