132 lines
3.2 KiB
JavaScript
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}`);
|