feat: 首次推送到Gitea - 完整项目代码 + 安全加固 + 知识库
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
#!/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) =>
|
||||
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" opacity="0.9"/><circle cx="${cx-8}" cy="${cy-2}" r="3" fill="${color}"/><circle cx="${cx+8}" cy="${cy-2}" r="3" fill="${color}"/><path d="M${cx-8} ${cy+6} Q${cx} ${cy+16} ${cx+8} ${cy+6}" stroke="${color}" fill="none" stroke-width="2" stroke-linecap="round"/>`,
|
||||
|
||||
face_smile: (cx, cy, r, color) =>
|
||||
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" opacity="0.9"/><circle cx="${cx-7}" cy="${cy-3}" r="2.5" fill="${color}"/><circle cx="${cx+7}" cy="${cy-3}" r="2.5" fill="${color}"/><path d="M${cx-8} ${cy+5} Q${cx} ${cy+16} ${cx+8} ${cy+5}" stroke="${color}" fill="none" stroke-width="2.5" stroke-linecap="round"/>`,
|
||||
|
||||
face_cool: (cx, cy, r, color) =>
|
||||
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" opacity="0.9"/><rect x="${cx-12}" y="${cy-6}" width="8" height="5" rx="2" fill="${color}"/><rect x="${cx+4}" y="${cy-6}" width="8" height="5" rx="2" fill="${color}"/><path d="M${cx-5} ${cy+6} Q${cx} ${cy+14} ${cx+5} ${cy+6}" stroke="${color}" fill="none" stroke-width="2" stroke-linecap="round"/>`,
|
||||
|
||||
face_wink: (cx, cy, r, color) =>
|
||||
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" opacity="0.9"/><circle cx="${cx-7}" cy="${cy-3}" r="2.5" fill="${color}"/><path d="M${cx+3} ${cy-5} L${cx+11} ${cy-1}" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/><path d="M${cx-8} ${cy+5} Q${cx} ${cy+15} ${cx+8} ${cy+5}" stroke="${color}" fill="none" stroke-width="2.5" stroke-linecap="round"/>`,
|
||||
};
|
||||
|
||||
// 职业/商务图标
|
||||
const BUSINESS_ICONS = {
|
||||
briefcase: (cx, cy, s, color) =>
|
||||
`<rect x="${cx-s}" y="${cy-s*0.3}" width="${s*2}" height="${s*1.4}" rx="${s*0.2}" fill="white" opacity="0.85"/><rect x="${cx-s*0.3}" y="${cy-s*0.8}" width="${s*0.6}" height="${s*0.5}" rx="${s*0.1}" fill="white" opacity="0.85"/><rect x="${cx-s*0.5}" y="${cy-s*0.15}" width="${s}" height="${s*0.3}" rx="${s*0.1}" fill="${color}" opacity="0.6"/>`,
|
||||
|
||||
tie: (cx, cy, s, color) =>
|
||||
`<polygon points="${cx},${cy-s} ${cx-s*0.4},${cy-s*0.2} ${cx-s*0.6},${cy+s}" fill="white" opacity="0.8"/><polygon points="${cx},${cy-s} ${cx+s*0.4},${cy-s*0.2} ${cx+s*0.6},${cy+s}" fill="white" opacity="0.8"/><polygon points="${cx},${cy-s} ${cx-s*0.65},${cy+s} ${cx+s*0.65},${cy+s}" fill="white" opacity="0.9"/>`,
|
||||
|
||||
chart: (cx, cy, s, color) =>
|
||||
`<rect x="${cx-s*0.8}" y="${cy-s*0.2}" width="${s*1.6}" height="${s*1.4}" rx="${s*0.15}" fill="white" opacity="0.15"/><rect x="${cx-s*0.5}" y="${cy+s*0.1}" width="${s*0.3}" height="${s*0.8}" rx="${s*0.1}" fill="white" opacity="0.7"/><rect x="${cx-s*0.1}" y="${cy-s*0.3}" width="${s*0.3}" height="${s*1.2}" rx="${s*0.1}" fill="white" opacity="0.7"/><rect x="${cx+s*0.3}" y="${cy}" width="${s*0.3}" height="${s*0.5}" rx="${s*0.1}" fill="white" opacity="0.7"/>`,
|
||||
};
|
||||
|
||||
// 科技/游戏图标
|
||||
const TECH_ICONS = {
|
||||
circuit: (cx, cy, s, color) =>
|
||||
`<rect x="${cx-s*0.8}" y="${cy-s*0.8}" width="${s*1.6}" height="${s*1.6}" rx="${s*0.2}" fill="white" opacity="0.12"/><path d="M${cx-s*0.5} ${cy} L${cx-s*0.2} ${cy} L${cx-s*0.2} ${cy-s*0.4} L${cx+s*0.2} ${cy-s*0.4} L${cx+s*0.2} ${cy+s*0.4} L${cx+s*0.5} ${cy+s*0.4}" stroke="white" fill="none" stroke-width="3" opacity="0.6"/><circle cx="${cx-s*0.5}" cy="${cy}" r="${s*0.1}" fill="white" opacity="0.8"/><circle cx="${cx+s*0.5}" cy="${cy+s*0.4}" r="${s*0.1}" fill="white" opacity="0.8"/>`,
|
||||
|
||||
gamepad: (cx, cy, s, color) =>
|
||||
`<rect x="${cx-s}" y="${cy-s*0.4}" width="${s*2}" height="${s*1.2}" rx="${s*0.3}" fill="white" opacity="0.85"/><rect x="${cx-s*0.4}" y="${cy-s*0.8}" width="${s*0.4}" height="${s*0.5}" rx="${s*0.1}" fill="white" opacity="0.85"/><rect x="${cx+s*0.05}" y="${cy-s*0.8}" width="${s*0.4}" height="${s*0.5}" rx="${s*0.1}" fill="white" opacity="0.85"/><circle cx="${cx-s*0.5}" cy="${cy}" r="${s*0.2}" fill="${color}" opacity="0.5"/><circle cx="${cx+s*0.5}" cy="${cy}" r="${s*0.2}" fill="${color}" opacity="0.5"/>`,
|
||||
|
||||
code: (cx, cy, s, color) =>
|
||||
`<path d="M${cx-s*0.6} ${cy} L${cx-s*0.2} ${cy-s*0.5} L${cx-s*0.2} ${cy+s*0.5}" stroke="white" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/><path d="M${cx+s*0.6} ${cy} L${cx+s*0.2} ${cy-s*0.5} L${cx+s*0.2} ${cy+s*0.5}" stroke="white" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>`,
|
||||
};
|
||||
|
||||
// 自然/景观图标
|
||||
const NATURE_ICONS = {
|
||||
mountain: (cx, cy, s, color) =>
|
||||
`<polygon points="${cx-s*0.8},${cy+s*0.5} ${cx},${cy-s*0.8} ${cx+s*0.8},${cy+s*0.5}" fill="white" opacity="0.2"/><polyline points="${cx-s*0.4},${cy} ${cx-s*0.2},${cy} ${cx},${cy-s*0.3}" stroke="white" fill="none" stroke-width="3" opacity="0.8"/><polyline points="${cx},${cy-s*0.3} ${cx+s*0.3},${cy+s*0.2} ${cx+s*0.6},${cy+s*0.2}" stroke="white" fill="none" stroke-width="3" opacity="0.8"/>`,
|
||||
|
||||
sun: (cx, cy, r, color) =>
|
||||
`<circle cx="${cx}" cy="${cy}" r="${r*0.5}" fill="white" opacity="0.9"/>${Array.from({length:8}, (_,i) => {const a = i*Math.PI/4; return `<line x1="${cx+Math.cos(a)*r*0.65}" y1="${cy+Math.sin(a)*r*0.65}" x2="${cx+Math.cos(a)*r}" y2="${cy+Math.sin(a)*r}" stroke="white" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>`}).join("")}`,
|
||||
|
||||
wave: (cx, cy, s, color) =>
|
||||
`<path d="M${cx-s*0.9} ${cy} Q${cx-s*0.45} ${cy-s*0.4} ${cx} ${cy} T${cx+s*0.9} ${cy}" stroke="white" fill="none" stroke-width="4" stroke-linecap="round" opacity="0.8"/>`,
|
||||
|
||||
leaf: (cx, cy, s, color) =>
|
||||
`<path d="M${cx} ${cy-s*0.8} Q${cx+s*0.6} ${cy} ${cx} ${cy+s*0.6} Q${cx-s*0.6} ${cy} ${cx} ${cy-s*0.8}Z" fill="white" opacity="0.8"/><line x1="${cx}" y1="${cy-s*0.8}" x2="${cx}" y2="${cy+s*0.6}" stroke="${color}" stroke-width="1.5" opacity="0.5"/>`,
|
||||
};
|
||||
|
||||
// 创意/艺术图标
|
||||
const ART_ICONS = {
|
||||
star: (cx, cy, r, color) =>
|
||||
`<polygon points="${cx},${cy-r} ${cx+r*0.3},${cy-r*0.3} ${cx+r},${cy} ${cx+r*0.3},${cy+r*0.3} ${cx},${cy+r} ${cx-r*0.3},${cy+r*0.3} ${cx-r},${cy} ${cx-r*0.3},${cy-r*0.3}" fill="white" opacity="0.85"/>`,
|
||||
|
||||
diamond: (cx, cy, s, color) =>
|
||||
`<polygon points="${cx},${cy-s} ${cx+s},${cy} ${cx},${cy+s} ${cx-s},${cy}" fill="white" opacity="0.8" transform="rotate(15 ${cx} ${cy})"/>`,
|
||||
|
||||
heart: (cx, cy, s, color) =>
|
||||
`<path d="M${cx} ${cy+s*0.5} C${cx-s*0.8} ${cy-s*0.3} ${cx-s*0.5} ${cy-s*0.9} ${cx} ${cy-s*0.3} C${cx+s*0.5} ${cy-s*0.9} ${cx+s*0.8} ${cy-s*0.3} ${cx} ${cy+s*0.5}Z" fill="white" opacity="0.85"/>`,
|
||||
|
||||
crown: (cx, cy, s, color) =>
|
||||
`<rect x="${cx-s*0.6}" y="${cy}" width="${s*1.2}" height="${s*0.6}" rx="${s*0.1}" fill="white" opacity="0.8"/><rect x="${cx-s*0.7}" y="${cy-s*0.4}" width="${s*0.3}" height="${s*0.5}" rx="${s*0.08}" fill="white" opacity="0.8"/><rect x="${cx-s*0.1}" y="${cy-s*0.7}" width="${s*0.2}" height="${s*0.8}" rx="${s*0.08}" fill="white" opacity="0.8"/><rect x="${cx+s*0.4}" y="${cy-s*0.4}" width="${s*0.3}" height="${s*0.5}" rx="${s*0.08}" fill="white" opacity="0.8"/>`,
|
||||
};
|
||||
|
||||
// 运动图标
|
||||
const SPORT_ICONS = {
|
||||
basketball: (cx, cy, r, color) =>
|
||||
`<circle cx="${cx}" cy="${cy}" r="${r}" fill="white" opacity="0.85"/><path d="M${cx-r} ${cy} Q${cx} ${cy-r*0.3} ${cx+r} ${cy}" stroke="${color}" fill="none" stroke-width="3" opacity="0.6"/><path d="M${cx} ${cy-r} Q${cx} ${cy} ${cx} ${cy+r}" stroke="${color}" fill="none" stroke-width="3" opacity="0.6"/>`,
|
||||
|
||||
trophy: (cx, cy, s, color) =>
|
||||
`<rect x="${cx-s*0.3}" y="${cy-s*0.2}" width="${s*0.6}" height="${s*0.8}" fill="white" opacity="0.85"/><rect x="${cx-s*0.2}" y="${cy+s*0.6}" width="${s*0.4}" height="${s*0.2}" rx="${s*0.05}" fill="white" opacity="0.7"/><path d="M${cx-s*0.5} ${cy-s*0.2} Q${cx-s*0.5} ${cy-s*0.6} ${cx-s*0.2} ${cy-s*0.6} L${cx-s*0.2} ${cy-s*0.2}" stroke="white" fill="none" stroke-width="2.5" opacity="0.6"/><path d="M${cx+s*0.5} ${cy-s*0.2} Q${cx+s*0.5} ${cy-s*0.6} ${cx+s*0.2} ${cy-s*0.6} L${cx+s*0.2} ${cy-s*0.2}" stroke="white" fill="none" stroke-width="2.5" opacity="0.6"/>`,
|
||||
};
|
||||
|
||||
// 抽象几何
|
||||
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(`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="white" stroke-width="${1.5 + i*0.5}" opacity="${0.7 - i*0.12}"/>`);
|
||||
}
|
||||
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(`<rect x="${x}" y="${y}" width="${s*0.32}" height="${s*0.32}" rx="${s*0.05}" fill="white" opacity="0.25"/>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
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(`<line x1="${cx}" y1="${cy}" x2="${cx + Math.cos(angle) * s * 0.9}" y2="${cy + Math.sin(angle) * s * 0.9}" stroke="white" stroke-width="${1 + i%3}" opacity="0.15" stroke-linecap="round"/>`);
|
||||
}
|
||||
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(`<circle cx="${x}" cy="${y}" r="${size}" fill="white" opacity="${0.6 - i * 0.02}"/>`);
|
||||
}
|
||||
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(`<polygon points="${x},${y-size} ${x-size*0.8},${y+size*0.5} ${x+size*0.8},${y+size*0.5}" fill="white" opacity="0.2" transform="rotate(${hash*13} ${x} ${y})"/>`);
|
||||
}
|
||||
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 = `<clipPath id="clip"><circle cx="256" cy="256" r="230"/></clipPath>`;
|
||||
bgRect = `<rect width="512" height="512" fill="url(#bg)" clip-path="url(#clip)"/>`;
|
||||
} else if (shapeType === "rounded") {
|
||||
bgDef = `<clipPath id="clip"><rect x="26" y="26" width="460" height="460" rx="120"/></clipPath>`;
|
||||
bgRect = `<rect width="512" height="512" fill="url(#bg)" clip-path="url(#clip)"/>`;
|
||||
} else if (shapeType === "hexagon") {
|
||||
const points = "256,36 476,146 476,366 256,476 36,366 36,146";
|
||||
bgDef = `<clipPath id="clip"><polygon points="${points}"/></clipPath>`;
|
||||
bgRect = `<rect width="512" height="512" fill="url(#bg)" clip-path="url(#clip)"/>`;
|
||||
bgRect += `<polygon points="${points}" fill="none" stroke="white" stroke-width="4" opacity="0.15"/>`;
|
||||
} else {
|
||||
// full square
|
||||
bgRect = `<rect width="512" height="512" fill="url(#bg)"/>`;
|
||||
}
|
||||
|
||||
// 渐变角度随机
|
||||
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 += `<circle cx="${x}" cy="${y}" r="${r}" fill="white" opacity="0.2"/>`;
|
||||
}
|
||||
} 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 += `<path d="M${x-s} ${y} L${x+s} ${y} M${x} ${y-s} L${x} ${y+s}" stroke="white" stroke-width="1.5" opacity="0.25" stroke-linecap="round"/>`;
|
||||
}
|
||||
} else if (decorType === 2) {
|
||||
// 波浪线底部
|
||||
decorations += `<path d="M0 400 Q50 380 100 400 T200 400 T300 400 T400 400 T512 400 L512 512 L0 512Z" fill="white" opacity="0.1"/>`;
|
||||
} else if (decorType === 3) {
|
||||
// 对角条纹
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const y = 80 + i * 55;
|
||||
decorations += `<line x1="0" y1="${y}" x2="512" y2="${y}" stroke="white" stroke-width="2" opacity="0.08"/>`;
|
||||
}
|
||||
} else if (decorType === 4) {
|
||||
// 边框圆角框
|
||||
decorations += `<rect x="30" y="30" width="452" height="452" rx="40" fill="none" stroke="white" stroke-width="3" opacity="0.15"/>`;
|
||||
decorations += `<rect x="50" y="50" width="412" height="412" rx="30" fill="none" stroke="white" stroke-width="1.5" opacity="0.1"/>`;
|
||||
} 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 `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
${bgDef ? `<defs>` : ""}
|
||||
${bgDef ? `<linearGradient id="bg" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
||||
<stop offset="0%" stop-color="${c1}"/>
|
||||
<stop offset="100%" stop-color="${c2}"/>
|
||||
</linearGradient>` : ""}
|
||||
${bgDef ? `</defs>` : ""}
|
||||
${bgRect}
|
||||
${decorations}
|
||||
${centerIcon}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 头像定义
|
||||
// ============================================================
|
||||
|
||||
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}`);
|
||||
Reference in New Issue
Block a user