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