diff --git a/driver-pwa/frontend/index.html b/driver-pwa/frontend/index.html index bdd67ec..c276196 100644 --- a/driver-pwa/frontend/index.html +++ b/driver-pwa/frontend/index.html @@ -4,6 +4,9 @@ + + + diff --git a/driver-pwa/frontend/public/icons/apple-touch-icon.png b/driver-pwa/frontend/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..5ba5e2c Binary files /dev/null and b/driver-pwa/frontend/public/icons/apple-touch-icon.png differ diff --git a/driver-pwa/frontend/public/icons/icon-192.png b/driver-pwa/frontend/public/icons/icon-192.png index ab76006..8dd1960 100644 Binary files a/driver-pwa/frontend/public/icons/icon-192.png and b/driver-pwa/frontend/public/icons/icon-192.png differ diff --git a/driver-pwa/frontend/public/icons/icon-512.png b/driver-pwa/frontend/public/icons/icon-512.png index 20799d3..b6b9efc 100644 Binary files a/driver-pwa/frontend/public/icons/icon-512.png and b/driver-pwa/frontend/public/icons/icon-512.png differ diff --git a/driver-pwa/frontend/public/icons/icon-maskable-512.png b/driver-pwa/frontend/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..e8d50a2 Binary files /dev/null and b/driver-pwa/frontend/public/icons/icon-maskable-512.png differ diff --git a/driver-pwa/frontend/public/manifest.webmanifest b/driver-pwa/frontend/public/manifest.webmanifest index bf6dde8..b80149f 100644 --- a/driver-pwa/frontend/public/manifest.webmanifest +++ b/driver-pwa/frontend/public/manifest.webmanifest @@ -7,7 +7,8 @@ "background_color": "#EEF1F5", "theme_color": "#EEF1F5", "icons": [ - { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } diff --git a/driver-pwa/frontend/scripts/gen-icons.mjs b/driver-pwa/frontend/scripts/gen-icons.mjs deleted file mode 100644 index a2fd789..0000000 --- a/driver-pwa/frontend/scripts/gen-icons.mjs +++ /dev/null @@ -1,76 +0,0 @@ -// Генерация PWA-иконок «Премиум Водитель» без сторонних либ (zlib PNG-энкодер). -// Дизайн: кремовый фон #F3EEE3, тёмная скруглённая плитка #1C1B19, -// внутри белая «карта» #F6F2E9 с тёмной полосой (финансовый мотив). Без жёлтого/текста. -import zlib from "node:zlib"; -import fs from "node:fs"; -import path from "node:path"; - -const CREAM = [243, 238, 227]; -const INK = [28, 27, 25]; -const CARD = [246, 242, 233]; - -function crc32(buf) { - let c = ~0; - for (let i = 0; i < buf.length; i++) { - c ^= buf[i]; - for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1)); - } - return (~c) >>> 0; -} -function chunk(type, data) { - const t = Buffer.from(type, "ascii"); - const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); - const body = Buffer.concat([t, data]); - const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(body), 0); - return Buffer.concat([len, body, crc]); -} -function encodePng(N, rgba) { - const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(N, 0); ihdr.writeUInt32BE(N, 4); ihdr[8] = 8; ihdr[9] = 6; - const stride = N * 4; - const raw = Buffer.alloc(N * (stride + 1)); - for (let y = 0; y < N; y++) { - raw[y * (stride + 1)] = 0; - rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride); - } - return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(raw)), chunk("IEND", Buffer.alloc(0))]); -} -function inRoundRect(px, py, x0, y0, x1, y1, r) { - if (px < x0 || px >= x1 || py < y0 || py >= y1) return false; - const cx = px < x0 + r ? x0 + r : px >= x1 - r ? x1 - r : px; - const cy = py < y0 + r ? y0 + r : py >= y1 - r ? y1 - r : py; - const dx = px - cx, dy = py - cy; - return dx * dx + dy * dy <= r * r; -} - -function draw(N) { - const buf = Buffer.alloc(N * N * 4); - // tile (centered ~64%) - const tile = Math.round(N * 0.64), tx0 = Math.round((N - tile) / 2), ty0 = tx0; - const tx1 = tx0 + tile, ty1 = ty0 + tile, tr = Math.round(tile * 0.24); - // card inside tile - const cw = Math.round(tile * 0.62), ch = Math.round(tile * 0.42); - const cx0 = Math.round((N - cw) / 2), cy0 = Math.round((N - ch) / 2); - const cx1 = cx0 + cw, cy1 = cy0 + ch, cr = Math.round(ch * 0.18); - // stripe near top of card - const sy0 = cy0 + Math.round(ch * 0.22), sy1 = sy0 + Math.round(ch * 0.14); - for (let y = 0; y < N; y++) { - for (let x = 0; x < N; x++) { - let col = CREAM; - if (inRoundRect(x, y, tx0, ty0, tx1, ty1, tr)) col = INK; - if (inRoundRect(x, y, cx0, cy0, cx1, cy1, cr)) col = CARD; - if (x >= cx0 && x < cx1 && y >= sy0 && y < sy1) col = INK; - const o = (y * N + x) * 4; - buf[o] = col[0]; buf[o + 1] = col[1]; buf[o + 2] = col[2]; buf[o + 3] = 255; - } - } - return encodePng(N, buf); -} - -const dir = path.resolve("public/icons"); -fs.mkdirSync(dir, { recursive: true }); -for (const N of [192, 512]) { - fs.writeFileSync(path.join(dir, `icon-${N}.png`), draw(N)); - console.log(`wrote icon-${N}.png`); -} diff --git a/driver-pwa/frontend/scripts/gen-icons.py b/driver-pwa/frontend/scripts/gen-icons.py new file mode 100644 index 0000000..e03e9f2 --- /dev/null +++ b/driver-pwa/frontend/scripts/gen-icons.py @@ -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' 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)