Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Successful in 43s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m13s
Desktop (Tauri) / Update manifest (push) Successful in 8s
Android / Kotlin + Rust (APK) (push) Canceled after 11m42s
Step 5 of milestone 481. Replaces the linked-notes constellation, which had been stale since note links were dropped (alembic 0024). The colour scheme stays. packaging/icons.py draws the mark once and renders every variant from it: the rounded tile (web, desktop), the maskable full-bleed web icon, and the Android adaptive foreground. The detail (shaft, vane splits, glint) is cut out of the ink with a mask rather than painted on in yellow, because the Android foreground is now transparent and its alpha is also the themed-icon silhouette. The old foreground was the opaque maskable tile, which a themed icon would have drawn as a solid square. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.2 KiB
Python
129 lines
5.2 KiB
Python
"""Every icon Inkwell ships, from one drawing.
|
|
|
|
The mark is a black inkpot with a quill standing in it, on the brand yellow
|
|
(Scribe note 5071). It is drawn ONCE below, in a 512 box, and each variant places it
|
|
with its own scale, because each platform crops differently:
|
|
|
|
- icon.svg the rounded tile: web favicon/PWA "any", desktop app icon
|
|
- icon-maskable.svg full bleed, mark inside the central 80% (PWA "maskable")
|
|
- Android foreground mark alone on transparency inside the 66dp safe circle of a
|
|
108dp adaptive canvas; the yellow is the background layer, and
|
|
the transparent mark is also what themed (monochrome) icons need
|
|
|
|
Run from the repo root after changing the drawing; needs rsvg-convert (librsvg).
|
|
Generated output is committed, like any other generated file.
|
|
|
|
python3 packaging/icons.py
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
INK = "#18181b"
|
|
GOLD = "#F5C518"
|
|
|
|
# The quill is drawn upright at the pot mouth (0,0), then rotated into place.
|
|
QUILL_AT = "translate(256 232) rotate(34)"
|
|
|
|
QUILL_INK = f'''
|
|
<g transform="{QUILL_AT}">
|
|
<path d="M0 6 L0 -46" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>
|
|
<path d="M0 -34 C 46 -78 58 -176 4 -252 C -34 -196 -40 -104 0 -34 Z" fill="{INK}"/>
|
|
</g>'''
|
|
|
|
POT_INK = f'''
|
|
<path fill="{INK}" d="M222 262 C 166 268 140 300 140 338 L140 374 C140 396 156 410 178 410
|
|
L334 410 C356 410 372 396 372 374 L372 338 C372 300 346 268 290 262 Z"/>
|
|
<rect x="214" y="236" width="84" height="34" rx="9" fill="{INK}"/>
|
|
<rect x="200" y="226" width="112" height="22" rx="11" fill="{INK}"/>'''
|
|
|
|
# The detail — the quill's shaft, two splits in its vane, the glint on the pot — is
|
|
# CUT OUT of the ink rather than painted over it in yellow. On the tile the two look
|
|
# the same; on Android's transparent foreground only a cut-out is right, because a
|
|
# themed icon draws the foreground's alpha as a silhouette, and painted detail
|
|
# would come out as solid, with the vane splits sticking out past its edge.
|
|
CUTS = '''
|
|
<g transform="{at}">
|
|
<path d="M1 -52 L3 -232" stroke-width="6" stroke-linecap="round"/>
|
|
<path d="M62 -128 L30 -108 M64 -176 L32 -152" stroke-width="8"/>
|
|
</g>
|
|
<path d="M170 344 C 172 318 188 300 214 292" stroke-width="10" stroke-linecap="round"/>'''.format(at=QUILL_AT)
|
|
|
|
GLYPH = f'''
|
|
<defs>
|
|
<mask id="inkwell-cuts" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
|
<rect width="512" height="512" fill="#fff"/>
|
|
<g stroke="#000" fill="none">{CUTS}
|
|
</g>
|
|
</mask>
|
|
</defs>
|
|
<g mask="url(#inkwell-cuts)">{QUILL_INK}{POT_INK}
|
|
</g>'''
|
|
# Glyph bounds, measured from a render: centre the mark, then scale per variant.
|
|
CX, CY = 262, 226
|
|
|
|
def place(scale, cy=256):
|
|
return f'translate(256 {cy}) scale({scale}) translate({-CX} {-CY})'
|
|
|
|
def svg(body, label=True):
|
|
aria = ' role="img" aria-label="Inkwell"' if label else ''
|
|
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"{aria}>\n{body}\n</svg>\n'
|
|
|
|
def tile(scale=0.92):
|
|
return svg(f''' <!-- Rounded-tile app icon: brand yellow, and the inkwell — a black inkpot with a
|
|
quill standing in it. Source for every raster icon; see step 5 of milestone 481. -->
|
|
<rect width="512" height="512" rx="112" fill="{GOLD}"/>
|
|
<g transform="{place(scale)}">{GLYPH}
|
|
</g>''')
|
|
|
|
def maskable(scale=0.8):
|
|
return svg(f''' <!-- Maskable variant: full-bleed background so platform masks (circle, squircle,
|
|
rounded-square) never clip the mark, which sits inside the central 80% safe
|
|
zone. Same mark as icon.svg. -->
|
|
<rect width="512" height="512" fill="{GOLD}"/>
|
|
<g transform="{place(scale)}">{GLYPH}
|
|
</g>''')
|
|
|
|
def foreground(scale=0.64):
|
|
# Android adaptive foreground: the mark alone on transparency, inside the 66dp
|
|
# safe circle of a 108dp canvas. The yellow lives in the background layer.
|
|
return svg(f''' <g transform="{place(scale)}">{GLYPH}
|
|
</g>''', label=False)
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
WEB = ROOT / "frontend" / "public"
|
|
DESKTOP = ROOT / "desktop" / "src-tauri"
|
|
ANDROID = ROOT / "android" / "app" / "src" / "main" / "res" / "mipmap-xxxhdpi"
|
|
|
|
|
|
def render(svg_text: str, size: int, out: Path, background: str | None = None) -> None:
|
|
cmd = ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(out)]
|
|
if background:
|
|
cmd += ["-b", background]
|
|
subprocess.run(cmd, input=svg_text.encode(), check=True)
|
|
|
|
|
|
def main() -> None:
|
|
icon, mask, fg = tile(), maskable(), foreground()
|
|
(WEB / "icon.svg").write_text(icon)
|
|
(WEB / "icon-maskable.svg").write_text(mask)
|
|
|
|
render(icon, 32, WEB / "favicon-32.png")
|
|
render(icon, 192, WEB / "icon-192.png")
|
|
render(icon, 512, WEB / "icon-512.png")
|
|
render(mask, 512, WEB / "icon-maskable-512.png")
|
|
# iOS rounds the corners itself and shows transparency as black.
|
|
render(mask, 180, WEB / "apple-touch-icon.png")
|
|
|
|
render(icon, 1024, DESKTOP / "app-icon.png")
|
|
render(icon, 32, DESKTOP / "icons" / "32x32.png")
|
|
render(icon, 128, DESKTOP / "icons" / "128x128.png")
|
|
render(icon, 256, DESKTOP / "icons" / "128x128@2x.png")
|
|
render(icon, 512, DESKTOP / "icons" / "icon.png")
|
|
|
|
render(fg, 512, ANDROID / "ic_launcher_foreground.png")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|