/**
* 画个标 · DiceBear 头像风格引擎(M4)
* 29 种 DiceBear v10 风格定义内置(js/data/sources/dicebear-*.js,由构建脚本生成),
* 本模块:按"风格 × seed"确定性组合出无限头像 SVG(不占图标配额)。
*
* 风格定义结构(v10 单文件 JSON):
* canvas 画布模板(elements 树,component 节点按 name 引用 components)
* components { name: { width, height, rotate/scale/translate 随机范围, variants: { 名: { elements, weight } } } }
* colors { 色名: { values: [...] } }(element 属性 fill/stroke 以 {type:'color',name} 引用)
* attributes 根 svg 属性(fill、shape-rendering 等)
*
* 渲染:seed → 确定性 PRNG → 每 component 加权选 variant + 随机变换 → color 引用取色 → 组装 SVG
*/
const HGB_Dicebear = (function () {
'use strict';
const VERSION = '20260822e';
const STYLES = [
'adventurer', 'adventurer-neutral', 'avataaars', 'avataaars-neutral',
'big-ears', 'big-ears-neutral', 'big-smile', 'blobs', 'bottts', 'bottts-neutral',
'clay', 'constellation', 'critters', 'croodles', 'croodles-neutral',
'cutouts', 'disco', 'dylan', 'fun-emoji', 'glass', 'glyphs', 'icons',
'identicon', 'initial-face', 'initials', 'landscape', 'line-face', 'loops',
'lorelei', 'lorelei-neutral', 'micah', 'miniavs', 'moods', 'notionists',
'notionists-neutral', 'open-peeps', 'patchwork', 'personas', 'pixel-art',
'pixel-art-neutral', 'pixelbot', 'planets', 'rings', 'shape-grid', 'shapes',
'sprouts', 'squircles', 'stripes', 'thumbs', 'toon-head', 'triangles',
'voxel-art', 'voxel-bot', 'waves', 'weave'
];
const CN = {
adventurer: '冒险家', 'adventurer-neutral': '冒险家中性', avataaars: '阿凡达',
'avataaars-neutral': '阿凡达中性', 'big-ears': '大耳朵', 'big-ears-neutral': '大耳朵中性',
'big-smile': '大笑脸', blobs: '斑点', bottts: '机器人', 'bottts-neutral': '机器人中性',
clay: '黏土', constellation: '星座', critters: '小动物',
croodles: '卡通线条', 'croodles-neutral': '卡通线条中性', cutouts: '剪贴画',
disco: '迪斯科', dylan: '迪伦', 'fun-emoji': '趣味表情', glass: '玻璃拟态',
glyphs: '字形符', icons: '图标风', identicon: '指纹几何', 'initial-face': '首字脸',
initials: '首字母', landscape: '风景', 'line-face': '线条脸', loops: '回环',
lorelei: '洛雷莱', 'lorelei-neutral': '洛雷莱中性', micah: '米迦',
miniavs: '迷你头像', moods: '心情', notionists: 'Notion 风格', 'notionists-neutral': 'Notion 中性',
'open-peeps': '开放小人', patchwork: '拼布', personas: '人物剪影', 'pixel-art': '像素风',
'pixel-art-neutral': '像素风中性', pixelbot: '像素机器人', planets: '星球',
rings: '圆环几何', 'shape-grid': '形状网格', shapes: '几何形状',
sprouts: '萌芽', squircles: '圆角方', stripes: '条纹', thumbs: '拇指人',
'toon-head': '卡通头', triangles: '三角形', 'voxel-art': '体素艺术',
'voxel-bot': '体素机器人', waves: '波浪', weave: '编织'
};
/* ---------- 确定性 PRNG(seed 字符串 → mulberry32) ---------- */
function hash(str) {
let h = 2166136261;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
function rng(seedStr) {
let s = hash(seedStr) || 1;
return function () {
s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function pick(r, arr) {
return arr[Math.floor(r() * arr.length)];
}
function weightedPick(r, items) {
let total = 0;
for (const i of items) total += (i.weight != null ? i.weight : 0.01);
if (total <= 0) return items[0].key;
let x = r() * total;
for (const i of items) {
x -= (i.weight != null ? i.weight : 0.01);
if (x <= 0) return i.key;
}
return items[items.length - 1].key;
}
function rn(v) { return Math.round(v * 100) / 100; }
/** 变量引用解析(initials 风格:initial/fontFamily/fontWeight 等) */
function resolveVariable(name, seed, opts) {
switch (name) {
case 'initial':
case 'initials': {
const s = String(opts.initial || seed || '').trim();
if (s) return s[0].toUpperCase();
return opts.defaultInitial || 'D';
}
case 'fontFamily': return opts.fontFamily || 'Arial';
case 'fontWeight': return String(opts.fontWeight != null ? opts.fontWeight : 600);
default: return '';
}
}
/** 属性值:color 引用 → 从色板取色;variable 引用 → 变量解析;其他原样 */
function resolveAttr(v, r, colors, opts) {
if (v && typeof v === 'object') {
if (v.type === 'color' && v.name) {
const c = colors[v.name];
if (typeof c === 'string') return c; // 已解析的单一颜色
if (c && Array.isArray(c.values) && c.values.length) return pick(r, c.values);
return '#000000';
}
if (v.type === 'variable' && v.name) {
return resolveVariable(v.name, opts.seed, opts);
}
return '';
}
return v;
}
/* ---------- 递归渲染 element / component / text 节点 ---------- */
function renderComponent(comp, r, colors, components, opts) {
const names = Object.keys(comp.variants || {});
if (!names.length) return '';
const items = names.map(n => ({ key: n, weight: comp.variants[n].weight }));
const variant = comp.variants[weightedPick(r, items)] || {};
// 随机变换(rotate/scale/translate 范围;个别风格 translate 只有 x 或 y,缺字段视为 0)
const t = [];
if (comp.translate) {
const tx = comp.translate.x;
const ty = comp.translate.y;
const x = tx ? tx.min + r() * (tx.max - tx.min) : 0;
const y = ty ? ty.min + r() * (ty.max - ty.min) : 0;
if (x || y) t.push('translate(' + rn(x) + ' ' + rn(y) + ')');
}
if (comp.rotate) {
const deg = comp.rotate.min + r() * (comp.rotate.max - comp.rotate.min);
if (deg) t.push('rotate(' + rn(deg) + ')');
}
if (comp.scale) {
const s = comp.scale.min + r() * (comp.scale.max - comp.scale.min);
if (s !== 1) t.push('scale(' + rn(s) + ')');
}
const inner = (variant.elements || []).map(e => renderNode(e, r, colors, components, opts)).join('');
return t.length ? '' + inner + '' : inner;
}
/**
* 解析组件继承(DiceBear schema 的 extends 字段):
* - identicon 的 row01~row05 → row;glass 的 shape01/shape02 → shape
* 继承语义 = 复用父组件定义(无自身 variants 时完全继承;带环保护)
*/
function resolveComp(name, components) {
const seen = new Set();
let cur = name;
while (cur && !seen.has(cur)) {
seen.add(cur);
const c = (components || {})[cur];
if (!c) return (components || {})[name] || null;
if (!c.extends) return c;
if (c.variants && Object.keys(c.variants).length) return c;
cur = c.extends;
}
return (components || {})[name] || null;
}
/**
* 统一渲染节点:支持三种类型
* - component 节点(任意层级出现,如 shapes 的 )→ 渲染其变体
* - text 节点 {type:'text', value} → 渲染为纯文本(initials 风格文字,value 可为变量引用)
* - element 节点 → 拼属性 + 递归 children
* components 仅在 canvas 层传入(renderComponent 内部复用自身 variants 而非 components 表)
*/
function renderNode(el, r, colors, components, opts) {
if (!el) return '';
if (el.type === 'component') {
const comp = resolveComp(el.name, components);
if (!comp) return '';
const inner = renderComponent(comp, r, colors, components, opts);
// component 节点自带 transform 等属性(如 shapes 的 matrix 定位)
const attrs = [];
for (const k of Object.keys(el.attributes || {})) {
const v = el.attributes[k];
if (v == null) continue;
attrs.push(k + '="' + String(v).replace(/"/g, '"') + '"');
}
return '' + inner + '';
}
if (el.type === 'text') {
const v = resolveAttr(el.value, r, colors, opts);
return String(v != null ? v : '');
}
// element:拼属性(color 引用取色,数字取整)
const attrs = [];
for (const k of Object.keys(el.attributes || {})) {
const v = resolveAttr(el.attributes[k], r, colors, opts);
if (v === '' || v == null) continue;
const sv = typeof v === 'number' ? rn(v) : String(v);
attrs.push(k + '="' + sv.replace(/"/g, '"') + '"');
}
const children = (el.children || []).map(c => renderNode(c, r, colors, components, opts)).join('');
const name = (el.name || 'g').replace(/[^a-zA-Z0-9-]/g, '') || 'g';
return '<' + name + (attrs.length ? ' ' + attrs.join(' ') : '') +
(children ? '>' + children + '' + name + '>' : '/>');
}
/* ---------- 画布组装 ---------- */
function renderCanvas(canvas, r, colors, components, opts) {
return (canvas.elements || []).map(n => renderNode(n, r, colors, components, opts)).join('');
}
/** 画布是否已含显式 background 色元素(避免重复垫底) */
function hasBgElement(canvas) {
function scan(el) {
if (!el || typeof el !== 'object') return false;
const a = el.attributes || {};
for (const k of Object.keys(a)) {
const v = a[k];
if (v && typeof v === 'object' && v.type === 'color' && v.name === 'background') return true;
if (v === 'background') return true;
}
return (el.children || []).some(scan);
}
return (canvas && canvas.elements || []).some(scan);
}
/**
* 渲染单个风格头像
* @param {string} styleId 风格 id(如 adventurer / pixel-art)
* @param {string} seed 确定性种子(同 seed 同输出)
* @returns {{svg:string, styleId:string}} svg 为完整