"""Le champ de pastilles — génère le motif signature de Klimat (adopté le 6 novembre 2026).

Usage : python3 pastilles.py SORTIE.svg LARGEUR HAUTEUR GRAINE THEME [ZONES_CALMES]

Une trame de points en quinconce dont la taille suit un champ lisse fait de cellules de pression :
chaque point est une personne, l'ensemble un collectif, et l'on ne voit jamais un point seul. Le
plus gros point, hors des marges, devient la pastille Véroné : elle répond au disque du logo et au
point des pictogrammes. Il n'y en a jamais deux.

THEME : brume | nuit | blanc.
ZONES_CALMES : une ou plusieurs ellipses « cx,cy,rx,ry » séparées par « ; », en fractions de la
largeur et de la hauteur. Les points s'y effacent (fondu sur 35 % du rayon) : un titre, un logo ou
le nom d'une organisation s'y posent, jamais sur la trame.
Exemple : python3 pastilles.py couverture.svg 1920 1080 23 brume "0.30,0.86,0.46,0.30;0.10,0.09,0.14,0.12"
Aucune dépendance hors numpy.
"""
import math, sys
import numpy as np

THEMES = {
    #        fond       points     opacité  pastille
    'brume': ('#eef3f1', '#9fb1ab', .95, '#009f7f'),
    'nuit':  ('#0e1917', '#6cc9b0', .50, '#6cc9b0'),
    'blanc': ('#ffffff', '#b9c6c2', .95, '#009f7f'),
}
COLONNES = 72        # points par largeur : le pas vaut LARGEUR / 72
RAYON_MAX = .44      # rayon maximal, en fraction du pas
PASTILLE = .62       # rayon de la pastille Véroné, en fraction du pas
SEUIL = .18          # sous ce niveau du champ, aucun point


def champ(W, H, graine):
    """Somme de 14 reliefs gaussiens serrés (hautes et basses pressions), normalisée de 0 à 1."""
    rng = np.random.default_rng(graine)
    bosses = [(rng.uniform(-.1, 1.1), rng.uniform(-.1, H / W + .1), rng.uniform(.05, .16),
               rng.choice([-1, 1]) * rng.uniform(.35, 1)) for _ in range(14)]

    def f(x, y):
        u, v = x / W, y / W
        z = .35 * u + .15 * v
        for cx, cy, s, a in bosses:
            z = z + a * np.exp(-((u - cx) ** 2 + (v - cy) ** 2) / (2 * s * s))
        return z
    xs, ys = np.meshgrid(np.linspace(0, W, 160), np.linspace(0, H, 90))
    Z = f(xs, ys)
    lo, hi = Z.min(), Z.max()
    return lambda x, y: (f(x, y) - lo) / (hi - lo)


def attenuation(W, H, zones):
    def a(x, y):
        m = 1.0
        for cx, cy, rx, ry in zones:
            e = math.hypot((x / W - cx) / rx, (y / H - cy) / ry)
            m = min(m, max(0.0, min(1.0, (e - 1) / .35)))
        return m
    return a


def trame(W, H, graine=23, theme='brume', zones=()):
    fond, encre, opacite, signe = THEMES[theme]
    F, att = champ(W, H, graine), attenuation(W, H, zones)
    pas = W / COLONNES
    points = []
    for j in range(int(H / (pas * .866)) + 2):
        for i in range(COLONNES + 2):
            x = i * pas + (pas / 2 if j % 2 else 0)
            y = j * pas * .866
            v = max(0.0, (float(F(x, y)) - SEUIL) / (1 - SEUIL)) ** 1.15 * att(x, y)
            r = pas * RAYON_MAX * v
            if r >= .55:
                points.append((x, y, r))
    dedans = [p for p in points if pas * 3 < p[0] < W - pas * 3 and pas * 2 < p[1] < H - pas * 2]
    p0 = max(dedans or points, key=lambda p: p[2])
    corps = ''.join(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.2f}"/>' for x, y, r in points if (x, y, r) != p0)
    return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">'
            f'<rect width="{W}" height="{H}" fill="{fond}"/>'
            f'<g fill="{encre}" fill-opacity="{opacite}">{corps}</g>'
            f'<circle cx="{p0[0]:.1f}" cy="{p0[1]:.1f}" r="{pas * PASTILLE:.2f}" fill="{signe}"/></svg>\n')


# Les fichiers du kit : nom, largeur, hauteur, fond, zones calmes, emploi. Lu par outils/construire.py et outils/page.py.
TITRE_BAS_GAUCHE, LOGO_HAUT_GAUCHE = (.30, .86, .46, .30), (.10, .09, .14, .12)
FICHIERS = [
    ('pastilles-brume-16x9', 1920, 1080, 'brume', [TITRE_BAS_GAUCHE, LOGO_HAUT_GAUCHE], 'Couverture de présentation, connexion, page d’erreur'),
    ('pastilles-nuit-16x9', 1920, 1080, 'nuit', [TITRE_BAS_GAUCHE, LOGO_HAUT_GAUCHE], 'Diapositive de clôture, connexion en thème sombre'),
    ('pastilles-brume-bandeau', 1600, 400, 'brume', [(.16, .5, .30, .9)], 'Bandeau d’en-tête, haut de page web'),
    ('pastilles-nuit-bandeau', 1600, 400, 'nuit', [(.16, .5, .30, .9)], 'Visuels sombres, réseaux sociaux'),
    ('pastilles-brume-email', 1200, 240, 'brume', [(.15, .5, .26, .75), (.92, .5, .22, .75)], 'En-tête des e-mails, affiché à 600 × 120'),
    ('pastilles-brume-partage', 1200, 630, 'brume', [(.30, .80, .40, .42), (.10, .10, .14, .14)], 'Visuel de partage, titre en bas à gauche'),
    ('pastilles-nuit-partage', 1200, 630, 'nuit', [(.30, .80, .40, .42), (.10, .10, .14, .14)], 'Visuel de partage sombre'),
    ('pastilles-brume-banniere', 1584, 396, 'brume', [(.36, .55, .30, .62)], 'Bannière LinkedIn'),
    ('pastilles-blanc-carre', 1200, 1200, 'blanc', [(.5, .5, .24, .24)], 'Filigrane d’état vide, pictogramme dans la clairière'),
]


if __name__ == '__main__':
    sortie, W, H, graine, theme = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4]), sys.argv[5]
    zones = [tuple(float(v) for v in z.split(',')) for z in sys.argv[6].split(';')] if len(sys.argv) > 6 and sys.argv[6] else []
    svg = trame(W, H, graine, theme, zones)
    open(sortie, 'w').write(svg)
    print(sortie, len(svg) // 1024, 'Kio')
