| name | hora-launcher-icon |
| description | Generate ALL Hora-family app icons AND brand-mark wide cards (Android launcher + notification, iOS, web, Play feature graphic, web OG/social image, GitHub repo social preview) from the shared Baloo Chettan 2 wordmark engine in hora-core. Use when creating or refining any Hora app's icons or brand marks, on any surface. |
Hora app icons & brand marks — one engine, Baloo Chettan 2 wordmark, every surface (STRICT)
No surface, anywhere, ships a hand-authored icon or an unbranded platform default —
not a generic favicon, not GitHub's auto-generated social card (owner's personal photo),
not a web link with no share-preview image. Every icon and every wide brand card comes
from this one engine. Full enumerated list: conventions.md → "Brand mark standard".
As of 2026-06 there IS a shared engine — use it, don't hand-tune per app. The family icon is the
app's short Malayalam name (Pathivu പതി, Varisankya വരി, Muthal മുത) set in Baloo Chettan 2
(700, +45% vertical stretch), slate #445353 on #FCFCFC, centered.
Geometry (locked 2026-07-03 — the v3 "six-line rule"; Y-shift added + tuned to 2% on 2026-07-04):
in every icon, on every surface, four guides are family invariants — the base-letter band top +
baseline (band renders exactly BAND_FRAC × canvas high, centred Y_SHIFT_FRAC (2%) of canvas
below canvas centre — 0% read as too high, 4% was tried and over-corrected Muthal (its descender-only
extent sank under a shared line that looked right for the two ascender-only apps); 2% is the final,
reviewed value) and the ink left/right edges (ink width = 2.4741 × band height, each wordmark
x-stretched to it — Pathivu 1.0 reference, Varisankya 1.116, Muthal 1.066). Vowel-sign
ascenders/descenders extend naturally (per-app), and the full ink is verified against each surface's
safe circle (adaptive 0.305, maskable 0.40 — the 2% shift keeps a worst-case 3.43pp of margin under
the tightest one, Muthal on FG); the engine raises if a new wordmark needs an x-stretch outside
[0.98, 1.20] — a family decision, not a per-app tweak. Canonical generator + font + full spec:
brand/launcher-icon/ → gen_launcher_icon.py.
pip install uharfbuzz freetype-py fonttools brotli numpy pillow
python gen_launcher_icon.py <app> # writes launcher (all densities) + monochrome + legacy/round
# + notification (ic_notification) + iOS + web
# + Play 512 + Play feature graphic
# + web OG/Twitter social image (opengraph-image.png)
# + GitHub repo social preview (github_social_preview.png)
It uses harfbuzz (shaping) + FreeType (the font's own nonzero rasteriser — correct fill, no holes
in self-intersecting glyphs like ത). Add a new sibling by adding an APPS entry (Malayalam name +
initial + repo path + iOS dir + English name_en/tagline_en for the wide-card surfaces). Always
get explicit user sign-off before shipping — this icon is highly scrutinised.
Wide brand cards (_wide_card(), added 2026-07-04): one composer, reused at every
"glyph + Latin name + tagline" surface — glyph left, Latin name (bold) + tagline (regular) in
Google Sans Flex at 'ROND' maxed to 100 to the right, slate accent bar on the right edge. Three
thin wrappers call it at different fixed sizes: feature_graphic() (Play, 1024×500), og_image()
(Next.js Open Graph/Twitter, 1200×630 — written to web/app/opengraph-image.png, no manual step,
Next.js auto-injects the meta tags), github_social_preview() (GitHub's recommended 1280×640 —
no API for this, upload by hand: repo Settings → General → Social preview). The text column
shrinks to fit (the floor is a fixed pixel legibility limit, not scaled by canvas size — narrower
cards like OG/GitHub need the full range) and raises if the tagline doesn't fit even at the floor —
shorten it, don't widen the column. hora-core's own repo uses family_social_preview() instead — all
three apps' wordmarks side by side, since hora-core is the shared foundation, not a consumer app.
Full listing design language (screenshots, title/description copy conventions): conventions.md →
"Play Store listing copy & screenshots". Full enumerated surface list: conventions.md →
"Brand mark standard".
The legacy material below (matching the old hand-drawn Varisankya reference with per-app font tuning,
varisankya-vari-reference.xml) is superseded by the engine; kept only as history.
Method (what actually works — learned over several iterations on prior apps)
- Graft any shared subglyph exactly, when the app's name shares a letterform with the
reference (e.g. a vowel sign). Extract that subpath verbatim from the reference vector XML
(
brand/launcher-icon/varisankya-vari-reference.xml; parse the path commands; identify the
shared subpath) rather than redrawing it, so it stays
pixel-identical.
- Render each letter SEPARATELY rather than as a shaped string — engines like Pillow lack
proper complex-script shaping, so render via a shaping-aware path (e.g. GDI+/
System.Drawing
on Windows) and control inter-letter gap manually. Pick a face deliberately and get user
sign-off on the face choice early — don't chase a stroke-width match by swapping fonts later;
match weight by trimming the chosen face instead. The family has so far standardized on
Baloo 2 Bold (rounded, bold; a monoline face like Manjari was tried and rejected on sight) —
start there and confirm, rather than re-deriving the face per app.
- Match the reference's stroke weight by uniform trim, measured against the reference's own
stem width at a fixed x-height. Apply uniform erosion (shrinks bowls too, not just stems)
rather than directional erosion, which leaves bowls heavy and never reads as thinner.
- Match height first, then fit the bounding circle. Render at the reference's x-height,
compress width to the reference's aspect ratio so it fits the same centered circle, then
restore stroke weight lost to compression via horizontal-only dilation (so the compression
doesn't re-thin the glyph).
- Assemble with the inter-letter gap added after stroke-restore, not before (adding it before
compression collapses it into a collision). Keep any grafted shared subglyph (step 1) out of
any width compression — compressing the whole word also shrinks the shared part, which should
stay pixel-identical to the reference.
- Contour-smooth the composed master before downsampling. The horizontal-only
weight-restore (step 4) leaves flat rectangular ledges — "tabs/spikes" — on the vertical stems
that don't follow the curve and survive the per-density downsample as pixel-level spikes.
Gaussian-blur the hi-res master, then re-threshold at 50% alpha: this dissolves the tabs (and
any narrow notches) while leaving straight stem edges exactly in place, so stroke weight is
preserved (verify a stem stays its target px width after). Tune the blur radius on a 5× zoom
before/after (≈10 master-res px worked for one app).
- Generate the full mipmap set (
ic_launcher, ic_launcher_foreground, ic_launcher_round,
plus a monochrome drawable-nodpi for the adaptive icon) sized by the reference's own
bounding-circle fraction, so the new icon visually matches the reference's fill/padding inside
the adaptive mask.
Gotchas
- Any dilation/erosion step must grow into a padded canvas first, or it clips the edge letters.
- Add the inter-letter gap on separate glyphs, after compression — not before.
- Keep grafted shared subglyphs out of width-compression so they don't shrink.
- Horizontal-only weight-restore leaves jagged stem tabs; contour-smooth the master (blur +
re-threshold at 50%) before downsampling (step 6), or they survive as pixel spikes at small sizes.
- The Play Store listing icon is a separate asset, sized/measured differently — see
hora-play-store.
Verify before shipping
Render a side-by-side comparison against the reference icon and get explicit user sign-off
before generating final assets. Then ship via hora-app-release.