Files
drawlogo/js/core/avatarGen.js
T

152 lines
6.2 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.
/**
* 画个标 · 核心:头像生成(Avatar)
* 头像 = 复用 Mark 纯图形引擎 + 头像专属约束 + 平台规格导出
* - 仅纯图形:过滤带文字布局(monogram 首字 / ribbon 绶带区),保证小尺寸清晰
* - 圆形构图优先:头像布局以圆形/圆角/对称构图为倾向,缩小不变形
* - 圆形裁切预览与导出:社交平台头像均以圆形展示
* - 平台规格一键导出:微信 / QQ / 抖音 / YouTube / App 图标 / Favicon + 自定义
*/
const HGB_AvatarGen = (function () {
'use strict';
/** 适合头像的纯图形布局(Mark 层,全部无文字) */
const AVATAR_LAYOUTS = [
'orb', // 圆标容器(径向渐变圆)
'badge', // 双环徽章
'seal', // 双环印章
'burst', // 光芒徽章(射线环)
'duo', // 双图标对称
'emblem', // 渐变盾徽
'hexagon', // 六边形徽章
'diamond', // 菱形徽章
'arch', // 拱门徽章
'geo', // 几何构形
'techcorner', // 科技角标
'fullbleed', // 头像专属:全出血大图标(无容器)
'concentric', // 头像专属:同心圆环
'centroid' // 头像专属:中心单环
];
/** 平台头像导出规格预设 */
const AVATAR_SIZES = [
{ id: 'wechat', name: '微信头像', size: 300, note: '微信/微博 300' },
{ id: 'qq', name: 'QQ 头像', size: 640, note: 'QQ 640' },
{ id: 'douyin', name: '抖音头像', size: 400, note: '抖音/快手 400' },
{ id: 'youtube', name: 'YouTube 头像', size: 800, note: '频道头像 800' },
{ id: 'app', name: 'App 图标', size: 1024, note: 'iOS/安卓 1024' },
{ id: 'favicon', name: '网站 Favicon', size: 64, note: '浏览器标签 64' }
];
/** 头像词根扩充的主力单色源(懒加载优先预载;多色 emoji/品牌源不参与词根搜索) */
const AVATAR_CORE_SOURCES = [
'lucide', 'tabler', 'feather', 'mdi', 'fluent', 'solar', 'msymbols',
'iconpark', 'hugeicons', 'mingcute', 'carbon', 'majesticons', 'pixelarticons', 'akar-icons'
];
/** 布局 id 是否适合头像(在头像白名单内) */
function isAvatarLayout(layoutId) {
return AVATAR_LAYOUTS.includes(layoutId);
}
/** 洗牌(保证多套头像图标各异) */
function shuffle(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
/** 内置库关键词搜索:图标名包含任一英文词根即命中(返回带库前缀 id 的图标数组;懒加载适配:先预载核心源再搜已加载源) */
async function searchBuiltin(roots, limit) {
// 词根搜索依赖已加载源 → 预载头像核心源(容忍单个失败)
await HGB_IconLib.preload(AVATAR_CORE_SOURCES);
const out = [];
const seen = new Set();
const kws = roots.filter(k => k.length >= 3);
if (!kws.length) return out;
for (const kw of kws) {
for (const ic of HGB_IconLib.searchLoaded(kw, limit)) {
const fullId = ic.id;
if (seen.has(fullId)) continue;
seen.add(fullId);
out.push({ id: fullId, name: ic.name, svg: ic.svg, source: ic.source, type: ic.type });
if (out.length >= limit) return out;
}
}
return out;
}
/** 头像素材池:用户选中 + 行业图标优先,内置库按行业图标词根扩充(解决"素材不够/重复") */
async function avatarIconPool(ind, userIcons) {
const picked = (userIcons || []).filter(Boolean).slice(0, 5);
const indIcons = (ind.icons || []);
const poolMap = new Map();
// 本地段:用户选中 + 行业图标(保持插入顺序)
for (const ic of picked) poolMap.set(ic.id, ic);
for (const ic of indIcons) if (!poolMap.has(ic.id)) poolMap.set(ic.id, ic);
const localCount = poolMap.size;
// 词根:行业图标 id 按 '-' 拆分(英文语义词根,如 chef-hat → chef/hat)
const roots = [];
for (const ic of indIcons) {
for (const part of String(ic.id || '').split('-')) {
const k = part.toLowerCase();
if (k.length >= 3 && !roots.includes(k)) roots.push(k);
}
}
if (roots.length) {
for (const ic of await searchBuiltin(roots, 60)) {
if (!poolMap.has(ic.id)) poolMap.set(ic.id, ic);
}
}
const all = Array.from(poolMap.values());
// 分段洗牌:行业/用户图标在前(高度相关),内置库扩充在后;各自洗牌保证多套各异
return shuffle(all.slice(0, localCount)).concat(shuffle(all.slice(localCount)));
}
/** 批量生成头像(复用 Mark 生成引擎 + 头像布局白名单 + 内置库素材扩充) */
async function generateAvatars(opt) {
const ind = opt.industry || {};
const count = Math.min(Math.max(opt.count || 5, 1), 20);
// 1) 基础批量生成(风格全覆盖 + 头像布局白名单)
const base = HGB_Generator.generatePlans({ ...opt, count, layoutWhitelist: AVATAR_LAYOUTS });
// 2) 头像素材池扩充(行业/用户图标在前 + 内置库词根扩充;懒加载预载核心源)
const pool = await avatarIconPool(ind, opt.icons);
if (pool.length < 2) return base;
// 3) 逐套替换为池中不同图标(保持风格/布局/配色不变,图标素材多样)
return base.map((p, i) => {
const ic1 = pool[i % pool.length];
const ic2 = pool[(i + Math.ceil(pool.length / 2)) % pool.length];
if (!ic1) return p;
const v = HGB_Generator.generateVariant({
...opt,
layoutWhitelist: AVATAR_LAYOUTS,
layoutId: p.layoutId,
styleId: p.styleId,
icons: ic2 && ic2.id !== ic1.id ? [ic1, ic2] : [ic1],
lockup: p.lockup
});
return Object.assign({}, v, {
id: p.id, name: p.name, concept: p.concept, aiIcon: ic1.id
});
});
}
/** 生成单个头像变体(预览弹窗微调) */
function generateVariant(opt) {
return HGB_Generator.generateVariant({
...opt,
layoutWhitelist: AVATAR_LAYOUTS
});
}
return {
AVATAR_LAYOUTS,
AVATAR_SIZES,
isAvatarLayout,
generateAvatars,
generateVariant
};
})();