// Trace a flattened pose (from smooth-flatten-for-trace.cjs) with potrace, one colour at a time, plus its shadow. // potrace fits far smoother curves than vtracer on small art. Colours stack largest first, and each layer's mask // also covers every layer above it, so a lower colour runs under the upper ones and no hairline gaps can open. // usage: potrace-pose.cjs const sharp = require("module").createRequire("/home/ubuntu/kalki-cli/projects/mybazaar-web/package.json")("sharp"); const { execFileSync } = require("node:child_process"); const fs = require("node:fs"), os = require("node:os"), path = require("node:path"); const [, , flatFile, shadowFile, outFile] = process.argv; const SHADOW_INK = "#113833", SHADOW_OPACITY = "0.194"; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "potrace-pose-")); const trace = (mask, W, H, name) => { const rowBytes = Math.ceil(W / 8), bits = Buffer.alloc(rowBytes * H); for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) if (mask[y * W + x]) bits[y * rowBytes + (x >> 3)] |= 0x80 >> (x & 7); const pbm = path.join(tmp, name + ".pbm"), svg = path.join(tmp, name + ".svg"); fs.writeFileSync(pbm, Buffer.concat([Buffer.from(`P4\n${W} ${H}\n`), bits])); execFileSync("potrace", [pbm, "-s", "-o", svg, "--turdsize", "20", "--alphamax", "1.1", "--opttolerance", "0.2"]); const s = fs.readFileSync(svg, "utf8"); return { transform: / m[1].replace(/\s+/g, " ")) }; }; (async () => { const { data, info } = await sharp(flatFile).ensureAlpha().raw().toBuffer({ resolveWithObject: true }); const W = info.width, H = info.height, n = W * H; const key = (p) => (data[p * 4] << 16) | (data[p * 4 + 1] << 8) | data[p * 4 + 2]; const area = new Map(); for (let p = 0; p < n; p++) if (data[p * 4 + 3] > 127) area.set(key(p), (area.get(key(p)) || 0) + 1); const order = [...area.keys()].sort((a, b) => area.get(b) - area.get(a)); const rank = new Map(order.map((c, i) => [c, i])); const sm = await sharp(shadowFile).metadata(); const shadowGrey = await sharp(shadowFile).resize(W, H, { kernel: "lanczos3" }).flatten({ background: "#ffffff" }) .greyscale().blur(4).extractChannel(0).raw().toBuffer(); if (sm.width * (W / sm.width) !== W) throw new Error("shadow layer is not the same pose size"); const out = [``]; const shadow = trace(Uint8Array.from(shadowGrey, (v) => (v < 136 ? 1 : 0)), W, H, "shadow"); if (shadow.paths.length) out.push(``, ...shadow.paths.map((d) => ``), ""); order.forEach((c, i) => { const mask = new Uint8Array(n); for (let p = 0; p < n; p++) if (data[p * 4 + 3] > 127 && rank.get(key(p)) >= i) mask[p] = 1; const t = trace(mask, W, H, "c" + i); const hex = "#" + c.toString(16).padStart(6, "0"); if (t.paths.length) out.push(``, ...t.paths.map((d) => ``), ""); console.log(` ${hex} ${(100 * area.get(c) / n).toFixed(1)}% -> ${t.paths.length} paths`); }); out.push("", ""); fs.writeFileSync(outFile, out.join("\n")); fs.rmSync(tmp, { recursive: true }); })();