Il favicon: un Regolino in pixel art con la corona (Codex, tools/favicon.py), da 16 a 512 pixel e per l'iPhone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
|
After Width: | Height: | Size: 772 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 835 B |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -4,6 +4,11 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<meta name="theme-color" content="#1b1420"><meta name="description" content="La Leggenda del Regolo: un'avventura punta-e-clicca a San Piero a Sieve, durante il Palio della Fortezza del 2007.">
|
||||
<title>La Leggenda del Regolo</title>
|
||||
<!-- Il Regolino (tools/favicon.py, disegnato da Codex pixel per pixel) -->
|
||||
<link rel="icon" href="assets/favicon/favicon.ico" sizes="16x16 32x32 48x48">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/favicon/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/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()
|
||||