feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
+542
@@ -0,0 +1,542 @@
|
||||
/**
|
||||
* 画个标 · UI:动态表情工作台(独立模块,不加载共享 panels.js)
|
||||
* 职责:行业/动画类型/图标配置、表情方案批量生成、方案库动画预览、
|
||||
* 预览弹窗(换动画/换配色/换布局)、GIF/WebM/PNG 导出、内置库选择
|
||||
* 动画预览与导出同源:卡片/预览 rAF 驱动 HGB_Motion.apply,导出逐帧采样同一 apply
|
||||
*/
|
||||
const HGB_EmojiUI = (function () {
|
||||
'use strict';
|
||||
|
||||
/* ================= 工具 ================= */
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
function svgInline(svgStr) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = svgStr;
|
||||
const svg = div.querySelector('svg');
|
||||
return svg ? svg.outerHTML : '';
|
||||
}
|
||||
let toastTimer = null;
|
||||
function toast(msg) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.remove('hidden');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => el.classList.add('hidden'), 2600);
|
||||
}
|
||||
function download(blob, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 6000);
|
||||
}
|
||||
function safeName(plan) {
|
||||
return (String(plan.name || '表情').replace(/[\\/:*?"<>|]/g, '') || '表情');
|
||||
}
|
||||
|
||||
/* ================= 状态 ================= */
|
||||
const state = {
|
||||
industry: null,
|
||||
mood: 'happy', // 情绪(影响动画动效;与 emotion-params 技能同源)
|
||||
icons: [], // 已选图标 [{id,name,svg,source}]
|
||||
plans: [], // 同图 × 全部动画类型的一组表情
|
||||
activePlan: null,
|
||||
genParam: null
|
||||
};
|
||||
|
||||
// 情绪 → 动画决策选项(与 emotion-params 技能映射一致,AI 与内置引擎共用)
|
||||
const MOODS = [
|
||||
{ id: 'happy', emoji: '😄', name: '开心' },
|
||||
{ id: 'angry', emoji: '😠', name: '生气' },
|
||||
{ id: 'surprised', emoji: '😲', name: '惊讶' },
|
||||
{ id: 'sad', emoji: '😢', name: '难过' },
|
||||
{ id: 'excited', emoji: '🤩', name: '兴奋' },
|
||||
{ id: 'calm', emoji: '😌', name: '平静' }
|
||||
];
|
||||
// 情绪 → emotion-fit 评审关键词(中文)
|
||||
const MOOD_KEYWORD = { happy: '开心', angry: '生气', surprised: '惊讶', sad: '难过', excited: '兴奋', calm: '平静' };
|
||||
|
||||
// 动画预览节点(rAF 驱动)
|
||||
let animNodes = [];
|
||||
let rafId = null;
|
||||
|
||||
/* ================= 动画循环 ================= */
|
||||
function collectNodes(svgEl, plan) {
|
||||
const els = HGB_EmojiGen.markElements(plan).filter(e => e.visible && e.type === 'icon');
|
||||
const gs = Array.from(svgEl.children).filter(c => c.tagName === 'g');
|
||||
return gs.slice(0, els.length).map((g, i) => ({
|
||||
g, el: els[i], i, base: g.getAttribute('transform') || ''
|
||||
}));
|
||||
}
|
||||
function registerAnim(svgEl, plan) {
|
||||
animNodes.push({ svg: svgEl, plan, nodes: collectNodes(svgEl, plan) });
|
||||
}
|
||||
function tick(now) {
|
||||
animNodes = animNodes.filter(an => an.svg && an.svg.isConnected);
|
||||
for (const an of animNodes) {
|
||||
const t = HGB_Motion.previewT(now, an.plan.anim);
|
||||
for (const nd of an.nodes) {
|
||||
if (!nd.el) continue;
|
||||
// 分层动画:各元素按索引错峰取时间,与导出帧同源
|
||||
const tEl = HGB_Motion.elementT(t, an.plan.anim, nd.i, an.nodes.length);
|
||||
const f = HGB_Motion.apply(nd.el, tEl, an.plan.anim);
|
||||
nd.g.setAttribute('transform', HGB_Motion.composeTransform(nd.el, f, nd.base));
|
||||
if (f.opacity < 1) nd.g.setAttribute('opacity', String(Math.round(f.opacity * 1000) / 1000));
|
||||
else nd.g.removeAttribute('opacity');
|
||||
}
|
||||
}
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
function startLoop() {
|
||||
if (!rafId) rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
/* ================= 渲染:行业 ================= */
|
||||
function renderIndustries() {
|
||||
const wrap = document.getElementById('design-industry');
|
||||
wrap.innerHTML = HGB_Industries.map(ind => `
|
||||
<button class="industry-tab ${ind.id === state.industry.id ? 'active' : ''}" data-industry="${ind.id}" title="${esc(ind.desc)}">
|
||||
${ind.emoji}<span>${ind.name}</span>
|
||||
</button>`).join('');
|
||||
wrap.querySelectorAll('.industry-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
state.industry = HGB_Industries.find(btn.dataset.industry);
|
||||
state.icons = [];
|
||||
state.plans = [];
|
||||
state.activePlan = null;
|
||||
renderIndustries();
|
||||
renderIcons();
|
||||
renderPlans();
|
||||
toast('🏭 已切换到「' + state.industry.name + '」行业');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 渲染:情绪 ================= */
|
||||
function renderMood() {
|
||||
const wrap = document.getElementById('design-mood');
|
||||
wrap.innerHTML = MOODS.map(m => `
|
||||
<button class="industry-tab ${m.id === state.mood ? 'active' : ''}" data-mood="${m.id}"
|
||||
title="影响动画动效:${m.emoji}${m.name}(如开心→弹跳、生气→抖动、惊讶→心跳、难过→漂浮、兴奋→弹转、平静→呼吸)">
|
||||
${m.emoji}<span>${m.name}</span>
|
||||
</button>`).join('');
|
||||
wrap.querySelectorAll('[data-mood]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
state.mood = btn.dataset.mood;
|
||||
renderMood();
|
||||
toast('😊 情绪已设为「' + MOODS.find(m => m.id === state.mood).name + '」');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 渲染:选中图标 ================= */
|
||||
function renderIcons() {
|
||||
const wrap = document.getElementById('design-icons');
|
||||
if (!state.icons.length) {
|
||||
wrap.innerHTML = '<p class="pane-tip">未选择图标(AI 自动从行业与内置库选用)</p>';
|
||||
return;
|
||||
}
|
||||
wrap.innerHTML = state.icons.map((ic, i) => `
|
||||
<div class="design-icon" title="${esc(ic.name)}">
|
||||
${svgInline(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">${ic.svg}</svg>`)}
|
||||
<button class="icon-del" data-del="${i}">✕</button>
|
||||
</div>`).join('');
|
||||
wrap.querySelectorAll('.icon-del').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
state.icons.splice(Number(btn.dataset.del), 1);
|
||||
renderIcons();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 生成 ================= */
|
||||
async function generate() {
|
||||
const genBtn = document.getElementById('btn-generate');
|
||||
genBtn.disabled = true;
|
||||
const old = genBtn.textContent;
|
||||
genBtn.textContent = '⏳ 生成中…';
|
||||
try {
|
||||
const opt = {
|
||||
industry: state.industry,
|
||||
brand: '表情', // 纯图形无文字,品牌名不影响
|
||||
sub: '',
|
||||
desc: '',
|
||||
mood: state.mood, // 情绪 → AI 编排/降级均按情绪匹配主动画
|
||||
icons: state.icons.map(i => ({ id: i.id, name: i.name, svg: i.svg })),
|
||||
styleIds: [],
|
||||
skills: [],
|
||||
count: 1 // 一张底图 → 展开为全部动画类型
|
||||
};
|
||||
const plans = await HGB_EmojiGen.generateEmojiSet(opt);
|
||||
state.plans = plans;
|
||||
state.activePlan = plans[0] || null;
|
||||
state.genParam = opt;
|
||||
renderPlans();
|
||||
toast('✅ 已生成表情包:同图 × ' + plans.length + ' 种动画');
|
||||
} catch (err) {
|
||||
toast('❌ 生成失败:' + err.message);
|
||||
} finally {
|
||||
genBtn.disabled = false;
|
||||
genBtn.textContent = old;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 渲染:方案库卡片 ================= */
|
||||
function animLabel(anim) {
|
||||
const t = HGB_Motion.animTypes().find(x => x.id === (anim && anim.type));
|
||||
return t ? t.emoji + ' ' + t.name : '动画';
|
||||
}
|
||||
function renderPlans() {
|
||||
const grid = document.getElementById('plans-grid');
|
||||
const empty = document.getElementById('plans-empty');
|
||||
const aiTag = state.plans.length && state.plans[0]._aiDesign ? ' · 🤖 AI 设计' : '';
|
||||
document.getElementById('plans-title').textContent = `😀 表情包(共 ${state.plans.length} 张 · 同图 × 全动画)${aiTag}`;
|
||||
empty.classList.add('hidden');
|
||||
if (!state.plans.length) {
|
||||
empty.classList.remove('hidden');
|
||||
grid.innerHTML = '';
|
||||
animNodes = [];
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = state.plans.map(p => {
|
||||
const markSvg = HGB_Exporter.buildSvgFrom(HGB_EmojiGen.markElements(p), p.bg, null, { trim: true });
|
||||
const active = state.activePlan && state.activePlan.id === p.id;
|
||||
return `
|
||||
<div class="plan-card ${active ? 'active' : ''} plan-card-emoji" data-plan="${p.id}">
|
||||
<div class="plan-preview plan-preview-emoji" title="动态表情预览(自动播放)">${svgInline(markSvg)}</div>
|
||||
<div class="plan-meta">
|
||||
<div class="plan-name">${animLabel(p.anim)}</div>
|
||||
${p.concept ? `<div class="plan-concept">${esc(p.name)} · ${esc(p.concept)}</div>` : ''}
|
||||
<div class="plan-tags">
|
||||
<span>🏷 ${esc(p.layoutName)}</span>
|
||||
<span>🎨 ${esc(p.paletteName)}</span>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="tb-btn apply" data-pact="preview">👁 预览</button>
|
||||
<button class="tb-btn" data-pact="png">导出 PNG</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
animNodes = [];
|
||||
grid.querySelectorAll('.plan-card').forEach(card => {
|
||||
const plan = state.plans.find(p => p.id === card.dataset.plan);
|
||||
if (!plan) return;
|
||||
const svgEl = card.querySelector('.plan-preview-emoji svg');
|
||||
if (svgEl) registerAnim(svgEl, plan);
|
||||
card.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const act = btn.dataset.pact;
|
||||
if (act === 'preview') {
|
||||
openPreview(plan);
|
||||
} else if (act === 'png') {
|
||||
state.activePlan = plan;
|
||||
renderPlans();
|
||||
try {
|
||||
const canvas = await HGB_EmojiGen.exportPng(plan, 512, plan.bg);
|
||||
canvas.toBlob(b => { if (b) download(b, safeName(plan) + '_512px.png'); });
|
||||
toast('✅ 已导出静态 PNG(512px)');
|
||||
} catch (err) { toast('❌ 导出失败:' + err.message); }
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
startLoop();
|
||||
}
|
||||
|
||||
/* ================= 预览弹窗 ================= */
|
||||
function openPreview(plan) {
|
||||
state.activePlan = plan;
|
||||
renderPlans();
|
||||
renderPreview();
|
||||
document.getElementById('plan-modal').classList.remove('hidden');
|
||||
}
|
||||
function closePreview() {
|
||||
document.getElementById('plan-modal').classList.add('hidden');
|
||||
}
|
||||
function renderPreview() {
|
||||
const p = state.activePlan;
|
||||
if (!p) return;
|
||||
const wrap = document.getElementById('plan-mark');
|
||||
wrap.innerHTML = '';
|
||||
const markSvg = HGB_Exporter.buildSvgFrom(HGB_EmojiGen.markElements(p), p.bg, null, { trim: true });
|
||||
wrap.innerHTML = svgInline(markSvg);
|
||||
const svgEl = wrap.querySelector('svg');
|
||||
if (svgEl) {
|
||||
animNodes = animNodes.filter(an => an.svg !== svgEl && an.svg.isConnected);
|
||||
registerAnim(svgEl, p);
|
||||
startLoop();
|
||||
}
|
||||
document.getElementById('plan-modal-sub').textContent = `${p.layoutName} · ${p.paletteName} · ${animLabel(p.anim)}`;
|
||||
document.getElementById('plan-concept').innerHTML = p.concept
|
||||
? `<div class="plan-concept-inner">💡 设计理念:${esc(p.concept)}</div>` : '';
|
||||
// 动画类型切换(预览内)
|
||||
const aw = document.getElementById('plan-anim');
|
||||
aw.innerHTML = HGB_Motion.animTypes().map(t => `
|
||||
<button class="industry-tab ${p.anim.type === t.id ? 'active' : ''}" data-panim="${t.id}">${t.emoji}<span>${t.name}</span></button>`).join('');
|
||||
aw.querySelectorAll('.industry-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
p.anim = HGB_Motion.normalize({ type: btn.dataset.panim, speed: p.anim.speed, amplitude: p.anim.amplitude });
|
||||
renderPreview();
|
||||
renderPlans();
|
||||
});
|
||||
});
|
||||
// 技能评审提示(表情专属技能)
|
||||
renderStickerAudit(p);
|
||||
}
|
||||
function renderStickerAudit(p) {
|
||||
const el = document.getElementById('plan-audit');
|
||||
// 通用入参:表情技能按需取字段(追加 motion 类技能即自动生效)
|
||||
const els = HGB_EmojiGen.markElements(p).filter(e => e.visible && e.type === 'icon');
|
||||
const common = {
|
||||
duration: p.anim.duration, fps: p.anim.fps, type: p.anim.type,
|
||||
amplitude: p.anim.amplitude, speed: p.anim.speed,
|
||||
layered: p.anim.layered, layerSpread: p.anim.layerSpread,
|
||||
elementCount: els.length, size: 240, platform: 'wechat', styleId: p.styleId,
|
||||
mood: state.mood, keyword: MOOD_KEYWORD[state.mood] || '' // 情绪评审(emotion-fit)生效
|
||||
};
|
||||
const items = HGB_StickerSkills.all()
|
||||
.filter(s => s.category === 'motion') // 评审类技能全跑
|
||||
.map(s => {
|
||||
let r;
|
||||
try { r = HGB_StickerSkills.run(s.id, common); } catch (e) { r = { error: e.message }; }
|
||||
return { name: s.name, ok: !r.error && r.ok !== false, detail: r.error ? r.error : (r.tip || '') };
|
||||
});
|
||||
el.innerHTML = `
|
||||
<div class="plan-audit-head">🔬 动画技能检查 <span class="plan-audit-sub">(表情专属 · ${items.filter(i => i.ok).length}/${items.length} 项通过)</span></div>
|
||||
<div class="plan-audit-body">
|
||||
<div class="plan-audit-items">
|
||||
${items.map(it => `
|
||||
<div class="plan-audit-item ${it.ok ? 'ok' : 'warn'}">
|
||||
<span class="pa-mark">${it.ok ? '✓' : '⚠'}</span>
|
||||
<span class="pa-name">${it.name}</span>
|
||||
<span class="pa-detail">${esc(it.detail)}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ================= 变体微调 ================= */
|
||||
function reVariant(opts) {
|
||||
const p = state.activePlan;
|
||||
if (!p) return;
|
||||
const opt = {
|
||||
...(state.genParam || {}),
|
||||
layoutId: p.layoutId,
|
||||
styleId: p.styleId,
|
||||
anim: p.anim,
|
||||
...opts
|
||||
};
|
||||
const v = HGB_EmojiGen.generateVariant(opt);
|
||||
// 整组以新底图重新展开,保持「同图 × 全动画」一致性
|
||||
state.plans = HGB_EmojiGen.expandAnimations(v);
|
||||
state.activePlan = state.plans[0] || null;
|
||||
renderPlans();
|
||||
renderPreview();
|
||||
}
|
||||
function variantColor() {
|
||||
// 换配色:轮换到下一套风格(配色/背景跟随风格)
|
||||
const styles = HGB_Styles.slice();
|
||||
const cur = styles.findIndex(s => s.id === state.activePlan.styleId);
|
||||
const next = styles[(cur + 1) % styles.length];
|
||||
reVariant({ styleId: next.id });
|
||||
toast('🎨 已换底图配色/风格');
|
||||
}
|
||||
function variantLayout() {
|
||||
const layouts = HGB_EmojiGen.EMOJI_LAYOUTS;
|
||||
const cur = layouts.indexOf(state.activePlan.layoutId);
|
||||
const next = layouts[(cur + 1) % layouts.length];
|
||||
reVariant({ layoutId: next });
|
||||
toast('🅰 已换底图布局');
|
||||
}
|
||||
|
||||
/* ================= 换一张底图 ================= */
|
||||
function regenerate() {
|
||||
if (!state.activePlan) { generate(); return; }
|
||||
// 随机组合风格 + 布局(保留用户图标),保证换出的底图与当前不同
|
||||
const styles = HGB_Styles.slice();
|
||||
const style = styles[Math.floor(Math.random() * styles.length)];
|
||||
const layouts = HGB_EmojiGen.EMOJI_LAYOUTS;
|
||||
const layout = layouts[Math.floor(Math.random() * layouts.length)];
|
||||
reVariant({ styleId: style.id, layoutId: layout });
|
||||
toast('🎲 已换一张底图(' + style.name + ' · ' + layout + ')');
|
||||
}
|
||||
|
||||
/* ================= 导出 ================= */
|
||||
function exportOpts() {
|
||||
const size = Number(document.querySelector('input[name="em-size"]:checked').value);
|
||||
const fps = Number(document.getElementById('em-fps').value);
|
||||
const bg = document.querySelector('input[name="em-bg"]:checked').value;
|
||||
const a = HGB_Motion.normalize({ ...state.activePlan.anim, fps });
|
||||
return { size, fps, bg, anim: a };
|
||||
}
|
||||
async function exportGif() {
|
||||
const p = state.activePlan;
|
||||
if (!p) return;
|
||||
const { size, bg, anim } = exportOpts();
|
||||
toast('⏳ 正在生成 GIF(' + HGB_Motion.frameCount(anim) + ' 帧)…');
|
||||
try {
|
||||
const blob = await HGB_EmojiGen.exportGif(p, size, anim, { bg });
|
||||
download(blob, safeName(p) + '_' + anim.type + '_' + size + 'px.gif');
|
||||
toast('✅ GIF 已导出(' + (blob.size / 1024).toFixed(0) + 'KB)');
|
||||
} catch (err) { toast('❌ GIF 导出失败:' + err.message); }
|
||||
}
|
||||
async function exportWebm() {
|
||||
const p = state.activePlan;
|
||||
if (!p) return;
|
||||
const { size, bg, anim } = exportOpts();
|
||||
toast('⏳ 正在录制 WebM…');
|
||||
try {
|
||||
const blob = await HGB_EmojiGen.exportWebm(p, size, anim, { bg });
|
||||
download(blob, safeName(p) + '_' + anim.type + '_' + size + 'px.webm');
|
||||
toast('✅ WebM 已导出');
|
||||
} catch (err) { toast('❌ WebM 导出失败:' + err.message); }
|
||||
}
|
||||
async function exportPng() {
|
||||
const p = state.activePlan;
|
||||
if (!p) return;
|
||||
const { size, bg } = exportOpts();
|
||||
try {
|
||||
const canvas = await HGB_EmojiGen.exportPng(p, size, bg);
|
||||
canvas.toBlob(b => { if (b) download(b, safeName(p) + '_' + size + 'px.png'); });
|
||||
toast('✅ 静态 PNG 已导出(' + size + 'px)');
|
||||
} catch (err) { toast('❌ PNG 导出失败:' + err.message); }
|
||||
}
|
||||
|
||||
/* ================= 内置库选择(简化版) ================= */
|
||||
let builtinList = [], builtinOffset = 0, builtinQuery = '';
|
||||
async function openBuiltin() {
|
||||
builtinQuery = '';
|
||||
document.getElementById('builtin-search').value = '';
|
||||
document.getElementById('builtin-modal').classList.remove('hidden');
|
||||
// 预载表情主力单色源,保证搜索可用
|
||||
const core = (typeof HGB_AvatarGen !== 'undefined' && HGB_AvatarGen.AVATAR_CORE_SOURCES) ||
|
||||
['lucide', 'tabler', 'feather', 'mdi', 'fluent', 'solar'];
|
||||
await HGB_IconLib.preload(core);
|
||||
await searchBuiltin();
|
||||
}
|
||||
async function searchBuiltin(append) {
|
||||
const kw = builtinQuery.toLowerCase();
|
||||
let hits;
|
||||
if (kw) {
|
||||
hits = HGB_IconLib.searchLoaded(kw, 60);
|
||||
if (hits.length < 30) {
|
||||
const idx = HGB_IconLib.searchIndex(kw, 60);
|
||||
for (const it of idx) {
|
||||
if (!HGB_IconLib.loaded(it.source)) { await HGB_IconLib.loadSource(it.source).catch(() => null); }
|
||||
}
|
||||
hits = HGB_IconLib.searchLoaded(kw, 60);
|
||||
}
|
||||
} else {
|
||||
// 无关键词:遍历已加载源前若干
|
||||
hits = [];
|
||||
for (const sid of HGB_IconLib.loadedIds()) {
|
||||
for (const ic of HGB_IconLib.getSource(sid).icons || []) {
|
||||
hits.push({ id: sid + ':' + ic.name, name: ic.name, svg: ic.svg, source: sid, type: ic.type });
|
||||
if (hits.length >= 200) break;
|
||||
}
|
||||
if (hits.length >= 200) break;
|
||||
}
|
||||
}
|
||||
builtinList = hits;
|
||||
builtinOffset = append ? builtinOffset : 0;
|
||||
renderBuiltinGrid();
|
||||
}
|
||||
function renderBuiltinGrid() {
|
||||
const grid = document.getElementById('builtin-grid');
|
||||
const page = builtinList.slice(builtinOffset, builtinOffset + 30);
|
||||
const selIds = new Set(state.icons.map(i => i.id));
|
||||
if (builtinOffset === 0) grid.innerHTML = '';
|
||||
grid.insertAdjacentHTML('beforeend', page.map(ic => `
|
||||
<div class="gh-item ${selIds.has(ic.id) ? 'sel' : ''}" data-icid="${esc(ic.id)}" title="${esc(ic.name)}">
|
||||
${svgInline(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">${ic.svg}</svg>`)}
|
||||
<span class="gh-item-name">${esc(ic.name)}</span>
|
||||
</div>`).join(''));
|
||||
builtinOffset += page.length;
|
||||
document.getElementById('builtin-count').textContent = `已选 ${state.icons.length} / 最多 5`;
|
||||
document.getElementById('builtin-load-more').classList.toggle('hidden', builtinOffset >= builtinList.length);
|
||||
grid.querySelectorAll('.gh-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const ic = builtinList.find(x => x.id === item.dataset.icid);
|
||||
if (!ic) return;
|
||||
if (state.icons.some(i => i.id === ic.id)) {
|
||||
state.icons = state.icons.filter(i => i.id !== ic.id);
|
||||
} else {
|
||||
if (state.icons.length >= 5) { toast('⚠ 最多选择 5 个图标'); return; }
|
||||
state.icons.push({ id: ic.id, name: ic.name, svg: ic.svg, source: ic.source });
|
||||
}
|
||||
renderBuiltinGrid();
|
||||
renderIcons();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ================= 事件绑定 ================= */
|
||||
function bindAll() {
|
||||
document.getElementById('btn-generate').addEventListener('click', generate);
|
||||
const regen = document.getElementById('btn-regenerate');
|
||||
if (regen) regen.addEventListener('click', regenerate);
|
||||
document.getElementById('btn-builtin-icons').addEventListener('click', openBuiltin);
|
||||
document.getElementById('builtin-search').addEventListener('input', (e) => {
|
||||
builtinQuery = e.target.value.trim();
|
||||
searchBuiltin();
|
||||
});
|
||||
document.getElementById('builtin-load-more').addEventListener('click', () => searchBuiltin(true));
|
||||
document.querySelectorAll('#builtin-modal [data-builtin-close]').forEach(b => {
|
||||
b.addEventListener('click', () => document.getElementById('builtin-modal').classList.add('hidden'));
|
||||
});
|
||||
document.getElementById('builtin-modal').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget) e.currentTarget.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 预览弹窗
|
||||
document.querySelectorAll('#plan-modal [data-plan-close]').forEach(b => {
|
||||
b.addEventListener('click', closePreview);
|
||||
});
|
||||
document.getElementById('plan-modal').addEventListener('click', (e) => {
|
||||
if (e.target === e.currentTarget) closePreview();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !document.getElementById('plan-modal').classList.contains('hidden')) closePreview();
|
||||
});
|
||||
document.getElementById('btn-var-color').addEventListener('click', variantColor);
|
||||
document.getElementById('btn-var-layout').addEventListener('click', variantLayout);
|
||||
document.getElementById('btn-export-gif').addEventListener('click', exportGif);
|
||||
document.getElementById('btn-export-webm').addEventListener('click', exportWebm);
|
||||
document.getElementById('btn-export-png').addEventListener('click', exportPng);
|
||||
|
||||
// 项目名持久化
|
||||
const nameInput = document.getElementById('project-name');
|
||||
if (nameInput) {
|
||||
nameInput.addEventListener('change', () => {
|
||||
HGB_Store.get().name = nameInput.value || '我的品牌';
|
||||
HGB_Store.persistToLocal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 启动 ================= */
|
||||
function init() {
|
||||
state.industry = HGB_Industries.current();
|
||||
bindAll();
|
||||
renderIndustries();
|
||||
renderMood();
|
||||
renderIcons();
|
||||
renderPlans();
|
||||
startLoop();
|
||||
}
|
||||
|
||||
function boot() {
|
||||
HGB_Store.init(true);
|
||||
init();
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
|
||||
else boot();
|
||||
|
||||
return { init };
|
||||
})();
|
||||
Reference in New Issue
Block a user