Files
Pierre MARTINandClaude Opus 5.5 1c722cca9f Identité visuelle : logo, icônes et ADR 0010
Symbole P3d (deux pions sur une flèche brique), logotype L3 (« mm » en
flèches alternées), accent brique, icône sur fond ivoire, choisis par
Pierre. Fichiers SVG et PNG dans docs/identite/logo/, générateur et notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:01:06 +01:00

193 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Génère les fichiers SVG de l'identité Ustagammon : symbole P3d, logotype L3.
Le symbole : deux pions vus de trois quarts (le maître en noyer devant,
l'apprenti en ivoire derrière) posés sur une flèche brique ; leurs deux
silhouettes dessinent un 8, clin d'œil à l'Atelier du Huit.
Le logotype : « Ustagammon » en Alegreya Sans Bold (SIL OFL), dont les deux
« m » sont redessinés en quatre flèches au trait, alternant noyer et brique.
Usage : python3 -I generer-logo.py <AlegreyaSans-Bold.ttf> <dossier_sortie>
Licence des dessins produits : CC BY-SA 4.0.
"""
import os
import sys
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.ttLib import TTFont
FONT, OUT = sys.argv[1], sys.argv[2]
os.makedirs(OUT, exist_ok=True)
# ---------- Palettes ----------
# d/ds/de : face, tranche et filet du pion noyer ; l/ls : face et tranche du pion ivoire ;
# o : contour du pion ivoire ; a : flèche et filet brique ; sep : liseré du pion noyer ;
# txt / acc : texte du logotype et flèches brique des « mm ».
COULEUR = dict(d="#4A2E1F", ds="#2B1A10", de="#7A5038", l="#F3EAD8", ls="#CDB898",
o="#2B1A10", a="#A8432B", sep="#F3EAD8", txt="#2B1D15", acc="#A8432B")
SOMBRE = dict(COULEUR, a="#C9583C", txt="#F3EAD8", acc="#D0694B")
# Monochromes : I = encre, H = trou (transparent)
MONO = dict(d="I", ds="I", de="H", l="H", ls="I", o="I", a="I", sep="H", txt="I", acc="I")
def symbol(p, simple=False):
"""Symbole P3d dans une boîte de 64 × 64. `simple` : sans filets, pour le favicon."""
s = [f'<polygon points="6,62 58,62 32,1" fill="{p["a"]}"/>']
ow = 3 if simple else 2.2
s.append(f'<path d="M21.5,21 v4.5 a10.5,7 0 0 0 21,0 v-4.5 z" fill="{p["ls"]}" stroke="{p["o"]}" stroke-width="{ow}" stroke-linejoin="round"/>')
s.append(f'<ellipse cx="32" cy="21" rx="10.5" ry="7" fill="{p["l"]}" stroke="{p["o"]}" stroke-width="{ow}"/>')
if not simple:
s.append(f'<ellipse cx="32" cy="21" rx="5.5" ry="3.6" fill="none" stroke="{p["a"]}" stroke-width="2"/>')
sw = 2.6 if simple else 2
s.append(f'<path d="M17.5,43 v6.5 a14.5,9.5 0 0 0 29,0 v-6.5 z" fill="{p["ds"]}" stroke="{p["sep"]}" stroke-width="{sw}" stroke-linejoin="round"/>')
s.append(f'<ellipse cx="32" cy="43" rx="14.5" ry="9.5" fill="{p["d"]}" stroke="{p["sep"]}" stroke-width="{sw}"/>')
if not simple:
s.append(f'<ellipse cx="32" cy="43" rx="8.3" ry="5.4" fill="none" stroke="{p["de"]}" stroke-width="1.6"/>')
return "".join(s)
# ---------- Logotype ----------
font = TTFont(FONT)
gs = font.getGlyphSet()
cmap = font.getBestCmap()
hmtx = font["hmtx"]
XH = font["OS/2"].sxHeight # hauteur d'x (465 pour Alegreya Sans Bold)
STROKE = 0.21 * XH # épaisseur du trait des flèches, proche du fût des lettres
MM_W = 3.0 * XH # largeur du bloc « mm »
GAP = 20
def glyph(ch, x, fill):
g = cmap[ord(ch)]
pen = SVGPathPen(gs)
gs[g].draw(pen)
return f'<path transform="translate({x:.1f},0) scale(1,-1)" d="{pen.getCommands()}" fill="{fill}"/>', hmtx[g][0]
def inter(p1, d1, p2, d2):
"""Intersection de deux droites données par un point et une direction."""
(x1, y1), (dx1, dy1), (x2, y2), (dx2, dy2) = p1, d1, p2, d2
det = dx1 * -dy2 - dy1 * -dx2
t = ((x2 - x1) * -dy2 - (y2 - y1) * -dx2) / det
return (x1 + t * dx1, y1 + t * dy1)
def chevron(x0, x1, top):
"""Flèche « ^ » au trait : pieds coupés sur la ligne de base (y = 0), sommet en onglet."""
peak = ((x0 + x1) / 2, -top)
h = STROKE / 2
lines = []
for a, b in (((x0, 0), peak), (peak, (x1, 0))):
dx, dy = b[0] - a[0], b[1] - a[1]
n = (dx * dx + dy * dy) ** 0.5
nx, ny = -dy / n, dx / n
lines.append(((a[0] + nx * h, a[1] + ny * h), (dx, dy), (a[0] - nx * h, a[1] - ny * h)))
(lo, ld, li), (ro, rd, ri) = lines
base = ((0, 0), (1, 0))
l_ext, l_int = (li, lo) if li[1] < lo[1] else (lo, li)
r_ext, r_int = (ri, ro) if ri[1] < ro[1] else (ro, ri)
pts = [inter(l_ext, ld, *base), inter(l_ext, ld, r_ext, rd), inter(r_ext, rd, *base),
inter(r_int, rd, *base), inter(l_int, ld, r_int, rd), inter(l_int, ld, *base)]
return " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
def logotype(p, x=0.0):
parts = []
for ch in "Ustaga":
s, adv = glyph(ch, x, p["txt"])
parts.append(s)
x += adv
x += GAP
unit = MM_W / 300 # coordonnées de la maquette : boîte de 300 × 100
top = XH * 1.06 # léger dépassement de la hauteur d'x, comme une lettre pointue
for i, (a, b) in enumerate(((8, 74), (74, 140), (160, 226), (226, 292))):
fill = p["txt"] if i % 2 == 0 else p["acc"]
parts.append(f'<polygon points="{chevron(x + a * unit, x + b * unit, top)}" fill="{fill}"/>')
x += MM_W + GAP
for ch in "on":
s, adv = glyph(ch, x, p["txt"])
parts.append(s)
x += adv
return "".join(parts), x
ASC, DESC = 680, 240 # marges verticales autour de la ligne de base
def mono_wrap(body, vb, ink, mid):
"""Transforme un dessin encre/trou en dessin d'une seule couleur, trous transparents."""
body = body.replace('"I"', '"#fff"').replace('"H"', '"#000"')
x, y, w, h = vb
return (f'<mask id="{mid}" maskUnits="userSpaceOnUse" x="{x}" y="{y}" width="{w}" height="{h}">'
f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="#000"/>{body}</mask>'
f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="{ink}" mask="url(#{mid})"/>')
def svg(vb, body, title):
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{" ".join(f"{v:g}" for v in vb)}" '
f'width="{vb[2]:g}" height="{vb[3]:g}" role="img" aria-label="{title}"><title>{title}</title>{body}</svg>\n')
def write(name, content):
with open(os.path.join(OUT, name), "w", encoding="utf-8") as f:
f.write(content)
VARIANTS = {"couleur": COULEUR, "sombre": SOMBRE, "noir": "#000000", "blanc": "#FFFFFF"}
def palette(v):
return VARIANTS[v] if isinstance(VARIANTS[v], dict) else MONO
def ink(body, v):
return body if isinstance(VARIANTS[v], dict) else body.replace('"I"', f'"{VARIANTS[v]}"')
# ---------- Symbole ----------
for v in VARIANTS:
vb = (0, 0, 64, 64)
body = symbol(palette(v))
if not isinstance(VARIANTS[v], dict):
body = mono_wrap(body, vb, VARIANTS[v], "m")
write(f"symbole-{v}.svg", svg(vb, body, "Ustagammon"))
# ---------- Logotype seul ----------
for v in VARIANTS:
body, w = logotype(palette(v))
write(f"logotype-{v}.svg", svg((-10, -ASC, round(w + 20), ASC + DESC), ink(body, v), "Ustagammon"))
# ---------- Logo horizontal ----------
SYM = 1450 # hauteur du symbole, pour un corps de 1000
CENTER = -322 # centre optique : mi-hauteur des capitales
for v in VARIANTS:
p = palette(v)
sym = f'<g transform="translate(0,{CENTER - SYM / 2}) scale({SYM / 64:.4f})">{symbol(p)}</g>'
text, w = logotype(p, x=SYM + 280)
vb = (-20, CENTER - SYM / 2 - 20, round(w + 40), SYM + 40)
if not isinstance(VARIANTS[v], dict):
sym = mono_wrap(sym, vb, VARIANTS[v], "m")
write(f"logo-horizontal-{v}.svg", svg(vb, sym + ink(text, v), "Ustagammon"))
# ---------- Icônes ----------
IVOIRE = "#F3EAD8"
def placed(frac, size, pal=COULEUR, simple=False):
"""Symbole centré dans un carré de `size`, occupant la fraction `frac` du côté."""
s = size * frac / 64
off = (size - 64 * s) / 2
return f'<g transform="translate({off:.2f},{off:.2f}) scale({s:.4f})">{symbol(pal, simple)}</g>'
# Icône « any » : tuile ivoire aux coins arrondis, transparente autour
write("icone.svg", svg((0, 0, 512, 512), f'<rect width="512" height="512" rx="112" fill="{IVOIRE}"/>' + placed(0.70, 512), "Ustagammon"))
# Icône « maskable » : fond plein, symbole dans la zone de sécurité (cercle de 80 %)
write("icone-maskable.svg", svg((0, 0, 512, 512), f'<rect width="512" height="512" fill="{IVOIRE}"/>' + placed(0.56, 512), "Ustagammon"))
# Favicon : dessin simplifié (sans filets), traits épaissis, fond transparent
write("favicon.svg", svg((0, 0, 64, 64), symbol(COULEUR, simple=True), "Ustagammon"))
# Android, icône adaptative : canevas de 108 × 108 dp, zone sûre de 66 dp au centre
write("android-premier-plan.svg", svg((0, 0, 108, 108), placed(0.58, 108), "Ustagammon, premier plan"))
write("android-arriere-plan.svg", svg((0, 0, 108, 108), f'<rect width="108" height="108" fill="{IVOIRE}"/>', "Ustagammon, arrière-plan"))
write("android-monochrome.svg", svg((0, 0, 108, 108), mono_wrap(placed(0.58, 108, MONO), (0, 0, 108, 108), "#000000", "m"), "Ustagammon, monochrome"))
print("\n".join(sorted(os.listdir(OUT))))