feat(pwa): иконка приложения — фирменный знак парка

Была нарисованная «карта» на кремовом фоне из первых набросков. Теперь знак
«Премиум Парк» из фирменного набора, на белой подложке сайта.

Собирается из вектора скриптом scripts/gen-icons.py (три пути, команды M/L/H/V/C,
сглаживание сверхдискретизацией ×4) — прежний gen-icons.mjs рисовал плитку
вручную и к логотипу отношения не имел. Готовый PNG из набора не подошёл: знак
внутри него занимает 264×195 из 512, и на иконке он бы размылился.

Отдельная maskable-версия с полями: Android режет иконку по кругу, и знак без
запаса теряет края. Ссылка apple-touch-icon в index.html — iOS манифест не
читает и берёт иконку «на экран Домой» только оттуда.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 18:08:00 +10:00
co-authored by Claude Opus 5
parent 3bdd5ce818
commit 388c8e7343
8 changed files with 98 additions and 78 deletions
+92
View File
@@ -0,0 +1,92 @@
"""Иконки приложения из фирменного знака «Премиум Парк».
Знак — вектор (assets/logos/premium_park_favicon_pack/favicon.svg, поле 64×64,
чёрные штрихи + янтарная дуга). Растеризуем его сами: путей всего три, команды —
только M/L/H/V/C/Z, а тянуть в проект зависимость ради этого не хочется.
Сглаживание — сверхдискретизацией ×4: рисуем крупнее и уменьшаем. Так края
получаются чище, чем у любой заливки «в лоб».
Запуск (нужен Pillow):
python scripts/gen-icons.py
"""
from pathlib import Path
import re
from PIL import Image, ImageDraw
SVG = Path(r"C:\VibeCoding\assets\logos\premium_park_favicon_pack\favicon.svg")
OUT = Path(__file__).resolve().parent.parent / "public" / "icons"
BG = (255, 255, 255, 255) # --s-paper сайта: знак нарисован для светлой подложки
SS = 4 # кратность сверхдискретизации
# Полезная часть знака внутри поля 64×64 (замерено по путям).
MARK = (11.0, 8.0, 59.4, 43.7)
def _curve(p0, p1, p2, p3, steps=24):
for i in range(1, steps + 1):
t = i / steps
u = 1 - t
yield (u*u*u*p0[0] + 3*u*u*t*p1[0] + 3*u*t*t*p2[0] + t*t*t*p3[0],
u*u*u*p0[1] + 3*u*u*t*p1[1] + 3*u*t*t*p2[1] + t*t*t*p3[1])
def flatten(d: str) -> list[tuple[float, float]]:
"""Путь SVG → многоугольник. Поддержаны M, L, H, V, C, Z — больше в знаке нет."""
tokens = re.findall(r"[MLHVCZmlhvcz]|-?\d*\.?\d+", d)
pts: list[tuple[float, float]] = []
cur = (0.0, 0.0)
cmd = ""
i = 0
while i < len(tokens):
t = tokens[i]
if t.isalpha():
cmd = t
i += 1
continue
n = lambda k: float(tokens[i + k])
if cmd in "ML":
cur = (n(0), n(1)); pts.append(cur); i += 2
elif cmd == "H":
cur = (n(0), cur[1]); pts.append(cur); i += 1
elif cmd == "V":
cur = (cur[0], n(0)); pts.append(cur); i += 1
elif cmd == "C":
p1, p2, p3 = (n(0), n(1)), (n(2), n(3)), (n(4), n(5))
pts.extend(_curve(cur, p1, p2, p3)); cur = p3; i += 6
else:
i += 1
return pts
def paths() -> list[tuple[list[tuple[float, float]], str]]:
svg = SVG.read_text(encoding="utf-8")
out = []
for d, fill in re.findall(r'<path d="([^"]+)"\s+fill="([^"]+)"', svg):
out.append((flatten(d), fill))
return out
def compose(size: int, inset: float) -> Image.Image:
"""inset — доля стороны под знак. Для maskable нужна безопасная зона:
Android режет иконку по кругу, и знак без запаса теряет края."""
big = size * SS
img = Image.new("RGBA", (big, big), BG)
draw = ImageDraw.Draw(img)
x0, y0, x1, y1 = MARK
scale = min(big * inset / (x1 - x0), big * inset / (y1 - y0))
dx = (big - (x1 - x0) * scale) / 2 - x0 * scale
dy = (big - (y1 - y0) * scale) / 2 - y0 * scale
for pts, fill in paths():
draw.polygon([(x * scale + dx, y * scale + dy) for x, y in pts], fill=fill)
return img.resize((size, size), Image.LANCZOS)
if __name__ == "__main__":
OUT.mkdir(parents=True, exist_ok=True)
for name, size, inset in (("icon-192.png", 192, 0.82),
("icon-512.png", 512, 0.82),
("icon-maskable-512.png", 512, 0.60),
("apple-touch-icon.png", 180, 0.82)):
compose(size, inset).save(OUT / name)
print("собрано", name)