Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Disegna il Regolino, pixel per pixel, senza immagini sorgenti.
|
||
|
||
python3 tools/favicon.py
|
||
|
||
Scrive assets/favicon/ e /tmp/regolo-shots/favicon-anteprima.png.
|
||
La matrice 16×16 è il disegno originale: ogni simbolo è un pixel.
|
||
Tutti gli ingrandimenti sono interi e usano NEAREST, anche nell'ICO.
|
||
Colori e carattere riprendono regolo() in src/engine/briscola-render.js.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
OUTPUT = ROOT / "assets" / "favicon"
|
||
PREVIEW = Path("/tmp/regolo-shots/favicon-anteprima.png")
|
||
BACKGROUND = "#1b1420"
|
||
PALETTE = {
|
||
".": (0, 0, 0, 0),
|
||
"#": "#38271f", # contorno del dorso
|
||
"G": "#dba32b", # oro
|
||
"H": "#ffe396", # luce sulla corona
|
||
"R": "#aa4034", # rubino
|
||
"L": "#94b365", # testa verde salvia
|
||
"S": "#487348", # ombra / spira
|
||
"V": "#a3bc72", # luce sulla spira
|
||
"B": "#d0cd83", # ventre
|
||
"W": "#fff0cd", # occhi crema
|
||
}
|
||
|
||
# Corona a tre punte, grande testa sorridente, collo e coda arrotolata.
|
||
# Nessun tratto viene interpolato: anche gli occhi sono singoli pixel.
|
||
PIXELS = (
|
||
"....#..##..#....",
|
||
"...#H##HH##H#...",
|
||
"...#HGGGGGGH#...",
|
||
"....#GGRGGG#....",
|
||
"...##########...",
|
||
"..#LLLLLLLLLL#..",
|
||
".#LLWWLLWWLLLL#.",
|
||
".#LLW#LL#WLLLS#.",
|
||
".#LLLLLLLLLLLS#.",
|
||
"..#L#LLLLL#LS#..",
|
||
"...#L#####LS#...",
|
||
"....###BS###....",
|
||
"..###SSBSLSS##..",
|
||
".#SVV##BS##SSS#.",
|
||
".#SVVVVVVVVSS#..",
|
||
"..###########...",
|
||
)
|
||
|
||
|
||
def sprite():
|
||
"""Interpreta la matrice letterale, mantenendo solo alpha 0 o 255."""
|
||
if len(PIXELS) != 16 or any(len(row) != 16 for row in PIXELS):
|
||
raise ValueError("Il disegno deve contenere esattamente 16×16 pixel")
|
||
result = Image.new("RGBA", (16, 16))
|
||
for y, row in enumerate(PIXELS):
|
||
for x, symbol in enumerate(row):
|
||
color = PALETTE[symbol]
|
||
if isinstance(color, str):
|
||
color = tuple(bytes.fromhex(color[1:])) + (255,)
|
||
result.putpixel((x, y), color)
|
||
return result
|
||
|
||
|
||
def enlarge(source, factor):
|
||
return source.resize(
|
||
(source.width * factor, source.height * factor), Image.Resampling.NEAREST
|
||
)
|
||
|
||
|
||
def preview(icons):
|
||
"""Mostra ogni PNG a 1× e 8×, sia su chiaro sia su scuro.
|
||
|
||
Le sei righe includono anche le icone grandi: il foglio è volutamente
|
||
ampio, così la scala dichiarata resta esatta per tutti i file.
|
||
"""
|
||
margin, gap, label_height = 24, 24, 32
|
||
largest = max(icon.width for _, icon in icons)
|
||
panel_width = margin * 2 + largest + gap + largest * 8
|
||
height = margin + sum(label_height + icon.height * 8 + gap for _, icon in icons)
|
||
sheet = Image.new("RGB", (panel_width * 2, height))
|
||
draw = ImageDraw.Draw(sheet)
|
||
font = ImageFont.load_default(size=16)
|
||
for panel, (background, foreground) in enumerate(
|
||
(("#f5f1e9", "#38271f"), (BACKGROUND, "#fff0cd"))
|
||
):
|
||
left = panel * panel_width
|
||
draw.rectangle((left, 0, left + panel_width - 1, height - 1), fill=background)
|
||
y = margin
|
||
for name, icon in icons:
|
||
draw.text(
|
||
(left + margin, y),
|
||
f"{name} | {icon.width} x {icon.height} | 1x e 8x",
|
||
fill=foreground,
|
||
font=font,
|
||
)
|
||
y += label_height
|
||
sheet.paste(icon, (left + margin, y), icon)
|
||
magnified = enlarge(icon, 8)
|
||
sheet.paste(magnified, (left + margin + icon.width + gap, y), magnified)
|
||
y += icon.height * 8 + gap
|
||
PREVIEW.parent.mkdir(parents=True, exist_ok=True)
|
||
sheet.save(PREVIEW, optimize=True)
|
||
|
||
|
||
def main():
|
||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||
base = sprite()
|
||
icons = []
|
||
for size in (16, 32, 48):
|
||
icon = enlarge(base, size // 16)
|
||
name = f"favicon-{size}.png"
|
||
icon.save(OUTPUT / name, optimize=True)
|
||
icons.append((name, icon))
|
||
|
||
# 16×9 = 144: margini di 18 px, centratura esatta sul fondo opaco.
|
||
apple = Image.new("RGBA", (180, 180), BACKGROUND)
|
||
apple.alpha_composite(enlarge(base, 9), (18, 18))
|
||
apple = apple.convert("RGB")
|
||
apple.save(OUTPUT / "apple-touch-icon.png", optimize=True)
|
||
icons.append(("apple-touch-icon.png", apple.convert("RGBA")))
|
||
|
||
for size in (192, 512):
|
||
icon = enlarge(base, size // 16)
|
||
name = f"icon-{size}.png"
|
||
icon.save(OUTPUT / name, optimize=True)
|
||
icons.append((name, icon))
|
||
|
||
# Fornire tutti i frame evita il ridimensionamento implicito di Pillow.
|
||
icons[2][1].save(
|
||
OUTPUT / "favicon.ico",
|
||
format="ICO",
|
||
sizes=[(16, 16), (32, 32), (48, 48)],
|
||
append_images=[icons[0][1], icons[1][1]],
|
||
)
|
||
preview(icons)
|
||
for name, _ in icons:
|
||
print(OUTPUT / name)
|
||
print(OUTPUT / "favicon.ico")
|
||
print(PREVIEW)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|