97 lines
3.6 KiB
JavaScript
97 lines
3.6 KiB
JavaScript
// Generates optimized map art from the user's source images (CR-003, decision 0.5.37).
|
|
//
|
|
// node scripts/map-art.mjs [outDir] regenerate (default public/assets/map-gen)
|
|
// node scripts/map-art.mjs --if-available as above, but if sharp is not installed,
|
|
// only check the committed output exists
|
|
//
|
|
// Sources in the manifest's `sourceDir` are only ever read.
|
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const webRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const manifest = JSON.parse(readFileSync(join(webRoot, "src/map/art/manifest.json"), "utf8"));
|
|
|
|
/** Long-side sizes in px for the 1x and 2x variants of each sprite kind. */
|
|
export const SPRITE_SIZES = { body: [256, 512], badge: [256, 512], ship: [128, 256] };
|
|
export const BACKGROUND_MAX = 2048;
|
|
export const DEFAULT_OUT_DIR = join(webRoot, "public/assets/map-gen");
|
|
|
|
/** Every file the script writes for the manifest, relative to the output folder. */
|
|
export function expectedOutputs() {
|
|
const outputs = [];
|
|
for (const [id, asset] of Object.entries(manifest.assets)) {
|
|
if (asset.file === null) continue;
|
|
if (asset.kind === "background") outputs.push({ id, file: `${id}.webp`, kind: asset.kind });
|
|
else {
|
|
const [one, two] = SPRITE_SIZES[asset.kind];
|
|
outputs.push({ id, file: `${id}@1x.png`, kind: asset.kind, longSide: one });
|
|
outputs.push({ id, file: `${id}@2x.png`, kind: asset.kind, longSide: two });
|
|
}
|
|
}
|
|
return outputs;
|
|
}
|
|
|
|
const png = { compressionLevel: 9, adaptiveFiltering: false, palette: false };
|
|
|
|
async function generate(sharp, outDir) {
|
|
mkdirSync(outDir, { recursive: true });
|
|
for (const [id, asset] of Object.entries(manifest.assets)) {
|
|
if (asset.file === null) {
|
|
console.warn(`map-art: ${id} has no source file; the map will use its placeholder`);
|
|
continue;
|
|
}
|
|
const source = join(webRoot, manifest.sourceDir, asset.file);
|
|
if (asset.kind === "background") {
|
|
await sharp(source)
|
|
.resize({
|
|
width: BACKGROUND_MAX,
|
|
height: BACKGROUND_MAX,
|
|
fit: "inside",
|
|
withoutEnlargement: true,
|
|
})
|
|
.webp({ quality: 80, effort: 6 })
|
|
.toFile(join(outDir, `${id}.webp`));
|
|
continue;
|
|
}
|
|
// Trim transparent padding once, then export each size from the trimmed image.
|
|
const trimmed = await sharp(source).trim().png().toBuffer();
|
|
const [one, two] = SPRITE_SIZES[asset.kind];
|
|
for (const [suffix, size] of [
|
|
["1x", one],
|
|
["2x", two],
|
|
]) {
|
|
await sharp(trimmed)
|
|
.resize({ width: size, height: size, fit: "inside" })
|
|
.png(png)
|
|
.toFile(join(outDir, `${id}@${suffix}.png`));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main(args) {
|
|
const ifAvailable = args.includes("--if-available");
|
|
const outDir = resolve(args.find((a) => !a.startsWith("--")) ?? DEFAULT_OUT_DIR);
|
|
let sharp;
|
|
try {
|
|
sharp = (await import("sharp")).default;
|
|
} catch (error) {
|
|
if (!ifAvailable) throw error;
|
|
const missing = expectedOutputs().filter((o) => !existsSync(join(outDir, o.file)));
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`map-art: sharp unavailable and output missing: ${missing.map((o) => o.file)}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
console.warn("map-art: sharp unavailable; using the committed output");
|
|
return;
|
|
}
|
|
await generate(sharp, outDir);
|
|
console.log(`map-art: wrote ${expectedOutputs().length} files to ${outDir}`);
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
await main(process.argv.slice(2));
|
|
}
|