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>
This commit is contained in:
Pierre MARTIN
2026-11-03 01:01:06 +01:00
co-authored by Claude Opus 5.5
parent b4421dcea5
commit 1c722cca9f
31 changed files with 328 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
"""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))))