286 lines
14 KiB
JavaScript
286 lines
14 KiB
JavaScript
/**
|
||
* 画个标 · 技能库:字体类(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 };
|
||
}
|
||
}
|
||
]);
|
||
})();
|