#!/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 `
`;
}
// ============================================================
// 头像定义
// ============================================================
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}`);