Files
movieloop-frontend/scripts/make-icons.mjs
T
TehRiehlDeal ef7b8aa878
frontend-ci / secrets-scan (push) Successful in 4s
frontend-ci / sast (push) Successful in 7s
frontend-ci / fs-scan (push) Successful in 14s
frontend-ci / typecheck (push) Successful in 24s
frontend-ci / lint (push) Successful in 26s
frontend-ci / test (push) Successful in 32s
frontend-ci / build (push) Successful in 38s
frontend-ci / push (push) Skipped
frontend-ci / lint (pull_request) Successful in 24s
frontend-ci / test (pull_request) Successful in 31s
frontend-ci / secrets-scan (pull_request) Successful in 5s
frontend-ci / sast (pull_request) Successful in 8s
frontend-ci / fs-scan (pull_request) Successful in 15s
frontend-ci / typecheck (pull_request) Successful in 14s
frontend-ci / build (pull_request) Successful in 38s
frontend-ci / push (pull_request) Skipped
feat(icons): render the app icons from the Movie Loop mark
Safari, the iOS home screen, Android install and push notifications
still showed the old film-reel artwork beside the new theme colour. The
PNGs are now rendered from the SVG mark by `npm run icons` (Inkscape,
no image library in node_modules): the bare mark for the 32px favicon,
the mark on the stage colour for app icons, and a maskable variant with
the extra margin Android's adaptive crop needs.
2026-09-12 22:52:39 -07:00

80 lines
3.1 KiB
JavaScript

// Renders the raster icons in public/ from their SVG sources with Inkscape.
//
// The browser tab uses public/favicon.svg directly, but Safari, the iOS home
// screen, Android's PWA install and push notifications all want PNGs, and
// those can't be transparent (iOS fills a transparent icon with black) or
// edge-to-edge (Android crops adaptive icons to a circle or squircle). So
// there are two sources: the bare mark for the 32px favicon, and the mark on
// the app's stage colour for everything else, sized to sit inside the
// maskable safe zone.
//
// Inkscape is used because it is already a fixture of the design workflow;
// rendering with it means no image library in node_modules. Its installer
// does not put it on PATH on Windows, so the standard install folders are
// tried after PATH and an explicit INKSCAPE environment variable.
//
// Usage: node scripts/make-icons.mjs
// INKSCAPE="C:\path\to\inkscape.com" node scripts/make-icons.mjs
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
/** Every PNG the app ships, with the SVG it is rendered from. */
export const ICONS = [
{ source: 'public/favicon.svg', out: 'public/favicon-32.png', size: 32 },
{ source: 'scripts/icons/app-icon.svg', out: 'public/apple-touch-icon.png', size: 180 },
{ source: 'scripts/icons/app-icon.svg', out: 'public/icon-192.png', size: 192 },
{ source: 'scripts/icons/app-icon.svg', out: 'public/icon-512.png', size: 512 },
{ source: 'scripts/icons/app-icon-maskable.svg', out: 'public/icon-512-maskable.png', size: 512 },
];
/** Where Inkscape's installer puts the console binary when it is not on PATH. */
export const INKSCAPE_CANDIDATES = [
'C:\\Program Files\\Inkscape\\bin\\inkscape.com',
'C:\\Program Files (x86)\\Inkscape\\bin\\inkscape.com',
'/Applications/Inkscape.app/Contents/MacOS/inkscape',
'/usr/bin/inkscape',
'/usr/local/bin/inkscape',
];
/**
* The Inkscape binary to run: the INKSCAPE variable if set, otherwise the
* first standard location that exists, otherwise the bare name and PATH.
*/
export function findInkscape(env = process.env, exists = existsSync) {
if (env.INKSCAPE) return env.INKSCAPE;
const found = INKSCAPE_CANDIDATES.find((p) => exists(p));
return found ?? 'inkscape';
}
/** The Inkscape 1.x command line for one PNG. */
export function exportArgs({ source, out, size }) {
return [
source,
'--export-type=png',
`--export-filename=${out}`,
`--export-width=${size}`,
`--export-height=${size}`,
];
}
export function makeIcons(root, run = execFileSync) {
const inkscape = findInkscape();
for (const icon of ICONS) {
const args = exportArgs({
source: resolve(root, icon.source),
out: resolve(root, icon.out),
size: icon.size,
});
run(inkscape, args, { stdio: 'inherit' });
console.log(`[icons] ${icon.out} (${icon.size}px) <- ${icon.source}`);
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
makeIcons(root);
}