File size: 5,640 Bytes
5e0d7d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""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()