Files
kegel/store/icons/make.js
marcuspaico 55999b7458
Some checks failed
Build & Release APK / build (push) Has been cancelled
feat: store release setup — EAS config, listing copy, screenshots, new icon
- eas.json production/simulator profiles; iOS plist flags; EAS project linked
- literal versionCode/versionName in build.gradle so EAS can parse it; Gitea CI
  stamps the run number with sed instead of env-driven gradle expressions
- store/: listing copy + keyword rationale, screenshot composer (headless
  Chrome), raw simulator captures, rendered App Store / Play slides, feature
  graphic, privacy policy source, icon generator
- replace Expo placeholder icon with the session-ring mark; regenerate Android
  launcher/splash res and theme colours

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WontRuPqKcod9XK4ZN9MXv
2026-09-07 16:10:27 +10:00

86 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// Generate the app icon set from one SVG mark, rendered through headless Chrome.
//
// node store/icons/make.js
//
// Writes into assets/images/:
// icon.png 1024×1024 opaque (iOS + fallback)
// android-icon-foreground.png 1024×1024 alpha (adaptive foreground, mark in the 66% safe zone)
// android-icon-monochrome.png 1024×1024 alpha (themed icon, white mark)
// splash-icon.png 512×512 alpha (splash, mark only)
//
// The mark is the session ring from the app: a thick accent ring with a filled
// core — "squeeze". Colours from src/lib/theme.ts. After running, regenerate the
// committed Android res with `npx expo prebuild -p android --no-install`.
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const OUT = path.join(__dirname, "..", "..", "assets", "images");
const TMP = path.join(__dirname, ".tmp");
const C = { bg: "#160B0B", bg2: "#2A1112", accent: "#E7402E", accentDeep: "#B8271A", text: "#F7ECEA" };
// size: canvas px; scale: fraction of canvas the mark spans; color overrides for monochrome.
function mark(size, scale, { ring = C.accent, core = C.accent, glow = true } = {}) {
const c = size / 2;
const R = (size * scale) / 2; // outer radius of the ring
const stroke = R * 0.3;
const ringR = R - stroke / 2;
const coreR = R * 0.32;
return `
<defs>
<radialGradient id="g" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="${ring}" stop-opacity="0.35"/>
<stop offset="100%" stop-color="${ring}" stop-opacity="0"/>
</radialGradient>
</defs>
${glow ? `<circle cx="${c}" cy="${c}" r="${R * 1.35}" fill="url(#g)"/>` : ""}
<!-- open arc, ~82% of the ring, like the session progress ring mid-hold; gap at the top-right, dash starts at 12 o'clock -->
<circle cx="${c}" cy="${c}" r="${ringR}" fill="none" stroke="${ring}" stroke-width="${stroke}" stroke-linecap="round"
stroke-dasharray="${2 * Math.PI * ringR * 0.82} ${2 * Math.PI * ringR}" transform="rotate(-90 ${c} ${c})"/>
<circle cx="${c}" cy="${c}" r="${coreR}" fill="${core}"/>`;
}
function svg(size, body, bg) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
${bg ? `<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${C.bg2}"/><stop offset="100%" stop-color="${C.bg}"/></linearGradient></defs>
<rect width="${size}" height="${size}" fill="url(#bg)"/>` : ""}
${body}
</svg>`;
}
function render(svgText, size, out, transparent) {
fs.mkdirSync(TMP, { recursive: true });
const file = path.join(TMP, path.basename(out, ".png") + ".html");
fs.writeFileSync(
file,
`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;width:${size}px;height:${size}px;overflow:hidden;background:${transparent ? "transparent" : C.bg}}svg{display:block}</style></head><body>${svgText}</body></html>`,
);
const args = [
"--headless=new", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=1",
`--window-size=${size},${size}`, `--screenshot=${out}`,
];
if (transparent) args.push("--default-background-color=00000000");
args.push("file://" + file);
execFileSync(CHROME, args, { stdio: "ignore" });
}
function main() {
// iOS: opaque, mark fills ~62% of the canvas (Apple masks the corners itself).
render(svg(1024, mark(1024, 0.62), true), 1024, path.join(OUT, "icon.png"), false);
// Android adaptive foreground: content must sit inside the central 66% safe zone.
render(svg(1024, mark(1024, 0.46, { glow: false })), 1024, path.join(OUT, "android-icon-foreground.png"), true);
// Android monochrome: single-colour silhouette, same safe zone.
render(svg(1024, mark(1024, 0.46, { ring: "#FFFFFF", core: "#FFFFFF", glow: false })), 1024, path.join(OUT, "android-icon-monochrome.png"), true);
// Splash: mark only, on transparent; app.json sets the background colour.
render(svg(512, mark(512, 0.8, { glow: false })), 512, path.join(OUT, "splash-icon.png"), true);
fs.rmSync(TMP, { recursive: true, force: true });
console.log("wrote icon.png, android-icon-foreground.png, android-icon-monochrome.png, splash-icon.png");
}
main();