106 lines
2.9 KiB
JavaScript
106 lines
2.9 KiB
JavaScript
import { mkdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import sharp from "sharp";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const projectRoot = path.resolve(__dirname, "..");
|
|
const sourceIconPath = path.join(projectRoot, "icon.png");
|
|
const pwaOutputDir = path.join(projectRoot, "public", "pwa");
|
|
|
|
const androidResArg = process.argv.find((arg) =>
|
|
arg.startsWith("--android-res-dir="),
|
|
);
|
|
const androidResDir = androidResArg
|
|
? androidResArg.slice("--android-res-dir=".length)
|
|
: process.env.ANDROID_RES_DIR;
|
|
|
|
function createCircleMask(size) {
|
|
return Buffer.from(`
|
|
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="${size / 2}" cy="${size / 2}" r="${size / 2}" fill="white" />
|
|
</svg>
|
|
`);
|
|
}
|
|
|
|
async function writePng(size, outputPath) {
|
|
await sharp(sourceIconPath).resize(size, size).png().toFile(outputPath);
|
|
}
|
|
|
|
async function writeMaskablePng(size, outputPath) {
|
|
const foreground = await sharp(sourceIconPath)
|
|
.resize(Math.round(size * 0.82), Math.round(size * 0.82), {
|
|
fit: "contain",
|
|
})
|
|
.png()
|
|
.toBuffer();
|
|
|
|
await sharp({
|
|
create: {
|
|
width: size,
|
|
height: size,
|
|
channels: 4,
|
|
background: "#35548d",
|
|
},
|
|
})
|
|
.composite([
|
|
{
|
|
input: foreground,
|
|
gravity: "center",
|
|
},
|
|
])
|
|
.png()
|
|
.toFile(outputPath);
|
|
}
|
|
|
|
async function writeAndroidIcons(resDir) {
|
|
const densities = [
|
|
{ dir: "mipmap-mdpi", size: 48 },
|
|
{ dir: "mipmap-hdpi", size: 72 },
|
|
{ dir: "mipmap-xhdpi", size: 96 },
|
|
{ dir: "mipmap-xxhdpi", size: 144 },
|
|
{ dir: "mipmap-xxxhdpi", size: 192 },
|
|
];
|
|
|
|
for (const density of densities) {
|
|
const targetDir = path.join(resDir, density.dir);
|
|
await mkdir(targetDir, { recursive: true });
|
|
|
|
const squareBuffer = await sharp(sourceIconPath)
|
|
.resize(density.size, density.size)
|
|
.png()
|
|
.toBuffer();
|
|
|
|
await sharp(squareBuffer)
|
|
.webp({ quality: 92 })
|
|
.toFile(path.join(targetDir, "ic_launcher.webp"));
|
|
|
|
await sharp(squareBuffer)
|
|
.composite([
|
|
{
|
|
input: createCircleMask(density.size),
|
|
blend: "dest-in",
|
|
},
|
|
])
|
|
.webp({ quality: 92 })
|
|
.toFile(path.join(targetDir, "ic_launcher_round.webp"));
|
|
}
|
|
}
|
|
|
|
await mkdir(pwaOutputDir, { recursive: true });
|
|
|
|
await Promise.all([
|
|
writePng(192, path.join(pwaOutputDir, "icon-192x192.png")),
|
|
writePng(512, path.join(pwaOutputDir, "icon-512x512.png")),
|
|
writePng(180, path.join(pwaOutputDir, "apple-touch-icon.png")),
|
|
writePng(96, path.join(pwaOutputDir, "badge-96x96.png")),
|
|
writeMaskablePng(512, path.join(pwaOutputDir, "icon-maskable-512x512.png")),
|
|
]);
|
|
|
|
if (androidResDir) {
|
|
await writeAndroidIcons(androidResDir);
|
|
}
|
|
|
|
console.log("Icons generated successfully."); |