diff --git a/scripts/generate-icons.py b/scripts/generate-icons.py index 9a66c70..c0a974d 100644 --- a/scripts/generate-icons.py +++ b/scripts/generate-icons.py @@ -15,31 +15,57 @@ os.makedirs(ICONS_DIR, exist_ok=True) GOLD = (0xC5, 0xA5, 0x5A) NAVY = (0x1A, 0x1A, 0x2E) RENDER_SIZE = 2048 +ART_SCALE = 0.8 # art occupies this fraction of the canvas (maskable safe zone) -def generate_icon(size): + +def render_svg(): with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp: tmp_path = tmp.name try: subprocess.run([ 'rsvg-convert', - '-w', str(RENDER_SIZE), '-h', str(RENDER_SIZE), + '--keep-aspect-ratio', SVG_PATH, '-o', tmp_path, ], check=True) - img = Image.open(tmp_path).convert('RGBA') - result = Image.new('RGBA', (size, size), NAVY + (255,)) - cross = img.resize((size, size), Image.LANCZOS) - gold_layer = Image.new('RGBA', (size, size), GOLD + (255,)) - result.paste(gold_layer, mask=cross.split()[3]) - return result + return Image.open(tmp_path).convert('RGBA') finally: os.unlink(tmp_path) + +def gold_silhouette(): + img = render_svg() + gold = Image.new('RGBA', img.size, GOLD + (255,)) + gold.putalpha(img.split()[3]) + return gold + + +def gold_art(long_side): + img = gold_silhouette() + scale = long_side / max(img.size) + return img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS) + + +def compose_app_icon(size): + canvas = Image.new('RGBA', (size, size), NAVY + (255,)) + art = gold_art(round(size * ART_SCALE)) + canvas.alpha_composite(art, ((size - art.width) // 2, (size - art.height) // 2)) + return canvas + + +def compose_transparent_art(size): + canvas = Image.new('RGBA', (size, size), (0, 0, 0, 0)) + art = gold_art(size) + canvas.alpha_composite(art, ((size - art.width) // 2, (size - art.height) // 2)) + return canvas + + for size in [192, 512]: - icon = generate_icon(size) out = os.path.join(ICONS_DIR, f'icon-{size}.png') - icon.save(out, 'PNG') + compose_app_icon(size).save(out, 'PNG') print(f'Saved {out} ({size}x{size})') -print('Icons generated.') +transparent_out = os.path.join(ICONS_DIR, 'cross-gold.png') +compose_transparent_art(512).save(transparent_out, 'PNG') +print(f'Saved {transparent_out} (512x512)') diff --git a/src/ui/app.ts b/src/ui/app.ts index 130f622..a35418d 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -29,12 +29,47 @@ export class App { this.applyTheme(); } + private faviconImage: HTMLImageElement | null = null; + private faviconCanvas: HTMLCanvasElement | null = null; + private applyTheme() { document.documentElement.className = 'theme-' + (this.settings.theme || 'default'); const chromeColor = getComputedStyle(document.documentElement).getPropertyValue('--clr-browser-chrome').trim(); if (chromeColor) { document.querySelector('meta[name="theme-color"]')?.setAttribute('content', chromeColor); } + this.updateFavicon(chromeColor || '#1a1a2e'); + } + + private updateFavicon(bgColor: string) { + const size = 64; + if (!this.faviconCanvas) this.faviconCanvas = document.createElement('canvas'); + const canvas = this.faviconCanvas; + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, size, size); + const draw = () => { + if (!this.faviconImage?.complete || !this.faviconImage.naturalWidth) return; + const artSize = Math.round(size * 0.8); + ctx.drawImage(this.faviconImage, (size - artSize) / 2, (size - artSize) / 2, artSize, artSize); + let link = document.querySelector('link[rel="icon"]'); + if (!link) { + link = document.createElement('link'); + link.rel = 'icon'; + document.head.appendChild(link); + } + link.type = 'image/png'; + link.href = canvas.toDataURL('image/png'); + }; + if (!this.faviconImage) { + this.faviconImage = new Image(); + this.faviconImage.onload = draw; + this.faviconImage.src = '/icons/cross-gold.png'; + } + draw(); } private async checkDataVersion() { diff --git a/static/icons/cross-gold.png b/static/icons/cross-gold.png new file mode 100644 index 0000000..6228b55 Binary files /dev/null and b/static/icons/cross-gold.png differ diff --git a/static/icons/icon-192.png b/static/icons/icon-192.png index ee84491..ee5f876 100644 Binary files a/static/icons/icon-192.png and b/static/icons/icon-192.png differ diff --git a/static/icons/icon-512.png b/static/icons/icon-512.png index 7b46ecd..010b7d8 100644 Binary files a/static/icons/icon-512.png and b/static/icons/icon-512.png differ diff --git a/vite.config.ts b/vite.config.ts index b080ef3..ea40800 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -51,8 +51,8 @@ export default defineConfig({ theme_color: '#1a1a2e', orientation: 'portrait', icons: [ - { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' }, - { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' }, + { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' }, + { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, ], }, }),