diff --git a/README.md b/README.md index 46c37f1..023a15d 100644 --- a/README.md +++ b/README.md @@ -417,14 +417,15 @@ What's included: | Piece | Detail | | --- | --- | | `app/manifest.ts` | `display: standalone`, portrait, theme/background colours, app shortcuts | -| Icons | 192/512 px, a maskable 512 px icon, and 180/167/152 px `apple-touch-icon`s | +| Icons | 192/512 px, a maskable 512 px icon, and 180/167/152 px `apple-touch-icon`s, all rendered from `assets/app-icon-source.jpeg` | | iOS meta | `apple-mobile-web-app-capable`, `-title`, `-status-bar-style`, `viewport-fit=cover` | | Service worker | `public/sw.js`, **production only**: cached app shell, stale-while-revalidate for static assets, network-first navigations with an `/offline` fallback. `/api/*` is never cached | | Offline page | `/offline` — "your decks are still here, reconnect to generate" | | Safe areas | `pb-safe` / `pt-safe` utilities plus `pb-tabbar` so nothing hides behind the tab bar | -The icons are generated by `scripts/make-icons.mjs` (headless Chromium); it is a one-off tool and -is not part of the app's dependencies. +The icons are generated by `scripts/make-icons.mjs` from `assets/app-icon-source.jpeg` (headless +Chromium); it is a one-off tool and is not part of the app's dependencies. It also writes +`app/favicon.ico`, the Next.js file convention for the browser tab. ## Security notes @@ -574,9 +575,11 @@ lib/ public/ sw.js service worker (production only) icons/ PWA + iOS icons and splash screens +assets/ + app-icon-source.jpeg master artwork the icons are rendered from scripts/ copy-pdf-worker.mjs copies the pdf.js worker to /public (runs on predev/prebuild) - make-icons.mjs regenerates the PWA/iOS icons (one-off tool) + make-icons.mjs regenerates the PWA/iOS icons from the master (one-off tool) ``` --- diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..a1820db 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/icon.svg b/app/icon.svg deleted file mode 100644 index 02c15b6..0000000 --- a/app/icon.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/assets/app-icon-source.jpeg b/assets/app-icon-source.jpeg new file mode 100644 index 0000000..568724d Binary files /dev/null and b/assets/app-icon-source.jpeg differ diff --git a/components/InstallSheet.tsx b/components/InstallSheet.tsx index 6f370bd..32da146 100644 --- a/components/InstallSheet.tsx +++ b/components/InstallSheet.tsx @@ -12,7 +12,6 @@ import { EllipsisIcon, PlusIcon, ShareIcon, - SnowflakeIcon, XIcon, } from "@/components/icons"; import { Spinner } from "@/components/ui"; @@ -269,12 +268,13 @@ export function InstallSheet() { {/* app card */}
- - - + className="size-11 shrink-0 rounded-[11px]" + /> Freezmo diff --git a/public/icons/apple-touch-icon-152.png b/public/icons/apple-touch-icon-152.png index 96925b1..66711f2 100644 Binary files a/public/icons/apple-touch-icon-152.png and b/public/icons/apple-touch-icon-152.png differ diff --git a/public/icons/apple-touch-icon-167.png b/public/icons/apple-touch-icon-167.png index 3b1cc53..9830837 100644 Binary files a/public/icons/apple-touch-icon-167.png and b/public/icons/apple-touch-icon-167.png differ diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png index 1b6ed5e..a63d772 100644 Binary files a/public/icons/apple-touch-icon.png and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png index 9dfc98a..8542258 100644 Binary files a/public/icons/favicon-32.png and b/public/icons/favicon-32.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png index c3db25a..40e85d5 100644 Binary files a/public/icons/icon-192.png and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png index 4d6a8d2..c276345 100644 Binary files a/public/icons/icon-512.png and b/public/icons/icon-512.png differ diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png index 64f7072..8159539 100644 Binary files a/public/icons/maskable-512.png and b/public/icons/maskable-512.png differ diff --git a/public/icons/splash-1170x2532.png b/public/icons/splash-1170x2532.png index b65daf2..7ca01e8 100644 Binary files a/public/icons/splash-1170x2532.png and b/public/icons/splash-1170x2532.png differ diff --git a/public/icons/splash-1179x2556.png b/public/icons/splash-1179x2556.png index 1242dee..76f3b3c 100644 Binary files a/public/icons/splash-1179x2556.png and b/public/icons/splash-1179x2556.png differ diff --git a/public/icons/splash-1284x2778.png b/public/icons/splash-1284x2778.png index 0e46800..6d84b82 100644 Binary files a/public/icons/splash-1284x2778.png and b/public/icons/splash-1284x2778.png differ diff --git a/public/icons/splash-1290x2796.png b/public/icons/splash-1290x2796.png index 818e823..2b9957e 100644 Binary files a/public/icons/splash-1290x2796.png and b/public/icons/splash-1290x2796.png differ diff --git a/public/icons/splash-1536x2048.png b/public/icons/splash-1536x2048.png index aa642b2..f1bbb14 100644 Binary files a/public/icons/splash-1536x2048.png and b/public/icons/splash-1536x2048.png differ diff --git a/public/icons/splash-1668x2388.png b/public/icons/splash-1668x2388.png index c41ed25..c20c2cc 100644 Binary files a/public/icons/splash-1668x2388.png and b/public/icons/splash-1668x2388.png differ diff --git a/public/icons/splash-750x1334.png b/public/icons/splash-750x1334.png index 762f792..af08089 100644 Binary files a/public/icons/splash-750x1334.png and b/public/icons/splash-750x1334.png differ diff --git a/public/icons/splash-828x1792.png b/public/icons/splash-828x1792.png index 4520641..f186617 100644 Binary files a/public/icons/splash-828x1792.png and b/public/icons/splash-828x1792.png differ diff --git a/public/sw.js b/public/sw.js index f93a9b3..eb2c435 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v2"; +const CACHE_VERSION = "freezmo-v3"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/scripts/make-icons.mjs b/scripts/make-icons.mjs index 2e4ab28..c3f16b8 100644 --- a/scripts/make-icons.mjs +++ b/scripts/make-icons.mjs @@ -1,64 +1,121 @@ /** - * Renders the Freezmo app icons (PWA + iOS) with headless Chromium. - * Run: node scripts/make-icons.mjs (requires the Playwright browser cache) + * Renders the Freezmo app icons (PWA + iOS) from the master artwork with + * headless Chromium. + * Run: node scripts/make-icons.mjs (requires a Playwright Chromium cache) + * + * Master: assets/app-icon-source.jpeg — a 1024x1024 navy rounded square. + * 1. The master is masked at full resolution first (a rounded rect 4px + * inside the artwork's own corner radius), so the JPEG's anti-aliased + * white edge never survives and small sizes downscale with clean alpha. + * 2. "any" icons use that masked master as-is. + * Apple touch icons are full-bleed, because iOS paints transparency + * black; the artwork is zoomed 20% so its corners fall outside the + * square (the master's corners are ~85px, so nothing else is cropped). + * The maskable icon keeps the artwork inside the 80% safe circle on a + * matching navy field. + * Also writes app/favicon.ico, the Next.js file convention for the browser + * tab (the larger sizes are declared in app/layout.tsx metadata). */ import { chromium } from "playwright-core"; -import { mkdirSync, existsSync } from "node:fs"; +import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs"; const CHROME = process.env.CHROME ?? "/home/martin/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome"; -const OUT = new URL("../public/icons/", import.meta.url).pathname; +const ROOT = new URL("../", import.meta.url).pathname; +const OUT = `${ROOT}public/icons/`; +const SOURCE = `${ROOT}assets/app-icon-source.jpeg`; mkdirSync(OUT, { recursive: true }); -const SNOWFLAKE = (size, stroke, inset) => ` - - - - - - - `; +/** Sampled from the master artwork: background colour, size, corner radius. */ +const NAVY = "#101528"; +const SIZE = 1024; +const CORNER = 253; +const ERODE = 4; // keeps the clip inside the artwork's anti-aliased edge -function pageHtml({ size, radius, inset, background, stroke }) { +const sourceUri = `data:image/jpeg;base64,${readFileSync(SOURCE).toString("base64")}`; + +const browser = await chromium.launch({ executablePath: CHROME, args: ["--no-sandbox"] }); + +/* Stage 1: mask the master at full resolution. */ +const masterHtml = ` + + + + + + + +`; +const masterPage = await browser.newPage({ viewport: { width: SIZE, height: SIZE }, deviceScaleFactor: 1 }); +await masterPage.setContent(masterHtml, { waitUntil: "load" }); +const master = await masterPage.screenshot({ omitBackground: true }); +await masterPage.close(); +const masterUri = `data:image/png;base64,${master.toString("base64")}`; + +/* Stage 2: render each target from the masked master. */ +function pageHtml(size, mode) { + const img = mode === "bleed" ? sourceUri : masterUri; return `
${SNOWFLAKE(size, stroke, inset)}
`; + html,body{margin:0;padding:0;overflow:hidden;background:transparent} + .icon{width:${size}px;height:${size}px;overflow:hidden} + .icon img{display:block;width:100%;height:100%} + .bleed img{transform:scale(1.2)} + .maskable{background:${NAVY};display:grid;place-items:center} + .maskable img{width:80%;height:80%} +
`; } const targets = [ - // Standard PWA icons: rounded square, iOS-like gradient-free accent - { file: "icon-192.png", size: 192, radius: 42, inset: 15, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 3.4 }, - { file: "icon-512.png", size: 512, radius: 112, inset: 15, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 3.4 }, - // Maskable: keep the glyph inside the safe zone (80% circle) - { file: "maskable-512.png", size: 512, radius: 0, inset: 22, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 3.6 }, - // iOS home screen icon: square, the OS applies its own mask - { file: "apple-touch-icon.png", size: 180, radius: 0, inset: 15, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 3.4 }, - { file: "apple-touch-icon-152.png", size: 152, radius: 0, inset: 15, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 3.4 }, - { file: "apple-touch-icon-167.png", size: 167, radius: 0, inset: 15, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 3.4 }, - { file: "favicon-32.png", size: 32, radius: 7, inset: 13, background: "linear-gradient(180deg,#5c88a8,#476e8c)", stroke: 4 }, + // Standard PWA icons: the artwork's rounded square, transparent outside + { file: "icon-192.png", size: 192, mode: "rounded" }, + { file: "icon-512.png", size: 512, mode: "rounded" }, + // Maskable: full-bleed navy field, artwork inside the safe circle + { file: "maskable-512.png", size: 512, mode: "maskable" }, + // iOS home screen: square, the OS applies its own mask + { file: "apple-touch-icon.png", size: 180, mode: "bleed" }, + { file: "apple-touch-icon-167.png", size: 167, mode: "bleed" }, + { file: "apple-touch-icon-152.png", size: 152, mode: "bleed" }, + // Browser tab + { file: "favicon-32.png", size: 32, mode: "rounded" }, ]; -const browser = await chromium.launch({ executablePath: CHROME, args: ["--no-sandbox"] }); for (const target of targets) { const page = await browser.newPage({ viewport: { width: target.size, height: target.size }, deviceScaleFactor: 1 }); - await page.setContent(pageHtml(target), { waitUntil: "load" }); + await page.setContent(pageHtml(target.size, target.mode), { waitUntil: "load" }); await page.screenshot({ path: `${OUT}${target.file}`, omitBackground: true }); await page.close(); console.log(` ✓ ${target.file}`); } -/* iOS splash screens are optional; a plain background keeps them tiny. */ +/* Next.js file convention: /favicon.ico is the browser-tab icon (the other + sizes live in public/icons and are declared in app/layout.tsx metadata). + The ICO is a PNG payload in an ICO container, which every browser of + consequence reads. */ +const favicon = readFileSync(`${OUT}favicon-32.png`); +const header = Buffer.alloc(6); +header.writeUInt16LE(0, 0); // reserved +header.writeUInt16LE(1, 2); // type: icon +header.writeUInt16LE(1, 4); // image count +const entry = Buffer.alloc(16); +entry.writeUInt8(32, 0); // width +entry.writeUInt8(32, 1); // height +entry.writeUInt16LE(1, 4); // colour planes +entry.writeUInt16LE(32, 6); // bits per pixel +entry.writeUInt32LE(favicon.length, 8); +entry.writeUInt32LE(22, 12); // payload offset +writeFileSync(`${ROOT}app/favicon.ico`, Buffer.concat([header, entry, favicon])); +console.log(" ✓ app/favicon.ico"); + +/* iOS splash screens: a light stage with the new mark and the wordmark. */ +const badgeUri = `data:image/png;base64,${readFileSync(`${OUT}icon-192.png`).toString("base64")}`; const splash = `
${SNOWFLAKE(96, 3.4, 18)}
Freezmo
`; +
Freezmo
`; const splashTargets = [ [1170, 2532], [1284, 2778], [1179, 2556], [1290, 2796], [750, 1334], [828, 1792], [1536, 2048], [1668, 2388],