80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
/**
|
||
* 🤖 简洁科技感头像 PNG 批量生成(DiceBear bottts-neutral 机器人中性)
|
||
* 从资源库读取风格定义,seed 确定性渲染;黑灰机器人反白成线框感,
|
||
* 配深蓝渐变圆形底 + 细亮蓝圆环,Chrome headless 导出 1024×1024 PNG。
|
||
*
|
||
* 用法:node scripts/gen-tech-avatars-png.js
|
||
*/
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const vm = require('vm');
|
||
const { execFileSync } = require('child_process');
|
||
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const OUT_DIR = path.join(ROOT, 'output', 'agent-avatars-png');
|
||
const TMP_DIR = path.join(ROOT, 'output', '.tmp-tech');
|
||
const CHROME = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
||
|
||
// —— 1. 加载资源库风格定义 + 渲染引擎 ——
|
||
global.window = global;
|
||
const styleFile = path.join(ROOT, 'js', 'data', 'sources', 'dicebear-bottts-neutral.js');
|
||
vm.runInThisContext(fs.readFileSync(styleFile, 'utf8'), { filename: styleFile });
|
||
vm.runInThisContext(fs.readFileSync(path.join(ROOT, 'js', 'core', 'dicebear.js'), 'utf8'), { filename: 'dicebear.js' });
|
||
|
||
// —— 2. 渲染 10 个 seed,反白(黑→白线框感),剥离 svg 外壳取 inner ——
|
||
const COUNT = 10;
|
||
const S = 120; // bottts-neutral 画布 120
|
||
const ZOOM = 5.4; // 放大到 ~648px
|
||
const BG_FROM = '#0b1220', BG_TO = '#16294a'; // 深蓝科技渐变
|
||
const RING = '#3b82f6'; // 亮蓝圆环
|
||
|
||
function renderInner(seed) {
|
||
const { svg } = HGB_Dicebear.renderAvatar('bottts-neutral', seed);
|
||
const inner = svg.replace(/^<\?xml[^>]*\?>\s*<svg[^>]*>/, '').replace(/<\/svg>\s*$/, '');
|
||
return inner.replace(/fill="black"/g, 'fill="white"'); // 反白
|
||
}
|
||
|
||
function buildHtml(inner) {
|
||
return `<!DOCTYPE html>
|
||
<html><head><meta charset="utf-8">
|
||
<style>html,body{margin:0;padding:0;width:1024px;height:1024px;overflow:hidden;background:transparent}</style>
|
||
</head><body>
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||
<defs>
|
||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0%" stop-color="${BG_FROM}"/><stop offset="100%" stop-color="${BG_TO}"/>
|
||
</linearGradient>
|
||
<clipPath id="circle"><circle cx="512" cy="512" r="512"/></clipPath>
|
||
</defs>
|
||
<rect width="1024" height="1024" fill="url(#bg)" clip-path="url(#circle)"/>
|
||
<circle cx="512" cy="512" r="452" fill="none" stroke="${RING}" stroke-width="5" opacity=".45"/>
|
||
<g transform="translate(512 512) scale(${ZOOM}) translate(${-S / 2} ${-S / 2})">${inner}</g>
|
||
</svg>
|
||
</body></html>`;
|
||
}
|
||
|
||
// —— 3. 导出 PNG ——
|
||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
fs.mkdirSync(TMP_DIR, { recursive: true });
|
||
for (let i = 1; i <= COUNT; i++) {
|
||
const idx = String(i).padStart(2, '0');
|
||
const htmlFile = path.join(TMP_DIR, `avatar-${idx}.html`);
|
||
const pngFile = path.join(OUT_DIR, `agent-${idx}.png`);
|
||
fs.writeFileSync(htmlFile, buildHtml(renderInner('agent-' + i)), 'utf8');
|
||
try {
|
||
execFileSync(CHROME, [
|
||
'--headless=new', '--disable-gpu', '--hide-scrollbars',
|
||
'--force-device-scale-factor=1', '--default-background-color=00000000',
|
||
'--window-size=1024,1024', '--virtual-time-budget=1000',
|
||
`--screenshot=${pngFile}`,
|
||
'file:///' + htmlFile.replace(/\\/g, '/')
|
||
], { stdio: 'pipe' });
|
||
console.log(`✅ agent-${idx}.png seed=agent-${i}`);
|
||
} catch (e) {
|
||
console.error(`❌ agent-${idx}.png 失败:`, e.message.split('\n')[0]);
|
||
}
|
||
}
|
||
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
||
console.log('\n🎉 完成 → output/agent-avatars-png/');
|