Something went wrong. Try again.
🌿 Collaborative wiki on ATProto
Something went wrong. Try again.
2.5 kB · 76 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677#!/usr/bin/env bun
// Generates the PWA icon set from public/logo.svg.//// bun run scripts/gen-icons.ts//// The output is committed rather than gitignored: Caddy serves /public/* straight// from /opt/lichen/public on the host (see deploy/Caddyfile.snippet), so icons// built only inside the container image would never be reachable in production.
import { mkdir } from "node:fs/promises";import { join } from "node:path";import sharp from "sharp";
const PUBLIC_DIR = join(import.meta.dir, "..", "public");const OUT_DIR = join(PUBLIC_DIR, "icons");
// Matches THEME.light.bg — the mark is teal, so it needs a light ground to read.const BACKGROUND = "#fafaf9";
// Android crops maskable icons to a circle 80% of the canvas width. A square mark// only fits inside that circle at ~56%, so maskable variants shrink the mark// instead of letting the crop clip it.const STANDARD_MARK_RATIO = 0.72;const MASKABLE_MARK_RATIO = 0.55;
// librsvg rasterizes at this DPI before the downscale; the viewBox is ~135 units// wide, so 384 renders ~720px and keeps the 512px icons crisp.const RENDER_DENSITY = 384;
interface IconSpec { file: string; size: number; markRatio: number;}
const ICONS: IconSpec[] = [ { file: "icon-192.png", size: 192, markRatio: STANDARD_MARK_RATIO }, { file: "icon-512.png", size: 512, markRatio: STANDARD_MARK_RATIO }, { file: "icon-192-maskable.png", size: 192, markRatio: MASKABLE_MARK_RATIO }, { file: "icon-512-maskable.png", size: 512, markRatio: MASKABLE_MARK_RATIO }, // iOS ignores transparency and applies its own rounding, so this is the // standard ratio on the same opaque ground. { file: "apple-touch-icon.png", size: 180, markRatio: STANDARD_MARK_RATIO },];
async function renderIcon(logo: Buffer, spec: IconSpec): Promise<void> { const markSize = Math.round(spec.size * spec.markRatio); const mark = await sharp(logo, { density: RENDER_DENSITY }) .resize(markSize, markSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 }, }) .png() .toBuffer();
await sharp({ create: { width: spec.size, height: spec.size, channels: 4, background: BACKGROUND, }, }) .composite([{ input: mark, gravity: "centre" }]) .png() .toFile(join(OUT_DIR, spec.file));}
const logo = Buffer.from( await Bun.file(join(PUBLIC_DIR, "logo.svg")).arrayBuffer(),);await mkdir(OUT_DIR, { recursive: true });await Promise.all(ICONS.map((spec) => renderIcon(logo, spec)));
console.log(`Wrote ${ICONS.length} icons to public/icons/`);