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
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.
80 lines
3.1 KiB
JavaScript
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);
|
|
}
|