feat(icons): rebrand app icon; slice the full set from one master
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
The SVG tracer export failed, so the icon was designed collaboratively from
scratch: a wooden «Э» tile with a ✻ score-subscript in Spectral Bold, wood
grain and diagonal light/shadow. Construction is specified in fractions of the
side in docs/ICON_BRANDBOOK.md; the reference generator, pinned Spectral font
and committed masters live in assets/icons/brand/.
One master -> the whole set (assets/icons/brand/build-set.mjs + build-android-res.mjs):
- web (ui/public): favicon.svg (light+dark via prefers-color-scheme), favicon.ico,
apple-touch, PWA any + maskable (+ dark variants), og-image reskinned to
«Эрудит» / Игра в слова
- Capacitor layers: ui/assets/{icon,icon-foreground,icon-background}.png
- Android launcher res, all densities, NO inset, + monochrome themed layer
(anydpi xml: background+foreground+monochrome)
- manual-upload art: assets/icons/brand/{vk,tg,store} (square, no rounding)
Primary variant light; dark where the platform can theme (favicon.svg). The old
LiberationSans generator (assets/icons/build) is removed; docs/ICONS.md retargeted
onto the single master. ANDROID_PLAN icon-rebrand item marked done.
This commit is contained in:
+60
-81
@@ -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 `<inset>`**, with
|
||||
`<background>` + `<foreground>` + `<monochrome>` (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`).
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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) |  |  |
|
||||
| **Android background** |  |  |
|
||||
| **Android foreground** |  |  |
|
||||
Reference in New Issue
Block a user