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 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.">
|
<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>
|
<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">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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()
|
||||||