font update
This commit is contained in:
1 parent
f754cafbe4
commit
17731364bd
66 files changed
+10342
-76
No files matched your search
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Генератор PWA-ассетов JapLearn: иконки + iOS splash-экраны.
|
||||
|
||||
Запуск (из корня проекта): python3 tools/gen_pwa_assets.py
|
||||
Требуется Pillow (PIL) и ImageFont из системного Python.
|
||||
|
||||
Что делает:
|
||||
- public/icons/*.png — 192/512 (any), 192/512 (maskable), apple-touch 180, favicon 32
|
||||
- public/splash/*.png — apple-touch-startup-image под iPhone/iPad
|
||||
- печатает готовый HTML-сниппет splash-ссылок для index.html
|
||||
|
||||
Палитра — Gruvbox Dark из src/style.css (--bg #282828, --primary-strong #49a8ac).
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FONT_PATH = ROOT / "public/fonts/NotoSansJP-VariableFont_wght.ttf"
|
||||
ICONS_DIR = ROOT / "public/icons"
|
||||
SPLASH_DIR = ROOT / "public/splash"
|
||||
|
||||
BG = (40, 40, 40) # --bg #282828
|
||||
ACCENT = (73, 168, 172) # --primary-strong #49a8ac
|
||||
MUTED = (144, 140, 124) # --muted #908c7c
|
||||
GLYPH = "語" # «слово» — символ приложения
|
||||
|
||||
# Порядок: (файл, CSS-ширина, CSS-высота, DPR, подпись модели)
|
||||
SPLASHES = [
|
||||
("iphone-se", 375, 667, 2, "iPhone SE / 8"),
|
||||
("iphone-8-plus", 414, 736, 3, "iPhone 8 Plus"),
|
||||
("iphone-x", 375, 812, 3, "iPhone X/XS/11 Pro/12 mini/13 mini"),
|
||||
("iphone-xr", 414, 896, 2, "iPhone XR/11"),
|
||||
("iphone-xs-max", 414, 896, 3, "iPhone XS Max/11 Pro Max"),
|
||||
("iphone-12", 390, 844, 3, "iPhone 12/13/14"),
|
||||
("iphone-12-pro-max", 428, 926, 3, "iPhone 12/13/14 Pro Max"),
|
||||
("iphone-14-pro", 393, 852, 3, "iPhone 14 Pro/15/15 Pro/16"),
|
||||
("iphone-14-pro-max", 430, 932, 3, "iPhone 14 Pro Max/15 Plus/15 Pro Max"),
|
||||
("iphone-16-pro", 402, 874, 3, "iPhone 16 Pro"),
|
||||
("iphone-16-pro-max", 440, 956, 3, "iPhone 16 Pro Max/16 Plus"),
|
||||
("ipad-9-7", 768, 1024, 2, "iPad 9.7/10.2"),
|
||||
("ipad-pro-11", 834, 1194, 2, "iPad Pro 11\"/Air"),
|
||||
("ipad-pro-12-9", 1024, 1366, 2, "iPad Pro 12.9\""),
|
||||
]
|
||||
|
||||
|
||||
def load_font(size: int, weight: int = 700) -> ImageFont.FreeTypeFont:
|
||||
"""Noto Sans JP — variable, поэтому вес выставляем через ось wght."""
|
||||
font = ImageFont.truetype(str(FONT_PATH), size)
|
||||
try:
|
||||
font.set_variation_by_axes([weight])
|
||||
except Exception:
|
||||
pass # статическая сборка шрифта — берём как есть
|
||||
return font
|
||||
|
||||
|
||||
def centered(draw: ImageDraw.ImageDraw, text: str, font, box_w: int, box_h: int, fill) -> None:
|
||||
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
|
||||
draw.text(
|
||||
((box_w - (right - left)) / 2 - left, (box_h - (bottom - top)) / 2 - top),
|
||||
text,
|
||||
font=font,
|
||||
fill=fill,
|
||||
)
|
||||
|
||||
|
||||
def make_icon(size: int, glyph_ratio: float, radius_ratio: float, opaque: bool) -> Image.Image:
|
||||
img = Image.new("RGBA", (size, size), (*BG, 255 if opaque else 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
if radius_ratio > 0:
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle(
|
||||
(0, 0, size - 1, size - 1), radius=int(size * radius_ratio), fill=255
|
||||
)
|
||||
img.putalpha(mask)
|
||||
draw = ImageDraw.Draw(img)
|
||||
centered(draw, GLYPH, load_font(int(size * glyph_ratio)), size, size, ACCENT)
|
||||
return img
|
||||
|
||||
|
||||
def make_splash(w: int, h: int) -> Image.Image:
|
||||
img = Image.new("RGB", (w, h), BG)
|
||||
draw = ImageDraw.Draw(img)
|
||||
glyph_size = int(w * 0.34)
|
||||
centered(draw, GLYPH, load_font(glyph_size), w, int(h * 0.46), ACCENT)
|
||||
centered(draw, "JapLearn", load_font(int(w * 0.075), 500), w, int(h * 0.60), MUTED)
|
||||
return img
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ICONS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SPLASH_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Иконки: any — со скруглением (как покажет Android/desktop), maskable —
|
||||
# full-bleed без прозрачности и со «сжатым» логотипом (safe zone 80%),
|
||||
# apple-touch — full-bleed без альфы, iOS скруглит сам.
|
||||
for size in (192, 512):
|
||||
make_icon(size, 0.56, 0.18, opaque=False).save(ICONS_DIR / f"icon-{size}.png")
|
||||
make_icon(size, 0.44, 0.0, opaque=True).save(ICONS_DIR / f"maskable-{size}.png")
|
||||
make_icon(180, 0.56, 0.0, opaque=True).save(ICONS_DIR / "apple-touch-icon-180.png")
|
||||
make_icon(32, 0.62, 0.18, opaque=False).save(ICONS_DIR / "favicon-32.png")
|
||||
|
||||
for name, cw, ch, dpr, label in SPLASHES:
|
||||
make_splash(cw * dpr, ch * dpr).save(SPLASH_DIR / f"{name}.png")
|
||||
|
||||
print("Иконки:")
|
||||
for p in sorted(ICONS_DIR.glob("*.png")):
|
||||
print(f" {p.relative_to(ROOT)} {p.stat().st_size // 1024} КБ")
|
||||
print("Splash:")
|
||||
total = sum(p.stat().st_size for p in SPLASH_DIR.glob("*.png"))
|
||||
print(f" {len(list(SPLASH_DIR.glob('*.png')))} файлов, {total // 1024} КБ всего")
|
||||
|
||||
print("\nHTML для index.html:\n")
|
||||
for name, cw, ch, dpr, label in SPLASHES:
|
||||
print(
|
||||
f' <link rel="apple-touch-startup-image" href="/splash/{name}.png"\n'
|
||||
f' media="(device-width: {cw}px) and (device-height: {ch}px) '
|
||||
f'and (-webkit-device-pixel-ratio: {dpr}) and (orientation: portrait)" />'
|
||||
f" <!-- {label} -->"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user