#!/usr/bin/env node // scripts/generate-avatar-library.mjs // 生成 500 个多样化 SVG 头像库 // 分类:男性 170 / 女性 170 / 中性 160 // 多种主题风格:商务、创意、科技、运动、自然、抽象、可爱、复古等 import { writeFileSync, mkdirSync, existsSync } from "fs"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AVATAR_DIR = resolve(__dirname, "..", "public", "avatars"); const MANIFEST_PATH = resolve(AVATAR_DIR, "manifest.json"); if (!existsSync(AVATAR_DIR)) mkdirSync(AVATAR_DIR, { recursive: true }); // ============================================================ // 色彩体系 - 参考主流论坛的头像配色 // ============================================================ const COLOR_SETS = { // 商务/专业系 business: [ ["#1e3a5f", "#2d5f8a"], ["#2c3e50", "#34495e"], ["#1a365d", "#2b6cb0"], ["#1e3a5f", "#4338ca"], ["#1b2838", "#3182ce"], ["#1a202c", "#2d3748"], ["#13294b", "#1e4d8c"], ["#1e3a5f", "#7c3aed"], ], // 创意/艺术系 creative: [ ["#e11d48", "#be185d"], ["#7c3aed", "#db2777"], ["#dc2626", "#ea580c"], ["#6d28d9", "#c026d3"], ["#be123c", "#f43f5e"], ["#9333ea", "#ec4899"], ["#b91c1c", "#f97316"], ["#8b5cf6", "#d946ef"], ], // 自然/清新系 nature: [ ["#059669", "#10b981"], ["#047857", "#34d399"], ["#166534", "#22c55e"], ["#15803d", "#84cc16"], ["#065f46", "#14b8a6"], ["#0d9488", "#2dd4bf"], ["#047857", "#a3e635"], ["#0f766e", "#5eead4"], ], // 科技/未来系 tech: [ ["#2563eb", "#7c3aed"], ["#1d4ed8", "#06b6d4"], ["#3730a3", "#8b5cf6"], ["#1e40af", "#0ea5e9"], ["#312e81", "#6366f1"], ["#0369a1", "#22d3ee"], ["#1d4ed8", "#f0abfc"], ["#4338ca", "#38bdf8"], ], // 温暖/活力系 warm: [ ["#ea580c", "#f97316"], ["#dc2626", "#f59e0b"], ["#b91c1c", "#fb923c"], ["#c2410c", "#fbbf24"], ["#9a3412", "#f59e0b"], ["#d97706", "#fcd34d"], ["#e11d48", "#fb923c"], ["#b45309", "#fbbf24"], ], // 柔和/可爱系 cute: [ ["#db2777", "#f472b6"], ["#e11d48", "#fb7185"], ["#c026d3", "#e879f9"], ["#f43f5e", "#fda4af"], ["#a855f7", "#c4b5fd"], ["#ec4899", "#f9a8d4"], ["#be185d", "#fbbf24"], ["#d946ef", "#f0abfc"], ], // 深色/质感系 dark: [ ["#0f172a", "#334155"], ["#18181b", "#3f3f46"], ["#1c1917", "#44403c"], ["#171717", "#404040"], ["#0f172a", "#475569"], ["#1e1e2e", "#313244"], ], // 复古系 retro: [ ["#92400e", "#b45309"], ["#854d0e", "#a16207"], ["#78716c", "#57534e"], ["#78350f", "#92400e"], ["#713f12", "#a16207"], ["#9a3412", "#b45309"], ], }; // ============================================================ // 图标路径 - 各种风格的简单图形 // ============================================================ // 面部/人物相关图标 const FACE_ICONS = { // 抽象头像系列 - 不同发型/风格 face_round: (cx, cy, r, color) => ``, face_smile: (cx, cy, r, color) => ``, face_cool: (cx, cy, r, color) => ``, face_wink: (cx, cy, r, color) => ``, }; // 职业/商务图标 const BUSINESS_ICONS = { briefcase: (cx, cy, s, color) => ``, tie: (cx, cy, s, color) => ``, chart: (cx, cy, s, color) => ``, }; // 科技/游戏图标 const TECH_ICONS = { circuit: (cx, cy, s, color) => ``, gamepad: (cx, cy, s, color) => ``, code: (cx, cy, s, color) => ``, }; // 自然/景观图标 const NATURE_ICONS = { mountain: (cx, cy, s, color) => ``, sun: (cx, cy, r, color) => `${Array.from({length:8}, (_,i) => {const a = i*Math.PI/4; return ``}).join("")}`, wave: (cx, cy, s, color) => ``, leaf: (cx, cy, s, color) => ``, }; // 创意/艺术图标 const ART_ICONS = { star: (cx, cy, r, color) => ``, diamond: (cx, cy, s, color) => ``, heart: (cx, cy, s, color) => ``, crown: (cx, cy, s, color) => ``, }; // 运动图标 const SPORT_ICONS = { basketball: (cx, cy, r, color) => ``, trophy: (cx, cy, s, color) => ``, }; // 抽象几何 const ABSTRACT_SHAPES = { concentric: (cx, cy, s, color, hash) => { const rings = []; for (let i = 0; i < 5; i++) { const r = s * 0.25 + i * s * 0.15; rings.push(``); } return rings.join(""); }, checker: (cx, cy, s, color, hash) => { const cells = []; for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if ((r + c + hash) % 2 === 0) { const x = cx - s * 0.6 + c * s * 0.4; const y = cy - s * 0.6 + r * s * 0.4; cells.push(``); } } } return cells.join(""); }, rays: (cx, cy, s, color, hash) => { const lines = []; for (let i = 0; i < 12; i++) { const angle = (i * 30 + hash * 7) * Math.PI / 180; lines.push(``); } return lines.join(""); }, dots: (cx, cy, s, color, hash) => { const dots = []; for (let i = 0; i < 25; i++) { const angle = i * 0.8; const r = s * 0.1 + i * s * 0.03; const x = cx + Math.cos(angle) * r; const y = cy + Math.sin(angle) * r; const size = 2 + (i % 4); dots.push(``); } return dots.join(""); }, triangles: (cx, cy, s, color, hash) => { const tris = []; for (let i = 0; i < 6; i++) { const angle = i * 60 * Math.PI / 180; const x = cx + Math.cos(angle) * s * 0.45; const y = cy + Math.sin(angle) * s * 0.45; const size = s * 0.25; tris.push(``); } return tris.join(""); }, }; // ============================================================ // SVG 生成函数 // ============================================================ function hash(str) { let h = 0x811c9dc5; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h; } function generateSvg(id, category, colors, shapeType, iconType, params) { const [c1, c2] = colors; const idHash = hash(id); // 背景形状 let bgDef = ""; let bgRect = ""; if (shapeType === "circle") { bgDef = ``; bgRect = ``; } else if (shapeType === "rounded") { bgDef = ``; bgRect = ``; } else if (shapeType === "hexagon") { const points = "256,36 476,146 476,366 256,476 36,366 36,146"; bgDef = ``; bgRect = ``; bgRect += ``; } else { // full square bgRect = ``; } // 渐变角度随机 const angles = ["0%", "100%", "45deg", "135deg", "225deg", "315deg", "180deg", "270deg"]; const gradAngle = angles[idHash % angles.length]; const x1 = gradAngle.includes("deg") ? (idHash % 2 === 0 ? "0%" : "50%") : "0%"; const y1 = gradAngle.includes("deg") ? "0%" : "0%"; const x2 = gradAngle.includes("deg") ? (idHash % 2 === 0 ? "100%" : "50%") : "100%"; const y2 = gradAngle.includes("deg") ? "100%" : "100%"; // 装饰元素 let decorations = ""; const decorType = idHash % 6; if (decorType === 0) { // 圆点装饰 for (let i = 0; i < 8; i++) { const angle = (i * 45 + idHash * 3) * Math.PI / 180; const dist = 160 + (idHash % 40); const x = 256 + Math.cos(angle) * dist; const y = 256 + Math.sin(angle) * dist; const r = 6 + (idHash % 15); decorations += ``; } } else if (decorType === 1) { // 十字星装饰 for (let i = 0; i < 6; i++) { const x = 60 + (idHash * (i + 1)) % 400; const y = 60 + (idHash * (i + 3)) % 400; const s = 5 + (idHash % 8); decorations += ``; } } else if (decorType === 2) { // 波浪线底部 decorations += ``; } else if (decorType === 3) { // 对角条纹 for (let i = 0; i < 8; i++) { const y = 80 + i * 55; decorations += ``; } } else if (decorType === 4) { // 边框圆角框 decorations += ``; decorations += ``; } else { // 无额外装饰 } // 中央图标 let centerIcon = ""; const cx = 256, cy = 250; if (iconType === "face") { const faces = Object.values(FACE_ICONS); centerIcon = faces[idHash % faces.length](cx, cy, 70, c1); } else if (iconType === "business") { const icons = Object.values(BUSINESS_ICONS); centerIcon = icons[idHash % icons.length](cx, cy, 60, c1); } else if (iconType === "tech") { const icons = Object.values(TECH_ICONS); centerIcon = icons[idHash % icons.length](cx, cy, 60, c1); } else if (iconType === "nature") { const icons = Object.values(NATURE_ICONS); centerIcon = icons[idHash % icons.length](cx, cy, 65, c1); } else if (iconType === "art") { const icons = Object.values(ART_ICONS); centerIcon = icons[idHash % icons.length](cx, cy, 55, c1); } else if (iconType === "sport") { const icons = Object.values(SPORT_ICONS); centerIcon = icons[idHash % icons.length](cx, cy, 60, c1); } else if (iconType === "abstract") { const keys = Object.keys(ABSTRACT_SHAPES); centerIcon = ABSTRACT_SHAPES[keys[idHash % keys.length]](cx, cy, 180, c1, idHash); } return ` ${bgDef ? `` : ""} ${bgDef ? ` ` : ""} ${bgDef ? `` : ""} ${bgRect} ${decorations} ${centerIcon} `; } // ============================================================ // 头像定义 // ============================================================ const AVATAR_DEFS = []; // --- 男性头像 (170个) --- const MALE_THEMES = [ { name: "business", icon: "business", shape: "circle", colors: "business", count: 35 }, { name: "business", icon: "face", shape: "rounded", colors: "business", count: 15 }, { name: "tech", icon: "tech", shape: "rounded", colors: "tech", count: 25 }, { name: "tech", icon: "face", shape: "circle", colors: "tech", count: 15 }, { name: "sport", icon: "sport", shape: "circle", colors: "warm", count: 20 }, { name: "sport", icon: "face", shape: "rounded", colors: "warm", count: 15 }, { name: "creative", icon: "art", shape: "rounded", colors: "creative", count: 20 }, { name: "casual", icon: "face", shape: "circle", colors: "nature", count: 15 }, { name: "dark", icon: "abstract", shape: "full", colors: "dark", count: 10 }, ]; let maleIdx = 0; for (const theme of MALE_THEMES) { const colorSet = COLOR_SETS[theme.colors]; for (let i = 0; i < theme.count; i++) { const id = `male_${theme.name}_${String(i).padStart(3, "0")}`; const colors = colorSet[(maleIdx + i) % colorSet.length]; const shape = theme.icon === "abstract" ? "full" : ((maleIdx + i) % 3 === 0 ? theme.shape : (maleIdx + i) % 3 === 1 ? "rounded" : "circle"); AVATAR_DEFS.push({ id, gender: "male", theme: theme.name, colors, shape, iconType: theme.icon, }); } maleIdx += theme.count; } // --- 女性头像 (170个) --- const FEMALE_THEMES = [ { name: "creative", icon: "art", shape: "rounded", colors: "creative", count: 30 }, { name: "creative", icon: "face", shape: "circle", colors: "creative", count: 15 }, { name: "business", icon: "business", shape: "circle", colors: "business", count: 20 }, { name: "business", icon: "face", shape: "rounded", colors: "business", count: 15 }, { name: "cute", icon: "face", shape: "circle", colors: "cute", count: 25 }, { name: "cute", icon: "nature", shape: "rounded", colors: "cute", count: 15 }, { name: "tech", icon: "tech", shape: "rounded", colors: "tech", count: 20 }, { name: "nature", icon: "nature", shape: "circle", colors: "nature", count: 15 }, { name: "sport", icon: "sport", shape: "circle", colors: "warm", count: 15 }, ]; let femaleIdx = 0; for (const theme of FEMALE_THEMES) { const colorSet = COLOR_SETS[theme.colors]; for (let i = 0; i < theme.count; i++) { const id = `female_${theme.name}_${String(i).padStart(3, "0")}`; const colors = colorSet[(femaleIdx + i) % colorSet.length]; const shape = (femaleIdx + i) % 3 === 0 ? theme.shape : (femaleIdx + i) % 3 === 1 ? "rounded" : "circle"; AVATAR_DEFS.push({ id, gender: "female", theme: theme.name, colors, shape, iconType: theme.icon, }); } femaleIdx += theme.count; } // --- 中性头像 (160个) --- const NEUTRAL_THEMES = [ { name: "abstract", icon: "abstract", shape: "full", colors: "dark", count: 30 }, { name: "nature", icon: "nature", shape: "circle", colors: "nature", count: 25 }, { name: "minimal", icon: "abstract", shape: "circle", colors: "tech", count: 25 }, { name: "cute", icon: "face", shape: "circle", colors: "cute", count: 20 }, { name: "retro", icon: "art", shape: "rounded", colors: "retro", count: 20 }, { name: "geometric", icon: "abstract", shape: "hexagon", colors: "creative", count: 20 }, { name: "space", icon: "nature", shape: "full", colors: "dark", count: 20 }, ]; let neutralIdx = 0; for (const theme of NEUTRAL_THEMES) { const colorSet = COLOR_SETS[theme.colors]; for (let i = 0; i < theme.count; i++) { const id = `neutral_${theme.name}_${String(i).padStart(3, "0")}`; const colors = colorSet[(neutralIdx + i) % colorSet.length]; const shape = theme.icon === "abstract" ? ((neutralIdx + i) % 4 === 0 ? "full" : (neutralIdx + i) % 4 === 1 ? "circle" : (neutralIdx + i) % 4 === 2 ? "rounded" : "hexagon") : theme.shape; AVATAR_DEFS.push({ id, gender: "neutral", theme: theme.name, colors, shape, iconType: theme.icon, }); } neutralIdx += theme.count; } // ============================================================ // 生成并写入 // ============================================================ console.log(`生成 ${AVATAR_DEFS.length} 个头像...`); const manifest = { generatedAt: new Date().toISOString(), total: AVATAR_DEFS.length, categories: { male: AVATAR_DEFS.filter(a => a.gender === "male").length, female: AVATAR_DEFS.filter(a => a.gender === "female").length, neutral: AVATAR_DEFS.filter(a => a.gender === "neutral").length, }, themes: {}, avatars: [], }; for (const def of AVATAR_DEFS) { const svg = generateSvg(def.id, def.theme, def.colors, def.shape, def.iconType, {}); const filename = `${def.id}.svg`; writeFileSync(resolve(AVATAR_DIR, filename), svg, "utf-8"); if (!manifest.themes[def.theme]) manifest.themes[def.theme] = 0; manifest.themes[def.theme]++; manifest.avatars.push({ id: def.id, gender: def.gender, theme: def.theme, url: `/avatars/${filename}`, colors: def.colors, shape: def.shape, }); } writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf-8"); console.log(`✅ 完成!`); console.log(` 男性: ${manifest.categories.male} 个`); console.log(` 女性: ${manifest.categories.female} 个`); console.log(` 中性: ${manifest.categories.neutral} 个`); console.log(` 主题分布:`, manifest.themes); console.log(` 输出目录: ${AVATAR_DIR}`); console.log(` 清单文件: ${MANIFEST_PATH}`);