diff --git a/ANDROID_PLAN.md b/ANDROID_PLAN.md index fc7223b..8937aea 100644 --- a/ANDROID_PLAN.md +++ b/ANDROID_PLAN.md @@ -64,11 +64,9 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif > ### ▶ RESUME HERE — 2026-07-13 (documents track + release-prep) > -> **Where we are.** The legal-documents track and the release-prep hygiene are DONE. The RuStore -> release is now gated on **owner-side** items (icon vector, keystore, RuStore account). The owner is -> bringing the **icon vector** next; **when it lands, resume by generating the icon set** per -> [`docs/ICONS.md`](docs/ICONS.md), then continue the release chain below. Do not re-do or re-litigate -> the finished work; do not re-ask the locked decisions. +> **Where we are.** The legal-documents track, the release-prep hygiene, **and the icon rebrand** are +> DONE. The RuStore release is now gated on **owner-side** items only (keystore, RuStore account) plus +> the merge/tag chain. Do not re-do or re-litigate the finished work; do not re-ask the locked decisions. > > **Branches / PRs (current working branch: `feature/android-release-prep`).** > - `development` — has the **legal-documents track**, merged via **PR #253** (green, deployed to the @@ -102,13 +100,16 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif > added [`docs/ICONS.md`](docs/ICONS.md) (the single-master icon/logo format reference). > > **Remaining to release (owner-gated unless the agent is named).** -> 1. **Icon rebrand.** OWNER brings a final **vector (SVG)** master (+ optional format guides). AGENT -> then generates every target per `docs/ICONS.md`. Obligatory source files: the vector + a -> **1024×1024 opaque PNG**; recommended: `ui/assets/icon-foreground.png` + `icon-background.png` for -> a proper Android/iOS adaptive icon. Today the web favicon generator (`assets/icons/`) and the -> hand-placed PWA icons drift — unify onto the new master. **Open sub-decision (settle with the owner -> when the vector arrives):** retarget the `assets/icons/` generator onto the new vector, or drop in -> the output files and keep the generator for the OG card only. +> 1. **Icon rebrand — DONE.** The SVG-tracer export failed, so the icon was designed **collaboratively +> from scratch** (wooden «Э» tile + ✻ subscript, Spectral Bold). The construction is fully specified +> in fractions of the side in [`docs/ICON_BRANDBOOK.md`](docs/ICON_BRANDBOOK.md), with the reference +> generator + pinned font + masters in [`assets/icons/brand/`](assets/icons/brand/). One master → +> the whole set sliced by `brand/build-set.mjs` (+ `build-android-res.mjs`): web (favicon light+dark, +> apple-touch, PWA any + maskable, og-image reskinned to «Эрудит» / Игра в слова), the Capacitor +> layers `ui/assets/{icon,icon-foreground,icon-background}.png`, the Android launcher res (all +> densities, **no inset**, **+ monochrome** themed layer), and manual-upload art (`brand/{vk,tg,store}`). +> Primary variant **light**; dark where the platform can theme (favicon.svg). The old LiberationSans +> generator (`assets/icons/build/`) is removed; targets are mapped in `docs/ICONS.md`. > 2. **Merge PR #254** → `development` (owner approval), then **promote `development` → `master`** and > **tag `vX.Y.Z`** for the release (the version stamps the APK `versionName`/`versionCode` and the > `X-Client-Version` header). @@ -138,9 +139,9 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif builds a debug APK that installs and **cold-launches to the Login screen** on the Pixel_10 emulator. `svelte-check` 0 errors, `vitest` 584 passed. Build recipe + toolchain gotchas live in `.claude/CLAUDE.md` → "Native Android build (Capacitor)". - **Launcher icon:** a **temporary** placeholder is in place — `ui/assets/icon.png` (the maskable «Э» - brand mark upscaled 512→1024) → `pnpm android:assets` regenerated the Android launcher/adaptive - icons + splashes. Superseded by the icon rebrand below. + **Launcher icon:** the temporary placeholder was **superseded by the icon rebrand** — the launcher + now uses the two-layer adaptive icon (background + foreground + monochrome) generated from the brand + master; see the RESUME block (item 1), `docs/ICON_BRANDBOOK.md` and `docs/ICONS.md`. - **B. Native web-build correctness — ✅ DONE & verified (2026-07-12).** New `ui/src/lib/origin.ts` `gatewayOrigin()`; the two `location.origin` landmines fixed (`game/Game.svelte` export/share URL, `screens/Wallet.svelte` site-root) — `transport.ts:32` already resolves via `VITE_GATEWAY_URL`, left @@ -251,11 +252,10 @@ Kept current as parts land so a fresh session resumes without re-deriving. Verif then O2–O7; the release chain (PR→development→contour→master→tag→dispatch `android-build`) follows. None of O1–O7 is started. Branch `feature/android-native`, local/unpushed. -Open logistics (not code): **mandatory icon rebrand (future)** — author ONE vector master and generate -*every* icon from it (web `favicon.svg` / PWA `icon-*` / maskable, Android adaptive, future iOS). Today -the formats drift — `favicon.svg` is a rounded bordered tile, `icon-maskable-512.png` a full-bleed -square, visibly different — and the Android launcher icon is only a temporary upscale of the maskable -(`capacitor-assets` insets it 16.7% into the adaptive safe zone). `~/.claude/bin/gitea-ci-watch.py` is present. +Icon rebrand — **DONE** (was "one master → every icon"): a single Spectral «Э» master now drives web +(favicon light+dark / PWA any + maskable / apple-touch / og), the Capacitor layers and the Android +adaptive res (no inset, + monochrome), plus VK/TG/store art. Specced in `docs/ICON_BRANDBOOK.md`, +mapped in `docs/ICONS.md`, generated from `assets/icons/brand/`. `~/.claude/bin/gitea-ci-watch.py` is present. RuStore publication prerequisites gate release only. --- @@ -404,8 +404,8 @@ is: scaffold → native-correct → gate → offline-first → CI → docs → r ``` Call once from bootstrap. `atNavigationRoot` = current route is `lobby`/`login` (mirror `routeDepth(...) === 0` from `App.svelte:50-54`, reading `router.svelte.ts`). -- **Launcher icon (owner asset):** owner supplies a 1024×1024 «Э» icon at `ui/assets/icon.png` - (+ optional `splash.png`); `pnpm android:assets` generates adaptive icons. +- **Launcher icon:** DONE — generated from the brand master into the Capacitor layers + `ui/assets/{icon,icon-foreground,icon-background}.png` and the Android res; see `docs/ICONS.md`. **Done when:** `npx cap sync android` succeeds against a real `pnpm build`, and (from `ui/android/`) `./gradlew assembleDebug` produces a debug APK that installs and cold-launches to the login screen. @@ -768,8 +768,9 @@ Wire in `ui/android/app/build.gradle` `defaultConfig`: > **Status 2026-07-13 — see the ▶ RESUME block at the top of §Progress.** Since this chain was > written, a **legal-documents track** was added and merged (PR #253 — the RuStore hosted > privacy-policy URL and the `/privacy/ /eula/ /offer/` pages), and a **release-prep hygiene** patch is -> in flight (PR #254, open). The remaining gates are the **icon vector**, the **keystore** and the -> **RuStore account** (all owner-side). The steps below are the original release chain; the resume +> in flight (PR #254, open), and the **icon rebrand is DONE** (brand master → full set; see the RESUME +> block). The remaining gates are the **keystore** and the **RuStore account** (owner-side). The steps +> below are the original release chain; the resume > block supersedes them with the current, exhaustive state. 0. **Land the offline-model redesign — ✅ DONE (O1–O7, 2026-07-13; staged detail below).** Resolved the deferred diff --git a/assets/icons/README.md b/assets/icons/README.md index 3b8cde5..efbfca6 100644 --- a/assets/icons/README.md +++ b/assets/icons/README.md @@ -1,41 +1,18 @@ -# Erudit — site icons + Open Graph card +# Icons -The favicon set and the `og:image` link-preview card for the public landing -(`ui/landing.html`) and the SPA shell (`ui/index.html`). Same design language as the -[VK loading-screen logo](../vk/README.md): the wooden Erudit «Э» tile (score `8`), -with the wordmark on the app's dark board green (`ui/src/app.css` tokens). - -| Output (committed to `ui/public/`) | Purpose | -|------|---------| -| `favicon.svg` | Vector favicon, transparent; tile + «Э» only (the score is illegible below ~32 px). | -| `favicon.ico` | 32×32 PNG-in-ICO fallback (also answers the browsers' blind `/favicon.ico` probe). | -| `apple-touch-icon.png` | 180×180 opaque full-bleed tile; iOS masks its own corners. | -| `og-image.png` | 1200×630 card: tile + «Эрудит / Скрэббл — игра в слова». Referenced absolutely as `https://erudit-game.ru/og-image.png`. | - -## How it works - -`build/extract.js` extracts the needed glyph outlines (tile glyphs + every wordmark -character) from LiberationSans (Arial-metric, the game's font stack) with their -advance widths into `build/glyphs.json` (committed). `build/generate.js` composes -plain SVG from those outlines — no font is needed at generation time — writes -`favicon.svg` and rasterises the PNG/ICO outputs by screenshotting the SVGs with the -`ui` package's Playwright chromium (`@playwright/test`); the `.ico` container is -assembled in-script (a single PNG entry). Raster bytes therefore depend on the -installed chromium version; the SVG sources are deterministic. - -## Regenerate - -Requirements: Node ≥ 18, `ui` installed (`pnpm install`, provides Playwright). -`extract.js` additionally needs `opentype.js` (`npm i opentype.js`); **`generate.js` -needs no extra packages**. +The whole shipped icon set is sliced from **one master** — see +[`brand/`](brand/) for the reference generator, the pinned font and the committed +outputs, and [`../../docs/ICON_BRANDBOOK.md`](../../docs/ICON_BRANDBOOK.md) for how the +mark is constructed. The per-platform target map (which file goes where) is in +[`../../docs/ICONS.md`](../../docs/ICONS.md). ```sh -cd assets/icons - -# 1. (optional) re-extract the glyphs — only if the font or the wordmark changes: -# default font: /usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf -node build/extract.js [/path/to/font.ttf] # -> build/glyphs.json - -# 2. regenerate everything in ui/public/: -node build/generate.js +cd brand +npm i opentype.js +node build-set.mjs # web (ui/public) + Capacitor layers (ui/assets) + vk/ tg/ store/ +node build-android-res.mjs # Android launcher resources (all densities + monochrome) ``` + +> The earlier `build/` pipeline (LiberationSans glyph outlines → favicon/apple-touch/OG) +> was replaced by `brand/` when the icon was rebranded onto the Spectral «Э» master. +> The separate VK loading-screen animation lives in [`../vk/`](../vk/) and is unrelated. diff --git a/assets/icons/brand/.gitignore b/assets/icons/brand/.gitignore new file mode 100644 index 0000000..504afef --- /dev/null +++ b/assets/icons/brand/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/assets/icons/brand/README.md b/assets/icons/brand/README.md new file mode 100644 index 0000000..c259ca2 --- /dev/null +++ b/assets/icons/brand/README.md @@ -0,0 +1,32 @@ +# Erudit icon — brand master + +The reference construction of the Erudit app icon. The authoritative, human-readable +spec lives in [`docs/ICON_BRANDBOOK.md`](../../../docs/ICON_BRANDBOOK.md); this folder +holds the machine reproduction of exactly what that document describes. + +| File | What | +|------|------| +| `build-icon.mjs` | reference generator — every dimension is a fraction of the side, matching the brand book | +| `render-png.mjs` | rasterises an icon SVG to PNG via the `ui` package's Playwright chromium | +| `Spectral-Bold.ttf` | the «Э» typeface (SIL OFL, `Spectral-OFL.txt`) — pinned so the letter never drifts | +| `icon-light.svg` / `.png` | the light master (1024) | +| `icon-dark.svg` / `.png` | the dark master (1024) | +| `icon-construction.svg` / `.png` | the percent-annotated construction scheme (figure in the brand book) | + +## Regenerate + +```sh +cd assets/icons/brand +npm i opentype.js # the only extra dep; render uses ui's chromium + +node build-icon.mjs --variant light > icon-light.svg +node build-icon.mjs --variant dark > icon-dark.svg +node build-icon.mjs --variant light --scheme > icon-construction.svg + +node render-png.mjs icon-light.svg icon-light.png 1024 +node render-png.mjs icon-dark.svg icon-dark.png 1024 +node render-png.mjs icon-construction.svg icon-construction.png 2048 +``` + +The SVGs are resolution-free; render at any size. Downstream slicing (favicon / PWA / +iOS / Android) is a separate step — see [`docs/ICONS.md`](../../../docs/ICONS.md). diff --git a/assets/icons/brand/Spectral-Bold.ttf b/assets/icons/brand/Spectral-Bold.ttf new file mode 100644 index 0000000..5019e08 Binary files /dev/null and b/assets/icons/brand/Spectral-Bold.ttf differ diff --git a/assets/icons/brand/Spectral-OFL.txt b/assets/icons/brand/Spectral-OFL.txt new file mode 100644 index 0000000..163d3a5 --- /dev/null +++ b/assets/icons/brand/Spectral-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2017 The Spectral Project Authors (https://github.com/productiontype/Spectral) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/assets/icons/brand/android-monochrome.png b/assets/icons/brand/android-monochrome.png new file mode 100644 index 0000000..fe88567 Binary files /dev/null and b/assets/icons/brand/android-monochrome.png differ diff --git a/assets/icons/brand/build-android-res.mjs b/assets/icons/brand/build-android-res.mjs new file mode 100644 index 0000000..b237389 --- /dev/null +++ b/assets/icons/brand/build-android-res.mjs @@ -0,0 +1,47 @@ +'use strict'; +// Generate the Android launcher resources from the committed layers, at every +// density, with NO inset (our layers already fill the 108 dp canvas) and a +// monochrome (themed) layer. Run build-set.mjs first (it writes the layer PNGs). +// +// npm i opentype.js # (only build-icon/build-set need it; this reads PNGs) +// node build-set.mjs && node build-android-res.mjs +// +// Writes ui/android/app/src/main/res/mipmap-*/ic_launcher{,_round,_foreground, +// _background,_monochrome}.png. The two mipmap-anydpi-v26/*.xml are committed by hand. +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const DIR = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(DIR, '..', '..', '..'); +const RES = join(ROOT, 'ui', 'android', 'app', 'src', 'main', 'res'); +const uri = p => 'data:image/png;base64,' + readFileSync(p).toString('base64'); +const BG = uri(join(ROOT, 'ui', 'assets', 'icon-background.png')); +const FG = uri(join(ROOT, 'ui', 'assets', 'icon-foreground.png')); +const MONO = uri(join(DIR, 'android-monochrome.png')); + +// density: [adaptive-layer px (108 dp), legacy launcher px (48 dp)] +const D = { ldpi: [81, 36], mdpi: [108, 48], hdpi: [162, 72], xhdpi: [216, 96], xxhdpi: [324, 144], xxxhdpi: [432, 192] }; + +const pw = await import(join(ROOT, 'ui', 'node_modules', '@playwright', 'test', 'index.js')); +const { chromium } = pw.default ?? pw; +const browser = await chromium.launch(); +const page = await browser.newPage({ deviceScaleFactor: 1 }); +const im = (u, s) => ``; +async function shot(html, s, transparent) { + await page.setViewportSize({ width: s, height: s }); + await page.setContent(`
${html}
`, { waitUntil: 'networkidle' }); + return page.locator('#r').screenshot({ omitBackground: !!transparent }); +} +for (const [d, [fg, lg]] of Object.entries(D)) { + const dir = join(RES, `mipmap-${d}`); + writeFileSync(join(dir, 'ic_launcher_background.png'), await shot(im(BG, fg), fg, false)); + writeFileSync(join(dir, 'ic_launcher_foreground.png'), await shot(im(FG, fg), fg, true)); + writeFileSync(join(dir, 'ic_launcher_monochrome.png'), await shot(im(MONO, fg), fg, true)); + const comp = `
${im(BG, lg)}
${im(FG, lg)}
`; + writeFileSync(join(dir, 'ic_launcher.png'), await shot(comp, lg, false)); + const round = `
${im(BG, lg)}
${im(FG, lg)}
`; + writeFileSync(join(dir, 'ic_launcher_round.png'), await shot(round, lg, true)); + console.log('mipmap-' + d, 'ok'); +} +await browser.close(); diff --git a/assets/icons/brand/build-icon.mjs b/assets/icons/brand/build-icon.mjs new file mode 100644 index 0000000..7d5b1ea --- /dev/null +++ b/assets/icons/brand/build-icon.mjs @@ -0,0 +1,159 @@ +'use strict'; +// Erudit app-icon — the reference generator. Every dimension below is a FRACTION +// of the icon side, so this file reads the same as docs/ICON_BRANDBOOK.md and the +// icon reproduces at any resolution. Emits one variant/layer's SVG to stdout. +// +// node build-icon.mjs --variant light|dark [--layer full|background|foreground] [--scheme] > icon.svg +// +// Layers (for Android adaptive icons): +// full the standalone master (rounded tile + mark) — iOS / PWA / favicon +// background wood + grain + light/shadow, full-bleed square (the OS applies the mask) +// foreground only «Э» + ✻, transparent, re-placed for the round mask (see FG_* below) +// +// Deps: opentype.js (npm i opentype.js). Font: ./Spectral-Bold.ttf (OFL, bundled). +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import opentype from 'opentype.js'; + +const DIR = dirname(fileURLToPath(import.meta.url)); +const args = process.argv.slice(2); +const has = f => args.includes('--' + f); +const val = (f, d) => { const i = args.indexOf('--' + f); return i >= 0 ? args[i + 1] : d; }; + +// ---- the design, in fractions of the side ---- +const S = 1024; // reference side (the icon is resolution-free) +const RADIUS = 0.17; // tile corner radius (full master) +const STAR_BULB = 0.22; // spoke-end (and hub) radius, as fraction of star radius +const STAR_SPOKES = 6; +const GRAIN_COLS = 6, GRAIN_W = 1 / 32, GRAIN_SHIFT = 1 / 16, GRAIN_TILT = 4; +const SHEEN = 0.15; // white, transparent bottom-left -> this alpha top-right +const SHADE = 0.15; // black, this alpha bottom-left -> transparent top-right + +// The mark, placed for the standalone master (bottom-right biased, full-bleed): +const MASTER = { lh: 0.6055, lc: [0.4814, 0.4922], sd: 0.1729, sc: [0.7881, 0.7881] }; +// The mark, placed for the Android foreground layer (centred-ish inside the 61% safe +// zone, nudged right 8% / down 3% for optical balance under the round mask; smaller ✻): +const FOREGROUND = { lh: 0.5391, lc: [0.5234, 0.4951], sd: 0.1318, sc: [0.7627, 0.7266] }; + +const PALETTE = { + light: { wood: '#e6b053', ink: '#5a3a12', grain: '#d8a54e' }, + dark: { wood: '#4a3316', ink: '#f2d9a0', grain: '#432e14' }, +}; + +// Layers: +// full rounded tile + master mark (favicon.svg — browser rounds nothing) +// flat square tile + master mark (apple-touch, PWA "any" — OS rounds) +// background square tile, no mark (Android adaptive background) +// foreground mark only, transparent (Android adaptive foreground) +// maskable square tile + foreground mark (PWA maskable — safe-zone aware) +// monochrome foreground mark only, flat colour (Android 13+ themed layer) +const LAYERS = ['full', 'flat', 'background', 'foreground', 'maskable', 'monochrome']; +const variant = val('variant', 'light'); +const layer = val('layer', 'full'); +const P = PALETTE[variant]; +if (!P) { console.error(`unknown --variant ${variant} (light|dark)`); process.exit(1); } +if (!LAYERS.includes(layer)) { console.error(`unknown --layer ${layer} (${LAYERS.join('|')})`); process.exit(1); } + +const usesForegroundPlacement = ['foreground', 'maskable', 'monochrome'].includes(layer); +const drawBg = ['full', 'flat', 'background', 'maskable'].includes(layer); +const drawMark = layer !== 'background'; +const MARK = usesForegroundPlacement ? FOREGROUND : MASTER; +const inkColour = layer === 'monochrome' ? val('mono', '#ffffff') : P.ink; +const RX = layer === 'full' ? RADIUS * S : 0; // only the standalone master keeps rounded corners +const font = opentype.parse(readFileSync(join(DIR, 'Spectral-Bold.ttf')).buffer); + +// «Э» outline scaled so its ink bounding box is MARK.lh of the side, box centred on MARK.lc. +function letterSvg() { + const g = font.charToGlyph('Э'); + const h0 = (() => { const b = g.getPath(0, 0, 1000).getBoundingBox(); return b.y2 - b.y1; })(); + const scale = (MARK.lh * S) / h0; + const p = g.getPath(0, 0, 1000 * scale); + const bb = p.getBoundingBox(); + const w = bb.x2 - bb.x1, h = bb.y2 - bb.y1; + const tx = MARK.lc[0] * S - (bb.x1 + w / 2); + const ty = MARK.lc[1] * S - (bb.y1 + h / 2); + return { svg: ``, + box: { x: bb.x1 + tx, y: bb.y1 + ty, w, h } }; +} + +// Teardrop-spoked asterisk ✻: each spoke tapers to a point at the centre and ends +// in a round bulb; a central disc equal to the bulb fuses them. +function starPath() { + const cx = MARK.sc[0] * S, cy = MARK.sc[1] * S, Rout = MARK.sd * S / 2; + const rb = Rout * STAR_BULB, R = Rout - rb; + const L = Math.sqrt(R * R - rb * rb), theta = Math.asin(rb / R); + const rot = (v, t) => [v[0] * Math.cos(t) - v[1] * Math.sin(t), v[0] * Math.sin(t) + v[1] * Math.cos(t)]; + let d = ''; + for (let k = 0; k < STAR_SPOKES; k++) { + const a = -Math.PI / 2 + k * 2 * Math.PI / STAR_SPOKES; + const u = [Math.cos(a), Math.sin(a)]; + const d1 = rot(u, theta), d2 = rot(u, -theta); + const T1 = [cx + L * d1[0], cy + L * d1[1]], T2 = [cx + L * d2[0], cy + L * d2[1]]; + d += `M${cx.toFixed(2)} ${cy.toFixed(2)} L${T1[0].toFixed(2)} ${T1[1].toFixed(2)} A${rb.toFixed(2)} ${rb.toFixed(2)} 0 1 0 ${T2[0].toFixed(2)} ${T2[1].toFixed(2)} Z `; + } + d += `M${cx} ${cy} m${-rb} 0 a${rb} ${rb} 0 1 0 ${2 * rb} 0 a${rb} ${rb} 0 1 0 ${-2 * rb} 0 Z`; + return ``; +} + +function grain() { + const colW = S / GRAIN_COLS, sw = GRAIN_W * S, shift = GRAIN_SHIFT * S; + const yTop = -0.2 * S, yBot = 1.2 * S, cy = S / 2; + let s = ''; + for (let i = 1; i <= GRAIN_COLS; i++) { + const xr = i * colW - shift, x = xr - sw, cx = xr - sw / 2; + s += ``; + } + return `${s}`; +} + +let body = ''; +const L = letterSvg(); +if (drawBg) { // tile + grain + light/shadow (the background) + body += `` + + `` + + `` + + `` + + `` + + grain() + + `` + + ``; +} +if (drawMark) body += L.svg + starPath(); // the mark +if (has('scheme')) body += scheme(L); + +process.stdout.write(`${body}\n`); + +// Percent-based construction overlay for the brand book (full master only). +function scheme(L) { + const pc = n => (100 * n / S).toFixed(1) + '%'; + const GC = '#0f172a', BX = '#d81b60', AC = '#0d9488'; + const halo = 'paint-order="stroke" stroke="#fff" stroke-width="4"'; + const cx = MARK.sc[0] * S, cy = MARK.sc[1] * S, sd = MARK.sd * S; + let g = ``; + for (let f = 1; f <= 3; f++) { + const p = f * S / 4; + g += ``; + g += ``; + g += `${f * 25}%`; + g += `${f * 25}%`; + } + g += ``; + g += ``; + g += `light: white 0%→15%`; + g += `shadow: black 15%→0%`; + const { x, y, w, h } = L.box; + g += ``; + g += ``; + g += `«Э» Spectral Bold · box H ${pc(h)} · W ${pc(w)}`; + g += `box center ${pc(MARK.lc[0] * S)} / ${pc(MARK.lc[1] * S)}`; + g += ``; + g += ``; + g += `✻ Ø ${pc(sd)} · center ${pc(cx)} / ${pc(cy)}`; + g += `corner r = 17%`; + g += ``; + g += ``; + g += `Wood grain: 6 equal columns; a vertical stripe on each column's right edge,`; + g += `width 1/32 of side; the whole set shifted left 1/16; every stripe tilted 4°.`; + return g + ``; +} diff --git a/assets/icons/brand/build-set.mjs b/assets/icons/brand/build-set.mjs new file mode 100644 index 0000000..e25561d --- /dev/null +++ b/assets/icons/brand/build-set.mjs @@ -0,0 +1,105 @@ +'use strict'; +// Slice the whole shipped icon set from the brand master (build-icon.mjs). Renders +// with the ui package's Playwright chromium. See docs/ICONS.md for the target map. +// +// npm i opentype.js && node build-set.mjs +// +// Writes: ui/public/* (web), ui/assets/* (Capacitor layers), ./vk/* and ./store/* +// (manual-upload art). Wiring (manifest, html, Android res) is done separately. +import { execFileSync } from 'node:child_process'; +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const DIR = dirname(fileURLToPath(import.meta.url)); +const UI = join(DIR, '..', '..', '..', 'ui'); +const PUB = join(UI, 'public'), ASSETS = join(UI, 'assets'); +const VK = join(DIR, 'vk'), STORE = join(DIR, 'store'), TG = join(DIR, 'tg'); +[VK, STORE, TG].forEach(d => mkdirSync(d, { recursive: true })); + +// one SVG string for a variant+layer from the reference generator +const svgFor = (variant, layer) => + execFileSync('node', [join(DIR, 'build-icon.mjs'), '--variant', variant, '--layer', layer], { encoding: 'utf8' }); + +const pw = await import(join(UI, 'node_modules', '@playwright', 'test', 'index.js')); +const { chromium } = pw.default ?? pw; +const browser = await chromium.launch(); +const page = await browser.newPage(); + +async function pngFromSvg(svg, size, transparent) { + await page.setViewportSize({ width: size, height: size }); + await page.setContent(`${svg}`, { waitUntil: 'networkidle' }); + return page.locator('svg').screenshot({ omitBackground: !!transparent }); +} +async function shootHtml(html, w, h) { + await page.setViewportSize({ width: w, height: h }); + await page.setContent(html, { waitUntil: 'networkidle' }); + return page.screenshot(); +} +function icoFromPNG(png, sizePx) { + const h = Buffer.alloc(22); + h.writeUInt16LE(0, 0); h.writeUInt16LE(1, 2); h.writeUInt16LE(1, 4); + h.writeUInt8(sizePx, 6); h.writeUInt8(sizePx, 7); + h.writeUInt16LE(1, 10); h.writeUInt16LE(32, 12); + h.writeUInt32LE(png.length, 14); h.writeUInt32LE(22, 18); + return Buffer.concat([h, png]); +} +const write = (p, buf) => { writeFileSync(p, buf); console.log('wrote', p.replace(join(DIR, '..', '..', '..'), '.'), buf.length, 'b'); }; + +// ---- pre-render the SVG sources we need ---- +const S = { + fullLight: svgFor('light', 'full'), fullDark: svgFor('dark', 'full'), + flatLight: svgFor('light', 'flat'), flatDark: svgFor('dark', 'flat'), + maskLight: svgFor('light', 'maskable'), maskDark: svgFor('dark', 'maskable'), + bgLight: svgFor('light', 'background'), fgLight: svgFor('light', 'foreground'), + monoLight: svgFor('light', 'monochrome'), +}; + +// ---- favicon.svg: light + dark in one file, toggled by prefers-color-scheme ---- +const inner = svg => svg.replace(/^]*>/, '').replace(/<\/svg>\s*$/, ''); +const faviconSvg = + `` + + `` + + `${inner(S.fullLight)}${inner(S.fullDark)}\n`; +write(join(PUB, 'favicon.svg'), Buffer.from(faviconSvg)); + +// ---- web rasters ---- +write(join(PUB, 'favicon.ico'), icoFromPNG(await pngFromSvg(S.flatLight, 32, false), 32)); +write(join(PUB, 'apple-touch-icon.png'), await pngFromSvg(S.flatLight, 180, false)); +write(join(PUB, 'icon-192.png'), await pngFromSvg(S.flatLight, 192, false)); +write(join(PUB, 'icon-512.png'), await pngFromSvg(S.flatLight, 512, false)); +write(join(PUB, 'icon-192-dark.png'), await pngFromSvg(S.flatDark, 192, false)); +write(join(PUB, 'icon-512-dark.png'), await pngFromSvg(S.flatDark, 512, false)); +write(join(PUB, 'icon-maskable-512.png'), await pngFromSvg(S.maskLight, 512, false)); +write(join(PUB, 'icon-maskable-512-dark.png'), await pngFromSvg(S.maskDark, 512, false)); + +// ---- Capacitor layers (ui/assets) ---- +write(join(ASSETS, 'icon.png'), await pngFromSvg(S.flatLight, 1024, false)); // legacy single +write(join(ASSETS, 'icon-foreground.png'), await pngFromSvg(S.fgLight, 1024, true)); // adaptive fg +write(join(ASSETS, 'icon-background.png'), await pngFromSvg(S.bgLight, 1024, false)); // adaptive bg +write(join(DIR, 'android-monochrome.png'), await pngFromSvg(S.monoLight, 1024, true)); // themed layer (wired manually) + +// ---- VK (no rounding — flat light) ---- +for (const px of [576, 278, 150, 32]) write(join(VK, `vk-${px}.png`), await pngFromSvg(S.flatLight, px, false)); + +// ---- Telegram bot: square, no rounding (TG crops the avatar to a circle) ---- +write(join(TG, 'tg-512.png'), await pngFromSvg(S.flatLight, 512, false)); // bot avatar + Mini App icon + +// ---- store art ---- +write(join(STORE, 'rustore-512.png'), await pngFromSvg(S.flatLight, 512, false)); + +// ---- wordmark banner (board-green bg + master tile + «Эрудит» / Игра в слова) ---- +const b64 = readFileSync(join(DIR, 'Spectral-Bold.ttf')).toString('base64'); +const banner = (w, h, tile, t1, t2, gap, pad) => `
${S.fullLight}
«Эрудит»
Игра в слова
`; +write(join(PUB, 'og-image.png'), await shootHtml(banner(1200, 630, 300, 150, 74, 64, 96), 1200, 630)); +write(join(TG, 'tg-demo-640x360.png'), await shootHtml(banner(640, 360, 150, 72, 38, 30, 44), 640, 360)); + +await browser.close(); +console.log('done'); diff --git a/assets/icons/brand/icon-construction.png b/assets/icons/brand/icon-construction.png new file mode 100644 index 0000000..f50fe0e Binary files /dev/null and b/assets/icons/brand/icon-construction.png differ diff --git a/assets/icons/brand/icon-construction.svg b/assets/icons/brand/icon-construction.svg new file mode 100644 index 0000000..3ea5ff5 --- /dev/null +++ b/assets/icons/brand/icon-construction.svg @@ -0,0 +1 @@ +25%25%50%50%75%75%light: white 0%→15%shadow: black 15%→0%«Э» Spectral Bold · box H 60.6% · W 53.2%box center 48.1% / 49.2%✻ Ø 17.3% · center 78.8% / 78.8%corner r = 17%Wood grain: 6 equal columns; a vertical stripe on each column's right edge,width 1/32 of side; the whole set shifted left 1/16; every stripe tilted 4°. diff --git a/assets/icons/brand/icon-dark-android-background.png b/assets/icons/brand/icon-dark-android-background.png new file mode 100644 index 0000000..9c6a138 Binary files /dev/null and b/assets/icons/brand/icon-dark-android-background.png differ diff --git a/assets/icons/brand/icon-dark-android-background.svg b/assets/icons/brand/icon-dark-android-background.svg new file mode 100644 index 0000000..29d5d4f --- /dev/null +++ b/assets/icons/brand/icon-dark-android-background.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brand/icon-dark-android-foreground.png b/assets/icons/brand/icon-dark-android-foreground.png new file mode 100644 index 0000000..a4a22d6 Binary files /dev/null and b/assets/icons/brand/icon-dark-android-foreground.png differ diff --git a/assets/icons/brand/icon-dark-android-foreground.svg b/assets/icons/brand/icon-dark-android-foreground.svg new file mode 100644 index 0000000..ee10f82 --- /dev/null +++ b/assets/icons/brand/icon-dark-android-foreground.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brand/icon-dark.png b/assets/icons/brand/icon-dark.png new file mode 100644 index 0000000..00db3ce Binary files /dev/null and b/assets/icons/brand/icon-dark.png differ diff --git a/assets/icons/brand/icon-dark.svg b/assets/icons/brand/icon-dark.svg new file mode 100644 index 0000000..9f61d7c --- /dev/null +++ b/assets/icons/brand/icon-dark.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brand/icon-light-android-background.png b/assets/icons/brand/icon-light-android-background.png new file mode 100644 index 0000000..1d76eb2 Binary files /dev/null and b/assets/icons/brand/icon-light-android-background.png differ diff --git a/assets/icons/brand/icon-light-android-background.svg b/assets/icons/brand/icon-light-android-background.svg new file mode 100644 index 0000000..d28c53b --- /dev/null +++ b/assets/icons/brand/icon-light-android-background.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brand/icon-light-android-foreground.png b/assets/icons/brand/icon-light-android-foreground.png new file mode 100644 index 0000000..12b4a8e Binary files /dev/null and b/assets/icons/brand/icon-light-android-foreground.png differ diff --git a/assets/icons/brand/icon-light-android-foreground.svg b/assets/icons/brand/icon-light-android-foreground.svg new file mode 100644 index 0000000..fbe995c --- /dev/null +++ b/assets/icons/brand/icon-light-android-foreground.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brand/icon-light.png b/assets/icons/brand/icon-light.png new file mode 100644 index 0000000..c764425 Binary files /dev/null and b/assets/icons/brand/icon-light.png differ diff --git a/assets/icons/brand/icon-light.svg b/assets/icons/brand/icon-light.svg new file mode 100644 index 0000000..4a192a3 --- /dev/null +++ b/assets/icons/brand/icon-light.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brand/package.json b/assets/icons/brand/package.json new file mode 100644 index 0000000..422140f --- /dev/null +++ b/assets/icons/brand/package.json @@ -0,0 +1,12 @@ +{ + "name": "erudit-icon-brand", + "private": true, + "type": "module", + "description": "Reference generator for the Erudit app-icon set (see docs/ICON_BRANDBOOK.md).", + "scripts": { + "build": "node build-set.mjs && node build-android-res.mjs" + }, + "dependencies": { + "opentype.js": "^1.3.4" + } +} diff --git a/assets/icons/brand/render-png.mjs b/assets/icons/brand/render-png.mjs new file mode 100644 index 0000000..54bb35c --- /dev/null +++ b/assets/icons/brand/render-png.mjs @@ -0,0 +1,21 @@ +'use strict'; +// Rasterise an icon SVG to PNG at a given size, using the `ui` package's cached +// Playwright chromium (no extra install). Usage: +// node render-png.mjs [size=1024] +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const DIR = dirname(fileURLToPath(import.meta.url)); +const pw = await import(join(DIR, '..', '..', '..', 'ui', 'node_modules', '@playwright', 'test', 'index.js')); +const { chromium } = pw.default ?? pw; + +const [, , svgPath, pngPath, size] = process.argv; +const S = Number(size) || 1024; +const svg = readFileSync(svgPath, 'utf8'); +const b = await chromium.launch(); +const pg = await b.newPage({ viewport: { width: S, height: S }, deviceScaleFactor: 1 }); +await pg.setContent(`${svg}`, { waitUntil: 'networkidle' }); +writeFileSync(pngPath, await pg.locator('svg').screenshot({ omitBackground: true })); +await b.close(); +console.log('wrote', pngPath, S + 'px'); diff --git a/assets/icons/brand/store/rustore-512.png b/assets/icons/brand/store/rustore-512.png new file mode 100644 index 0000000..bc88014 Binary files /dev/null and b/assets/icons/brand/store/rustore-512.png differ diff --git a/assets/icons/brand/tg/tg-512.png b/assets/icons/brand/tg/tg-512.png new file mode 100644 index 0000000..bc88014 Binary files /dev/null and b/assets/icons/brand/tg/tg-512.png differ diff --git a/assets/icons/brand/tg/tg-demo-640x360.png b/assets/icons/brand/tg/tg-demo-640x360.png new file mode 100644 index 0000000..3d7a4a8 Binary files /dev/null and b/assets/icons/brand/tg/tg-demo-640x360.png differ diff --git a/assets/icons/brand/vk/vk-150.png b/assets/icons/brand/vk/vk-150.png new file mode 100644 index 0000000..1e91c53 Binary files /dev/null and b/assets/icons/brand/vk/vk-150.png differ diff --git a/assets/icons/brand/vk/vk-278.png b/assets/icons/brand/vk/vk-278.png new file mode 100644 index 0000000..216bce7 Binary files /dev/null and b/assets/icons/brand/vk/vk-278.png differ diff --git a/assets/icons/brand/vk/vk-32.png b/assets/icons/brand/vk/vk-32.png new file mode 100644 index 0000000..ff52a07 Binary files /dev/null and b/assets/icons/brand/vk/vk-32.png differ diff --git a/assets/icons/brand/vk/vk-576.png b/assets/icons/brand/vk/vk-576.png new file mode 100644 index 0000000..558d111 Binary files /dev/null and b/assets/icons/brand/vk/vk-576.png differ diff --git a/assets/icons/build/extract.js b/assets/icons/build/extract.js deleted file mode 100644 index 0df2800..0000000 --- a/assets/icons/build/extract.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; -// Extract the glyph outlines the site icons and the og-image wordmark need from a -// grotesque font (LiberationSans = Arial-metric, matching the game's system-ui/Arial -// stack) and emit cubic-bezier contours per character, baseline at y=0, y-down, -// plus the advance width so generate.js can lay out words without the font. -// Same outline conversion as ../../vk/build/extract.js, generalised to a char set. -const opentype = require('opentype.js'); -const fs = require('fs'); - -const FONT = process.argv[2] || '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'; -const b = fs.readFileSync(FONT); -const font = opentype.parse(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)); -const FS = 1000; // em scale - -// The tile glyphs («Э», «8») + every character of the og-image wordmark lines -// («Эрудит», «Скрэббл — игра в слова»). The space carries only an advance. -const CHARS = [...new Set('Э8рудитСкэббл—игра в слова')]; - -function glyphData(ch) { - const g = font.charToGlyph(ch); - const p = g.getPath(0, 0, FS); // baseline at y=0, y-down - const contours = []; - let cur = null, prev = null; - for (const c of p.commands) { - if (c.type === 'M') { - if (cur) contours.push(cur); - cur = [{ v: [c.x, c.y], i: [c.x, c.y], o: [c.x, c.y] }]; - prev = { x: c.x, y: c.y }; - } else if (c.type === 'L') { - cur.push({ v: [c.x, c.y], i: [c.x, c.y], o: [c.x, c.y] }); - prev = { x: c.x, y: c.y }; - } else if (c.type === 'C') { - cur[cur.length - 1].o = [c.x1, c.y1]; - cur.push({ v: [c.x, c.y], i: [c.x2, c.y2], o: [c.x, c.y] }); - prev = { x: c.x, y: c.y }; - } else if (c.type === 'Q') { - const c1 = [prev.x + 2 / 3 * (c.x1 - prev.x), prev.y + 2 / 3 * (c.y1 - prev.y)]; - const c2 = [c.x + 2 / 3 * (c.x1 - c.x), c.y + 2 / 3 * (c.y1 - c.y)]; - cur[cur.length - 1].o = c1; - cur.push({ v: [c.x, c.y], i: c2, o: [c.x, c.y] }); - prev = { x: c.x, y: c.y }; - } else if (c.type === 'Z') { - if (cur && cur.length > 1) { - const last = cur[cur.length - 1], first = cur[0]; - if (Math.hypot(last.v[0] - first.v[0], last.v[1] - first.v[1]) < 1e-3) { - first.i = last.i; // fold the duplicate closing point into the first - cur.pop(); - } - } - if (cur) { contours.push(cur); cur = null; } - } - } - if (cur) contours.push(cur); - - let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity; - const out = contours.map(ct => { - const v = [], i = [], o = []; - ct.forEach(pt => { - v.push(pt.v); - i.push([pt.i[0] - pt.v[0], pt.i[1] - pt.v[1]]); - o.push([pt.o[0] - pt.v[0], pt.o[1] - pt.v[1]]); - minx = Math.min(minx, pt.v[0]); maxx = Math.max(maxx, pt.v[0]); - miny = Math.min(miny, pt.v[1]); maxy = Math.max(maxy, pt.v[1]); - }); - return { i, o, v, c: true }; - }); - const bbox = out.length - ? { x: minx, y: miny, w: maxx - minx, h: maxy - miny } - : { x: 0, y: 0, w: 0, h: 0 }; // the space has no outline - return { adv: g.advanceWidth * (FS / font.unitsPerEm), bbox, contours: out }; -} - -const glyphs = {}; -for (const ch of CHARS) glyphs[ch] = glyphData(ch); - -const out = __dirname + '/glyphs.json'; -fs.writeFileSync(out, JSON.stringify({ em: FS, glyphs })); -console.log('wrote', out, fs.statSync(out).size, 'bytes;', CHARS.length, 'glyphs:', CHARS.join('')); diff --git a/assets/icons/build/generate.js b/assets/icons/build/generate.js deleted file mode 100644 index 4ac9928..0000000 --- a/assets/icons/build/generate.js +++ /dev/null @@ -1,128 +0,0 @@ -'use strict'; -// Site icons + the Open Graph card for the public landing, drawn from the same -// design as ../../vk (the wooden Erudit tile: face «Э», score «8») and the app's -// board palette (ui/src/app.css). Everything is composed as SVG from the committed -// glyph outlines (build/extract.js -> glyphs.json), so no font is needed at build -// time; the PNG/ICO rasters are screenshots taken with the ui package's Playwright -// chromium (@playwright/test re-exports the browser API). Outputs go straight to -// ui/public/: -// favicon.svg 96 viewBox, transparent, tile + «Э» (the score is illegible small) -// favicon.ico 32x32 PNG-in-ICO render of the same -// apple-touch-icon.png 180x180 opaque full-bleed tile (iOS masks its own corners) -// og-image.png 1200x630 card: tile + wordmark on the board green -const fs = require('fs'); -const path = require('path'); - -const G = JSON.parse(fs.readFileSync(path.join(__dirname, 'glyphs.json'), 'utf8')); -const UI = path.resolve(__dirname, '../../../ui'); -const OUT = path.join(UI, 'public'); - -// ---- palette (vk loader tile + app.css board tokens) ------------------------ -const FACE = '#D9B978', BORDER = '#B49559', GLYPH = '#1A1A1A'; -const BOARD_DARK = '#2a3330', TEXT = '#e7ece8', TEXT_MUTED = '#cdd6cf'; - -// ---- glyph outlines -> SVG path data ---------------------------------------- -const r2 = n => Math.round(n * 100) / 100; -// pathD renders one glyph's contours scaled by sc and translated by (tx, ty). -function pathD(g, sc, tx, ty) { - const pt = (v, d) => `${r2(v[0] * sc + tx + d[0] * sc)} ${r2(v[1] * sc + ty + d[1] * sc)}`; - const Z = [0, 0]; - return g.contours.map(ct => { - const n = ct.v.length; - let d = `M${pt(ct.v[0], Z)}`; - for (let k = 1; k <= n; k++) { - const a = k - 1, b = k % n; - d += `C${pt(ct.v[a], ct.o[a])} ${pt(ct.v[b], ct.i[b])} ${pt(ct.v[b], Z)}`; - } - return d + 'Z'; - }).join(''); -} -// glyphAt centres a glyph's bbox at (cx, cy) with the given pixel cap height, the -// same placement rule as the vk loader's glyph(). stroke fattens it slightly. -function glyphAt(ch, capPx, cx, cy, stroke, colour) { - const g = G.glyphs[ch], sc = capPx / g.bbox.h; - const d = pathD(g, sc, cx - (g.bbox.x + g.bbox.w / 2) * sc, cy - (g.bbox.y + g.bbox.h / 2) * sc); - return ``; -} -// textLine lays out a string on a baseline from the per-glyph advances; capPx sets -// the capital height (measured on «Э»). Returns the combined path + the width. -function textLine(str, capPx, x, y, colour) { - const sc = capPx / G.glyphs['Э'].bbox.h; - let d = '', w = 0; - for (const ch of str) { - const g = G.glyphs[ch]; - if (g.contours.length) d += pathD(g, sc, x + w, y); - w += g.adv * sc; - } - return { svg: ``, width: w }; -} -// tile draws the rounded wooden tile centred at (cx, cy): size px wide/high, with -// the vk loader's corner (6/52) and rim proportions, «Э» and optionally the «8». -function tile(cx, cy, size, withScore) { - const h = size / 2, rx = size * (6 / 52), rim = size * (1.8 / 52); - let s = ``; - s += glyphAt('Э', size * (32 / 52), cx, cy - size * (1 / 52), size * (1 / 52), GLYPH); - if (withScore) s += glyphAt('8', size * (8.5 / 52), cx + size * (19.5 / 52), cy + size * (18.5 / 52), size * (0.5 / 52), GLYPH); - return s; -} -const svg = (w, h, body) => `${body}`; - -// ---- favicon.svg (the committed vector master) ------------------------------- -const favicon = svg(96, 96, tile(48, 48, 88, false)) + '\n'; - -// ---- apple-touch-icon: opaque full bleed, iOS applies its own corner mask ---- -const appleTouch = svg(180, 180, - `` + - `` + - glyphAt('Э', 100, 90, 88, 3, GLYPH) + - glyphAt('8', 26, 146, 142, 1.5, GLYPH)); - -// ---- og-image: tile + wordmark, centred as one group on the board green ------ -function ogImage() { - const W = 1200, H = 630, tileSize = 340, gap = 84; - const l1 = textLine('Эрудит', 112, 0, 0, TEXT); - const l2 = textLine('Скрэббл — игра в слова', 44, 0, 0, TEXT_MUTED); - const textW = Math.max(l1.width, l2.width); - const left = (W - (tileSize + gap + textW)) / 2; - const tx = left + tileSize + gap; - // Two baselines around the vertical centre; the tile centre sits between them. - const b1 = 295, b2 = 408; - const body = - `` + - tile(left + tileSize / 2, H / 2, tileSize, true) + - textLine('Эрудит', 112, tx, b1, TEXT).svg + - textLine('Скрэббл — игра в слова', 44, tx, b2, TEXT_MUTED).svg; - console.log(`og-image: text ${Math.round(textW)}px wide, group left ${Math.round(left)}px`); - return svg(W, H, body); -} - -// ---- rasterisation (Playwright chromium from ui/node_modules) ---------------- -async function shoot(page, markup, w, h, transparent) { - await page.setViewportSize({ width: w, height: h }); - await page.setContent(`${markup}`); - return page.screenshot({ omitBackground: transparent }); -} -// icoFromPNG wraps one PNG as a single-entry .ico (ICONDIR + ICONDIRENTRY + PNG). -function icoFromPNG(png, sizePx) { - const h = Buffer.alloc(22); - h.writeUInt16LE(0, 0); h.writeUInt16LE(1, 2); h.writeUInt16LE(1, 4); // icon, 1 image - h.writeUInt8(sizePx, 6); h.writeUInt8(sizePx, 7); // 32x32 - h.writeUInt16LE(1, 10); h.writeUInt16LE(32, 12); // planes, 32bpp - h.writeUInt32LE(png.length, 14); h.writeUInt32LE(22, 18); // size, offset - return Buffer.concat([h, png]); -} - -(async () => { - fs.writeFileSync(path.join(OUT, 'favicon.svg'), favicon); - const { chromium } = require(path.join(UI, 'node_modules', '@playwright/test')); - const browser = await chromium.launch(); - const page = await browser.newPage(); - const fav32 = await shoot(page, svg(32, 32, tile(16, 16, 29.33, false)), 32, 32, true); - fs.writeFileSync(path.join(OUT, 'favicon.ico'), icoFromPNG(fav32, 32)); - fs.writeFileSync(path.join(OUT, 'apple-touch-icon.png'), await shoot(page, appleTouch, 180, 180, false)); - fs.writeFileSync(path.join(OUT, 'og-image.png'), await shoot(page, ogImage(), 1200, 630, false)); - await browser.close(); - for (const f of ['favicon.svg', 'favicon.ico', 'apple-touch-icon.png', 'og-image.png']) { - console.log('wrote', path.join(OUT, f), fs.statSync(path.join(OUT, f)).size, 'bytes'); - } -})(); diff --git a/assets/icons/build/glyphs.json b/assets/icons/build/glyphs.json deleted file mode 100644 index a3baa7a..0000000 --- a/assets/icons/build/glyphs.json +++ /dev/null @@ -1 +0,0 @@ -{"em":1000,"glyphs":{"8":{"adv":556.15234375,"bbox":{"x":43.45703125,"y":-698.2421875,"w":469.23828125,"h":708.0078125},"contours":[{"i":[[0,-45.247395833333314],[0,0],[40.364583333333314,-35.48177083333333],[75.52083333333331,0],[0,0],[41.50390625,34.830729166666664],[0,64.12760416666666],[0,0],[-25.71614583333333,30.598958333333314],[-40.0390625,6.510416666666686],[0,0],[0,0],[21.647135416666657,29.296875],[0,39.388020833333314],[0,0],[-39.22526041666666,32.55208333333337],[-66.08072916666666,0],[0,0],[-39.225260416666686,-31.90104166666663],[0,-54.36197916666663],[0,0],[21.809895833333314,-29.296875],[37.760416666666686,-7.486979166666686],[0,0],[0,0],[-24.4140625,-30.110677083333314]],"o":[[0,0],[0,63.4765625],[-40.364583333333314,35.48177083333333],[0,0],[-73.56770833333331,0],[-41.50390625,-34.83072916666666],[0,0],[0,-44.921875],[25.71614583333333,-30.598958333333314],[0,0],[0,0],[-37.434895833333314,-8.7890625],[-21.647135416666657,-29.296875],[0,0],[0,-52.40885416666663],[39.22526041666666,-32.55208333333337],[0,0],[67.70833333333331,0],[39.225260416666686,31.90104166666663],[0,0],[0,39.388020833333314],[-21.809895833333314,29.296875],[0,0],[0,0],[43.9453125,7.161458333333314],[24.4140625,30.110677083333314]],"v":[[512.6953125,-191.89453125],[512.6953125,-191.89453125],[452.1484375,-43.45703125],[278.3203125,9.765625],[278.3203125,9.765625],[105.712890625,-42.48046875],[43.45703125,-190.91796875],[43.45703125,-190.91796875],[82.03125,-304.19921875],[180.6640625,-359.86328125],[180.6640625,-359.86328125],[180.6640625,-361.81640625],[92.041015625,-418.9453125],[59.5703125,-521.97265625],[59.5703125,-521.97265625],[118.408203125,-649.4140625],[276.3671875,-698.2421875],[276.3671875,-698.2421875],[436.767578125,-650.390625],[495.60546875,-520.99609375],[495.60546875,-520.99609375],[462.890625,-417.96875],[373.53515625,-362.79296875],[373.53515625,-362.79296875],[373.53515625,-360.83984375],[476.07421875,-304.931640625]],"c":true},{"i":[[0,42.643229166666686],[0,0],[85.28645833333331,0],[0,0],[21.647135416666657,-19.53125],[0,-38.73697916666663],[0,0],[-22.298177083333343,-20.670572916666686],[-40.69010416666666,0],[0,0],[-21.647135416666686,19.04296875]],"o":[[0,0],[0,-77.79947916666663],[0,0],[-41.341145833333314,0],[-21.647135416666657,19.53125],[0,0],[0,39.388020833333314],[22.298177083333343,20.670572916666686],[0,0],[41.341145833333314,0],[21.647135416666686,-19.04296875]],"v":[[404.296875,-516.11328125],[404.296875,-516.11328125],[276.3671875,-632.8125],[276.3671875,-632.8125],[181.884765625,-603.515625],[149.4140625,-516.11328125],[149.4140625,-516.11328125],[182.861328125,-426.025390625],[277.34375,-395.01953125],[277.34375,-395.01953125],[371.826171875,-423.583984375]],"c":true},{"i":[[0,50.130208333333314],[0,0],[25.390625,21.647135416666686],[45.8984375,0],[0,0],[25.065104166666657,-23.274739583333314],[0,-40.69010416666666],[0,0],[-96.6796875,0],[0,0],[-23.4375,22.94921875]],"o":[[0,0],[0,-42.64322916666666],[-25.390625,-21.647135416666686],[0,0],[-44.59635416666666,0],[-25.065104166666657,23.274739583333314],[0,0],[0,94.7265625],[0,0],[47.8515625,0],[23.4375,-22.94921875]],"v":[[421.38671875,-200.1953125],[421.38671875,-200.1953125],[383.30078125,-296.630859375],[276.3671875,-329.1015625],[276.3671875,-329.1015625],[171.875,-294.189453125],[134.27734375,-198.2421875],[134.27734375,-198.2421875],[279.296875,-56.15234375],[279.296875,-56.15234375],[386.23046875,-90.576171875]],"c":true}]},"Э":{"adv":718.75,"bbox":{"x":51.26953125,"y":-698.2421875,"w":632.8125,"h":708.0078125},"contours":[{"i":[[69.3359375,0],[0,0],[35.481770833333314,-21.97265625],[14.6484375,-38.41145833333337],[0,0],[0,0],[-49.15364583333333,28.64583333333337],[-73.2421875,0],[0,0],[-59.24479166666663,-62.17447916666663],[0,-109.70052083333331],[0,0],[26.3671875,-53.7109375],[50.29296875,-28.971354166666664],[69.98697916666663,0],[0,0],[59.89583333333333,121.09375],[0,0],[0,0],[-37.434895833333314,-24.90234375],[-47.200520833333314,0],[0,0],[-43.45703125,44.10807291666666],[-6.184895833333371,74.86979166666666],[0,0],[0,0],[0,0],[0,0],[41.82942708333337,41.17838541666663]],"o":[[0,0],[-49.479166666666686,0],[-35.481770833333314,21.97265625],[0,0],[0,0],[23.111979166666657,-56.31510416666663],[49.153645833333314,-28.64583333333337],[0,0],[104.81770833333331,0],[59.24479166666663,62.17447916666663],[0,0],[0,72.59114583333331],[-26.3671875,53.7109375],[-50.29296875,28.971354166666664],[0,0],[-138.671875,0],[0,0],[0,0],[22.4609375,45.57291666666666],[37.434895833333314,24.90234375],[0,0],[69.3359375,0],[43.45703125,-44.10807291666666],[0,0],[0,0],[0,0],[0,0],[-6.8359375,-74.54427083333331],[-41.829427083333314,-41.17838541666663]],"v":[[348.14453125,-622.0703125],[348.14453125,-622.0703125],[220.703125,-589.111328125],[145.5078125,-498.53515625],[145.5078125,-498.53515625],[57.12890625,-527.83203125],[165.52734375,-655.2734375],[349.12109375,-698.2421875],[349.12109375,-698.2421875],[595.21484375,-604.98046875],[684.08203125,-347.16796875],[684.08203125,-347.16796875],[644.53125,-157.71484375],[529.541015625,-33.69140625],[349.12109375,9.765625],[349.12109375,9.765625],[51.26953125,-171.875],[51.26953125,-171.875],[127.44140625,-209.9609375],[217.28515625,-104.248046875],[344.23828125,-66.89453125],[344.23828125,-66.89453125],[513.427734375,-133.056640625],[587.890625,-311.5234375],[587.890625,-311.5234375],[268.5546875,-311.5234375],[268.5546875,-386.71875],[587.890625,-386.71875],[514.892578125,-560.302734375]],"c":true}]},"р":{"adv":556.15234375,"bbox":{"x":64.453125,"y":-537.59765625,"w":449.70703125,"h":745.1171875},"contours":[{"i":[[0,-93.42447916666663],[0,0],[129.55729166666663,0],[0,0],[27.994791666666657,61.197916666666664],[0,0],[0,0],[0,-52.734375],[0,0],[0,0],[0,0],[0,0],[1.953125,17.578125],[0,0],[0,0],[-0.6510416666666572,-7.975260416666629],[-0.8138020833333428,-16.6015625],[0,-6.184895833333314],[0,0],[0,0],[-25.716145833333343,15.13671875],[-41.9921875,0],[0,0],[-32.2265625,-43.61979166666663]],"o":[[0,0],[0,184.24479166666666],[0,0],[-81.38020833333331,0],[0,0],[0,0],[1.3020833333333428,2.6041666666666714],[0,0],[0,0],[0,0],[0,0],[0,-54.361979166666686],[0,0],[0,0],[0.3255208333333428,1.3020833333333712],[0.6510416666666572,7.975260416666686],[0.8138020833333428,16.6015625],[0,0],[0,0],[15.625,-32.552083333333314],[25.716145833333343,-15.13671875],[0,0],[65.10416666666663,0],[32.2265625,43.619791666666686]],"v":[[514.16015625,-266.6015625],[514.16015625,-266.6015625],[319.82421875,9.765625],[319.82421875,9.765625],[155.76171875,-82.03125],[155.76171875,-82.03125],[153.3203125,-82.03125],[155.2734375,0.9765625],[155.2734375,0.9765625],[155.2734375,207.51953125],[67.3828125,207.51953125],[67.3828125,-420.41015625],[64.453125,-528.3203125],[64.453125,-528.3203125],[149.4140625,-528.3203125],[150.87890625,-514.404296875],[153.076171875,-477.5390625],[154.296875,-443.359375],[154.296875,-443.359375],[156.25,-443.359375],[218.26171875,-514.892578125],[319.82421875,-537.59765625],[319.82421875,-537.59765625],[465.8203125,-472.16796875]],"c":true},{"i":[[0,75.52083333333331],[0,0],[19.856770833333314,31.575520833333314],[43.294270833333314,0],[0,0],[19.694010416666657,-14.6484375],[10.25390625,-31.087239583333314],[0,-49.8046875],[0,0],[-22.135416666666657,-32.87760416666666],[-49.8046875,0],[0,0],[-20.01953125,32.06380208333333]],"o":[[0,0],[0,-73.56770833333331],[-19.856770833333314,-31.575520833333314],[0,0],[-34.830729166666686,0],[-19.694010416666657,14.6484375],[-10.25390625,31.087239583333314],[0,0],[0,69.3359375],[22.135416666666657,32.87760416666666],[0,0],[43.619791666666686,0],[20.01953125,-32.063802083333314]],"v":[[421.875,-264.6484375],[421.875,-264.6484375],[392.08984375,-422.36328125],[297.36328125,-469.7265625],[297.36328125,-469.7265625],[215.576171875,-447.75390625],[170.654296875,-379.150390625],[155.2734375,-257.8125],[155.2734375,-257.8125],[188.4765625,-104.4921875],[296.38671875,-55.17578125],[296.38671875,-55.17578125],[391.845703125,-103.271484375]],"c":true}]},"у":{"adv":500,"bbox":{"x":2.44140625,"y":-528.3203125,"w":495.60546875,"h":735.83984375},"contours":[{"i":[[28.971354166666657,0],[0,0],[16.276041666666664,3.580729166666657],[0,0],[0,0],[-14.973958333333329,0],[0,0],[-31.901041666666657,80.40364583333333],[0,0],[0,0],[0,0],[0,0],[0,0],[-2.278645833333343,-6.34765625],[-12.369791666666657,-36.1328125],[-0.9765625,-4.231770833333329],[0,0],[0,0],[0,0],[0,0],[0,0],[18.880208333333343,-27.506510416666657],[22.94921875,-13.509114583333343]],"o":[[0,0],[-24.088541666666657,0],[0,0],[0,0],[12.369791666666664,1.953125],[0,0],[54.6875,0],[0,0],[0,0],[0,0],[0,0],[0,0],[1.6276041666666572,4.557291666666657],[2.278645833333343,6.34765625],[12.369791666666657,36.1328125],[0,0],[0,0],[0,0],[0,0],[0,0],[-21.809895833333314,56.315104166666664],[-18.880208333333343,27.506510416666657],[-22.94921875,13.509114583333343]],"v":[[93.26171875,207.51953125],[93.26171875,207.51953125],[32.71484375,202.1484375],[32.71484375,202.1484375],[32.71484375,136.23046875],[73.73046875,139.16015625],[73.73046875,139.16015625],[203.61328125,18.5546875],[203.61328125,18.5546875],[211.9140625,-2.44140625],[2.44140625,-528.3203125],[96.19140625,-528.3203125],[207.51953125,-236.328125],[213.37890625,-219.970703125],[235.3515625,-156.25],[255.37109375,-95.703125],[255.37109375,-95.703125],[289.55078125,-191.89453125],[405.2734375,-528.3203125],[498.046875,-528.3203125],[294.921875,0],[233.88671875,125.732421875],[171.142578125,187.255859375]],"c":true}]},"д":{"adv":583.49609375,"bbox":{"x":9.765625,"y":-528.3203125,"w":556.640625,"h":727.5390625},"contours":[{"i":[[0,0],[0,0],[0,0],[14.6484375,-65.59244791666663],[20.833333333333343,-47.20052083333333],[0,0]],"o":[[0,0],[0,0],[-12.044270833333343,88.54166666666663],[-14.6484375,65.59244791666666],[0,0],[0,0]],"v":[[407.2265625,-63.96484375],[407.2265625,-464.35546875],[257.8125,-464.35546875],[217.7734375,-233.154296875],[164.55078125,-63.96484375],[164.55078125,-63.96484375]],"c":true},{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-17.90364583333333,74.86979166666666],[-14.973958333333343,119.140625],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[24.4140625,-40.69010416666666],[17.903645833333343,-74.86979166666663],[0,0],[0,0],[0,0],[0,0]],"v":[[566.40625,-63.96484375],[566.40625,199.21875],[486.81640625,199.21875],[486.81640625,0],[89.35546875,0],[89.35546875,199.21875],[9.765625,199.21875],[9.765625,-63.96484375],[67.87109375,-63.96484375],[131.34765625,-237.3046875],[180.6640625,-528.3203125],[180.6640625,-528.3203125],[495.1171875,-528.3203125],[495.1171875,-63.96484375]],"c":true}]},"и":{"adv":558.59375,"bbox":{"x":69.3359375,"y":-528.3203125,"w":419.921875,"h":528.3203125},"contours":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-1.1393229166666856,23.763020833333314],[-0.9765625,10.091145833333314],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-11.71875],[1.1393229166666856,-23.763020833333314],[0,0],[0,0],[0,0],[0,0]],"v":[[69.3359375,-528.3203125],[154.296875,-528.3203125],[154.296875,-239.2578125],[149.4140625,-104.00390625],[393.06640625,-528.3203125],[489.2578125,-528.3203125],[489.2578125,0],[405.2734375,0],[405.2734375,-322.265625],[406.982421875,-375.48828125],[410.15625,-426.26953125],[410.15625,-426.26953125],[163.0859375,0],[69.3359375,0]],"c":true}]},"т":{"adv":458.0078125,"bbox":{"x":17.08984375,"y":-528.3203125,"w":423.828125,"h":528.3203125},"contours":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[17.08984375,-464.35546875],[17.08984375,-528.3203125],[440.91796875,-528.3203125],[440.91796875,-464.35546875],[272.94921875,-464.35546875],[272.94921875,0],[185.05859375,0],[185.05859375,-464.35546875]],"c":true}]},"С":{"adv":722.16796875,"bbox":{"x":50.78125,"y":-698.2421875,"w":633.30078125,"h":708.0078125},"contours":[{"i":[[48.502604166666686,0],[0,0],[42.317708333333314,-48.99088541666663],[0,-85.28645833333331],[0,0],[-44.10807291666666,-51.26953125],[-75.1953125,0],[0,0],[-48.50260416666663,95.37760416666666],[0,0],[0,0],[51.26953125,-30.924479166666657],[67.70833333333331,0],[0,0],[50.618489583333314,28.80859375],[26.529947916666657,53.54817708333333],[0,73.2421875],[0,0],[-59.24479166666666,62.17447916666663],[-104.81770833333331,0],[0,0],[-49.15364583333337,-28.64583333333337],[-23.11197916666663,-56.31510416666663],[0,0],[0,0],[35.31901041666663,21.15885416666663]],"o":[[0,0],[-76.171875,0],[-42.317708333333314,48.990885416666686],[0,0],[0,84.30989583333331],[44.10807291666666,51.26953125],[0,0],[96.35416666666663,0],[0,0],[0,0],[-28.3203125,59.24479166666666],[-51.26953125,30.924479166666664],[0,0],[-69.3359375,0],[-50.618489583333314,-28.80859375],[-26.529947916666657,-53.548177083333314],[0,0],[0,-109.70052083333331],[59.24479166666666,-62.17447916666663],[0,0],[73.2421875,0],[49.15364583333337,28.64583333333337],[0,0],[0,0],[-15.950520833333371,-40.0390625],[-35.319010416666686,-21.15885416666663]],"v":[[386.71875,-622.0703125],[386.71875,-622.0703125],[208.984375,-548.583984375],[145.5078125,-347.16796875],[145.5078125,-347.16796875],[211.669921875,-143.798828125],[390.625,-66.89453125],[390.625,-66.89453125],[607.91015625,-209.9609375],[607.91015625,-209.9609375],[684.08203125,-171.875],[564.697265625,-36.62109375],[386.23046875,9.765625],[386.23046875,9.765625],[206.298828125,-33.447265625],[90.576171875,-156.982421875],[50.78125,-347.16796875],[50.78125,-347.16796875],[139.6484375,-604.98046875],[385.7421875,-698.2421875],[385.7421875,-698.2421875],[569.3359375,-655.2734375],[677.734375,-527.83203125],[677.734375,-527.83203125],[589.35546875,-498.53515625],[512.451171875,-590.33203125]],"c":true}]},"к":{"adv":437.5,"bbox":{"x":67.3828125,"y":-528.3203125,"w":375,"h":528.3203125},"contours":[{"i":[[0,0],[0,0],[0,0],[0,0],[-7.8125,2.6041666666666856],[-8.463541666666657,8.30078125],[-10.7421875,14.973958333333314],[-63.4765625,102.5390625],[0,0],[0,0],[0,0],[11.393229166666686,-9.440104166666686],[0,0],[0,0],[0,0],[0,0],[10.579427083333343,-1.953125],[8.463541666666657,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[9.765625,0],[7.8125,-2.6041666666666856],[8.463541666666657,-8.30078125],[10.7421875,-14.973958333333314],[0,0],[0,0],[0,0],[-30.2734375,44.270833333333314],[0,0],[0,0],[0,0],[0,0],[-6.184895833333343,2.604166666666657],[-10.579427083333343,1.953125],[0,0],[0,0],[0,0]],"v":[[67.3828125,0],[67.3828125,-528.3203125],[155.2734375,-528.3203125],[155.2734375,-296.875],[181.640625,-300.78125],[206.0546875,-317.138671875],[234.86328125,-352.05078125],[346.19140625,-528.3203125],[346.19140625,-528.3203125],[437.98828125,-528.3203125],[332.51953125,-369.62890625],[270.01953125,-289.0625],[270.01953125,-289.0625],[442.3828125,0],[344.7265625,0],[208.984375,-242.67578125],[183.837890625,-235.83984375],[155.2734375,-232.91015625],[155.2734375,-232.91015625],[155.2734375,0]],"c":true}]},"э":{"adv":510.25390625,"bbox":{"x":26.85546875,"y":-538.0859375,"w":440.91796875,"h":547.8515625},"contours":[{"i":[[6.8359375,50.45572916666666],[0,0],[0,0],[-21.647135416666657,-16.92708333333333],[-32.552083333333314,0],[0,0],[-23.111979166666686,28.80859375],[-3.90625,61.848958333333314],[0,0],[0,0],[0,0],[0,0],[21.97265625,27.018229166666686],[44.921875,0],[0,0],[10.416666666666657,-63.802083333333314],[0,0],[0,0],[-36.295572916666664,27.180989583333314],[-55.6640625,0],[0,0],[-39.876302083333314,-46.875],[0,-88.54166666666663],[0,0],[39.876302083333314,-46.54947916666666],[75.52083333333331,0],[0,0],[37.109375,29.296875]],"o":[[0,0],[0,0],[5.208333333333329,35.15625],[21.647135416666657,16.92708333333333],[0,0],[41.9921875,0],[23.111979166666686,-28.80859375],[0,0],[0,0],[0,0],[0,0],[-2.6041666666666856,-61.5234375],[-21.97265625,-27.018229166666686],[0,0],[-68.359375,0],[0,0],[0,0],[9.114583333333329,-48.502604166666686],[36.29557291666666,-27.18098958333337],[0,0],[74.86979166666663,0],[39.876302083333314,46.875],[0,0],[0,89.84375],[-39.876302083333314,46.549479166666664],[0,0],[-57.6171875,0],[-37.109375,-29.296875]],"v":[[26.85546875,-153.80859375],[26.85546875,-153.80859375],[115.72265625,-159.66796875],[156.005859375,-81.54296875],[237.3046875,-56.15234375],[237.3046875,-56.15234375],[334.9609375,-99.365234375],[375.48828125,-235.3515625],[375.48828125,-235.3515625],[173.828125,-235.3515625],[173.828125,-299.31640625],[375.48828125,-299.31640625],[338.623046875,-432.12890625],[238.28125,-472.65625],[238.28125,-472.65625],[120.1171875,-376.953125],[120.1171875,-376.953125],[29.78515625,-383.7890625],[97.900390625,-497.314453125],[235.83984375,-538.0859375],[235.83984375,-538.0859375],[407.958984375,-467.7734375],[467.7734375,-264.6484375],[467.7734375,-264.6484375],[407.958984375,-60.05859375],[234.86328125,9.765625],[234.86328125,9.765625],[92.7734375,-34.1796875]],"c":true}]},"б":{"adv":572.75390625,"bbox":{"x":58.59375,"y":-733.3984375,"w":472.16796875,"h":743.1640625},"contours":[{"i":[[0,65.75520833333331],[0,0],[21.484375,31.087239583333314],[48.502604166666686,0],[0,0],[24.251302083333343,-31.575520833333314],[0,-64.77864583333331],[0,0],[-22.623697916666657,-31.73828125],[-48.50260416666666,0],[0,0],[-22.786458333333314,30.436197916666657]],"o":[[0,0],[0,-65.75520833333331],[-21.484375,-31.087239583333314],[0,0],[-51.7578125,0],[-24.251302083333343,31.575520833333314],[0,0],[0,63.15104166666666],[22.623697916666657,31.73828125],[0,0],[52.408854166666686,0],[22.786458333333314,-30.436197916666657]],"v":[[438.4765625,-245.1171875],[438.4765625,-245.1171875],[406.25,-390.380859375],[301.26953125,-437.01171875],[301.26953125,-437.01171875],[187.255859375,-389.6484375],[150.87890625,-245.1171875],[150.87890625,-245.1171875],[184.814453125,-102.783203125],[291.50390625,-55.17578125],[291.50390625,-55.17578125],[404.296875,-100.830078125]],"c":true},{"i":[[-46.875,0],[0,0],[-34.1796875,-42.48046875],[0,-85.28645833333331],[0,0],[40.690104166666686,-42.15494791666666],[77.47395833333331,0],[0,0],[37.923177083333314,52.408854166666664],[0,108.72395833333331],[0,0],[-7.486979166666664,41.829427083333314],[-15.950520833333329,30.2734375],[-25.227864583333343,20.18229166666663],[-38.24869791666666,12.369791666666629],[-124.34895833333331,17.25260416666663],[0,0],[0,0],[40.201822916666686,-15.950520833333371],[16.276041666666657,-34.83072916666663],[1.3020833333333428,-69.3359375],[0,0],[-32.87760416666666,22.786458333333314]],"o":[[0,0],[72.265625,0],[34.1796875,42.48046875],[0,0],[0,86.26302083333331],[-40.690104166666686,42.154947916666664],[0,0],[-80.078125,0],[-37.92317708333333,-52.40885416666666],[0,0],[0,-55.338541666666686],[7.486979166666671,-41.829427083333314],[15.950520833333329,-30.2734375],[25.227864583333343,-20.18229166666663],[38.248697916666686,-12.369791666666629],[0,0],[0,0],[-136.39322916666663,17.578125],[-40.20182291666666,15.950520833333371],[-16.276041666666657,34.830729166666686],[0,0],[13.997395833333343,-40.690104166666686],[32.87760416666666,-22.786458333333314]],"v":[[319.82421875,-501.46484375],[319.82421875,-501.46484375],[479.4921875,-437.744140625],[530.76171875,-246.09375],[530.76171875,-246.09375],[469.7265625,-53.466796875],[292.48046875,9.765625],[292.48046875,9.765625],[115.478515625,-68.84765625],[58.59375,-310.546875],[58.59375,-310.546875],[69.82421875,-456.298828125],[104.98046875,-564.453125],[166.748046875,-640.13671875],[261.962890625,-688.96484375],[505.859375,-733.3984375],[505.859375,-733.3984375],[505.859375,-654.78515625],[240.966796875,-604.4921875],[156.25,-528.3203125],[129.8828125,-372.0703125],[129.8828125,-372.0703125],[200.1953125,-467.28515625]],"c":true}]},"л":{"adv":583.49609375,"bbox":{"x":5.37109375,"y":-528.3203125,"w":508.7890625,"h":538.0859375},"contours":[{"i":[[0,0],[0,0],[0,0],[0,0],[10.416666666666657,-49.641927083333314],[12.044270833333343,-25.390625],[17.740885416666657,-12.044270833333332],[27.994791666666657,0],[0,0],[14.322916666666664,4.231770833333333],[0,0],[0,0],[-13.997395833333332,0],[0,0],[-10.904947916666671,17.90364583333333],[-8.951822916666671,43.9453125],[-11.393229166666657,85.9375],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[-19.205729166666657,141.92708333333331],[-10.416666666666657,49.64192708333333],[-12.044270833333343,25.390625],[-17.740885416666657,12.044270833333332],[0,0],[-16.276041666666664,0],[0,0],[0,0],[8.138020833333332,2.9296875],[0,0],[17.90364583333333,0],[10.904947916666671,-17.90364583333333],[8.951822916666671,-43.9453125],[0,0],[0,0],[0,0],[0,0]],"v":[[514.16015625,0],[425.78125,0],[425.78125,-464.35546875],[242.67578125,-464.35546875],[198.2421875,-177.001953125],[164.55078125,-64.453125],[119.873046875,-8.30078125],[51.26953125,9.765625],[51.26953125,9.765625],[5.37109375,3.41796875],[5.37109375,3.41796875],[5.37109375,-59.5703125],[38.57421875,-55.17578125],[38.57421875,-55.17578125],[81.787109375,-82.03125],[111.572265625,-174.8046875],[142.08984375,-369.62890625],[142.08984375,-369.62890625],[163.57421875,-528.3203125],[514.16015625,-528.3203125]],"c":true}]},"—":{"adv":1000,"bbox":{"x":0,"y":-287.109375,"w":1000,"h":66.89453125},"contours":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1000,-220.21484375],[0,-220.21484375],[0,-287.109375],[1000,-287.109375]],"c":true}]},"г":{"adv":364.74609375,"bbox":{"x":69.3359375,"y":-528.3203125,"w":266.11328125,"h":528.3203125},"contours":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[69.3359375,-528.3203125],[335.44921875,-528.3203125],[335.44921875,-464.35546875],[157.2265625,-464.35546875],[157.2265625,0],[69.3359375,0]],"c":true}]},"а":{"adv":556.15234375,"bbox":{"x":42.48046875,"y":-538.0859375,"w":513.671875,"h":547.8515625},"contours":[{"i":[[42.64322916666666,0],[0,0],[26.69270833333333,27.994791666666664],[0,48.828125],[0,0],[-35.97005208333333,29.296875],[-80.078125,1.953125],[0,0],[0,0],[0,0],[18.229166666666686,18.5546875],[39.0625,0],[0,0],[17.903645833333343,-13.346354166666686],[3.580729166666657,-29.296875],[0,0],[0,0],[-126.30208333333331,0],[0,0],[-33.528645833333314,-30.43619791666663],[0,-57.6171875],[0,0],[0,0],[-6.8359375,-13.18359375],[-19.205729166666686,0],[0,0],[-10.7421875,2.2786458333333357],[0,0],[0,0],[23.111979166666686,0],[0,0],[14.811197916666686,17.08984375],[1.953125,36.45833333333333],[0,0],[0,0],[29.78515625,-16.764322916666664]],"o":[[0,0],[-53.059895833333314,0],[-26.69270833333333,-27.994791666666664],[0,0],[0,-54.6875],[35.970052083333314,-29.296875],[0,0],[0,0],[0,0],[0,-42.96875],[-18.229166666666686,-18.5546875],[0,0],[-39.388020833333314,0],[-17.903645833333343,13.346354166666686],[0,0],[0,0],[14.973958333333329,-95.05208333333331],[0,0],[66.40625,0],[33.528645833333314,30.436197916666686],[0,0],[0,0],[0,26.041666666666657],[6.8359375,13.18359375],[0,0],[8.463541666666629,0],[0,0],[0,0],[-22.13541666666663,5.208333333333333],[0,0],[-32.552083333333314,0],[-14.811197916666686,-17.08984375],[0,0],[0,0],[-22.4609375,40.36458333333333],[-29.78515625,16.764322916666664]],"v":[[202.1484375,9.765625],[202.1484375,9.765625],[82.51953125,-32.2265625],[42.48046875,-147.4609375],[42.48046875,-147.4609375],[96.435546875,-273.4375],[270.5078125,-320.3125],[270.5078125,-320.3125],[389.16015625,-322.265625],[389.16015625,-351.07421875],[361.81640625,-443.359375],[275.87890625,-471.19140625],[275.87890625,-471.19140625],[189.94140625,-451.171875],[157.71484375,-387.20703125],[157.71484375,-387.20703125],[65.91796875,-395.5078125],[277.83203125,-538.0859375],[277.83203125,-538.0859375],[427.734375,-492.431640625],[478.02734375,-360.3515625],[478.02734375,-360.3515625],[478.02734375,-132.8125],[488.28125,-73.974609375],[527.34375,-54.19921875],[527.34375,-54.19921875],[556.15234375,-57.6171875],[556.15234375,-57.6171875],[556.15234375,-2.9296875],[488.28125,4.8828125],[488.28125,4.8828125],[417.236328125,-20.751953125],[392.08984375,-101.07421875],[392.08984375,-101.07421875],[389.16015625,-101.07421875],[310.791015625,-15.380859375]],"c":true},{"i":[[-28.645833333333343,0],[0,0],[-25.065104166666686,14.6484375],[-14.485677083333314,25.553385416666657],[0,27.018229166666657],[0,0],[0,0],[0,0],[21.321614583333343,-7.8125],[11.393229166666657,-16.276041666666657],[0,-26.3671875],[0,0],[-15.462239583333343,-15.625]],"o":[[0,0],[32.2265625,0],[25.065104166666686,-14.6484375],[14.485677083333314,-25.553385416666657],[0,0],[0,0],[0,0],[-41.341145833333314,0.6510416666666856],[-21.321614583333343,7.8125],[-11.393229166666657,16.276041666666657],[0,0],[0,28.64583333333333],[15.462239583333343,15.625]],"v":[[222.16796875,-56.15234375],[222.16796875,-56.15234375],[308.10546875,-78.125],[367.431640625,-138.427734375],[389.16015625,-217.28515625],[389.16015625,-217.28515625],[389.16015625,-260.7421875],[292.96875,-258.7890625],[198.974609375,-246.09375],[149.90234375,-209.9609375],[132.8125,-145.99609375],[132.8125,-145.99609375],[156.005859375,-79.58984375]],"c":true}]}," ":{"adv":277.83203125,"bbox":{"x":0,"y":0,"w":0,"h":0},"contours":[]},"в":{"adv":531.25,"bbox":{"x":69.3359375,"y":-528.3203125,"w":418.45703125,"h":528.3203125},"contours":[{"i":[[0,0],[0,0],[-32.552083333333314,-22.78645833333337],[0,-42.643229166666686],[0,0],[18.5546875,-20.182291666666686],[35.481770833333314,-6.8359375],[0,0],[0,0],[-20.99609375,-21.158854166666686],[0,-34.83072916666666],[0,0],[35.64453125,-26.85546875],[62.825520833333314,0],[0,0],[0,0]],"o":[[0,0],[65.10416666666663,0],[32.552083333333314,22.786458333333314],[0,0],[0,30.2734375],[-18.5546875,20.182291666666686],[0,0],[0,0],[41.9921875,5.533854166666686],[20.99609375,21.158854166666657],[0,0],[0,47.8515625],[-35.64453125,26.85546875],[0,0],[0,0],[0,0]],"v":[[69.3359375,-528.3203125],[275.390625,-528.3203125],[421.875,-494.140625],[470.703125,-395.99609375],[470.703125,-395.99609375],[442.87109375,-320.3125],[361.81640625,-279.78515625],[361.81640625,-279.78515625],[361.81640625,-276.3671875],[456.298828125,-236.328125],[487.79296875,-152.34375],[487.79296875,-152.34375],[434.326171875,-40.283203125],[286.62109375,0],[286.62109375,0],[69.3359375,0]],"c":true},{"i":[[0,0],[0,0],[0,0],[-18.229166666666686,13.834635416666671],[0,30.2734375],[0,0],[19.368489583333314,13.509114583333343],[46.549479166666686,0],[0,0]],"o":[[0,0],[0,0],[44.921875,0],[18.229166666666686,-13.834635416666671],[0,0],[0,-32.87760416666666],[-19.368489583333314,-13.509114583333343],[0,0],[0,0]],"v":[[157.2265625,-241.69921875],[157.2265625,-64.94140625],[272.4609375,-64.94140625],[367.1875,-85.693359375],[394.53125,-151.85546875],[394.53125,-151.85546875],[365.478515625,-221.435546875],[266.6015625,-241.69921875],[266.6015625,-241.69921875]],"c":true},{"i":[[0,0],[0,0],[0,0],[0,0],[-17.740885416666686,11.71875],[0,29.296875],[0,0],[16.6015625,12.369791666666686],[39.388020833333314,0]],"o":[[0,0],[0,0],[0,0],[41.666666666666686,0],[17.740885416666686,-11.71875],[0,0],[0,-26.692708333333314],[-16.6015625,-12.369791666666686],[0,0]],"v":[[269.53125,-461.42578125],[157.2265625,-461.42578125],[157.2265625,-305.17578125],[262.6953125,-305.17578125],[351.806640625,-322.75390625],[378.41796875,-384.27734375],[378.41796875,-384.27734375],[353.515625,-442.87109375],[269.53125,-461.42578125]],"c":true}]},"с":{"adv":500,"bbox":{"x":42.48046875,"y":-538.0859375,"w":431.15234375,"h":547.8515625},"contours":[{"i":[[0,-73.2421875],[0,0],[-22.135416666666657,-33.85416666666666],[-44.59635416666666,0],[0,0],[-20.99609375,16.92708333333333],[-4.8828125,35.15625],[0,0],[0,0],[36.458333333333314,-30.2734375],[55.989583333333314,0],[0,0],[38.899739583333314,46.71223958333333],[0,89.51822916666666],[0,0],[-39.0625,46.712239583333314],[-72.91666666666666,0],[0,0],[-35.64453125,-27.99479166666663],[-9.114583333333314,-49.153645833333314],[0,0],[0,0],[18.5546875,17.252604166666686],[34.1796875,0],[0,0],[20.833333333333343,-30.924479166666686]],"o":[[0,0],[0,70.3125],[22.135416666666657,33.85416666666666],[0,0],[31.25,0],[20.99609375,-16.92708333333333],[0,0],[0,0],[-6.8359375,50.78125],[-36.458333333333314,30.2734375],[0,0],[-73.89322916666666,0],[-38.89973958333333,-46.71223958333333],[0,0],[0,-88.8671875],[39.0625,-46.71223958333337],[0,0],[54.036458333333314,0],[35.64453125,27.994791666666686],[0,0],[0,0],[-4.557291666666686,-29.296875],[-18.5546875,-17.252604166666686],[0,0],[-46.54947916666666,0],[-20.833333333333343,30.924479166666686]],"v":[[134.27734375,-266.6015625],[134.27734375,-266.6015625],[167.48046875,-110.3515625],[267.578125,-59.5703125],[267.578125,-59.5703125],[345.947265625,-84.9609375],[384.765625,-163.0859375],[384.765625,-163.0859375],[473.6328125,-157.2265625],[408.69140625,-35.64453125],[270.01953125,9.765625],[270.01953125,9.765625],[100.830078125,-60.302734375],[42.48046875,-264.6484375],[42.48046875,-264.6484375],[101.07421875,-468.017578125],[269.04296875,-538.0859375],[269.04296875,-538.0859375],[403.564453125,-496.09375],[470.703125,-380.37109375],[470.703125,-380.37109375],[380.37109375,-373.53515625],[345.703125,-443.359375],[266.6015625,-469.23828125],[266.6015625,-469.23828125],[165.52734375,-422.8515625]],"c":true}]},"о":{"adv":556.15234375,"bbox":{"x":41.9921875,"y":-538.0859375,"w":472.16796875,"h":547.8515625},"contours":[{"i":[[0,-93.42447916666663],[0,0],[40.690104166666686,-45.24739583333333],[77.47395833333331,0],[0,0],[39.388020833333314,47.037760416666664],[0,88.8671875],[0,0],[-157.87760416666666,0],[0,0],[-38.0859375,-44.43359375]],"o":[[0,0],[0,92.44791666666666],[-40.690104166666686,45.24739583333333],[0,0],[-77.1484375,0],[-39.38802083333333,-47.03776041666666],[0,0],[0,-182.29166666666663],[0,0],[80.72916666666663,0],[38.0859375,44.43359375]],"v":[[514.16015625,-264.6484375],[514.16015625,-264.6484375],[453.125,-58.10546875],[275.87890625,9.765625],[275.87890625,9.765625],[101.07421875,-60.791015625],[41.9921875,-264.6484375],[41.9921875,-264.6484375],[278.80859375,-538.0859375],[278.80859375,-538.0859375],[457.03125,-471.435546875]],"c":true},{"i":[[0,71.94010416666666],[0,0],[21.647135416666686,33.040364583333314],[51.106770833333314,0],[0,0],[22.94921875,-33.69140625],[0,-71.61458333333331],[0,0],[-22.623697916666657,-34.993489583333314],[-48.50260416666666,0],[0,0],[-22.623697916666686,33.85416666666666]],"o":[[0,0],[0,-72.91666666666663],[-21.647135416666686,-33.040364583333314],[0,0],[-51.43229166666666,0],[-22.94921875,33.69140625],[0,0],[0,69.66145833333331],[22.623697916666657,34.99348958333333],[0,0],[52.734375,0],[22.623697916666686,-33.85416666666666]],"v":[[421.875,-264.6484375],[421.875,-264.6484375],[389.404296875,-423.583984375],[280.2734375,-473.14453125],[280.2734375,-473.14453125],[168.701171875,-422.607421875],[134.27734375,-264.6484375],[134.27734375,-264.6484375],[168.212890625,-107.666015625],[274.90234375,-55.17578125],[274.90234375,-55.17578125],[387.939453125,-105.95703125]],"c":true}]}}} \ No newline at end of file diff --git a/docs/ICONS.md b/docs/ICONS.md index 53abc44..12ff0d0 100644 --- a/docs/ICONS.md +++ b/docs/ICONS.md @@ -1,111 +1,90 @@ # Icon & logo asset map -> Single reference for every icon/logo format the project ships, and the -> single-master workflow a rebrand should follow. Goal: one vector master → -> regenerate every target, miss nothing. The iOS / iPadOS rows are **future** -> (no iOS shell exists yet) but are listed so a rebrand covers them in one pass. +> The whole shipped icon set is sliced from **one master**. This file is the per-platform +> **target map** (which file goes where, at what size). How the mark itself is constructed +> — proportions, palette, wood grain, the light/shadow gradients, the «Э» + ✻ placement, +> all in fractions of the side — is specified in [`ICON_BRANDBOOK.md`](ICON_BRANDBOOK.md). +> The reference generator, the pinned Spectral font and the committed masters live in +> [`../assets/icons/brand/`](../assets/icons/brand/). -## Current state — three drifting sources +## The master (one source, six layers) -The «Э» mark is emitted today by three independent pipelines that visually drift -(the favicon is a rounded bordered tile, the maskable is a full-bleed square, the -Android launcher is an upscale). The rebrand collapses them onto one master. +`brand/build-icon.mjs` emits any `--variant light|dark` in any `--layer`: -1. **Web favicon set** — `assets/icons/build/generate.js` composes SVG from font - glyph outlines (`assets/icons/build/glyphs.json`) and rasterises with the `ui` - package's Playwright chromium → - `ui/public/{favicon.svg, favicon.ico, apple-touch-icon.png, og-image.png}`. -2. **PWA icons** — `ui/public/{icon-192, icon-512, icon-maskable-512}.png` are - hand-placed and covered by **no** generator (the main drift source). -3. **Android** — `capacitor-assets generate --android` (`pnpm android:assets`) - expands `ui/assets/icon.png` into the launcher + splash matrix under - `ui/android/app/src/main/res/`. +| Layer | What | Feeds | +|---|---|---| +| `full` | rounded tile + master mark | `favicon.svg` | +| `flat` | square tile + master mark (OS rounds) | apple-touch, PWA "any", VK, TG, store | +| `background` | square tile only, full-bleed | Android adaptive background | +| `foreground` | mark only, transparent, re-placed for the round mask | Android adaptive foreground | +| `maskable` | square tile + foreground-placed mark | PWA `maskable` | +| `monochrome` | foreground-placed silhouette, flat colour | Android 13+ themed layer | -## Source masters (author once; commit) +`brand/build-set.mjs` renders every raster below; `brand/build-android-res.mjs` renders +the Android launcher resources. -The minimum set the designer supplies so every target below regenerates: +## Generated targets -| Master | Size | Format | Background | Need | Drives | -|---|---|---|---|---|---| -| Vector mark | — | SVG | transparent | required | `favicon.svg`; the canonical shape | -| App icon | 1024×1024 | PNG | opaque | required | Android legacy, PWA raster, iOS, store icons | -| Adaptive foreground | 1024×1024 | PNG | transparent | recommended | Android/iOS adaptive foreground — mark inside the ~66 % safe area (outer third is masked) | -| Adaptive background | 1024×1024 | PNG | opaque | recommended | Android/iOS adaptive background layer | -| Maskable | 512×512 | PNG | opaque | recommended | PWA `maskable` (mark inside the maskable safe zone) | -| Splash | 2732×2732 (+ a `-dark` variant) | PNG | opaque | optional | Android/iOS splash / launch screen | -| OG card | 1200×630 | PNG | opaque | optional | link-preview card (mark **plus** wordmark text) | - -Capacitor reads the adaptive/splash masters from `ui/assets/`: -`icon.png`, `icon-foreground.png`, `icon-background.png`, `splash.png`, -`splash-dark.png`. Without the foreground/background pair it insets the single -`icon.png` into the adaptive safe zone (today's poorer look). - -## Generated targets (full coverage) - -### Web — `ui/public/`, via `node assets/icons/build/generate.js` +### Web — `ui/public/` (via `brand/build-set.mjs`) | File | Size | Note | |---|---|---| -| `favicon.svg` | vector | canonical vector output | +| `favicon.svg` | vector | **light + dark in one file** via `prefers-color-scheme` | | `favicon.ico` | 32×32 | PNG-in-ICO; answers the blind `/favicon.ico` probe | -| `apple-touch-icon.png` | 180×180 | opaque full-bleed; iOS Safari home-screen (iOS masks its own corners) | -| `og-image.png` | 1200×630 | link-preview card; has the wordmark, referenced by absolute URL | +| `apple-touch-icon.png` | 180×180 | opaque full-bleed (iOS masks its own corners) | +| `icon-192.png` / `icon-512.png` | 192 / 512 | PWA `any`, light | +| `icon-192-dark.png` / `icon-512-dark.png` | 192 / 512 | dark variants (generated; see PWA note) | +| `icon-maskable-512.png` | 512×512 | PWA `maskable`, light | +| `icon-maskable-512-dark.png` | 512×512 | dark variant (generated; see PWA note) | +| `og-image.png` | 1200×630 | board-green card: master tile + «Эрудит» / Игра в слова | -### PWA — `ui/public/`, referenced by `manifest.webmanifest` +### PWA — `ui/public/manifest.webmanifest` -| File | Size | `purpose` | -|---|---|---| -| `icon-192.png` | 192×192 | `any` | -| `icon-512.png` | 512×512 | `any` | -| `icon-maskable-512.png` | 512×512 | `maskable` | +Referenced icons are the **light** set (`icon-192`, `icon-512` = `any`; +`icon-maskable-512` = `maskable`). The manifest has **no reliable way to pick an icon by +colour scheme**, so the dark PNGs are **generated but not referenced** — theming the +in-browser tab is handled by `favicon.svg` instead. Wire the dark files in only if a +concrete installer target is known to honour them. -### Android — `ui/android/app/src/main/res/`, via `pnpm android:assets` +### Android — `ui/android/app/src/main/res/` (via `brand/build-android-res.mjs`) -Generated per density mdpi→xxxhdpi (exact px tool-managed); **do not hand-edit**: +Capacitor layer inputs are `ui/assets/{icon,icon-foreground,icon-background}.png`. The +resources are **hand-generated from the layers**, not `capacitor-assets` — our layers +already fill the 108 dp canvas (foreground inside the 61 % safe zone, background +full-bleed), so they must be placed with **no inset**, and we add the **monochrome** +themed layer, neither of which `capacitor-assets` does. Per density mdpi→xxxhdpi (+ldpi): -- `mipmap-*/ic_launcher.png` — legacy square (48 / 72 / 96 / 144 / 192) -- `mipmap-*/ic_launcher_round.png` — legacy round +- `mipmap-*/ic_launcher.png` / `ic_launcher_round.png` — legacy square / round (composite) - `mipmap-*/ic_launcher_foreground.png` + `ic_launcher_background.png` — adaptive layers -- `mipmap-anydpi-v26/ic_launcher.xml` + `ic_launcher_round.xml` — adaptive descriptors -- `drawable*/splash.png` — splash (portrait/landscape × light/night × density) +- `mipmap-*/ic_launcher_monochrome.png` — themed layer +- `mipmap-anydpi-v26/ic_launcher.xml` + `ic_launcher_round.xml` — **no ``**, with + `` + `` + `` (committed by hand) -### iOS / iPhone / iPad — FUTURE, via `capacitor-assets generate --ios` +### iOS / iPhone / iPad — FUTURE -No iOS shell today; listed so the rebrand covers it. Same masters produce -`App/App/Assets.xcassets/AppIcon.appiconset`: +No iOS shell today. When one exists, `flat` (opaque, no alpha, no rounded corners; the OS +masks) drives the asset catalog — a single 1024×1024 marketing icon (Xcode derives the +rest), plus light/dark/tinted where the catalog supports it. -- Modern (Xcode 14+): a single **1024×1024** marketing icon; Xcode derives the rest. -- Full asset-catalog px (if a full set is emitted): 20, 29, 40, 58, 60, 76, 80, - 87, 120, 152, 167, 180, 1024 — iPhone {40, 58, 60, 80, 87, 120, 180}, iPad - {20, 29, 40, 76, 152, 167}, App Store marketing {1024}. -- Splash: `splash-2732x2732.png` (+ dark) → the launch storyboard. -- iOS icons are **opaque, no alpha, no rounded corners** (the OS applies the mask). +### VK / Telegram / store — manual upload (`brand/{vk,tg,store}/`) -### Store-listing art — uploaded by hand (outside the repo) +Square, **no rounding** (each platform crops/rounds itself), light `flat`: -| Store | Asset | Spec | +| Folder | Files | Use | |---|---|---| -| RuStore (now) | app icon | 512×512 PNG | -| RuStore | screenshots | phone, per the RuStore listing spec | -| RuStore | promo / feature graphic | per the RuStore listing spec | -| App Store (future) | marketing icon | 1024×1024 | -| App Store (future) | screenshots | per device class | +| `vk/` | `vk-576/278/150/32.png` | VK community / app icons | +| `tg/` | `tg-512.png` | Telegram bot avatar + Mini App icon (512, square; TG crops circular) | +| `tg/` | `tg-demo-640x360.png` | BotFather `/newapp` demo banner (tile + wordmark) | +| `store/` | `rustore-512.png` | RuStore app icon | +| RuStore | screenshots / feature graphic | per the RuStore listing spec (uploaded by hand) | | Google Play (future) | icon / feature graphic | 512×512 / 1024×500 | +| App Store (future) | marketing icon / screenshots | 1024×1024 / per device class | ## Regenerate ```sh -cd assets/icons && node build/generate.js # web favicon set + og card -> ui/public/ -cd ui && pnpm android:assets # Android launcher + splash -cd ui && node_modules/.bin/capacitor-assets generate --ios # future, once the iOS shell exists +cd assets/icons/brand +npm i # opentype.js (rasterisation reuses the ui package's chromium) +node build-set.mjs # web (ui/public) + Capacitor layers (ui/assets) + vk/ tg/ store/ + og +node build-android-res.mjs # Android launcher resources (all densities + monochrome) ``` - -## Open items for the rebrand - -- Decide how the new vector master feeds `assets/icons/` — retarget the generator - onto the supplied vector, or drop in the output files and keep the generator for - the OG card only. -- Bring `icon-192 / icon-512 / icon-maskable-512` under a generator (today - hand-placed — the main drift source). -- Add `ui/assets/icon-foreground.png` + `icon-background.png` for a proper adaptive - icon (today a single inset `icon.png`). diff --git a/docs/ICON_BRANDBOOK.md b/docs/ICON_BRANDBOOK.md new file mode 100644 index 0000000..0a0e713 --- /dev/null +++ b/docs/ICON_BRANDBOOK.md @@ -0,0 +1,166 @@ +# Erudit app-icon — brand book + +The single source of truth for **how the Erudit icon is constructed**. It is written to +be reproducible *from words alone*: a designer with this document and the Spectral font +can rebuild the icon exactly, at any size, without pixel-matching anything. + +Two rules make that possible: + +1. **The icon is a square.** Every size and position below is a **fraction of the + square's side** (equivalently, a percentage). Nothing is in pixels. Draw the square + at whatever resolution you need and scale each value to it. +2. **The origin is the top-left corner.** `x` grows right, `y` grows down. So "center + `48.1% / 49.2%`" means a point `0.481·side` from the left and `0.492·side` from the top. + +The companion generator in [`../assets/icons/brand/`](../assets/icons/brand/) is the +machine version of this exact spec (`build-icon.mjs`, where each constant is the same +fraction printed here). The `docs/ICONS.md` map covers the *opposite* concern — which +files/sizes we slice this master into for each platform. + +## Anatomy + +![Construction scheme](../assets/icons/brand/icon-construction.png) + +Bottom-to-top the icon is six layers: **tile → wood grain → light sheen → shadow → +letter «Э» → star ✻**. The letter and star are always drawn last, at full ink colour; +the grain and the two gradients only ever touch the background. + +## Palette + +The icon ships as a **light** and a **dark** variant (a matched pair — the light tile +carries dark ink, the dark tile carries light ink). Each variant uses four colours. + +| Role | Light | Dark | +|------|-------|------| +| **Tile** — the wooden body | `#e6b053` | `#4a3316` | +| **Ink** — the «Э» and the ✻ | `#5a3a12` | `#f2d9a0` | +| **Grain** — the vertical stripes | `#d8a54e` | `#432e14` | +| **Sheen** — diagonal light | white, α 0 → 15 % | white, α 0 → 15 % | +| **Shadow** — diagonal dark | black, α 15 → 0 % | black, α 15 → 0 % | + +The grain colour is intentionally close to the tile — a hair darker — so the stripes +read as wood texture, not as stripes. + +## 1 — Tile + +A **square with rounded corners**, corner radius **17 %** of the side, filled with the +**Tile** colour. This is the whole background; there is no separate outer frame or +border. Corners are baked at 17 %; platform masks (iOS/Android) may round further on top. + +## 2 — Wood grain + +Subtle vertical planks over the tile: + +- Divide the width into **6 equal columns** (each `1/6` of the side wide). +- On the **right edge of each column**, draw one **vertical stripe**, width **`1/32` of + the side** (≈ 3.1 %), running the full height. +- Move the **whole set of six stripes left** by **`1/16` of the side** (= two stripe + widths). +- **Tilt every stripe 4°** (clockwise, about its own centre). +- Draw the stripes **past the top and bottom edges** so the tilt leaves no empty corners, + then **clip the whole set to the tile** (so the rounded corners stay clean). +- Colour: **Grain**. + +## 3 — Light sheen · 4 — Shadow + +Two linear gradients along the **same diagonal**, from the **bottom-left corner to the +top-right corner**, each clipped to the tile and sitting **above the grain, below the +letter**: + +- **Sheen** — white. Fully transparent at the bottom-left, rising to **15 % opacity** at + the top-right. Brightens the top-right. +- **Shadow** — black. **15 % opacity** at the bottom-left, fading to transparent at the + top-right. Deepens the bottom-left. + +Together they give the tile a lit-from-the-top-right, resting-in-shadow-at-the-bottom-left +sense of depth. Both percentages are a tuning knob (`--sheen`, `--shade`). + +## 5 — The letter «Э» + +- Typeface: **Spectral, Bold** (SIL OFL; pinned in the brand folder). +- Glyph: the Cyrillic capital **«Э»** (U+042D). +- Size: scale the glyph so its **ink bounding box is 60.5 % of the side tall** (its width + then follows the font — about 53.2 %). +- Position: place the **center of that bounding box** at **48.1 % / 49.2 %** (just up and + left of the icon center). +- Colour: **Ink**. + +## 6 — The star ✻ + +A **six-spoke teardrop asterisk** (the "score" mark that replaces a tile's point number): + +- Each spoke tapers to a **point at the center** and ends in a **round bulb**; a central + disc equal to a bulb fuses the six spokes. +- Outer diameter: **17.3 % of the side**. +- Bulb radius (and the central disc): **22 % of the star's radius**. +- Position: center at **78.8 % / 78.8 %** — tucked to the lower-right of the «Э», like a + subscript, with a clear gap from the letter. +- Colour: **Ink**. + +Spectral has no ✻ glyph, so the star is **drawn geometrically**, not set as type. The exact +construction is in `build-icon.mjs` (`starPath`). + +## Composition intent + +The «Э» and the ✻ are treated as **one group**. Its combined bounding box measures +about **65.9 % × 68.6 %** of the side, and it is placed so its **bottom-right corner sits +at 87.5 % / 87.5 %** of the icon (the lower-right corner of an inner `12.5 % … 87.5 %` +safe square). That is what pushes the composition down-and-right and leaves the star room +in the corner. When re-deriving positions, this is the anchor to preserve. + +> This bottom-right bias suits a **full-bleed** icon (iOS / PWA / favicon, which show +> almost the whole square). Android crops more, so it uses a **separate two-layer build** +> — see the next section. The master itself is never moved for Android. + +## Android adaptive icon (two layers) + +Android icons are **108×108 dp** and the OS applies its own mask (circle, squircle, +rounded square…). Only the **inner 66 dp = 61 % of the side, centred** is guaranteed +visible under every mask; the outer ~1/6 ring is used for the mask and motion effects. +So Android is **not** the full-bleed master — it is built as two layers: + +- **Background** — the tile with wood grain and the light/shadow gradients, **full-bleed + and square** (no rounded corners; the OS supplies the shape). It fills the whole 108 dp + layer, so the sacrificial outer ring is just more wood. +- **Foreground** — **only the «Э» + ✻**, transparent, **re-placed** to sit safely inside + the mask: + - «Э»: box height **53.9 %**, box center **52.3 % / 49.5 %**. + - ✻: outer diameter **13.2 %** (smaller than the master's), center **76.3 % / 72.7 %**, + tucked closer to the letter. + - The mark is centred, then nudged **right 8 % / down 3 %** so it reads optically + centred under the round mask, and the whole group fits within the **61 % safe zone**. + +Everything else (palette, grain, gradients, star construction) is identical to the master. + +## Variants in use + +The **light** variant is the primary icon (launcher, favicon, apple-touch, PWA). The +**dark** variant is shipped in addition wherever the platform can pick by appearance +(e.g. an SVG favicon via `prefers-color-scheme`). Where a platform allows only one icon, +use **light**. + +## Reproduce + +```sh +cd assets/icons/brand +npm i opentype.js +# full master (iOS / PWA / favicon), either variant: +node build-icon.mjs --variant light --layer full > icon-light.svg +# Android adaptive layers: +node build-icon.mjs --variant light --layer background > icon-light-android-background.svg +node build-icon.mjs --variant light --layer foreground > icon-light-android-foreground.svg +# the construction figure: +node build-icon.mjs --variant light --scheme > icon-construction.svg +node render-png.mjs icon-light.svg icon-light.png 1024 # any size +``` + +`--layer` is `full` (default), `background`, or `foreground`. All outputs are committed +alongside the generator in [`../assets/icons/brand/`](../assets/icons/brand/). + +## Masters + +| | Light | Dark | +|-|-------|------| +| **Full** (iOS / PWA / favicon) | ![light](../assets/icons/brand/icon-light.png) | ![dark](../assets/icons/brand/icon-dark.png) | +| **Android background** | ![lbg](../assets/icons/brand/icon-light-android-background.png) | ![dbg](../assets/icons/brand/icon-dark-android-background.png) | +| **Android foreground** | ![lfg](../assets/icons/brand/icon-light-android-foreground.png) | ![dfg](../assets/icons/brand/icon-dark-android-foreground.png) | diff --git a/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 0aa82aa..cf79c3d 100644 --- a/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,9 +1,6 @@ - - - - - - - \ No newline at end of file + + + + diff --git a/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 0aa82aa..cf79c3d 100644 --- a/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/ui/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,9 +1,6 @@ - - - - - - - \ No newline at end of file + + + + diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 261f759..48fdd77 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png index 2744ee0..3082e42 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 8ecda4d..9024f34 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..f83e989 Binary files /dev/null and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index f4b8a05..8909874 100644 Binary files a/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png index eb6ffab..a4ff54b 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png index f4a4ecc..46cfef6 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png index ea6bd7c..eb08478 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..1d171d7 Binary files /dev/null and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png index 8c7d4ad..1be6faa 100644 Binary files a/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index f53c9b9..ee098ab 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png index d09e781..120338b 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index a33aada..e2a7460 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..497be0f Binary files /dev/null and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index fd3b881..05d4e1c 100644 Binary files a/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index a02e728..8e448c3 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png index cde6555..c1730ca 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 45f1836..69a8967 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..b0e40ea Binary files /dev/null and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 492464c..b517260 100644 Binary files a/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 6d66407..88ece49 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png index 942fa92..1b4ae5d 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 238e95f..15a62cf 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..1eae863 Binary files /dev/null and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index b4cd386..38ba881 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 1cbb47b..b705035 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png index 07b01b7..03b9ee8 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index 29b081c..717737b 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..c22f4e4 Binary files /dev/null and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 278b307..6133338 100644 Binary files a/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/ui/assets/icon-background.png b/ui/assets/icon-background.png new file mode 100644 index 0000000..1d76eb2 Binary files /dev/null and b/ui/assets/icon-background.png differ diff --git a/ui/assets/icon-foreground.png b/ui/assets/icon-foreground.png new file mode 100644 index 0000000..12b4a8e Binary files /dev/null and b/ui/assets/icon-foreground.png differ diff --git a/ui/assets/icon.png b/ui/assets/icon.png index 7489ca3..6fd8670 100644 Binary files a/ui/assets/icon.png and b/ui/assets/icon.png differ diff --git a/ui/package.json b/ui/package.json index a6206c3..f214386 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,7 @@ "build": "vite build", "preview": "vite preview", "cap:sync": "cap sync android", - "android:assets": "capacitor-assets generate --android", + "android:assets": "node ../assets/icons/brand/build-set.mjs && node ../assets/icons/brand/build-android-res.mjs", "check": "svelte-check --tsconfig ./tsconfig.json", "codegen": "rm -rf src/gen && flatc --ts -o src/gen/fbs ../pkg/fbs/scrabble.fbs && buf generate ../gateway --template buf.gen.yaml", "test:unit": "vitest run", diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png index ba28ea8..27372d0 100644 Binary files a/ui/public/apple-touch-icon.png and b/ui/public/apple-touch-icon.png differ diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico index ea41c87..dacda68 100644 Binary files a/ui/public/favicon.ico and b/ui/public/favicon.ico differ diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg index c78acf2..7d0ca13 100644 --- a/ui/public/favicon.svg +++ b/ui/public/favicon.svg @@ -1 +1 @@ - + diff --git a/ui/public/icon-192-dark.png b/ui/public/icon-192-dark.png new file mode 100644 index 0000000..9765b33 Binary files /dev/null and b/ui/public/icon-192-dark.png differ diff --git a/ui/public/icon-192.png b/ui/public/icon-192.png index b87e170..0bf8228 100644 Binary files a/ui/public/icon-192.png and b/ui/public/icon-192.png differ diff --git a/ui/public/icon-512-dark.png b/ui/public/icon-512-dark.png new file mode 100644 index 0000000..8edae27 Binary files /dev/null and b/ui/public/icon-512-dark.png differ diff --git a/ui/public/icon-512.png b/ui/public/icon-512.png index 0e1aee5..bc88014 100644 Binary files a/ui/public/icon-512.png and b/ui/public/icon-512.png differ diff --git a/ui/public/icon-maskable-512-dark.png b/ui/public/icon-maskable-512-dark.png new file mode 100644 index 0000000..9b6d045 Binary files /dev/null and b/ui/public/icon-maskable-512-dark.png differ diff --git a/ui/public/icon-maskable-512.png b/ui/public/icon-maskable-512.png index a94d40c..ee6e488 100644 Binary files a/ui/public/icon-maskable-512.png and b/ui/public/icon-maskable-512.png differ diff --git a/ui/public/og-image.png b/ui/public/og-image.png index 65bb19f..a50c5b2 100644 Binary files a/ui/public/og-image.png and b/ui/public/og-image.png differ