Files

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