feat(pwa): add icon generation script and generated PWA icons

This commit is contained in:
Arthur Belleville 2026-04-02 19:01:21 +02:00
parent ab38e8115b
commit 6d5f78aecf
No known key found for this signature in database
7 changed files with 74 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -0,0 +1,74 @@
import sharp from "sharp";
import path from "node:path";
import fs from "node:fs";
const PUBLIC_DIR = path.resolve(import.meta.dirname, "../public");
const OUTPUT_DIR = path.join(PUBLIC_DIR, "pwa-icons");
const ICON_SIZES = [
{ size: 16, name: "favicon-16x16.png" },
{ size: 32, name: "favicon-32x32.png" },
{ size: 180, name: "apple-touch-icon-180x180.png" },
{ size: 192, name: "pwa-192x192.png" },
{ size: 512, name: "pwa-512x512.png" },
{ size: 512, name: "pwa-512x512-maskable.png", maskable: true },
];
// Maskable icons need 10% safe zone padding (per spec).
// We add a colored background and shrink the source to 80% to stay within the safe zone.
const MASKABLE_PADDING_RATIO = 0.1;
const MASKABLE_BG_COLOR = "#ffffff";
async function generateIcons(sourceFile: string) {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
for (const icon of ICON_SIZES) {
const outputPath = path.join(OUTPUT_DIR, icon.name);
if (icon.maskable) {
// Create maskable icon with padding and background
const innerSize = Math.round(icon.size * (1 - MASKABLE_PADDING_RATIO * 2));
const offset = Math.round(icon.size * MASKABLE_PADDING_RATIO);
const resizedIcon = await sharp(sourceFile)
.resize(innerSize, innerSize, { fit: "contain" })
.toBuffer();
await sharp({
create: {
width: icon.size,
height: icon.size,
channels: 4,
background: MASKABLE_BG_COLOR,
},
})
.composite([{ input: resizedIcon, left: offset, top: offset }])
.png()
.toFile(outputPath);
} else {
await sharp(sourceFile)
.resize(icon.size, icon.size, { fit: "contain" })
.png()
.toFile(outputPath);
}
console.log(`Generated: ${icon.name} (${icon.size}x${icon.size})`);
}
}
// Determine source: pass "staging" as first arg for staging icons
const variant = process.argv[2];
const sourceFile =
variant === "staging"
? path.join(PUBLIC_DIR, "staging_icon.jpg")
: path.join(PUBLIC_DIR, "icon.jpg");
if (!fs.existsSync(sourceFile)) {
console.error(`Source icon not found: ${sourceFile}`);
process.exit(1);
}
console.log(`Generating PWA icons from: ${sourceFile}`);
generateIcons(sourceFile).then(() => console.log("Done!"));