395 lines
19 KiB
JavaScript
395 lines
19 KiB
JavaScript
/**
|
||
* 画个标 · 技能库:构图类(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 };
|
||
}
|
||
}
|
||
]);
|
||
})();
|