// Combine the two vtracer runs for a pose into one shipping SVG. // usage: yarn node - < assemble-pose-svg.cjs // // companion.md: the character and the shadow are traced separately because vtracer blends a // sentinel-coloured shadow with neighbouring ink and fringes it. This puts them back together // in the shipped order — shadow first so the character paints over it — and re-tints the // shadow with the authored opacity rather than whatever grey the trace produced. const fs = require('node:fs'); const SRC = process.argv[2] + '/', OUT = process.argv[3] + '/'; const SHADOW_INK = '#113833', SHADOW_OPACITY = '0.194'; // matches the shipped twenty fs.mkdirSync(OUT, { recursive: true }); const paths = (svg) => [...svg.matchAll(/]*?\/>/gs)].map(m => m[0]); const fillOf = (p) => (/fill="(#[0-9A-Fa-f]{6})"/).exec(p)?.[1]; const dOf = (p) => (/\bd="([^"]+)"/).exec(p)?.[1]; for (const f of fs.readdirSync(SRC).filter(f => f.endsWith('-char.svg')).sort()) { const name = f.replace('-char.svg', ''); const charSvg = fs.readFileSync(SRC + f, 'utf8'); // Sticker poses keep their own crop size rather than the 409x370 UI frame. const W = +/width="(\d+)"/.exec(charSvg)[1], H = +/height="(\d+)"/.exec(charSvg)[1]; const charPaths = paths(charSvg); // The shadow run traces ink on white, so the white plate is dropped and only the ink kept. const shadowPaths = paths(fs.readFileSync(`${SRC}${name}-shadow.svg`, 'utf8')) .filter(p => fillOf(p) === SHADOW_INK); const out = [``, '']; for (const p of shadowPaths) { out.push(``); } out.push(''); for (const p of charPaths) out.push(``); out.push('', ''); fs.writeFileSync(OUT + name + '.svg', out.join('\n')); console.log(` ${name.padEnd(14)} ${shadowPaths.length} shadow + ${charPaths.length} char paths`); }