Files
drawlogo/js/skills/typography.js
T

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