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>
193 lines
8.5 KiB
Python
193 lines
8.5 KiB
Python
"""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))))
|