feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+285
View File
@@ -0,0 +1,285 @@
/**
* 画个标 · 技能库:字体类(typography)
* 注册:HGB_Skills.register([...])
*/
(function () {
'use strict';
const U = HGB_Skills.utils;
HGB_Skills.register([
{
id: 'font-pairing',
name: '字体配对',
category: 'typography',
emoji: '🔤',
scene: '中英文品牌字体的专业配对',
summary: '依据文本语种(中文/西文)与行业推荐字体库,给出主标题与副标题的字体配对方案。',
params: [
{ name: 'text', type: 'string', required: true, desc: '品牌文本(自动识别中英文)' },
{ name: 'industry', type: 'string', required: false, desc: '行业 id' }
],
source: 'GitHub: design-resources-for-developers(Fontjoy 字体配对方法)+ 自研字体库',
run: function (args) {
const text = String(args.text || '');
const ind = HGB_Industries.find(x => x.id === args.industry) || HGB_Industries[0];
const favs = ind.fontFamilies || [];
const hasCn = /[\u4e00-\u9fa5]/.test(text);
const enPool = ['Arial', 'Georgia', 'Impact', 'Verdana', 'Montserrat', 'Playfair Display'];
const cnPool = ['SimHei', 'Microsoft YaHei', 'SimSun', 'KaiTi'];
const cn = (favs.find(f => cnPool.includes(f)) || 'SimHei');
const en = (favs.find(f => enPool.includes(f)) || 'Arial');
return {
text, language: hasCn ? '中文为主' : '西文为主',
pairing: hasCn ? cn + '(主标题) / ' + en + '(副标题)' : en + '(主标题) / Georgia(副标题)',
industryFav: favs
};
},
test: function () {
return { cn: this.run({ text: '远渡教育' }).pairing, en: this.run({ text: 'NEXUS' }).pairing };
}
},
{
id: 'font-hierarchy',
name: '字号层级检查',
category: 'typography',
emoji: '📐',
scene: 'LOGO 中主标题/副标题的字号比例审计',
summary: '检查主标题与副标题字号比是否在专业区间(主:副 约 2:1 ~ 3:1),字号过近会导致主次不分,给出调整建议。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' }
],
source: '自研(基于排版层级比例规范)',
run: function (args) {
const els = (args.plan || {}).elements || [];
const texts = els.filter(el => el.type === 'text' && el.visible !== false)
.sort((a, b) => (b.fontSize || 0) - (a.fontSize || 0));
if (texts.length < 2) {
return { pass: true, note: '仅 1 个文字元素或无需层级检查', texts: texts.map(t => ({ text: t.text, size: t.fontSize })) };
}
const main = texts[0], sub = texts[1];
const ratio = Math.round(((main.fontSize || 24) / (sub.fontSize || 18)) * 100) / 100;
const good = ratio >= 1.8 && ratio <= 3.5;
return {
pass: good,
ratio,
main: { text: main.text, size: main.fontSize },
sub: { text: sub.text, size: sub.fontSize },
verdict: good ? '层级比例专业(主:副 ≈ ' + ratio + ':1)' : '层级偏平(主:副 ≈ ' + ratio + ':1),建议副标题缩小或主标题加大'
};
},
test: function () {
const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '远渡教育咨询' });
return this.run({ plan });
}
},
{
id: 'text-fit',
name: '文字适配检查',
category: 'typography',
emoji: '✍️',
scene: '长品牌名文字是否超出容器的检查',
summary: '估算文字渲染宽度并与容器/画布宽度比较:文字过宽会破版(尤其徽章/印章圆形布局),给出缩字或换行建议。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' }
],
source: '自研(文字宽度估算 + 容器适配)',
run: function (args) {
const els = (args.plan || {}).elements || [];
const CW = 600;
const texts = els.filter(el => el.type === 'text' && el.visible !== false);
const issues = [], ok = [];
for (const t of texts) {
const w = U.estTextW(t);
const container = (t.containerW || (t.layoutId && t.layoutId.includes('badge') ? CW * 0.62 : CW * 0.9));
if (w > container) {
issues.push(`"${t.text}" 估算宽 ${Math.round(w)}px 超出容器 ${Math.round(container)}px,建议缩小字号或缩短文案`);
} else {
ok.push(`"${t.text}" 适配良好(${Math.round(w)}px / 容器 ${Math.round(container)}px)`);
}
}
return { pass: issues.length === 0, issues, ok, total: texts.length };
},
test: function () {
const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '一个非常长的品牌名称测试' });
return this.run({ plan });
}
},
{
id: 'font-weight-choose',
name: '字重选择建议',
category: 'typography',
emoji: '🔠',
scene: '按行业与品牌气质选择主标题字重',
summary: '依据行业与气质关键词推荐主标题字重:科技/力量用 Bold/Black,高端/优雅用 Light/Regular,教育培训用 Medium 兼具辨识度与亲和。',
params: [
{ name: 'industry', type: 'string', required: false, desc: '行业 id' },
{ name: 'desc', type: 'string', required: false, desc: '品牌气质描述(如 高端/活力/科技)' }
],
source: '自研(字重 × 气质映射规范)',
run: function (args) {
const ind = HGB_Industries.find(x => x.id === args.industry) || HGB_Industries[0];
const desc = String(args.desc || '');
const map = [
{ kw: ['高端', '奢华', '优雅', '轻奢', 'premium', 'luxury'], w: 'Light / Regular', note: '细字重传递优雅与克制' },
{ kw: ['科技', '力量', '运动', '硬朗', 'future'], w: 'Bold / Black', note: '粗字重强化力量与冲击' },
{ kw: ['活力', '年轻', '潮流', '时尚', 'fun'], w: 'SemiBold / Bold', note: '中等偏粗兼顾活力与辨识' },
{ kw: ['温馨', '亲切', '治愈', '自然'], w: 'Regular / Medium', note: '常规字重亲和耐读' }
];
const hit = map.find(m => m.kw.some(k => desc.includes(k)));
return {
industry: ind.name,
recommend: hit ? hit.w : 'Bold(标题)/ Regular(正文)',
note: hit ? hit.note : '默认粗标题 + 常规正文,按落地场景微调',
weightScale: ['Thin', 'Light', 'Regular', 'Medium', 'SemiBold', 'Bold', 'Black']
};
},
test: function () {
return { lux: this.run({ desc: '高端优雅' }).recommend, tech: this.run({ industry: 'tech' }).recommend };
}
},
{
id: 'tiny-readable',
name: '小尺寸可读性',
category: 'typography',
emoji: '🔎',
scene: 'LOGO/头像在小尺寸(64px 头像、favicon)下的可读性检查',
summary: '按渲染尺寸推算文字的有效像素大小:头像 64px、Favicon 32px 场景下字号过小会糊成一片,输出最小可读字号建议。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' }
],
source: '自研(画布 600px → 目标尺寸换算可读性)',
run: function (args) {
const els = (args.plan || {}).elements || [];
const CW = 600;
const texts = els.filter(el => el.type === 'text' && el.visible !== false);
if (!texts.length) return { pass: true, note: '无文字元素,跳过检查' };
const rows = texts.map(t => {
const fs = t.fontSize || 24;
const at64 = Math.round(fs * (64 / CW) * 10) / 10;
const at32 = Math.round(fs * (32 / CW) * 10) / 10;
return { text: t.text, fontSize: fs, at64px: at64, at32px: at32, readable: at64 >= 4 && at32 >= 2 };
});
const allOk = rows.every(r => r.readable);
return {
pass: allOk,
rows,
verdict: allOk ? '小尺寸下可读性良好' : '存在小尺寸不可读文字,建议加大字号或加粗',
suggest: allOk ? '无需调整' : '主文字建议 ≥ 40px(600 画布),64px 头像仍可辨;Favicon 场景建议纯图形'
};
},
test: function () {
const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '小尺寸测试' });
return this.run({ plan });
}
},
{
id: 'letter-spacing-check',
name: '字距检查',
category: 'typography',
emoji: '↔️',
scene: '品牌文字字距(letter-spacing)合理性检查',
summary: '检查文字字距是否过大或过小:字距过大显松散、过小拥挤,中英文场景给出相对字号的字距建议。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' }
],
source: '自研(字距可读性规范)',
run: function (args) {
const els = (args.plan || {}).elements || [];
const texts = els.filter(el => el.type === 'text' && el.visible !== false);
if (!texts.length) return { pass: true, note: '无文字元素' };
const rows = texts.map(t => {
const fs = t.fontSize || 24;
const ls = t.letterSpacing || 0;
const ratio = Math.round((ls / fs) * 1000) / 1000;
const pass = ratio >= -0.05 && ratio <= 0.25;
return { text: t.text, fontSize: fs, letterSpacing: ls, ratio, pass };
});
const allOk = rows.every(r => r.pass);
return {
pass: allOk, rows,
verdict: allOk ? '字距合理' : '存在字距异常(过大或过小),建议调整',
suggest: allOk ? '无需调整' : '中文字距建议 0~0.2em、西文标题 0~0.1em;过大松散、过小拥挤'
};
},
test: function () {
const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '字距测试' });
return this.run({ plan });
}
},
{
id: 'line-break',
name: '文字断行建议',
category: 'typography',
emoji: '↩️',
scene: '多行品牌文字的断行位置与行长均衡建议',
summary: '检查多行文字各行的字符长度是否均衡、是否出现孤行(单字/单字母成行):均衡断行更耐看,孤行与悬殊行长会破坏排版节奏。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' }
],
source: '自研(断行均衡/孤行规范)',
run: function (args) {
const els = (args.plan || {}).elements || [];
const multi = els.filter(el => el.type === 'text' && el.visible !== false && String(el.text || '').includes('\n'));
if (!multi.length) return { pass: true, note: '无多行文字,无需断行检查' };
const rows = multi.map(t => {
const lines = String(t.text).split('\n').map(s => s.trim());
const lens = lines.map(l => l.length);
const max = Math.max(...lens);
const balance = max === 0 ? 1 : Math.round((Math.min(...lens) / max) * 100) / 100;
const orphan = lens.length > 1 && Math.min(...lens) === 1;
const pass = balance >= 0.5 && !orphan;
return { text: t.text, lines: lens, balance, orphan, pass };
});
const allOk = rows.every(r => r.pass);
return {
pass: allOk, rows,
verdict: allOk ? '断行均衡、无孤行' : '存在断行不均衡或孤行,建议调整分行',
suggest: allOk ? '无需调整' : '尽量让各行长度接近(最短/最长 ≥ 0.5),避免单字/单字母孤行'
};
},
test: function () {
return { bad: this.run({ plan: { elements: [{ type: 'text', text: '远\n渡设计' }] } }).pass, ok: this.run({ plan: { elements: [{ type: 'text', text: '远渡\n设计' }] } }).pass };
}
},
{
id: 'cjk-mix-check',
name: '中英混排检查',
category: 'typography',
emoji: '🈴',
scene: 'LOGO/海报中中英文混排的间距、字距与搭配检查',
summary: '检测文字元素是否中英混排:混排场景检查字距是否异常(过大破坏西文连排、过小易粘连),并输出中西文间距、字体搭配与对齐建议。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' }
],
source: '自研(中西文混排排版规范)',
run: function (args) {
const els = (args.plan || {}).elements || [];
const texts = els.filter(el => el.type === 'text' && el.visible !== false);
if (!texts.length) return { pass: true, note: '无文字元素' };
const rows = texts.map(t => {
const s = String(t.text || '');
const hasCjk = /[\u4e00-\u9fa5]/.test(s);
const hasLatin = /[a-zA-Z0-9]/.test(s);
const mixed = hasCjk && hasLatin;
const fs = t.fontSize || 24;
const ls = t.letterSpacing || 0;
const ratio = fs ? Math.round((ls / fs) * 1000) / 1000 : 0;
const spOk = mixed ? (ratio >= -0.05 && ratio <= 0.1) : true;
return { text: s.slice(0, 18) + (s.length > 18 ? '…' : ''), mixed, cjk: hasCjk, latin: hasLatin, spacing: ratio, spOk };
});
const mixed = rows.filter(r => r.mixed);
const bad = mixed.filter(r => !r.spOk);
return {
pass: bad.length === 0, rows, mixedCount: mixed.length,
verdict: mixed.length === 0 ? '无中英混排文字,无需检查' : (bad.length === 0 ? '中英混排字距合理(' + mixed.length + ' 处)' : bad.length + ' 处中英混排字距异常,建议中西文之间加 1/4 空格并统一字距'),
suggest: mixed.length ? '中西文混排建议:① 中文与英文/数字之间加半角空格;② 中文用中文字体、西文用西文字体,避免西文挤占中文字宽;③ 标题场景字距不超过 0.1em,正文不超过 0.05em' : '无需调整'
};
},
test: function () {
const bad = { plan: { elements: [{ type: 'text', text: 'ABC远渡教育2026', fontSize: 24, letterSpacing: 12 }] } };
const pure = { plan: { elements: [{ type: 'text', text: '远渡教育', fontSize: 24 }] } };
return { mixed: this.run(bad).mixedCount, pure: this.run(pure).mixedCount, spBad: this.run(bad).rows[0].spOk === false };
}
}
]);
})();