Files
drawlogo/scripts/gen-100-bot-variants.js
T

211 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 🤖 100 个 bottts 机器人「技能变换」变体预览页
* 组合技能库变换(全部来自 js/skills/):
* 1. palette-build → 10 个行业色板(primary/secondary/accent)
* 2. gradient-suggest→ 每个主色的同色/互补/邻近渐变方向
* 3. lucide 源图标 → 科技徽章叠加
* 每个色板 × 5 渐变模式 × 2 徽章 = 10 变体 × 10 色板 = 100 个变体。
* 主体保持 bottts 机器人(资源库 DiceBear 源),渐变圆底 + 亮环 + 科技徽章。
*
* 用法:node scripts/gen-100-bot-variants.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ROOT = path.resolve(__dirname, '..');
const OUT_DIR = path.join(ROOT, 'output', 'agent-avatars-png');
// —— 1. 加载技能环境 ——
global.window = global;
function load(rel) { vm.runInThisContext(fs.readFileSync(path.join(ROOT, rel), 'utf8'), { filename: rel }); }
load('js/data/icons.js'); // HGB_Icons
load('js/data/industries.js'); // HGB_Industries
load('js/data/styles.js'); // HGB_Styles
load('js/data/palettes.js'); // HGB_Palettes
load('js/data/fonts.js'); // HGB_Fonts
load('js/data/decorations.js'); // HGB_Decorations
load('js/skills/core.js'); // HGB_Skills (const)
load('js/skills/color.js'); // palette-build / gradient-suggest 等
load('js/data/sources/lucide.js'); // HGB_Source_lucide(徽章图标)
load('js/data/sources/dicebear-bottts.js');
load('js/core/dicebear.js'); // HGB_Dicebear
const Skills = HGB_Skills;
console.log('✅ 技能已加载,技能数:', Skills.all().length);
// —— 2. 色板:6 行业 + 4 内置色板 = 10 ——
const inds = HGB_Industries.slice(0, 6);
const extraPals = HGB_Palettes.slice(0, 4);
const palettes = [
...inds.map(ind => {
const r = Skills.run('palette-build', { desc: '科技感 ' + ind.name, industry: ind.id });
return { industry: ind.id, indName: ind.name, palette: r.palette || { primary: '#2563eb', secondary: '#3b82f6', accent: '#60a5fa' } };
}),
...extraPals.map((p, i) => ({
industry: 'palette-' + i, indName: p.name,
palette: { primary: p.colors[0], secondary: p.colors[1] || p.colors[0], accent: p.colors[2] || p.colors[1] || p.colors[0] }
}))
];
console.log('✅ 色板数:', palettes.length, '→', palettes.map(p => p.indName).join(' / '));
// —— 3. HSL 工具(渐变派生)——
function hexToHsl(hex) {
let c = String(hex).replace('#', ''); if (c.length === 3) c = c.split('').map(x => x + x).join('');
const r = parseInt(c.slice(0, 2), 16) / 255, g = parseInt(c.slice(2, 4), 16) / 255, b = parseInt(c.slice(4, 6), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b); let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
else if (max === g) h = ((b - r) / d + 2) * 60;
else h = ((r - g) / d + 4) * 60;
}
return { h, s: s * 100, l: l * 100 };
}
function hslToHex(h, s, l) {
s /= 100; l /= 100;
const k = n => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = n => { const x = k(n); return Math.round(255 * (l - a * Math.max(-1, Math.min(x - 3, Math.min(9 - x, 1))))); };
return '#' + [f(0), f(8), f(4)].map(x => x.toString(16).padStart(2, '0')).join('');
}
function adjust(hex, dl) { const { h, s, l } = hexToHsl(hex); return hslToHex(h, s, Math.max(2, Math.min(98, l + dl))); }
// —— 4. 徽章图标(lucide 科技图标 10 个)——
const lucideIcons = window['HGB_Source_lucide'].icons;
const badgeNames = ['bot', 'sparkles', 'cpu', 'radar', 'satellite', 'zap', 'shield', 'atom', 'circuit-board', 'brain-circuit'];
const badges = badgeNames.map(n => {
const ic = lucideIcons.find(x => x.name === n);
return { name: n, body: ic ? ic.svg : '<circle cx="12" cy="12" r="9"/>' };
});
// —— 5. 生成 100 个变体 ——
const MODES = [
{ name: '主色渐变', f: (p, g) => [p.primary, p.secondary] },
{ name: '点缀渐变', f: (p, g) => [p.primary, p.accent] },
{ name: '深邃渐变', f: (p, g) => [p.primary, adjust(p.primary, -30)] },
{ name: '互补渐变', f: (p, g) => [p.primary, hslToHex((g.h + 180) % 360, 60, 50)] },
{ name: '邻近渐变', f: (p, g) => [p.primary, hslToHex((g.h + 30) % 360, 60, 50)] }
];
const cards = [];
let idx = 1;
for (let pi = 0; pi < palettes.length; pi++) {
const P = palettes[pi];
const gradInfo = Skills.run('gradient-suggest', { color: P.palette.primary });
for (let mi = 0; mi < MODES.length; mi++) {
for (let bi = 0; bi < 2; bi++) {
const badge = badges[(pi * 5 + mi * 2 + bi) % badges.length];
const [c1, c2] = MODES[mi].f(P.palette, gradInfo);
const seed = 'agent-' + idx;
const { svg } = HGB_Dicebear.renderAvatar('bottts', seed);
// 包裹:渐变圆底 + bottts + 亮环 + 科技徽章
const wrapped = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="96" height="96">
<defs>
<linearGradient id="g${idx}" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="${c1}"/><stop offset="100%" stop-color="${c2}"/>
</linearGradient>
<clipPath id="c${idx}"><circle cx="120" cy="120" r="120"/></clipPath>
</defs>
<rect width="240" height="240" fill="url(#g${idx})" clip-path="url(#c${idx})"/>
<g transform="translate(120 120) scale(1.28) translate(-${120 / 1.28} -${120 / 1.28})">${svg.replace(/^<svg[^>]*>|<\/svg>$/g, '')}</g>
<circle cx="120" cy="120" r="112" fill="none" stroke="#ffffff" stroke-width="3" opacity=".35"/>
<g transform="translate(188 188)"><circle r="24" fill="${c2}"/><g transform="translate(-12 -12) scale(1)" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${badge.body}</g></g>
</svg>`;
cards.push({ idx, pi, mi, bi, seed, svg: wrapped, name: `${P.indName} · ${MODES[mi].name} · ${badge.name}` });
idx++;
}
}
}
console.log('✅ 变体数:', cards.length);
// —— 6. 唯一性校验 ——
const { createHash } = require('crypto');
const hashes = new Set();
let dup = 0;
for (const c of cards) {
const h = createHash('md5').update(c.svg).digest('hex');
if (hashes.has(h)) dup++;
hashes.add(h);
}
console.log('唯一性:' + (cards.length - dup) + '/' + cards.length + (dup ? '(⚠️ ' + dup + ' 重复)' : ' ✅'));
// —— 7. 预览页 ——
let cardHtml = '';
for (const c of cards) {
const n = String(c.idx).padStart(2, '0');
cardHtml += `<label class="card" data-idx="${c.idx}">
<input type="checkbox" class="cb" value="${c.idx}" data-name="${c.name}">
${c.svg}
<div class="no">#${n}</div>
<div class="name" title="${c.name}">${c.name}</div>
</label>\n`;
}
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>🎨 100 个 bottts 技能变换变体 · 勾选后批量渲染</title>
<style>
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background:#0f1115; color:#e8eaed; margin:0; padding:24px; }
h1 { font-size:20px; margin:0 0 4px; }
.sub { color:#9aa0a6; font-size:13px; margin-bottom:16px; }
.toolbar { position:sticky; top:0; background:#0f1115ee; padding:10px 0; z-index:9; border-bottom:1px solid #2a2f3a; margin-bottom:16px; display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
.toolbar button { background:#2563eb; color:#fff; border:none; border-radius:6px; padding:6px 14px; font-size:13px; cursor:pointer; }
.toolbar .count { color:#8ab4f8; font-size:13px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:12px; }
.card { background:#1b1e26; border:1px solid #2a2f3a; border-radius:12px; padding:12px 8px 10px; text-align:center; cursor:pointer; position:relative; transition:border-color .15s; }
.card:hover { border-color:#3b82f6; }
.card.sel { border-color:#22c55e; background:#1a2b1f; }
.card svg { width:96px; height:96px; border-radius:50%; display:block; margin:0 auto; }
.card .no { font-size:11px; color:#80868b; margin-top:8px; }
.card .name { font-size:10px; color:#bdc1c6; word-break:break-all; }
.card .cb { position:absolute; top:8px; left:8px; width:16px; height:16px; accent-color:#22c55e; }
.out { margin-top:16px; }
.out textarea { width:100%; height:56px; background:#1b1e26; color:#8ab4f8; border:1px solid #2a2f3a; border-radius:8px; padding:8px; font-size:12px; box-sizing:border-box; }
</style>
</head>
<body>
<h1>🎨 100 个 bottts 技能变换变体</h1>
<div class="sub">技能库组合变换:palette-build 色板 × gradient-suggest 渐变 × lucide 科技徽章 · 点击勾选(默认全选)· 复制编号交渲染</div>
<div class="toolbar">
<button onclick="sel(true)">全选</button>
<button onclick="sel(false)">清空</button>
<span class="count" id="count">已选 0/100</span>
<button onclick="copySel()">复制已选编号</button>
</div>
<div class="grid">${cardHtml}</div>
<div class="out">
<textarea id="selText" placeholder="已选编号将显示在这里…" readonly></textarea>
</div>
<script>
const cards = document.querySelectorAll('.card');
const countEl = document.getElementById('count');
const selText = document.getElementById('selText');
function update() {
const sel = [...cards].filter(c => c.querySelector('.cb').checked);
cards.forEach(c => c.classList.toggle('sel', c.querySelector('.cb').checked));
countEl.textContent = '已选 ' + sel.length + '/' + cards.length;
selText.value = sel.map(c => c.dataset.idx).join(',');
}
function sel(v) { cards.forEach(c => c.querySelector('.cb').checked = v); update(); }
function copySel() {
if (!selText.value) return alert('请先勾选头像');
navigator.clipboard.writeText(selText.value).then(() => alert('已复制编号:' + selText.value)).catch(() => alert(selText.value));
}
cards.forEach(c => c.addEventListener('click', e => {
if (e.target.tagName === 'INPUT') return;
const cb = c.querySelector('.cb'); cb.checked = !cb.checked; update();
}));
sel(true);
</script>
</body>
</html>`;
fs.writeFileSync(path.join(OUT_DIR, 'preview-100-variants.html'), html, 'utf8');
console.log('🎉 已生成 100 个技能变换变体 → output/agent-avatars-png/preview-100-variants.html');