nordic-skating / files / tools / icons.mjs
icons.mjs
bd311616 2026-10-10 Pavel Safronov | 1 | /** |
| 2 | * Make the app's icons from assets/skate-shoes.png, in the sizes a phone asks for when | |
| 3 | * the app is put on its home screen, and the one a browser shows on its tab. The pictures | |
| 4 | * are kept in static/, so this is only run when the artwork or its colours change. | |
| 5 | * | |
| 6 | * npm run icons | |
| 7 | * | |
1ee103b4 2026-10-11 Pavel Safronov | 8 | * The artwork is a skate by Icongeek26, from Flaticon, as are the pictures on the app's |
| 9 | * buttons (assets/), all used under the Flaticon free | |
bd311616 2026-10-10 Pavel Safronov | 10 | * licence, which requires attribution (README, Attribution). It is black line on nothing; |
| 11 | * only its shape is used. On a home screen it is drawn pale on the colour of black ice. | |
| 12 | * On a browser's tab it is as it was drawn: black, on nothing, so that the tab shows | |
| 13 | * through it; and white on nothing for a browser whose tabs are dark, where black would | |
| 14 | * not show. | |
| 15 | */ | |
| 16 | import { readFileSync } from 'node:fs' | |
| 17 | import { chromium } from 'playwright' | |
| 18 | ||
| 19 | const skate = `data:image/png;base64,${readFileSync('assets/skate-shoes.png').toString('base64')}` | |
| 20 | const ICE = { ink: '#e8f4ff', paper: 'linear-gradient(#0b1a24,#1d3a4c)' } | |
| 21 | /** The file, its side in pixels, how much of that the skate fills, and its colours. */ | |
| 22 | const ICONS = [ | |
| 23 | ['static/icon-192.png', 192, 0.72, ICE], | |
| 24 | ['static/icon-512.png', 512, 0.72, ICE], | |
| 25 | // For Android, which crops this one to whatever shape its home screen uses: the skate | |
| 26 | // is kept inside the middle, where nothing of it is cropped away. | |
| 27 | ['static/icon-maskable-512.png', 512, 0.54, ICE], | |
| 28 | // For an iPhone's home screen, which rounds the corners itself. | |
| 29 | ['static/apple-touch-icon.png', 180, 0.72, ICE], | |
| 30 | // For a browser's tab. | |
| 31 | ['static/favicon.png', 96, 1, { ink: '#000', paper: 'transparent' }], | |
| 32 | ['static/favicon-dark.png', 96, 1, { ink: '#fff', paper: 'transparent' }], | |
| 33 | ] | |
| 34 | ||
| 35 | const browser = await chromium.launch() | |
| 36 | try { | |
| 37 | for (const [file, side, fill, { ink, paper }] of ICONS) { | |
| 38 | const page = await browser.newPage({ viewport: { width: side, height: side } }) | |
| 39 | const inner = Math.round(side * fill) | |
| 40 | await page.setContent(`<body style="margin:0;display:grid;place-items:center;height:100vh;background:${paper}"> | |
| 41 | <div style="width:${inner}px;height:${inner}px;background:${ink}; | |
| 42 | -webkit-mask:url(${skate}) center/contain no-repeat;mask:url(${skate}) center/contain no-repeat"></div></body>`) | |
| 43 | await page.screenshot({ path: file, omitBackground: paper === 'transparent' }) | |
| 44 | await page.close() | |
| 45 | console.log(` ${file}`) | |
| 46 | } | |
| 47 | } finally { | |
| 48 | await browser.close() | |
| 49 | } |