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:
co-authored by
Claude Opus 5.5
parent
b4421dcea5
commit
1c722cca9f
@@ -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))))
|
||||
Reference in New Issue
Block a user