Files
drawlogo/scripts/gen-100-avatars-preview.js
T

145 lines
7.7 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 个科技感头像候选预览页(资源库 lucide 源直选,无需渲染)
* 生成 preview-100.html:每张候选以最终头像样式展示(深蓝渐变圆底 + 白色线性图标),
* 支持勾选,底部可复制已选编号,供用户筛选后批量渲染 PNG。
*
* 用法:node scripts/gen-100-avatars-preview.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. 加载 lucide 源 ——
global.window = global;
const lucideFile = path.join(ROOT, 'js', 'data', 'sources', 'lucide.js');
vm.runInThisContext(fs.readFileSync(lucideFile, 'utf8'), { filename: lucideFile });
const lucideIcons = window['HGB_Source_lucide'].icons;
const byName = new Map(lucideIcons.map(x => [x.name, x]));
// —— 2. 精选 100 个(按主题分组)——
const GROUPS = [
{ label: '🤖 智能核心', items: ['bot', 'brain-circuit', 'brain', 'cpu', 'circuit-board', 'sparkles', 'sparkle', 'atom', 'orbit', 'radar', 'satellite', 'antenna', 'signal', 'wifi', 'bluetooth', 'usb', 'cable', 'plug', 'power', 'battery', 'battery-charging', 'battery-full'] },
{ label: '⌨️ 代码开发', items: ['terminal', 'code', 'code-xml', 'braces', 'brackets', 'file-code', 'git-branch', 'git-merge', 'git-pull-request', 'command', 'keyboard', 'monitor', 'laptop', 'smartphone', 'server', 'database', 'hard-drive'] },
{ label: '☁️ 网络云数据', items: ['cloud', 'cloud-upload', 'cloud-download', 'cloud-cog', 'cloud-lightning', 'network', 'share-2', 'link', 'globe', 'send', 'mail', 'message-circle', 'message-square', 'phone', 'mic', 'camera', 'video', 'qr-code', 'rss'] },
{ label: '🛡️ 安全防护', items: ['lock', 'key', 'key-round', 'shield', 'shield-check', 'shield-half', 'target', 'crosshair', 'eye', 'scan-face', 'scan'] },
{ label: '⚙️ 智能工具', items: ['settings', 'settings-2', 'cog', 'wrench', 'layers', 'layout-grid', 'box', 'package', 'blocks', 'puzzle', 'gem', 'diamond', 'sliders-horizontal', 'equal'] },
{ label: '🔷 抽象几何', items: ['hexagon', 'triangle', 'circle', 'octagon', 'infinity', 'asterisk', 'zap', 'activity', 'gauge', 'compass', 'navigation', 'navigation-2', 'map-pin', 'map'] },
{ label: '🚀 空间探索', items: ['rocket', 'plane', 'telescope'] }
];
let list = [];
const dup = new Set();
for (const g of GROUPS) {
for (const name of g.items) {
if (dup.has(name)) continue;
dup.add(name);
const icon = byName.get(name);
if (!icon) { console.warn('⚠️ 缺失:', name); continue; }
list.push({ name, id: icon.id, body: icon.svg, group: g.label });
}
}
console.log('候选数:', list.length);
// —— 3. 生成预览页 ——
const S = 24, BG_FROM = '#0b1220', BG_TO = '#16294a';
function avatarSvg(body) {
return `<svg viewBox="0 0 1024 1024" width="128" height="128">
<defs><linearGradient id="g${list.length}" 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="c${list.length}"><circle cx="512" cy="512" r="512"/></clipPath></defs>
<rect width="1024" height="1024" fill="url(#g${list.length})" clip-path="url(#c${list.length})"/>
<circle cx="512" cy="512" r="452" fill="none" stroke="#3b82f6" stroke-width="5" opacity=".45"/>
<g transform="translate(512 512) scale(20) translate(-12 -12)" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</g>
</svg>`;
}
let cards = '';
let idx = 1;
for (const g of GROUPS) {
const members = list.filter(x => x.group === g.label);
if (!members.length) continue;
cards += `<div class="group-title">${g.label}(${members.length})</div>\n<div class="row">\n`;
for (const m of members) {
const n = String(idx).padStart(2, '0');
cards += `<label class="card" data-idx="${idx}">
<input type="checkbox" class="cb" value="${idx}" data-name="${m.name}">
${avatarSvg(m.body)}
<div class="no">#${n}</div>
<div class="name">${m.name}</div>
</label>\n`;
idx++;
}
cards += '</div>\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 个科技感头像候选 · 勾选后批量渲染</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; }
.group-title { font-size:14px; color:#9aa0a6; margin:18px 0 10px; }
.row { 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:11px; 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 个科技感头像候选</h1>
<div class="sub">资源库 lucide 源直选 · 点击卡片勾选(默认全选)· 确认后在下方复制编号,交由渲染为 PNG</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 id="groups">${cards}</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.mkdirSync(OUT_DIR, { recursive: true });
fs.writeFileSync(path.join(OUT_DIR, 'preview-100.html'), html, 'utf8');
console.log('🎉 已生成 → output/agent-avatars-png/preview-100.html(候选 ' + list.length + ' 个)');