/** * 画个标 · 技能库:构图类(layout) * 注册:HGB_Skills.register([...]) */ (function () { 'use strict'; const U = HGB_Skills.utils; const { estTextW, elBox } = U; HGB_Skills.register([ { id: 'composition', name: '构图质量评估', category: 'layout', emoji: '📐', scene: 'LOGO 构图设计与质量检查(生成后自动审计)', summary: '依据"极简、充足负空间、精确比例"专业原则,检测方案的元素越界、重叠与文字过宽,输出质量评分与修正建议。', params: [ { name: 'plan', type: 'object', required: true, desc: '待检查的方案对象 {elements, bg}' } ], source: 'GitHub: op7418/logo-generator-skill(设计原则)+ 自研几何审计', run: function (args) { const plan = args.plan || {}; const els = plan.elements || []; const CW = 600, CH = 600; const issues = [], suggestions = []; for (const el of els) { if (el.visible === false) continue; const b = elBox(el); if (b.x < 0 || b.y < 0 || b.x + b.w > CW || b.y + b.h > CH) { issues.push(`"${el.name}" 超出画布边界`); } } for (let i = 0; i < els.length; i++) { for (let j = i + 1; j < els.length; j++) { const a = els[i], b = els[j]; const types = [a.type, b.type].sort().join('|'); if (types !== 'icon|text') continue; // 圆环/装饰条是有意的背景元素(徽章/印章中文字位于环内),不参与重叠判定 if (['ring', 'deco-bar'].includes(a.iconId) || ['ring', 'deco-bar'].includes(b.iconId)) continue; const ba = elBox(a), bb = elBox(b); const ox = Math.min(ba.x + ba.w, bb.x + bb.w) - Math.max(ba.x, bb.x); const oy = Math.min(ba.y + ba.h, bb.y + bb.h) - Math.max(ba.y, bb.y); if (ox > 0 && oy > 0 && ox * oy > (ba.w * ba.h) * 0.08) { issues.push(`"${a.name}" 与 "${b.name}" 存在重叠(建议加大间距)`); } } } for (const el of els) { if (el.type === 'text' && el.fontSize && el.fontSize >= 36) { const b = elBox(el); if (b.x + b.w > CW - 20) { suggestions.push(`"${el.name}" 文字偏宽(约 ${Math.round(b.w)}px),建议缩小字号或缩短文字`); } } } const score = issues.length ? Math.max(40, 95 - issues.length * 20) : (suggestions.length ? 88 : 96); return { score, pass: issues.length === 0, issues, suggestions }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '测试品牌名' }); const r = this.run({ plan }); return { score: r.score, pass: r.pass, issues: r.issues, suggestions: r.suggestions, elements: plan.elements.length }; } }, { id: 'layout-choice', name: '布局匹配推荐', category: 'layout', emoji: '🧱', scene: '根据品牌名长度与行业选择最合适的布局模板', summary: '按品牌名长度(中文字数/西文字符数)匹配布局:短名用徽章/印章/双图标,长名用上下/左右式,避免破版。', params: [ { name: 'brand', type: 'string', required: true, desc: '品牌名' }, { name: 'industry', type: 'string', required: false, desc: '行业 id' } ], source: 'GitHub: neonwatty/logo-designer-skill(interview 阶段)+ 自研', run: function (args) { const brand = String(args.brand || ''); const def = HGB_Generator.defaultLayout(brand); const cn = (brand.match(/[\u4e00-\u9fa5]/g) || []).length; const len = cn || brand.length; const pool = HGB_Generator.LAYOUTS.filter(l => { if (['badge', 'seal', 'duo'].includes(l.id)) return len <= 4; return true; }); return { brand, len, recommend: def.id, recommendName: def.name, candidates: pool.map(l => l.id + ':' + l.name) }; }, test: function () { return { short: this.run({ brand: '远渡' }).recommend, medium: this.run({ brand: '远渡教育咨询' }).recommend, long: this.run({ brand: 'FAR CROSS EDUCATION' }).recommend }; } }, { id: 'align-check', name: '对齐与居中检查', category: 'layout', emoji: '📏', scene: 'LOGO 元素对齐与居中规范审计', summary: '检查方案元素是否围绕画布中心对齐(水平/垂直居中),并检测同级元素是否存在明显错位,输出对齐修正建议。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: 'GitHub: op7418/logo-generator-skill(对齐规范)+ 自研', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600, CH = 600; const issues = []; const main = els.filter(el => el.visible !== false && !['ring', 'deco-bar'].includes(el.iconId)); if (!main.length) return { pass: true, issues: ['无可见主体元素'], centered: true }; // 主体元素中心应落在画布中心附近(容差 20px) let cxSum = 0, cySum = 0, n = 0; for (const el of main) { const b = elBox(el); cxSum += b.x + b.w / 2; cySum += b.y + b.h / 2; n++; } const cx = cxSum / n, cy = cySum / n; if (Math.abs(cx - CW / 2) > 20) issues.push(`主体水平中心偏移 ${Math.round(cx - CW / 2)}px(建议居中)`); if (Math.abs(cy - CH / 2) > 20) issues.push(`主体垂直中心偏移 ${Math.round(cy - CH / 2)}px(建议居中)`); return { pass: issues.length === 0, issues, centerX: Math.round(cx), centerY: Math.round(cy), canvasCenter: CW / 2 }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '对齐测试' }); const r = this.run({ plan }); return { pass: r.pass, issues: r.issues }; } }, { id: 'negative-space', name: '负空间评估', category: 'layout', emoji: '🕳️', scene: 'LOGO 负空间(留白)充足性评估', summary: '计算主体元素在画布中的占比:占比过高说明负空间不足(拥挤),占比过低说明主体偏小(空洞),给出建议区间。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(基于"极简 + 充足负空间"设计原则量化)', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600, CH = 600; const area = CW * CH; let used = 0; for (const el of els) { if (el.visible === false) continue; const b = elBox(el); const w = Math.max(0, Math.min(b.x + b.w, CW) - Math.max(b.x, 0)); const h = Math.max(0, Math.min(b.y + b.h, CH) - Math.max(b.y, 0)); used += w * h; } const ratio = Math.round((used / area) * 1000) / 10; const verdict = ratio > 55 ? '负空间不足,建议缩小主体或加大留白' : (ratio < 12 ? '主体偏小,建议放大增强存在感' : '负空间充足,比例健康'); return { ratio, verdict, suggest: ratio > 55 ? '缩小主体或精简元素' : (ratio < 12 ? '放大主体或增加细节' : '无需调整') }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '留白测试' }); return this.run({ plan }); } }, { id: 'spacing-check', name: '间距节奏检查', category: 'layout', emoji: '↔️', scene: '元素间距与画布边距的一致性检查', summary: '检查主体元素与画布边缘的留白是否充足一致,以及图标与文字间距是否过近(<8px 视为贴边),保证构图不挤不散。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(间距节奏规范)', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600, CH = 600; const issues = []; const main = els.filter(el => el.visible !== false && !['ring', 'deco-bar'].includes(el.iconId)); if (!main.length) return { pass: true, issues: ['无可见主体元素'] }; for (const el of main) { const b = elBox(el); const minPad = Math.min(b.x, CW - (b.x + b.w), b.y, CH - (b.y + b.h)); if (minPad < 16) issues.push(`"${el.name}" 距画布边缘仅 ${Math.round(minPad)}px,建议留白 ≥ 24px`); } const icon = main.find(el => el.type === 'icon'); const text = main.find(el => el.type === 'text'); if (icon && text) { const bi = elBox(icon), bt = elBox(text); // 仅当水平相邻(垂直中心距小于图标高)时检查水平间距 const verticalGap = Math.abs((bi.y + bi.h / 2) - (bt.y + bt.h / 2)); if (verticalGap < (bi.h + bt.h) / 2) { const gap = Math.abs((bi.x + bi.w / 2) - (bt.x + bt.w / 2)) - (bi.w + bt.w) / 2; if (gap < 8) issues.push('图标与文字水平间距过近(<8px),建议加大间距'); } } return { pass: issues.length === 0, issues, ok: issues.length ? [] : ['间距节奏协调,留白充足'] }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '间距测试' }); return this.run({ plan }); } }, { id: 'proportion-check', name: '黄金比例评估', category: 'layout', emoji: '⚖️', scene: '主体占比与黄金分割的匹配度评估', summary: '计算主体元素宽度与画布宽度之比:接近 0.618 黄金分割视觉最舒适,过高拥挤、过低空泛,输出调整方向。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(黄金分割比例规范)', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600; const main = els.filter(el => el.visible !== false && !['ring', 'deco-bar'].includes(el.iconId)); if (!main.length) return { pass: true, note: '无可见主体元素' }; let maxW = 0; for (const el of main) { const b = elBox(el); if (b.w > maxW) maxW = b.w; } const ratio = Math.round((maxW / CW) * 1000) / 1000; const pass = Math.abs(ratio - 0.618) < 0.15; return { pass, ratio, golden: 0.618, verdict: pass ? '主体占比接近黄金分割(' + ratio + '),比例协调' : '主体占比 ' + ratio + ',偏离黄金分割,建议' + (ratio > 0.768 ? '缩小主体' : '放大主体'), suggest: pass ? '无需调整' : (ratio > 0.768 ? '缩小主体或精简元素' : '放大主体增强存在感') }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '比例测试' }); return this.run({ plan }); } }, { id: 'visual-balance', name: '视觉重量平衡', category: 'layout', emoji: '⚖️', scene: '画布左右/上下的视觉重量均衡检查', summary: '计算所有可见元素的包围盒质心与画布中心的偏移:偏移过大说明构图偏一侧、视觉失衡,输出偏移方向与调整建议。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(视觉重量质心评估)', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600, CH = 600; const vis = els.filter(el => el.visible !== false); if (!vis.length) return { pass: true, note: '无可见元素' }; let sx = 0, sy = 0, sw = 0; for (const el of vis) { const b = elBox(el); const w = Math.max(b.w * b.h, 1); sx += ((b.x + b.w / 2) - CW / 2) * w; sy += ((b.y + b.h / 2) - CH / 2) * w; sw += w; } const dx = Math.round((sx / sw) * 10) / 10; const dy = Math.round((sy / sw) * 10) / 10; const off = Math.sqrt(dx * dx + dy * dy); const pass = off < 40; const dir = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? '右' : '左') : (dy > 0 ? '下' : '上'); return { pass, offsetX: dx, offsetY: dy, offset: Math.round(off), verdict: pass ? '视觉重心接近中心,构图平衡' : '视觉重心偏移 ' + Math.round(off) + 'px,构图偏' + dir, suggest: pass ? '无需调整' : '调整元素位置或补充留白,让视觉重心回归中心' }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '平衡测试' }); return this.run({ plan }); } }, { id: 'focal-point', name: '视觉焦点检查', category: 'layout', emoji: '🎯', scene: 'LOGO 是否有明确视觉焦点(主次分明)', summary: '找出面积最大的主体元素,判断其是否显著大于其他元素:主次分明有助于快速识别,多个等权元素会让视线分散。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(主次层级量化)', run: function (args) { const els = (args.plan || {}).elements || []; const vis = els.filter(el => el.visible !== false && !['ring', 'deco-bar'].includes(el.iconId)); if (vis.length < 2) return { pass: true, note: '元素过少,无需焦点检查' }; const areas = vis.map(el => { const b = elBox(el); return { el, area: b.w * b.h }; }).sort((a, b) => b.area - a.area); const top = areas[0], second = areas[1]; const ratio = Math.round((top.area / Math.max(second.area, 1)) * 100) / 100; const pass = ratio >= 1.5; return { pass, focusRatio: ratio, main: top.el.text || top.el.iconId || '主体', verdict: pass ? '主元素面积是次元素的 ' + ratio + ' 倍,主次分明' : '主次面积接近(' + ratio + ' 倍),视觉焦点不明确', suggest: pass ? '无需调整' : '放大主元素或缩小次要元素,强化视觉焦点' }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '焦点测试' }); return this.run({ plan }); } }, { id: 'rule-of-thirds', name: '构图位置分析', category: 'layout', emoji: '📍', scene: '主体构图位置(居中 / 三分 / 偏移)分析', summary: '计算主体元素重心相对画布中心与三分线交点的位置,判定构图类型:居中构图适合徽章/印章类经典版式,三分构图更具视觉张力,随机偏移则需要纠正。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(居中/三分构图规范)', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600, CH = 600; const main = els.filter(el => el.visible !== false && !['ring', 'deco-bar'].includes(el.iconId)); if (!main.length) return { pass: true, note: '无可见主体元素' }; let sx = 0, sy = 0, n = 0; for (const el of main) { const b = elBox(el); sx += b.x + b.w / 2; sy += b.y + b.h / 2; n++; } const cx = sx / n, cy = sy / n; const dc = Math.hypot(cx - CW / 2, cy - CH / 2); const xs = [CW / 3, CW * 2 / 3], ys = [CH / 3, CH * 2 / 3]; let dt = Infinity; for (const gx of xs) for (const gy of ys) dt = Math.min(dt, Math.hypot(cx - gx, cy - gy)); const type = dc < 60 ? '居中构图(经典稳健,适合徽章/印章版式)' : (dt < 60 ? '三分构图(主体落在分割线交点,视觉有张力)' : '偏移构图(主体既不在中心也不在分割点)'); const pass = dc < 60 || dt < 60; return { pass, type, centerOffset: Math.round(dc), thirdsOffset: Math.round(dt), centerX: Math.round(cx), centerY: Math.round(cy), verdict: pass ? '构图位置合理:' + type : '构图位置偏散:' + type, suggest: pass ? '无需调整' : '建议把主体重心移回画布中心或三分线交点附近,避免随意偏移' }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '构图测试' }); return this.run({ plan }); } }, { id: 'circle-crop-safety', name: '圆形裁切安全区', category: 'layout', emoji: '⭕', scene: '头像/贴纸/印章等圆形裁切场景的构图安全检查', summary: '以画布内切圆为裁切边界,检查各元素包围盒是否落在圆内:越界的元素在圆形裁切(头像、印章、贴纸)时会被切边,输出越界清单与缩放建议。', params: [ { name: 'plan', type: 'object', required: true, desc: '方案对象 {elements}' } ], source: '自研(圆形裁切安全区规范)', run: function (args) { const els = (args.plan || {}).elements || []; const CW = 600, CH = 600; const cx = CW / 2, cy = CH / 2, r = CW / 2; const vis = els.filter(el => el.visible !== false); if (!vis.length) return { pass: true, note: '无可见元素' }; const risky = []; let maxD = 0; for (const el of vis) { const b = elBox(el); const corners = [[b.x, b.y], [b.x + b.w, b.y], [b.x, b.y + b.h], [b.x + b.w, b.y + b.h]]; let d = 0; for (const [px, py] of corners) d = Math.max(d, Math.hypot(px - cx, py - cy)); maxD = Math.max(maxD, d); if (d > r) risky.push({ name: el.text || el.iconId || '元素', maxDist: Math.round(d) }); } const pass = risky.length === 0; return { pass, risky, radius: r, maxDist: Math.round(maxD), verdict: pass ? '所有元素均在内切圆内,圆形裁切安全' : risky.length + ' 个元素超出内切圆(' + risky.map(x => x.name).join('、') + '),圆形裁切时会被切边', suggest: pass ? '无需调整' : '建议缩小越界元素或整体向圆心内缩 ' + Math.max(10, Math.round(maxD - r)) + 'px,保证圆形裁切后主体完整' }; }, test: function () { const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '圆裁测试' }); const edge = { plan: { elements: [{ type: 'icon', iconId: 'x', x: 585, y: 300, w: 30, h: 30 }] } }; return { generated: this.run({ plan }).pass !== undefined, edge: this.run(edge).risky.length }; } } ]); })();