Files
gold_land_user/scripts/remove-treasure-bg.mjs
T
2026-08-01 10:52:38 +02:00

132 lines
3.2 KiB
JavaScript

import sharp from "sharp";
import { spawnSync } from "node:child_process";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const target = join(__dirname, "..", "src", "assets", "illustrations", "treasure-chest.png");
const tempOut = join(tmpdir(), "treasure-chest-out.png");
/** Near-black threshold: R+G+B <= this counts as background candidate */
const NEAR_BLACK = 40;
/** Soften: fade alpha within this distance (px) of flood-filled region */
const SOFT_RADIUS = 2;
const { data, info } = await sharp(target)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const { width: w, height: h } = info;
const n = w * h;
const out = Buffer.from(data);
const bg = new Uint8Array(n); // 1 = background to remove
const isNearBlack = (i) => {
const o = i * 4;
return out[o] + out[o + 1] + out[o + 2] <= NEAR_BLACK;
};
const queue = [];
const push = (x, y) => {
const i = y * w + x;
if (bg[i]) return;
if (!isNearBlack(i)) return;
bg[i] = 1;
queue.push(i);
};
// Seed BFS from all border pixels that are near-black
for (let x = 0; x < w; x++) {
push(x, 0);
push(x, h - 1);
}
for (let y = 0; y < h; y++) {
push(0, y);
push(w - 1, y);
}
// Flood-fill only connected near-black from edges (internal dark chest parts stay)
const dirs = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
];
let qh = 0;
while (qh < queue.length) {
const i = queue[qh++];
const x = i % w;
const y = (i / w) | 0;
for (const [dx, dy] of dirs) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
push(nx, ny);
}
}
// Soften edges: for non-bg pixels near bg, reduce alpha by proximity
const soft = new Float32Array(n);
for (let i = 0; i < n; i++) {
if (bg[i]) {
soft[i] = 0;
continue;
}
const x = i % w;
const y = (i / w) | 0;
let minD = SOFT_RADIUS + 1;
for (let dy = -SOFT_RADIUS; dy <= SOFT_RADIUS; dy++) {
for (let dx = -SOFT_RADIUS; dx <= SOFT_RADIUS; dx++) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
const j = ny * w + nx;
if (!bg[j]) continue;
const d = Math.hypot(dx, dy);
if (d < minD) minD = d;
}
}
if (minD <= SOFT_RADIUS) {
soft[i] = minD / (SOFT_RADIUS + 0.001);
} else {
soft[i] = 1;
}
}
let removed = 0;
for (let i = 0; i < n; i++) {
const a = i * 4 + 3;
if (bg[i]) {
out[a] = 0;
removed++;
} else if (soft[i] < 1) {
out[a] = Math.round(out[a] * soft[i]);
}
}
// Write via ASCII temp path — Node/libvips can fail writing Unicode apostrophe paths on Windows
await sharp(out, {
raw: { width: w, height: h, channels: 4 },
})
.png()
.toFile(tempOut);
const ps = spawnSync(
"powershell.exe",
[
"-NoProfile",
"-Command",
`Copy-Item -LiteralPath '${tempOut.replace(/'/g, "''")}' -Destination '${target.replace(/'/g, "''")}' -Force`,
],
{ encoding: "utf8" },
);
if (ps.status !== 0) {
console.error(ps.stderr || ps.stdout);
process.exit(ps.status ?? 1);
}
console.log(`Wrote ${target}`);
console.log(`Removed ${removed} background pixels (edge flood-fill); soft radius=${SOFT_RADIUS}`);