Files
drawlogo/js/poster-skills/skills.js
T

1451 lines
92 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.
/**
* 画个标 · 海报技能(8 类示例,后续按类拆分为独立文件)
* 每类先收录 1 个代表性技能,run(args) 纯函数返回建议,test() 自跑。
* 来源标注「自研」或 GitHub 仓库 + 方法。
*/
HGB_PosterSkills.register([
/* ---------- layout 版式构图 ---------- */
{
id: 'poster-balance', name: '版式平衡', category: 'layout', emoji: '⚖️',
scene: '评估海报左右/上下视觉重量是否均衡,避免一侧过重',
summary: '把海报划分为九宫格,按元素面积与位置计算视觉重心偏移,给出调整建议。',
params: { elements: '元素数组(含 x/y/w/h 与权重 weight)', canvas: '{w,h}' },
source: '自研',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
const cw = (args.canvas && args.canvas.w) || 1000;
const ch = (args.canvas && args.canvas.h) || 1500;
if (!els.length) return { ok: true, note: '无元素,无法评估' };
let mx = 0, mw = 0;
for (const e of els) {
const w = e.weight || e.w * e.h;
mx += (e.x + e.w / 2) * w; mw += w;
}
const cx = mw ? mx / mw : cw / 2;
const dx = (cx - cw / 2) / (cw / 2);
const ok = Math.abs(dx) < 0.15;
return { ok, offset: (dx * 100).toFixed(1) + '%', tip: ok ? '视觉重心均衡' : (dx > 0 ? '重心偏右,建议左侧补元素或右侧留白' : '重心偏左,建议右侧补元素或左侧留白') };
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, elements: [{ x: 100, y: 100, w: 300, h: 300 }, { x: 600, y: 100, w: 300, h: 300 }] }); }
},
/* ---------- color 配色 ---------- */
{
id: 'poster-palette', name: '海报配色', category: 'color', emoji: '🎨',
scene: '基于主色生成 60-30-10 配色比例(主色/辅助色/点缀色)',
summary: '输入主色 hex,返回辅助色与点缀色建议及使用比例,保证海报色彩层次。',
params: { primary: '主色 hex', style: '风格关键词(可选)' },
source: '自研(60-30-10 配色法则)',
run(args) {
const c = String(args.primary || '#4f46e5');
return {
ok: true,
ratio: '60-30-10',
primary: c,
secondary: '建议用主色的同色系浅色或中性灰(#eef2ff 类)',
accent: '建议用主色的对比色(如 #f59e0b)作点缀',
tip: '60% 主色背景 / 30% 辅助色信息区 / 10% 点缀色强调关键信息'
};
},
test() { return this.run({ primary: '#4f46e5' }); }
},
/* ---------- typography 字体排版 ---------- */
{
id: 'poster-type-scale', name: '字体层级', category: 'typography', emoji: '🔤',
scene: '为标题/副标题/正文建议字号比例与字重,建立清晰阅读层级',
summary: '按 1.25 比例(Major Third)由基准正文字号推导各层级字号。',
params: { base: '正文字号 px(默认 16)', levels: '层级数(默认 4)' },
source: '自研(Modular Scale)',
run(args) {
const base = Number(args.base) || 16;
const levels = Math.min(Math.max(Number(args.levels) || 4, 3), 6);
const scale = [];
for (let i = 0; i < levels; i++) scale.push(Math.round(base * Math.pow(1.25, i)));
return { ok: true, base, scale, tip: '层级字号(小→大):' + scale.join(' / ') + ' px;标题建议加粗、正文保持常规' };
},
test() { return this.run({ base: 16, levels: 4 }); }
},
/* ---------- grid 网格系统 ---------- */
{
id: 'poster-columns', name: '分栏网格', category: 'grid', emoji: '📐',
scene: '建议海报分栏数与栏间距,建立对齐骨架',
summary: '根据画布宽高比推荐分栏数与安全边距(栅格系统)。',
params: { ratio: '画布宽高比(如 0.66 竖版 / 1.78 横版)' },
source: '自研(栅格系统)',
run(args) {
const r = Number(args.ratio) || 0.66;
const cols = r < 1 ? 3 : 4; // 竖版 3 栏、横版 4 栏
return { ok: true, cols, gutter: '栏间距建议 24-40px,四周安全边距 ≥ 48px', tip: '关键信息对齐网格线,避免元素悬空' };
},
test() { return this.run({ ratio: 0.66 }); }
},
/* ---------- hierarchy 视觉层级 ---------- */
{
id: 'poster-focal', name: '视觉焦点', category: 'hierarchy', emoji: '🎯',
scene: '判断海报是否只有一个清晰的主视觉焦点,避免信息平铺',
summary: '按元素面积/字号/对比度估算视觉权重,找出最强焦点并检查是否唯一。',
params: { elements: '元素数组(含 w/h/fontSize/weight)' },
source: '自研',
run(args) {
const els = (args.elements || []).map(e => ({ ...e, score: (e.weight || 0) || (e.fontSize || 0) || (e.w * e.h) }));
els.sort((a, b) => b.score - a.score);
const top = els[0], second = els[1];
const unique = !second || top.score > second.score * 1.6;
return { ok: unique, top: top && top.id, tip: unique ? '主焦点清晰' : '存在多个相近焦点,建议强化主视觉或弱化次要元素' };
},
test() { return this.run({ elements: [{ id: 'title', fontSize: 120 }, { id: 'sub', fontSize: 40 }] }); }
},
/* ---------- print 印刷输出 ---------- */
{
id: 'poster-bleed', name: '出血安全区', category: 'print', emoji: '🖨️',
scene: '计算印刷出血与安全区,避免裁切误伤关键内容',
summary: '按常见印刷规范输出出血(3mm)与安全边距,给出裁切警戒提示。',
params: { size: '成品尺寸(如 A4/A3 或宽×高 mm)', dpi: '分辨率(默认 300)' },
source: '自研(印刷规范)',
run(args) {
const dpi = Number(args.dpi) || 300;
const bleedPx = Math.round(3 / 25.4 * dpi); // 3mm 出血
const safePx = Math.round(6 / 25.4 * dpi); // 6mm 安全区
return {
ok: true, bleedPx, safePx, dpi,
tip: `出血 ${bleedPx}px,安全区 ${safePx}px;关键文字/Logo 保持在安全区内,背景/底纹延展到出血线外`
};
},
test() { return this.run({ dpi: 300 }); }
},
/* ---------- creative 创意手法 ---------- */
{
id: 'poster-contrast', name: '对比手法', category: 'creative', emoji: '🆚',
scene: '通过大小/疏密/明暗/动静对比制造视觉张力',
summary: '输出 6 种常用海报对比手法,供设计时组合运用。',
params: {},
source: '自研(设计原则)',
run() {
return {
ok: true,
methods: ['大小对比(大标题 vs 小正文)', '疏密对比(留白 vs 密集信息)', '明暗对比(深底浅字)', '动静对比(斜切/旋转 vs 平直)', '色彩对比(互补色强调)', '虚实对比(模糊背景 vs 清晰主体)'],
tip: '一张海报优先强化 1-2 种对比,避免手法堆砌'
};
},
test() { return this.run({}); }
},
/* ---------- ai 智能工作流 ---------- */
{
id: 'poster-brief', name: '海报需求洞察', category: 'ai', emoji: '🔍',
scene: '把活动/主题描述提炼为关键词与设计方向,指导 AI 生成',
summary: '输入主题描述,提取受众/情绪/主色/版式倾向等设计决策线索。',
params: { brief: '海报主题描述', industry: '行业(可选)' },
source: '自研',
run(args) {
const b = String(args.brief || '');
const mood = /高端|奢华|商务|正式/.test(b) ? '稳重正式' : (/活泼|促销|狂欢|party|节日/.test(b) ? '热闹活泼' : '中性现代');
const warm = /餐饮|美食|促销|节日|母婴/.test(b);
return {
ok: true, mood,
palette: warm ? '暖色系(橙/红/黄)' : '冷色系(蓝/紫/绿)或中性色',
tip: `气质:${mood};建议配色 ${warm ? '暖色系' : '冷色系/中性色'};排版按受众与情绪选择对称或自由版式`
};
},
test() { return this.run({ brief: '夏季餐饮促销狂欢节' }); }
},
/* ============================================================
* 第二批补充技能(2026-08-21 录入,每类 +1,run 纯函数 + test 自测)
* ============================================================ */
/* ---------- layout 版式构图 ---------- */
{
id: 'poster-zpattern', name: 'Z 型视线流', category: 'layout', emoji: '👀',
scene: '按 Z / F 型阅读路径检查信息摆放顺序,避免用户视线跳跃',
summary: '根据元素位置与权重,评估是否形成自然的 Z 型(横版)/ F 型(竖版)视线流。',
params: { elements: '元素数组(含 x/y/w/h 与 weight)', ratio: '画布宽高比(默认 0.66 竖版)' },
source: '自研(视线流设计法)',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
const r = Number(args.ratio) || 0.66;
if (els.length < 3) return { ok: true, tip: '元素较少,视线流压力小,重点保证主标题在首屏上方' };
const z = r >= 1; // 横版 Z / 竖版 F
const row = (e) => Math.round(e.y / (e.h || 1)); // 简化按纵向分段
const top = els.filter(e => row(e) === 0);
const bottom = els.filter(e => row(e) === Math.max(...els.map(row)));
const hasCorner = top.some(e => e.x < 10) && bottom.some(e => e.x < 10) && bottom.some(e => e.x + e.w > 80);
return {
ok: hasCorner,
type: z ? 'Z 型(横版)' : 'F 型(竖版)',
tip: hasCorner ? '视线流完整:左上起点 → 对角 → 右下收尾' : (z ? '建议左上放 Logo/主标题、右下放行动按钮(CTA),形成 Z 字收尾' : '建议左侧竖列信息对齐,底部放 CTA 引导行动')
};
},
test() { return this.run({ ratio: 1.78, elements: [{ x: 5, y: 5, w: 30, h: 10, weight: 3 }, { x: 60, y: 40, w: 30, h: 10, weight: 2 }, { x: 5, y: 80, w: 20, h: 10, weight: 1 }, { x: 70, y: 80, w: 20, h: 10, weight: 2 }] }); }
},
/* ---------- color 配色 ---------- */
{
id: 'poster-wcag-contrast', name: '文字对比度', category: 'color', emoji: '🎚️',
scene: '计算前景/背景颜色对比度(WCAG),确保文字可读性',
summary: '输入前景色与背景色 hex,返回 WCAG 对比率与 AA/AAA 达标等级,并给出调整建议。',
params: { fg: '前景色 hex', bg: '背景色 hex', size: '文字大小 px(默认 16,大字号 18 粗体/24 起阈值更低)' },
source: '自研(WCAG 2.1 对比度规范)',
run(args) {
const lum = (hex) => {
const c = (hex || '').replace('#', '');
const n = c.length === 3 ? c.split('').map(x => x + x).join('') : c;
const rgb = [0, 2, 4].map(i => parseInt(n.slice(i, i + 2), 16) / 255)
.map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
};
const fg = lum(args.fg || '#111111'), bg = lum(args.bg || '#ffffff');
const ratio = (Math.max(fg, bg) + 0.05) / (Math.min(fg, bg) + 0.05);
const size = Number(args.size) || 16;
const large = size >= 24 || (size >= 18.66 && (args.bold === '1' || args.bold === true));
const aa = large ? ratio >= 3 : ratio >= 4.5;
const aaa = large ? ratio >= 4.5 : ratio >= 7;
return {
ok: aa, ratio: ratio.toFixed(2) + ':1',
grade: aaa ? 'AAA' : (aa ? 'AA' : '不达标'),
tip: aa ? (aaa ? '对比度优秀(AAA)' : '对比度达标(AA)') : `对比度 ${ratio.toFixed(2)}:1 偏低,建议加深文字或提亮背景至 ≥${large ? 3 : 4.5}:1`
};
},
test() { return this.run({ fg: '#111111', bg: '#ffffff', size: 16 }); }
},
/* ---------- typography 字体排版 ---------- */
{
id: 'poster-font-pairing', name: '字体搭配', category: 'typography', emoji: '✍️',
scene: '为海报推荐标题/正文的字体搭配组合与注意事项',
summary: '按风格关键词输出标题字体 × 正文字体的搭配建议,附版权提示。',
params: { style: '风格关键词(高端/现代/文艺/复古/促销/科技,默认 现代)' },
source: '自研(字体搭配方法论)',
run(args) {
const s = String(args.style || '现代');
const map = {
'高端': { title: '衬线/窄体(如 Didot、思源宋体)', body: '细黑体(如思源黑体 Light)', tip: '标题字距放宽、字号差拉大,突出优雅与克制' },
'文艺': { title: '手写/宋体(如 楷体、恋字体)', body: '雅黑或宋体', tip: '标题可用竖排或居中留白营造诗意氛围' },
'复古': { title: '粗衬线/木刻体(如 方正超粗黑、League Spartan Black)', body: '中等黑体', tip: '可叠加做旧纹理与印章元素增强年代感' },
'促销': { title: '超粗黑体(如 思源黑体 Heavy)', body: '中黑体', tip: '大字号 + 倾斜/描边强化冲击力,价格用粗体放大' },
'科技': { title: '几何无衬线(如 Montserrat、Raleway)', body: '细无衬线', tip: '标题可加渐变描边或字母间距,呼应数码感' },
'现代': { title: '几何无衬线(如 思源黑体 Bold)', body: '思源黑体 Regular', tip: '同族字重组合最稳;标题与正文字号比建议 ≥3:1' }
};
const r = map[s] || map['现代'];
return { ok: true, title: r.title, body: r.body, tip: r.tip + ';海报正文避免超过 2 款字体' };
},
test() { return this.run({ style: '促销' }); }
},
/* ---------- grid 网格系统 ---------- */
{
id: 'poster-golden-ratio', name: '黄金分割构图', category: 'grid', emoji: '🌻',
scene: '用黄金比例(1:1.618)切分画布,快速定位主视觉与信息区',
summary: '按黄金分割生成主视觉区块与信息安全区坐标,指导版面分配。',
params: { w: '画布宽 px', h: '画布高 px' },
source: '自研(黄金分割 1:1.618)',
run(args) {
const w = Number(args.w) || 1000, h = Number(args.h) || 1500;
const g = (n) => n / 1.618;
const mw = Math.round(g(w)), mh = Math.round(g(h));
return {
ok: true,
focus: `主视觉区:左下 ${mw}×${mh}(宽 × 高 或按方向旋转使用)`,
info: `信息区:剩余 ${w - mw}×${h - mh}`,
tip: '把最重要内容放进黄金区块,次要点放对角,留白保持 ≥10% 画布'
};
},
test() { return this.run({ w: 1000, h: 1500 }); }
},
/* ---------- hierarchy 视觉层级 ---------- */
{
id: 'poster-reading-order', name: '阅读顺序', category: 'hierarchy', emoji: '🔢',
scene: '为海报信息元素排序,确保受众按设计意图依次阅读',
summary: '按「主标题→副标题→时间地点→行动按钮」的标准骨架检查元素顺序并编号。',
params: { elements: '元素数组(含 id、role:title/subtitle/time/place/cta/body)' },
source: '自研(信息层级方法论)',
run(args) {
const roleOrder = { title: 1, subtitle: 2, time: 3, place: 4, cta: 5, body: 6 };
const els = (args.elements || []).map((e, i) => ({ ...e, _idx: i }));
const withRole = els.filter(e => roleOrder[e.role]);
if (!withRole.length) return { ok: false, tip: '请为元素标注 role(title/subtitle/time/place/cta/body)以生成阅读顺序' };
const sorted = withRole.slice().sort((a, b) => roleOrder[a.role] - roleOrder[b.role]);
const order = sorted.map(e => e.id || e.role);
return {
ok: true,
order: order.join(' → '),
tip: '时间地点建议放在中段、CTA 放末尾最易点击位置;同 role 元素按出现顺序排列'
};
},
test() { return this.run({ elements: [{ id: '主题', role: 'title' }, { id: '时间', role: 'time' }, { id: '地点', role: 'place' }, { id: '报名按钮', role: 'cta' }, { id: '副题', role: 'subtitle' }] }); }
},
/* ---------- print 印刷输出 ---------- */
{
id: 'poster-print-size', name: '印刷尺寸换算', category: 'print', emoji: '📏',
scene: '换算 A 系列/常见开本成品尺寸到指定 DPI 的像素值',
summary: '内置 A 系列与常见海报开本尺寸表,按 DPI 输出像素宽高与安全提示。',
params: { size: '尺寸代号(A4/A3/A2/A1/B2/6K/4K,默认 A4)', dpi: '分辨率(默认 300)' },
source: '自研(ISO 216 / 印刷开本规范)',
run(args) {
const MM = { A4: [210, 297], A3: [297, 420], A2: [420, 594], A1: [594, 841], B2: [500, 707], '6K': [360, 390], '4K': [390, 540] };
const k = String(args.size || 'A4').toUpperCase();
const mm = MM[k];
if (!mm) return { ok: false, tip: '支持尺寸:' + Object.keys(MM).join(' / ') };
const dpi = Number(args.dpi) || 300;
const px = (m) => Math.round(m / 25.4 * dpi);
return {
ok: true, size: k, mm: mm[0] + '×' + mm[1] + 'mm',
px: px(mm[0]) + '×' + px(mm[1]) + 'px @' + dpi + 'dpi',
tip: '海报源文件建议 ≥300dpi;如仅屏幕展示 72dpi 即可;导出 PDF 选「印刷质量」'
};
},
test() { return this.run({ size: 'A3', dpi: 300 }); }
},
/* ---------- creative 创意手法 ---------- */
{
id: 'poster-moodboard', name: '情绪风格映射', category: 'creative', emoji: '🧭',
scene: '根据情绪关键词输出风格手法、配色倾向与字体建议',
summary: '将情绪词(活力/高级/可爱/冷酷…)映射为设计手法清单,作为创意起点。',
params: { mood: '情绪关键词(活力/高级/可爱/冷酷/治愈/复古)' },
source: '自研(情绪板方法论)',
run(args) {
const m = String(args.mood || '高级');
const map = {
'活力': { color: '高饱和橙/黄 + 撞色', font: '超粗黑体 + 斜切', trick: '大色块 + 波点/放射线背景,元素可轻微旋转' },
'高级': { color: '黑白灰 + 单一强调色', font: '衬线/细黑体 + 宽字距', trick: '大留白 + 细线条点缀,克制用色' },
'可爱': { color: '马卡龙色系(粉/薄荷/淡黄)', font: '圆体/手写体', trick: '圆角元素 + 表情/贴纸点缀 + 柔和阴影' },
'冷酷': { color: '深黑 + 霓虹/金属色', font: '窄体无衬线', trick: '硬边几何 + 网格线 + 高反差打光' },
'治愈': { color: '低饱和莫兰迪色系', font: '细黑体/宋体', trick: '柔和渐变 + 自然元素(云/叶/光斑)' },
'复古': { color: '棕/米/芥末黄 + 旧红', font: '衬线/木刻体', trick: '做旧纹理 + 相框/邮戳元素 + 半调网点' }
};
const r = map[m] || map['高级'];
return { ok: true, mood: m, color: r.color, font: r.font, trick: r.trick };
},
test() { return this.run({ mood: '治愈' }); }
},
/* ---------- ai 智能工作流 ---------- */
{
id: 'poster-ai-prompt', name: 'AI 生成提示词', category: 'ai', emoji: '🤖',
scene: '把海报需求组织成结构化 AI 提示词,提升生成质量与可控性',
summary: '按「角色-主体-背景-配色-字体-构图-风格」输出可直接投喂 AI 的提示词模板。',
params: { brief: '海报主题描述', industry: '行业(可选)' },
source: '自研(AI 海报生成工作台提示词工程)',
run(args) {
const b = String(args.brief || '夏日饮品促销');
const ind = String(args.industry || '餐饮');
return {
ok: true,
prompt: `请为「${ind}」设计一张「${b}」主题的海报。要求:1) 主体清晰突出,背景与主体层次分明;2) 配色和谐并符合行业气质;3) 构图遵循视觉层级,信息主次有序;4) 预留标题区与行动信息区(时间地点);5) 输出可落地的设计决策 JSON(布局/配色/字体/元素清单)。`,
tip: '描述越具体(人群/场景/调性/禁用元素)生成越可控;可附加参考色板与参考图'
};
},
test() { return this.run({ brief: '秋季新品上市', industry: '时尚' }); }
},
/* ============================================================
* 第三批补充技能(2026-08-21 录入,每类 +1,run 纯函数 + test 自测)
* ============================================================ */
/* ---------- layout 版式构图 ---------- */
{
id: 'poster-thirds', name: '三分法构图', category: 'layout', emoji: '➗',
scene: '按三分法(Rule of Thirds)检查主视觉与交点分布,快速优化构图',
summary: '将画布横竖各分三份,检查关键元素是否落在 4 个交点附近,避免主体居中呆板。',
params: { elements: '元素数组(含 x/y/w/h 与 weight)', canvas: '{w,h}' },
source: '自研(Rule of Thirds 构图法)',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
const w = (args.canvas && args.canvas.w) || 1000, h = (args.canvas && args.canvas.h) || 1500;
if (!els.length) return { ok: true, note: '无元素,无法评估' };
const ix = [w * 1 / 3, w * 2 / 3], iy = [h * 1 / 3, h * 2 / 3];
const near = (v, arr) => arr.some(a => Math.abs(v - a) < Math.max(w, h) * 0.12);
const onPoint = els.filter(e => near(e.x + e.w / 2, ix) && near(e.y + e.h / 2, iy)).length;
return {
ok: onPoint > 0,
thirds: `${ix.map(v => Math.round(v)).join('/')}(横)× ${iy.map(v => Math.round(v)).join('/')}(纵)`,
tip: onPoint ? '已有元素落在三分点附近,视觉停留感好' : '建议把主视觉/标题放到三分线交点上(左上或右下),营造动势与留白'
};
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, elements: [{ x: 667, y: 500, w: 100, h: 100 }] }); }
},
/* ---------- color 配色 ---------- */
{
id: 'poster-harmony', name: '色彩和谐', category: 'color', emoji: '🌈',
scene: '基于主色生成相邻色/互补色和谐色板,保证画面协调',
summary: '输入主色 hex,输出色相偏移 ±30° 的相邻色与 ±180° 的互补色建议,附和谐度提示。',
params: { primary: '主色 hex', scheme: '方案:analogous(相邻)/complementary(互补),默认 analogous' },
source: '自研(色相环配色法则)',
run(args) {
const hex = String(args.primary || '#4f46e5').replace('#', '');
const r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), b = parseInt(hex.slice(4, 6), 16);
const toHsl = () => {
const rn = r / 255, gn = g / 255, bn = b / 255;
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);
else if (max === gn) h = (bn - rn) / d + 2;
else h = (rn - gn) / d + 4;
h *= 60;
}
return { h, s, l };
};
const hslToHex = (h, s, l) => {
const fn = (p, q, t) => {
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const c = [fn(p, q, h / 360 + 1 / 3), fn(p, q, h / 360), fn(p, q, h / 360 - 1 / 3)]
.map(v => Math.round(v * 255).toString(16).padStart(2, '0'));
return '#' + c.join('');
};
const { h, s, l } = toHsl();
const scheme = String(args.scheme || 'analogous');
const pairs = scheme === 'complementary'
? [[(h + 150 + 360) % 360, '相邻左'], [(h + 180) % 360, '互补'], [(h + 210) % 360, '相邻右']]
: [[(h - 30 + 360) % 360, '相邻左'], [(h + 30) % 360, '相邻右'], [(h + 180) % 360, '互补(点缀)']];
return {
ok: true, scheme: scheme === 'complementary' ? '互补配色' : '相邻配色',
colors: pairs.map(([hh, label]) => ({ color: hslToHex(hh, s, l), label })),
tip: scheme === 'complementary' ? '互补色对比强,建议主色占 60%、互补色仅作点缀' : '相邻色协调柔和,建议用明度差(l ± 0.15)建立层次'
};
},
test() { return this.run({ primary: '#4f46e5', scheme: 'complementary' }); }
},
/* ---------- typography 字体排版 ---------- */
{
id: 'poster-cjk-latin', name: '中英混排', category: 'typography', emoji: '🈴',
scene: '检查中文与英文/数字混排的对齐、字距与行高问题',
summary: '解析文本中的中英文片段占比,输出混排行高、字距与中西文间距(U+2005 薄空格)建议。',
params: { text: '待排版文案' },
source: '自研(中西文混排排版规范)',
run(args) {
const t = String(args.text || '');
const cn = (t.match(/[\u4e00-\u9fa5]/g) || []).length;
const en = (t.match(/[A-Za-z0-9]/g) || []).length;
const total = Math.max(cn + en, 1);
const hasMix = cn > 0 && en > 0;
return {
ok: true, mix: hasMix,
cn, en, cnRatio: (cn / total * 100).toFixed(0) + '%',
tip: hasMix
? '建议行高 1.6-1.8(纯中文 1.5-1.6 需提高以容纳西文上升部);中西文间加 U+2005 薄空格;西文数字用等宽或半角,避免与中文基线错位'
: '单语种文案,保持常规行高(中文 1.5 / 西文 1.4)即可'
};
},
test() { return this.run({ text: '活动时间 2026.09.01,欢迎参加 Summer Party' }); }
},
/* ---------- grid 网格系统 ---------- */
{
id: 'poster-symmetry', name: '对称检查', category: 'grid', emoji: '🪞',
scene: '判断版面是否刻意对称,并提示对称/不对称的适用场景',
summary: '按元素左右镜像位置计算对称偏差,输出对称类型(镜面对称/局部对称/非对称)建议。',
params: { elements: '元素数组(含 x/y/w/h)', canvas: '{w,h}' },
source: '自研(对称构图法)',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
const w = (args.canvas && args.canvas.w) || 1000;
if (!els.length) return { ok: true, note: '无元素,无法评估' };
const tolerance = w * 0.06;
const mirrored = els.map(e => ({ x2: w - e.x - e.w, y: e.y, w: e.w, h: e.h }))
.map(m => els.some(e => Math.abs(e.x - m.x2) <= tolerance && Math.abs(e.y - m.y) <= tolerance && Math.abs(e.w - m.w) <= tolerance));
const symN = mirrored.filter(Boolean).length;
const type = symN === els.length ? '严格镜面对称' : (symN > 0 ? '局部对称' : '非对称(自由)');
return {
ok: true, type, symCount: symN + '/' + els.length,
tip: type === '严格镜面对称' ? '正式/庄重场景合适,注意避免呆板(可加不对称点缀)' : (type === '局部对称' ? '对称与自由混搭,动感与秩序兼得' : '非对称构图更有张力,需保证视觉重心平衡')
};
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, elements: [{ x: 100, y: 100, w: 200, h: 100 }, { x: 700, y: 100, w: 200, h: 100 }] }); }
},
/* ---------- hierarchy 视觉层级 ---------- */
{
id: 'poster-priority', name: '信息优先级', category: 'hierarchy', emoji: '🏷️',
scene: '为海报信息元素排出视觉优先级并映射到字号/色彩/位置',
summary: '输入信息条目与角色,输出优先级排序及对应视觉强化手段(字号/颜色/留白/位置)。',
params: { items: '信息数组:{id, role: title/subtitle/body/cta/detail}' },
source: '自研(信息优先级方法论)',
run(args) {
const rank = { title: 1, cta: 2, subtitle: 3, detail: 4, body: 5 };
const items = (args.items || []).filter(i => rank[i.role]);
if (!items.length) return { ok: false, tip: '请为信息标注 role(title/cta/subtitle/detail/body)' };
const sorted = items.slice().sort((a, b) => rank[a.role] - rank[b.role]);
return {
ok: true,
order: sorted.map(i => i.id || i.role).join(' > '),
suggestion: {
title: '最大字号 + 高对比色,居上或视觉中心',
cta: '次大字号 + 强调色/描边,放右下或底部易点击区',
subtitle: '比标题小 40%-50%,靠近标题',
detail: '中号字,弱化(低对比/小号)',
body: '最小字号,最后阅读'
},
tip: '优先级最高只保留 1-2 个;其余信息通过字号差 ≥2 级、颜色降饱和、位置下移逐级弱化'
};
},
test() { return this.run({ items: [{ id: '主标题', role: 'title' }, { id: '报名按钮', role: 'cta' }, { id: '详情', role: 'detail' }] }); }
},
/* ---------- print 印刷输出 ---------- */
{
id: 'poster-cmyk', name: '印刷色域', category: 'print', emoji: '🖍️',
scene: '将 RGB 颜色换算为 CMYK 并提示印刷色差风险',
summary: '按标准公式把 hex 转为 CMYK 四色值,对高饱和/金属色给出偏色与备选建议。',
params: { hex: '颜色 hex' },
source: '自研(CMYK 转换公式,灰成分替代法)',
run(args) {
const hex = String(args.hex || '#e63946').replace('#', '');
const r = parseInt(hex.slice(0, 2), 16) / 255, g = parseInt(hex.slice(2, 4), 16) / 255, b = parseInt(hex.slice(4, 6), 16) / 255;
const k = 1 - Math.max(r, g, b);
const c = k < 1 ? (1 - r - k) / (1 - k) : 0;
const m = k < 1 ? (1 - g - k) / (1 - k) : 0;
const y = k < 1 ? (1 - b - k) / (1 - k) : 0;
const toPct = v => Math.round(v * 100);
const vivid = (1 - k) > 0.85;
return {
ok: true, cmyk: `C${toPct(c)} M${toPct(m)} Y${toPct(y)} K${toPct(k)}`,
risk: vivid ? '高饱和色在四色印刷中易偏灰/偏暗,建议降低饱和度或用专色(PANTONE)' : '常规印刷色域内,风险低',
tip: '屏幕色(RGB)与印刷色(CMYK)存在色差,重要物料建议打样确认;纯黑文字用 K100,避免套印发虚'
};
},
test() { return this.run({ hex: '#e63946' }); }
},
/* ---------- creative 创意手法 ---------- */
{
id: 'poster-rhythm', name: '节奏韵律', category: 'creative', emoji: '🥁',
scene: '通过元素重复/渐变/对比建立版面节奏,避免信息平铺',
summary: '分析元素间距序列的规律性,输出重复/渐变/放射等节奏手法建议。',
params: { elements: '元素数组(含 x/y/w/h 或 weight)' },
source: '自研(版面节奏设计法)',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
if (els.length < 3) return { ok: true, tip: '元素较少,可尝试将同类元素组复制成 3-5 组形成重复节奏' };
const xs = els.map(e => e.x).sort((a, b) => a - b);
const gaps = xs.slice(1).map((v, i) => v - xs[i]);
const same = gaps.every(g => Math.abs(g - gaps[0]) <= (Math.abs(gaps[0]) || 1) * 0.2);
return {
ok: same,
rhythm: same ? '重复节奏(等距)' : '渐变/自由节奏',
tip: same ? '等距重复稳重大气,可叠加尺寸/颜色渐变打破单调' : '间距不等已形成变化,建议按 1:1.6:2.6 之类比例递增,制造韵律而不凌乱'
};
},
test() { return this.run({ elements: [{ x: 0, w: 50 }, { x: 100, w: 50 }, { x: 200, w: 50 }, { x: 300, w: 50 }] }); }
},
/* ---------- ai 智能工作流 ---------- */
{
id: 'poster-ai-style', name: '风格迁移参考', category: 'ai', emoji: '🎭',
scene: '把目标风格关键词转译为 AI 可理解的设计参数(配色/字体/纹理/构图)',
summary: '输入风格名(极简/孟菲斯/波普/新丑风/赛博朋克…),输出该风格的可落地参数供 AI 生成。',
params: { style: '风格关键词(极简/孟菲斯/波普/新丑风/赛博朋克/酸性/手绘)' },
source: '自研(风格参数化方法)',
run(args) {
const s = String(args.style || '极简');
const map = {
'极简': { color: '黑白灰 + 1 个强调色', font: '细黑体/无衬线,大字号', texture: '大留白、细线框', compose: '居中或留白构图,元素 ≤3 个' },
'孟菲斯': { color: '高饱和撞色(粉/蓝/黄/绿)', font: '圆体/几何无衬线', texture: '波点、锯齿、几何碎片', compose: '不规则网格、错位排列' },
'波普': { color: '原色碰撞(红/蓝/黄/黑)', font: '粗衬线/木刻体', texture: '半调网点(Ben-Day Dots)', compose: '重复放大、漫画分格' },
'新丑风': { color: '荧光色 + 深底', font: '超粗黑体 + 描边', texture: '噪点、错位、倾斜', compose: '撞版、文字叠压、夸张比例' },
'赛博朋克': { color: '霓虹紫/青 + 黑', font: '窄体无衬线', texture: '故障(Glitch)、扫描线', compose: '透视网格、光效溢出' },
'酸性': { color: '荧光渐变(青/紫/粉)', font: '扭曲/液态文字', texture: '金属质感、折射光', compose: '流体造型、迷幻背景' },
'手绘': { color: '低饱和 + 纸张底色', font: '手写体/圆体', texture: '铅笔噪点、水彩边缘', compose: '自由随性、歪斜排列' }
};
const r = map[s] || map['极简'];
return { ok: true, style: s, color: r.color, font: r.font, texture: r.texture, compose: r.compose, tip: '把上述参数并入 AI 提示词,并在负面提示中禁止与该风格冲突的元素' };
},
test() { return this.run({ style: '赛博朋克' }); }
},
/* ---------- 第四批:layout 画框构图 ---------- */
{
id: 'poster-frame', name: '画框构图', category: 'layout', emoji: '🖼️',
scene: '检查元素是否侵入画布安全边距,建立「画框式」留白构图',
summary: '输入画布与元素,计算元素与画布边缘的最小间距,输出安全边距建议与越界元素清单。',
params: { canvas: '{w,h}', margin: '安全边距(默认 48px)', elements: '元素数组(含 x/y/w/h)' },
source: '自研(画框式构图法)',
run(args) {
const cw = (args.canvas && args.canvas.w) || 1000;
const ch = (args.canvas && args.canvas.h) || 1500;
const m = Number(args.margin) || 48;
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
if (!els.length) return { ok: true, note: '无元素,默认四周留白 ≥ ' + m + 'px 的「画框」式构图' };
const bad = els.filter(e => e.x < m || e.y < m || e.x + e.w > cw - m || e.y + e.h > ch - m);
return { ok: bad.length === 0, safeMargin: m, overflow: bad.map(e => e.id || e.name || '?'), tip: bad.length ? '有 ' + bad.length + ' 个元素侵入安全区,建议内移或缩放到 ' + m + 'px 边距内' : '所有元素均在安全边距内,画框式留白成立' };
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, margin: 48, elements: [{ id: 'a', x: 20, y: 50, w: 200, h: 100 }, { id: 'b', x: 400, y: 500, w: 200, h: 100 }] }); }
},
/* ---------- 第四批:color 渐变配色 ---------- */
{
id: 'poster-gradient', name: '渐变配色', category: 'color', emoji: '🌅',
scene: '基于主色生成海报背景渐变(线性/径向)',
summary: '输入主色 hex,按 RGB 明度插值生成 2-6 段渐变序列,输出适合海报背景的配色与点缀互补色。',
params: { primary: '主色 hex', steps: '渐变段数(默认 4)' },
source: '自研(亮度插值法)',
run(args) {
const hex = String(args.primary || '#4f46e5');
const steps = Math.min(Math.max(Number(args.steps) || 4, 2), 6);
const m = hex.match(/^#?([0-9a-f]{6})$/i);
if (!m) return { error: '无效 hex: ' + hex };
const n = parseInt(m[1], 16);
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
const hexs = (x, y, z) => '#' + [x, y, z].map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
const seq = [];
for (let i = 0; i < steps; i++) {
const t = steps === 1 ? 1 : i / (steps - 1);
const f = 0.65 + 0.35 * t; // 从暗到亮的整体提亮
seq.push(hexs(r * f, g * f, b * f));
}
const pair = hexs(255 - r, 255 - g, 255 - b);
return { ok: true, gradient: seq.join(' → '), accent: pair, tip: '背景建议用 ' + seq[0] + ' → ' + seq[seq.length - 1] + ' 做线性渐变;点缀色可用互补色 ' + pair };
},
test() { return this.run({ primary: '#4f46e5', steps: 4 }); }
},
/* ---------- 第四批:typography 行长断行 ---------- */
{
id: 'poster-line-length', name: '行长断行', category: 'typography', emoji: '📐',
scene: '按可用宽度建议字号与行长,保证标题/正文可读性',
summary: '根据可用宽度与用途(标题/正文),建议最佳字号与每行字符数(西文 45-75 字符),并给出断行提示。',
params: { width: '可用宽度 px', isTitle: '是否标题(默认 false)' },
source: '自研(阅读排版规范)',
run(args) {
const w = Number(args.width) || 640;
const isTitle = !!args.isTitle;
const best = isTitle ? Math.min(w / 14, 40) : Math.min(w / 12, 24);
const lineChars = Math.max(8, Math.floor(w / best));
return { ok: true, fontSize: Math.round(best), charsPerLine: lineChars, tip: isTitle ? '标题单行 ≤ ' + lineChars + ' 字,超长建议断成 2 行并保留主谓完整、避免孤字' : '西文行长建议 45-75 字符(当前约 ' + lineChars + '),中文可放宽至 20-35 字/行' };
},
test() { return this.run({ width: 640, isTitle: false }); }
},
/* ---------- 第四批:grid 对角线构图 ---------- */
{
id: 'poster-diagonal', name: '对角线构图', category: 'grid', emoji: '📉',
scene: '判断元素是否沿对角线排布,输出斜向构图参数',
summary: '把元素中心点投影到画布主对角线上,检查偏离度并给出沿对角线排列的调整建议。',
params: { canvas: '{w,h}', elements: '元素数组(含 x/y/w/h)' },
source: '自研',
run(args) {
const cw = (args.canvas && args.canvas.w) || 1000;
const ch = (args.canvas && args.canvas.h) || 1500;
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
if (els.length < 2) return { ok: true, tip: '元素过少,可将 3-5 个元素沿主对角线(左上→右下)排布制造动势' };
const devs = els.map(e => {
const cx = e.x + e.w / 2, cy = e.y + e.h / 2;
const proj = (cx / cw + cy / ch) / 2; // 对角投影 0..1
return Math.abs(proj - 0.5) * 2; // 偏离主对角线程度 0..1
});
const avg = devs.reduce((a, b) => a + b, 0) / devs.length;
const ok = avg < 0.35;
return { ok, deviation: (avg * 100).toFixed(0) + '%', tip: ok ? '元素基本贴合对角线,动势良好' : '偏离度较高,建议把元素中心对齐到「x/w = y/h」的斜线上' };
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, elements: [{ x: 50, y: 80, w: 100, h: 100 }, { x: 300, y: 450, w: 100, h: 100 }, { x: 600, y: 900, w: 100, h: 100 }] }); }
},
/* ---------- 第四批:hierarchy 色彩层级 ---------- */
{
id: 'poster-color-hierarchy', name: '色彩层级', category: 'hierarchy', emoji: '🎚️',
scene: '用颜色与背景的对比度区分信息等级,强化主次',
summary: '输入背景色与元素(权重 + 颜色),按 WCAG 相对对比度检查是否与信息权重匹配,输出色彩层级建议。',
params: { bg: '背景色 hex', elements: '元素数组(含 weight、color hex)' },
source: '自研(WCAG 对比度法)',
run(args) {
const bg = String(args.bg || '#ffffff');
const els = (args.elements || []).filter(e => e.color && e.weight);
if (!els.length) return { ok: true, note: '提供含 color 与 weight 的元素后可评估' };
const rel = c => {
const m = String(c).match(/^#?([0-9a-f]{6})$/i);
if (!m) return null;
const n = parseInt(m[1], 16);
const lin = v => { const x = ((n >> (v * 8)) & 255) / 255; return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); };
return 0.2126 * lin(2) + 0.7152 * lin(1) + 0.0722 * lin(0);
};
const L1 = rel(bg);
if (L1 == null) return { error: '无效背景色: ' + bg };
const rows = els.map(e => { const L2 = rel(e.color); if (L2 == null) return null; const hi = Math.max(L1, L2), lo = Math.min(L1, L2); return { e, ratio: (hi + 0.05) / (lo + 0.05) }; }).filter(Boolean);
if (!rows.length) return { ok: true, note: '元素颜色无效,无法评估' };
const byWeight = rows.slice().sort((a, b) => b.e.weight - a.e.weight);
const weakest = byWeight[byWeight.length - 1];
const mismatch = rows.filter(r => r.e.weight > weakest.e.weight * 2 && r.ratio < weakest.ratio);
return { ok: mismatch.length === 0, tip: mismatch.length ? '有 ' + mismatch.length + ' 个高权重元素对比度反而低于低权重元素,建议加强其颜色/字号' : '信息权重与色彩对比度匹配,层级分明' };
},
test() { return this.run({ bg: '#ffffff', elements: [{ weight: 5, color: '#111111' }, { weight: 3, color: '#666666' }, { weight: 1, color: '#aaaaaa' }] }); }
},
/* ---------- 第四批:print 输出分辨率 ---------- */
{
id: 'poster-dpi', name: '输出分辨率', category: 'print', emoji: '🖨️',
scene: '把海报物理尺寸换算成像素分辨率(印刷/屏幕)',
summary: '输入宽高(cm)与用途,输出所需像素尺寸与 dpi 建议,避免印刷糊图或导出失真。',
params: { width: '宽度 cm', height: '高度 cm', use: 'print | screen(默认 print)' },
source: '自研(印刷工艺标准)',
run(args) {
const w = Number(args.width) || 42, h = Number(args.height) || 59.4;
const dpi = args.use === 'screen' ? 96 : 300;
const px = (cm, d) => Math.round(cm / 2.54 * d);
return { ok: true, dpi, widthPx: px(w, dpi), heightPx: px(h, dpi), tip: args.use === 'screen' ? '屏幕输出 96dpi 即可(' + px(w, 96) + '×' + px(h, 96) + 'px);矢量素材可任意缩放不糊图' : '印刷 300dpi:' + px(w, 300) + '×' + px(h, 300) + 'px;大幅面喷绘可用 150dpi,超大尺寸再降' };
},
test() { return this.run({ width: 42, height: 59.4 }); }
},
/* ---------- 第四批:creative 破格张力 ---------- */
{
id: 'poster-grid-break', name: '破格张力', category: 'creative', emoji: '💥',
scene: '故意打破网格制造视觉张力,让海报跳出规整感',
summary: '在规整网格中推荐 1-2 个元素做破格(旋转/溢出/压叠),并给出幅度与数量建议。',
params: { grid: '{cols,rows}', elements: '元素数组' },
source: '自研(反网格设计法)',
run(args) {
const cols = Number(args.grid && args.grid.cols) || 3;
const els = (args.elements || []).length;
if (els < 3) return { ok: true, tip: '元素少于 3 个时破格效果弱,建议先做 5 个以上元素再破格 1 个' };
return { ok: true, breakNum: 1, rotate: '推荐将主标题旋转 5-8° 或溢出画布边缘 10-20%', overflow: '让 1 个强调元素溢出网格边界,其余保持对齐', tip: '破格元素不超过总数的 1/4,否则版面失控;用「对齐的主体 + 一个反叛元素」制造张力' };
},
test() { return this.run({ grid: { cols: 3 }, elements: [1, 2, 3, 4, 5] }); }
},
/* ---------- 第四批:ai 迭代优化 ---------- */
{
id: 'poster-ai-iterate', name: 'AI 迭代优化', category: 'ai', emoji: '🔁',
scene: '根据初稿与反馈生成下一轮 AI 生成指令',
summary: '输入初稿提示词与修改反馈(; 分隔),输出保留/调整清单与结构化迭代提示词。',
params: { draft: '初稿提示词', feedback: '修改反馈(多条,; 分隔)' },
source: '自研(迭代优化工作流)',
run(args) {
const d = String(args.draft || '');
const fb = String(args.feedback || '');
const items = fb.split(/[;;\n]/).map(s => s.trim()).filter(Boolean);
if (!items.length) return { ok: true, tip: '无反馈,可先跑一版初稿再收集意见(建议收集 3-5 条)' };
const change = items.filter(s => !/保留|挺好|ok|一致|满意/i.test(s));
const keep = items.filter(s => !change.includes(s));
return {
ok: true,
keep: keep.length ? keep : '无明确保留项',
adjust: change.length ? change : '无调整项',
prompt: ('基于初稿「' + d.slice(0, 40) + (d.length > 40 ? '…' : '') + '」' + (change.length ? '调整:' + change.join(';') : '微调并保持原风格')).slice(0, 200),
tip: '把 keep 保留进下一轮,change 转成具体指令(如「主色改 #123456」「标题字号加大一级」),每轮反馈不超过 5 条避免风格漂移'
};
},
test() { return this.run({ draft: '极简风格科技海报,深蓝背景', feedback: '保留配色;标题不够突出,加大并加粗;去掉底部装饰' }); }
},
/* ---------- 第五批:layout 非对称平衡 ---------- */
{
id: 'poster-asymmetry', name: '非对称平衡', category: 'layout', emoji: '🧱',
scene: '用非对称布局制造动感又不失平衡(以留白/暗色块/元素移动补偿)',
summary: '输入元素坐标与面积,计算左右视觉重量差,输出补偿建议(留白/暗度/位移)。',
params: { canvas: '{w,h}', elements: '元素数组(含 x/y/w/h、weight 可选)' },
source: '自研(视觉重量补偿法)',
run(args) {
const cw = (args.canvas && args.canvas.w) || 1000;
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
if (!els.length) return { ok: true, note: '无元素,可尝试「大留白 + 偏移主视觉」的非对称构图' };
let L = 0, R = 0;
for (const e of els) {
const w = e.weight || e.w * e.h;
if (e.x + e.w / 2 < cw / 2) L += w; else R += w;
}
const total = L + R;
const d = total ? (R - L) / total : 0;
const ok = Math.abs(d) < 0.25;
return { ok, left: (L / total * 100).toFixed(0) + '%', right: (R / total * 100).toFixed(0) + '%', tip: ok ? '非对称布局成立(左右差距 ≤ 25%)' : (d > 0 ? '右侧偏重,建议左侧补元素或加大左侧元素面积/暗度' : '左侧偏重,建议右侧补元素或加大右侧元素面积/暗度') };
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, elements: [{ x: 100, y: 200, w: 400, h: 400 }, { x: 700, y: 900, w: 200, h: 200 }] }); }
},
/* ---------- 第五批:color 品牌色适配 ---------- */
{
id: 'poster-brand-color', name: '品牌色适配', category: 'color', emoji: '🏷️',
scene: '基于品牌主色生成海报全套配色(主色/浅调/深调/中性/点缀)',
summary: '输入品牌主色 hex,输出同色系浅调、深调、中性灰与点缀色建议,保证品牌识别与海报视觉统一。',
params: { brand: '品牌主色 hex' },
source: '自研(品牌色延展法)',
run(args) {
const hex = String(args.brand || '#0a84ff');
const m = hex.match(/^#?([0-9a-f]{6})$/i);
if (!m) return { error: '无效 hex: ' + hex };
const n = parseInt(m[1], 16);
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
const c = (x, y, z) => '#' + [x, y, z].map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
const tint = c(r + (255 - r) * 0.8, g + (255 - g) * 0.8, b + (255 - b) * 0.8);
const shade = c(r * 0.55, g * 0.55, b * 0.55);
return { ok: true, primary: hex.toUpperCase(), tint, shade, neutral: '#f5f5f7', accent: c(255 - r, 255 - g, 255 - b), tip: '品牌主色用于标题/CTA,浅调 ' + tint + ' 做背景分区,深调 ' + shade + ' 做正文,中性 #f5f5f7 铺底,点缀色控制在 10% 以内' };
},
test() { return this.run({ brand: '#0a84ff' }); }
},
/* ---------- 第五批:typography 竖排文字 ---------- */
{
id: 'poster-vertical-text', name: '竖排文字', category: 'typography', emoji: '🀄',
scene: '中文竖排(直排)排版参数建议,适合文艺/传统风格海报',
summary: '输入文本长度与单列宽,输出竖排字号、行距、字距与换列建议(自上而下、从右向左)。',
params: { textLength: '文本字符数', columnWidth: '单列宽 px' },
source: '自研(中文直排规范)',
run(args) {
const len = Math.max(1, Number(args.textLength) || 12);
const cw = Number(args.columnWidth) || 60;
const fontSize = Math.round(cw * 0.72);
const lineHeight = Math.round(fontSize * 1.55);
const perCol = Math.floor(1500 / lineHeight);
const cols = Math.ceil(len / perCol);
return { ok: true, fontSize, lineHeight, letterSpacing: '字距建议 0.05-0.12em(竖排偏紧)', columns: cols, tip: '竖排自右向左换列,列距建议 1-1.5 个汉字宽;标点放字右下方,首列顶部可留 1-2 字天头' };
},
test() { return this.run({ textLength: 24, columnWidth: 60 }); }
},
/* ---------- 第五批:grid 错位网格 ---------- */
{
id: 'poster-offset-grid', name: '错位网格', category: 'grid', emoji: '🧩',
scene: '用错位/交错网格替代标准网格,营造活泼版面',
summary: '输入标准网格参数,输出错位偏移量与交错节奏建议(错位仅用于装饰块,文字仍对齐主轴)。',
params: { cols: '列数', cellW: '单元宽 px' },
source: '自研(交错网格法)',
run(args) {
const cols = Math.max(2, Number(args.cols) || 3);
const cell = Number(args.cellW) || 120;
const offset = Math.round(cell * 0.5);
const stagger = Math.round(cell * 0.35);
return { ok: true, cols, offset, stagger, tip: '奇数行整体右移 ' + offset + 'px、每行递增 ' + stagger + 'px 形成错落;错位仅用于装饰块,文字仍对齐主轴' };
},
test() { return this.run({ cols: 3, cellW: 120 }); }
},
/* ---------- 第五批:hierarchy 强调手法 ---------- */
{
id: 'poster-emphasis', name: '强调手法', category: 'hierarchy', emoji: '⭐',
scene: '为关键信息分配强调手段(放大/加粗/变色/描边/反白)',
summary: '输入信息层级数,按层级分配不冲突的强调手法,避免「全场都在强调」。',
params: { levels: '信息层级数(默认 3)' },
source: '自研(强调手段分配法)',
run(args) {
const n = Math.min(Math.max(Number(args.levels) || 3, 1), 6);
const methods = ['放大 + 加粗 + 主色', '增大字号或加粗', '变色(强调色)', '描边/反白', '加下划线或引号', '灰化弱化'];
const assign = [];
for (let i = 0; i < n; i++) assign.push('L' + (i + 1) + ' → ' + (methods[i] || '弱化'));
return { ok: true, assign, tip: '同一元素只叠加 ≤2 种手法,全页强调元素 ≤ 3 个,其余靠层级自然弱化' };
},
test() { return this.run({ levels: 3 }); }
},
/* ---------- 第五批:print 纸张规格 ---------- */
{
id: 'poster-paper-weight', name: '纸张规格', category: 'print', emoji: '📄',
scene: '为海报选择合适纸张克重与表面工艺(印刷)',
summary: '输入用途(宣传单/艺术海报/户外/吊旗),输出推荐纸张克重、表面处理与印刷方式建议。',
params: { use: '宣传单 | 艺术海报 | 户外 | 吊旗(默认宣传单)' },
source: '自研(印刷工艺标准)',
run(args) {
const map = {
'宣传单': { weight: '157g 铜版纸', surface: '光面/哑膜', print: '四色胶印', tip: '双面 157g 铜版最均衡,哑膜防指纹' },
'艺术海报': { weight: '250g 艺术纸/硫酸纸', surface: '哑面纹理', print: '四色 + 局部 UV', tip: '250g 以上挺括不易卷边,局部 UV 提亮主视觉' },
'户外': { weight: 'PVC 喷绘布', surface: '哑面抗 UV', print: '喷绘 720dpi', tip: '户外面料耐候抗晒,需留出血与安装孔位' },
'吊旗': { weight: '220g 旗帜布', surface: '双面印', print: '数码直喷', tip: '双面内容方向相反,顶部预留旗杆穿口' }
};
const u = String(args.use || '宣传单');
const r = map[u] || map['宣传单'];
return Object.assign({ ok: true, use: u }, r);
},
test() { return this.run({ use: '艺术海报' }); }
},
/* ---------- 第五批:creative 像素风设计 ---------- */
{
id: 'poster-pixel-style', name: '像素风设计', category: 'creative', emoji: '👾',
scene: '复古游戏/像素风海报的参数建议(对齐网格、配色、字体、边缘)',
summary: '输出像素风设计的网格对齐、颜色数量、字体与边缘参数建议(与 Pixelarticons 像素图标搭配使用)。',
params: { size: '像素尺寸(默认 24)' },
source: '自研(像素艺术规范)',
run(args) {
const s = Math.max(4, Number(args.size) || 24);
return { ok: true, grid: '元素对齐到 ' + s + 'px 整数倍网格', colors: '主色 ≤ 4 色(1 深色描边 + 2 主色 + 1 点缀)', font: '推荐像素字体或等宽字(如 JetBrains Mono)', edge: '描边 1px 硬边、禁用抗锯齿/柔边投影', tip: '背景可用深色 + 像素图标点缀,阴影用 2-4px 硬偏移模拟立体感' };
},
test() { return this.run({ size: 24 }); }
},
/* ---------- 第五批:ai 自检清单 ---------- */
{
id: 'poster-ai-review', name: 'AI 自检清单', category: 'ai', emoji: '🧾',
scene: 'AI 生成海报后的结构自检(构图/配色/字体/层级/安全区)',
summary: '输入设计需求与元素清单,返回逐项自检清单,把不通过项转成迭代指令交给 AI 修改。',
params: { brief: '设计需求一句话', elements: '元素清单(逗号分隔)' },
source: '自研(AI 质量检查法)',
run(args) {
const b = String(args.brief || '');
const els = String(args.elements || '').split(/[,,]/).map(s => s.trim()).filter(Boolean);
const checks = [
'构图:是否有明确主视觉焦点(建议 1 个)',
'配色:是否 ≤ 4 个主色 + 文字对比度达 WCAG AA',
'字体:标题/正文是否成体系(建议 1-2 款字体)',
'层级:信息主次是否一目了然(标题 > 时间/地点 > 详情)',
'安全区:关键内容是否在画布 10% 边距内'
];
return { ok: true, brief: b || '(未填写)', elements: els.length ? els : '(未提供)', checks, tip: '逐项自检后,把「不通过」项转成迭代指令交给 AI 修改' };
},
test() { return this.run({ brief: '高校迎新晚会海报', elements: '主标题,时间,地点,报名二维码' }); }
},
/* ---------- 第六批:layout 留白呼吸 ---------- */
{
id: 'poster-white-space', name: '留白呼吸', category: 'layout', emoji: '🌬️',
scene: '评估海报留白比例,避免版面过挤或过于空旷',
summary: '按元素占用面积与画布面积计算留白率,给出舒适区间建议与调整方向。',
params: { canvas: '{w,h}', elements: '元素数组(含 x/y/w/h)' },
source: '自研(留白比例法)',
run(args) {
const cw = (args.canvas && args.canvas.w) || 1000;
const ch = (args.canvas && args.canvas.h) || 1500;
const els = (args.elements || []).filter(e => e.w > 0 && e.h > 0);
const used = els.reduce((s, e) => s + e.w * e.h, 0);
const area = cw * ch;
const ratio = area > 0 ? used / area : 1;
const white = (1 - ratio) * 100;
let tip;
if (white < 15) tip = '留白过少,版面拥挤:建议删除次要装饰或增大元素间距';
else if (white > 70) tip = '留白过多,版面空旷:建议补充主视觉元素或放大标题';
else tip = '留白比例舒适,保持现有呼吸感';
return { ok: true, whiteRate: white.toFixed(1) + '%', level: white < 15 ? '拥挤' : (white > 70 ? '空旷' : '舒适'), tip };
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, elements: [{ x: 100, y: 100, w: 500, h: 300 }, { x: 100, y: 500, w: 500, h: 200 }] }); }
},
/* ---------- 第六批:color 色彩情绪 ---------- */
{
id: 'poster-color-psychology', name: '色彩情绪', category: 'color', emoji: '💭',
scene: '根据海报主题匹配色彩情绪,让配色传递正确氛围',
summary: '将主色按色相归类为红/橙/黄/绿/蓝/紫/中性,对照主题情绪词表给出匹配度与替换建议。',
params: { primary: '主色 hex', theme: '海报主题词(如 sale/tech/food)' },
source: '自研(色彩心理学)',
run(args) {
const c = String(args.primary || '#e53935');
const theme = String(args.theme || '').toLowerCase();
const hex = c.replace('#', '');
const r = parseInt(hex.slice(0, 2) || '0', 16) || 0;
const g = parseInt(hex.slice(2, 4) || '0', 16) || 0;
const b = parseInt(hex.slice(4, 6) || '0', 16) || 0;
let hue;
const mx = Math.max(r, g, b), mn = Math.min(r, g, b);
if (mx === mn) hue = 'neutral';
else {
const d = mx - mn;
let h;
if (mx === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
else if (mx === g) h = ((b - r) / d + 2) * 60;
else h = ((r - g) / d + 4) * 60;
hue = h < 15 || h >= 345 ? 'red' : h < 45 ? 'orange' : h < 70 ? 'yellow' : h < 160 ? 'green' : h < 200 ? 'cyan' : h < 270 ? 'blue' : h < 330 ? 'purple' : 'red';
}
const mood = {
red: ['热情', '紧迫', '促销', '危险'], orange: ['活力', '温暖', '食欲', '促销'],
yellow: ['醒目', '欢乐', '年轻', '警告'], green: ['自然', '健康', '成长', '环保'],
cyan: ['清新', '科技', '冷静', '治愈'], blue: ['专业', '信任', '理性', '商务'],
purple: ['高端', '神秘', '创意', '奢华'], neutral: ['极简', '高级', '稳重', '文艺']
};
const want = { sale: 'red/orange', tech: 'blue/cyan', food: 'orange/red', health: 'green/cyan', luxury: 'purple/black', kids: 'yellow/orange', edu: 'blue/green', sport: 'red/orange' }[theme];
return { ok: true, hue, mood: mood[hue], theme: theme || '(未指定)', suggest: want ? `主题 ${theme} 建议 ${want}` : '请指定主题词(sale/tech/food/health/luxury/kids/edu/sport)获得匹配建议', tip: '情绪联想是配色第一步:先定情绪,再定色相,最后用明度/饱和度微调' };
},
test() { return this.run({ primary: '#e53935', theme: 'sale' }); }
},
/* ---------- 第六批:typography 首字下沉 ---------- */
{
id: 'poster-initial-letter', name: '首字下沉', category: 'typography', emoji: '🔡',
scene: '西文海报/杂志风正文使用装饰首字母(Drop Cap)的参数建议',
summary: '根据正文行高推导首字下沉的行数与字号,让首字母融入正文块且视觉协调。',
params: { fontSize: '正文字号 px(默认 16)', lineHeight: '行高倍数(默认 1.6)' },
source: '自研(Drop Cap 排印规范)',
run(args) {
const fs = Number(args.fontSize) || 16;
const lh = Number(args.lineHeight) || 1.6;
const lines = 3; // 经典三行下沉
const capSize = Math.round(fs * lh * lines);
return { ok: true, fontSize: fs, lineHeight: lh, lines, capSize: capSize + 'px', tip: `首字下沉 ${lines} 行,字号约为 ${capSize}px(= 正文 × 行高 × ${lines});衬线字体效果最佳,下沉字母左对齐正文首行` };
},
test() { return this.run({ fontSize: 16, lineHeight: 1.6 }); }
},
/* ---------- 第六批:grid 基线网格 ---------- */
{
id: 'poster-baseline-grid', name: '基线网格', category: 'grid', emoji: '📏',
scene: '让海报中所有文字行落在统一基线网格上,版面整齐',
summary: '以正文行距为网格单位推导基线网格,输出标题/正文字号与行距的整数倍对齐规则。',
params: { base: '基准正文字号 px(默认 16)' },
source: '自研(Baseline Grid 规范)',
run(args) {
const base = Number(args.base) || 16;
const grid = Math.round(base * 1.5); // 1.5 倍行距作为网格单位
return { ok: true, grid: grid + 'px', rule: `所有文本行高取 ${grid}px 整数倍;标题字号 ${base * 2}px 行高 ${grid * 2}px`, tip: '正文 16px/行距 24px,标题 32px/行距 48px,多行文本行距 = 网格 × 行数,元素顶底对齐网格线' };
},
test() { return this.run({ base: 16 }); }
},
/* ---------- 第六批:hierarchy 信息密度 ---------- */
{
id: 'poster-info-density', name: '信息密度', category: 'hierarchy', emoji: '📊',
scene: '评估海报承载的信息量是否过载,决定取舍与分层',
summary: '按画布面积、文本字符数与元素数计算信息密度指数,给出过载/合适/空洞判断与精简建议。',
params: { canvas: '{w,h}', chars: '文本总字符数', elements: '视觉元素数' },
source: '自研(信息密度评估)',
run(args) {
const cw = (args.canvas && args.canvas.w) || 1000;
const ch = (args.canvas && args.canvas.h) || 1500;
const chars = Number(args.chars) || 0;
const els = Number(args.elements) || 0;
const area = cw * ch / 1e6; // 百万像素
const density = chars / Math.max(area, 1) + els * 3;
let tip, level;
if (density > 8) { level = '过载'; tip = '信息过载:建议精简为「1 标题 + 1 主信息 + 1 行动指令」,正文压缩或转二维码'; }
else if (density < 2) { level = '空洞'; tip = '信息不足:补充时间/地点/报名方式等必要信息,或加大标题与主视觉'; }
else { level = '合适'; tip = '信息密度合适,保持「标题一眼看懂、细节按需阅读」的层级'; }
return { ok: true, density: density.toFixed(1), level, tip };
},
test() { return this.run({ canvas: { w: 1000, h: 1500 }, chars: 120, elements: 6 }); }
},
/* ---------- 第六批:print 折页线规避 ---------- */
{
id: 'poster-fold', name: '折页线规避', category: 'print', emoji: '📃',
scene: '三折页/对折宣传单设计时避开折痕位置放置关键内容',
summary: '按页面宽高与折法计算折痕位置与安全距离,提醒关键文字/图像避开折线。',
params: { width: '展开宽度 mm(默认 210)', height: '高度 mm(默认 297)', folds: '折数(2=对折 / 3=三折,默认 3)' },
source: '自研(折页设计规范)',
run(args) {
const w = Number(args.width) || 210;
const h = Number(args.height) || 297;
const folds = Math.min(Math.max(Number(args.folds) || 3, 2), 4);
const panel = w / folds;
const creases = [];
for (let i = 1; i < folds; i++) creases.push(Math.round(panel * i));
return { ok: true, panel: panel.toFixed(0) + 'mm', creases: creases.map(v => v + 'mm').join(' / '), tip: `折痕位于 ${creases.join('、')}mm 处,距折痕两侧 ≥ 10mm 禁放关键文字/二维码/连续图案;封面面板宽度 ${panel.toFixed(0)}mm 内优先放主视觉` };
},
test() { return this.run({ width: 210, height: 297, folds: 3 }); }
},
/* ---------- 第六批:creative 文字海报 ---------- */
{
id: 'poster-typographic-poster', name: '文字海报', category: 'creative', emoji: '🔠',
scene: '纯文字排版海报(Typographic Poster)的构图手法建议',
summary: '根据标题长度与信息条目,推荐大标题 + 网格 + 对比的文字海报构图方案。',
params: { title: '主标题文本', info: '辅助信息条目数(默认 4)' },
source: '自研(文字海报构图法)',
run(args) {
const title = String(args.title || 'YOUR TITLE HERE');
const info = Math.max(1, Number(args.info) || 4);
const long = title.length > 12;
return {
ok: true,
layout: long ? '标题拆两行大字撑满版面,辅以细线分割信息区' : '标题居中或顶格大字,字号占画布宽 60-80%',
pair: '压缩大写标题(如 Bebas Neue/Oswald)配正文无衬线(如 Inter/Montserrat),对比强烈',
hierarchy: `辅助信息 ${info} 条按网格排布,用字号/字重/字色区分主次,可加序号或引导线`,
tip: '文字本身就是图形:尝试字重对比、大小对比、字距拉大、字母变形与文字溢出网格'
};
},
test() { return this.run({ title: 'MIDNIGHT JAZZ', info: 4 }); }
},
/* ---------- 第六批:ai 多方案生成 ---------- */
{
id: 'poster-ai-variant', name: '多方案生成', category: 'ai', emoji: '🃏',
scene: 'AI 生成海报前先产出多个差异化方向,再择优深化',
summary: '根据主题与风格数量生成 N 个差异化方案(构图/配色/字体方向),供挑选后深入迭代。',
params: { theme: '海报主题', styles: '方案风格数组或逗号分隔风格词', count: '方案数量(默认 3)' },
source: '自研(AI 多方案法)',
run(args) {
const theme = String(args.theme || '(未指定主题)');
const n = Math.min(Math.max(Number(args.count) || 3, 2), 5);
const dirs = ['大字居中 + 对比色', '非对称构图 + 大留白', '网格拼贴 + 多色块', '上下分区 + 渐变背景', '居中对称 + 装饰元素'];
const palettes = ['互补色(红/绿)', '类比色(蓝/青)', '同色系深浅', '黑金高反差', '黑白灰极简'];
const fonts = ['压缩大写标题 + 无衬线正文', '古典衬线标题 + 衬线正文', '几何无衬线全系列', '手写标题 + 无衬线正文', '等宽字体科技风'];
const plans = [];
for (let i = 0; i < n; i++) {
plans.push(`方案 ${i + 1}:${dirs[i % dirs.length]};配色 ${palettes[(i * 2) % palettes.length]};字体 ${fonts[(i * 3) % fonts.length]}`);
}
return { ok: true, theme, plans, tip: '先让 AI 各出一版缩略图/线稿对比,选 1-2 个方向深入迭代,避免在单方案上反复改' };
},
test() { return this.run({ theme: '咖啡新品上市', count: 3 }); }
},
/* ============================================================
* 第七批补充技能(2026-08-22 录入,每类 +1,run 纯函数 + test 自测)
* ============================================================ */
/* ---------- 第七批:layout 页边距系统 ---------- */
{
id: 'poster-margin', name: '页边距系统', category: 'layout', emoji: '📐',
scene: '海报版面统一页边距,保证不同尺寸下信息区与视觉呼吸',
summary: '按画布尺寸推导紧凑/标准/宽松三档页边距,并给出四边对齐与装订侧加量建议。',
params: { width: '画布宽 px(默认 1000)', height: '画布高 px(默认 1500)' },
source: '自研(版面边距系统)',
run(args) {
const w = Number(args.width) || 1000;
const h = Number(args.height) || 1500;
const short = Math.min(w, h);
const compact = Math.round(short * 0.04);
const standard = Math.round(short * 0.06);
const generous = Math.round(short * 0.09);
return { ok: true, compact: compact + 'px', standard: standard + 'px', generous: generous + 'px', tip: `海报推荐「标准」边距 ${standard}px(≈短边 6%);紧凑档 ${compact}px 适合信息密集海报,宽松档 ${generous}px 适合留白美学;四周边距保持一致,装订/打孔侧另加 15-25mm` };
},
test() { return this.run({ width: 1000, height: 1500 }); }
},
/* ---------- 第七批:color 用色配比 ---------- */
{
id: 'poster-color-ratio', name: '用色配比', category: 'color', emoji: '⚖️',
scene: '限制海报用色数量并按比例分配,避免画面杂乱',
summary: '按 60/30/10 法则将颜色划分为主色/辅色/点缀色三档,输出面积占比建议。',
params: { primary: '主色 hex(默认 #1a1a2e)', secondary: '辅色 hex(默认 #e94560)', accent: '点缀色 hex(默认 #f5f5f5)' },
source: '自研(60-30-10 配色法则)',
run(args) {
const p = String(args.primary || '#1a1a2e');
const s = String(args.secondary || '#e94560');
const a = String(args.accent || '#f5f5f5');
return { ok: true, primary: { color: p, ratio: '60%' }, secondary: { color: s, ratio: '30%' }, accent: { color: a, ratio: '10%' }, tip: `主色 ${p} 占 60%(背景/大面积),辅色 ${s} 占 30%(信息块/图形),点缀色 ${a} 占 10%(按钮/强调);点缀色优先用高饱和或补色` };
},
test() { return this.run({ primary: '#1a1a2e', secondary: '#e94560', accent: '#f5f5f5' }); }
},
/* ---------- 第七批:typography 字距节奏 ---------- */
{
id: 'poster-tracking', name: '字距节奏', category: 'typography', emoji: '↔️',
scene: '标题字距(Tracking)调整建议:大字距优雅、紧字距有力',
summary: '根据字号与文本长度推荐字距方向与增量,提升标题气质。',
params: { fontSize: '标题字号 px(默认 48)', chars: '标题字符数(默认 8)' },
source: '自研(标题字距规范)',
run(args) {
const fs = Number(args.fontSize) || 48;
const n = Number(args.chars) || 8;
const large = fs >= 60;
const short = n <= 10;
const track = large && short ? '宽' : (large ? '中' : '紧');
const val = track === '宽' ? Math.round(fs * 0.06) : (track === '中' ? Math.round(fs * 0.02) : 0);
return { ok: true, fontSize: fs + 'px', chars: n, track, tracking: (val ? '+' : '') + val + 'px', tip: `字号 ${fs}px + ${n} 字符 → 建议${track}字距(${val}px):${track === '宽' ? '大字距显优雅高级(时尚/奢侈风)' : track === '中' ? '适中字距平衡易读(多数标题)' : '紧字距显力量紧凑(压缩大写/运动风)'}` };
},
test() { return this.run({ fontSize: 48, chars: 8 }); }
},
/* ---------- 第七批:grid 同心圆构图 ---------- */
{
id: 'poster-concentric', name: '同心圆网格', category: 'grid', emoji: '⭕',
scene: '以同心圆/环形网格组织版面元素(徽章、唱片、雷达等主题)',
summary: '以画布中心为圆心生成同心圆网格,输出元素沿环分布的位置与数量建议。',
params: { size: '画布边长 px(默认 1000)', rings: '环数(默认 3)', items: '需分布的元素数(默认 8)' },
source: '自研(同心圆构图法)',
run(args) {
const size = Number(args.size) || 1000;
const rings = Math.min(Math.max(Number(args.rings) || 3, 1), 6);
const items = Math.min(Math.max(Number(args.items) || 8, 1), 24);
const radius = Math.floor(size / 2);
const ringStep = Math.floor(radius / rings);
const angle = 360 / items;
return { ok: true, radius: radius + 'px', rings: Array.from({ length: rings }, (_, i) => (i + 1) * ringStep + 'px').join(' / '), spacing: `每环元素间隔 ${angle.toFixed(0)}°`, tip: `圆心放主视觉/主体,${items} 个元素按每 ${angle.toFixed(0)}° 均匀分布;元素尺寸建议内小外大(沿半径递增)或统一` };
},
test() { return this.run({ size: 1000, rings: 3, items: 8 }); }
},
/* ---------- 第七批:hierarchy 层级自检 ---------- */
{
id: 'poster-hierarchy-check', name: '层级自检', category: 'hierarchy', emoji: '🪜',
scene: '检查海报文本层级字号是否清晰递减(标题→副题→正文→注释)',
summary: '输入各层字号,按相邻层级 ≥1.25× 的清晰度标准给出健康度与调整建议。',
params: { sizes: '层级字号数组,如 [64, 40, 24, 14]' },
source: '自研(视觉层级检查)',
run(args) {
const sizes = (args.sizes || []).map(Number).filter(Boolean);
if (sizes.length < 2) return { ok: true, tip: '至少输入 2 个层级字号才能自检(如 [64, 24])' };
const bad = [];
for (let i = 0; i < sizes.length - 1; i++) {
if (sizes[i] < sizes[i + 1] * 1.25) bad.push(`第 ${i + 1} 级(${sizes[i]}px) 与第 ${i + 2} 级(${sizes[i + 1]}px) 差距不足 1.25×`);
}
return { ok: true, levels: sizes.length, healthy: bad.length === 0, issues: bad.length ? bad : '无', tip: bad.length ? `建议增大相邻层级差至 ≥1.25×(标题 ${Math.round(sizes[1] * 1.5)}px+ 更醒目)` : '层级清晰:每级字号差 ≥1.25×,扫视即可分辨主次' };
},
test() { return this.run({ sizes: [64, 40, 24, 14] }); }
},
/* ---------- 第七批:print 印后工艺 ---------- */
{
id: 'poster-finishing', name: '印后工艺', category: 'print', emoji: '✨',
scene: '烫金 / 局部 UV / 模切等印后工艺对设计文件的要求与提醒',
summary: '选择印后工艺,输出对应的文件规范(出血、专色、镂空、对位精度)与设计注意项。',
params: { craft: '工艺类型:gold(烫金) / uv(局部UV) / diecut(模切) / emboss(压纹)(默认 gold)' },
source: '自研(印刷后期工艺规范)',
run(args) {
const craft = String(args.craft || 'gold');
const table = {
gold: { name: '烫金/烫银', file: '矢量稿需留专色通道,图案与底色至少 0.5mm 分离', note: '细线烫金 ≤0.1mm 会糊,建议线条 ≥0.3mm;大面积烫金易氧化需覆膜' },
uv: { name: '局部 UV', file: 'UV 区域单独建图层并加 0.3-0.5mm 扩边', note: 'UV 与底色色差小需打样确认;哑膜 + 局部亮 UV 效果最佳' },
diecut: { name: '模切/异形', file: '预留 ≥3mm 刀模线让位,关键元素距刀口 ≥5mm', note: '异形模切越大越稳,细窄卡榫易断;出血需覆盖刀模线外 3mm' },
emboss: { name: '压纹/击凸', file: '压纹图案与印刷图案对位公差 ±0.3mm', note: '击凸区域慎用深底色(压凹处颜色会偏移)' }
};
const t = table[craft] || table.gold;
return { ok: true, craft: t.name, file: t.file, note: t.note, tip: `印后工艺需与印刷厂提前确认:${t.name} 一般额外加收制版费,工期多 2-3 天;先打样 2-3 版再定稿` };
},
test() { return this.run({ craft: 'uv' }); }
},
/* ---------- 第七批:creative 双色调 ---------- */
{
id: 'poster-duotone', name: '双色调', category: 'creative', emoji: '🖤',
scene: '双色调(Duotone)处理:灰度图映射双色,复古/潮流海报常用',
summary: '输入主色/阴影色与灰度源建议,输出双色调参数(通道映射与混合模式)。',
params: { primary: '主色 hex(默认 #ff4d00)', shadow: '阴影色 hex(默认 #1a0033)', mood: '风格:retro/tech/film(默认 retro)' },
source: '自研(Duotone 双色调处理)',
run(args) {
const p = String(args.primary || '#ff4d00');
const s = String(args.shadow || '#1a0033');
const m = String(args.mood || 'retro');
const mix = m === 'tech' ? '硬边 + 网格纹理叠加' : (m === 'film' ? '低对比 + 胶片颗粒' : '高对比 + 半调网点');
return { ok: true, primary: p, shadow: s, mood: m, mode: '高光→主色,阴影→阴影色;混合模式 multiply/overlay 常用', mix, tip: `双色调三要素:① 灰度源图(对比强效果越好);② 主色 ${p} 映射高光、阴影色 ${s} 映射暗部;③ 可叠加半调网点/噪点增强印刷质感` };
},
test() { return this.run({ primary: '#ff4d00', shadow: '#1a0033', mood: 'retro' }); }
},
/* ---------- 第七批:ai 素材组合建议 ---------- */
{
id: 'poster-ai-combine', name: '素材组合建议', category: 'ai', emoji: '🧩',
scene: '根据海报主题推荐内置素材库的组合搭配(背景/装饰/字体/色板)',
summary: '输入主题与行业,推荐背景、装饰元素、字体与色板的内置素材组合方案。',
params: { theme: '海报主题', industry: '行业(可选:科技/餐饮/时尚)' },
source: '自研(内置素材组合引擎)',
run(args) {
const t = String(args.theme || '新品发布');
const ind = String(args.industry || '');
const map = {
'科技': { bg: '渐变/网格背景', deco: '几何线条 + 像素图标', font: '几何无衬线(Montserrat/Poppins)', palette: '深蓝 + 青 + 白(科技感)' },
'餐饮': { bg: '暖色渐变/纹理', deco: '圆润图形 + 食物 emoji', font: '圆润无衬线 + 手写标题', palette: '橙红 + 米黄 + 深棕(食欲感)' },
'时尚': { bg: '大色块/留白', deco: '线条装饰 + 双色调图', font: '高对比衬线(Playfair Display)+ 压缩大写', palette: '黑白 + 高饱和点缀' }
};
const hit = map[ind] || { bg: '纯色/渐变背景', deco: '主题相关装饰图标', font: '标题衬线 + 正文无衬线', palette: '主色 60% + 辅色 30% + 点缀 10%' };
return { ok: true, theme: t, bg: hit.bg, decoration: hit.deco, font: hit.font, palette: hit.palette, tip: '素材中心已按「类型 × 场景」双维度筛选,可按此组合直接检索内置库素材;AI 生成失败时此组合可作为降级方案' };
},
test() { return this.run({ theme: '咖啡新品上市', industry: '餐饮' }); }
},
/* ============================================================
* 第八批补充技能(2026-08-23 录入,每类 +1,run 纯函数 + test 自测)
* ============================================================ */
/* ---------- 第八批:layout 横竖版式 ---------- */
{
id: 'poster-orientation', name: '横竖版式', category: 'layout', emoji: '🖼️',
scene: '根据投放场景推荐海报横竖版式与画布比例',
summary: '按投放位置(门头/户外大牌/手机端/易拉宝/A4 纸)推荐横版/竖版/方图及宽高比,并给出该比例的构图要点。',
params: { placement: '投放场景:billboard/phone/banner/rollup/a4(默认 a4)', ratio: '自定义宽高比(可选)' },
source: '自研(海报版式比例规范)',
run(args) {
const p = String(args.placement || 'a4');
const ratio = args.ratio ? Number(args.ratio) : null;
const map = {
billboard: { orientation: '横版', ratio: '3:1(如 900×300)', note: '户外大牌视线停留短,信息越少越好,主视觉 + 一句标语即可' },
phone: { orientation: '竖版', ratio: '9:16(如 1080×1920)', note: '手机端信息流,顶部 1/3 放主视觉,关键信息居中偏下防被角标遮挡' },
banner: { orientation: '横版', ratio: '21:9(如 1680×720)', note: '官网横幅/开屏,左右留呼吸、主体居中偏左,CTA 靠右' },
rollup: { orientation: '竖版', ratio: '1:2.2(如 800×1760)', note: '易拉宝顶部 60cm 为视线区,标题与主视觉放上半部,底部留白放详情' },
a4: { orientation: '竖版', ratio: 'A4(210×297mm)', note: '通用打印版,四周安全边距 ≥ 15mm,标题层级用 1.25 倍字体比例' }
};
const hit = map[p] || map.a4;
return { ok: true, placement: p, orientation: hit.orientation, ratio: ratio ? ratio + ':1' : hit.ratio, note: hit.note, tip: '先定投放场景再定比例:竖版适合手机/易拉宝,横版适合门头/横幅;确定比例后再建网格(见「分栏网格」技能)' };
},
test() { return { a4: this.run({ placement: 'a4' }).orientation, phone: this.run({ placement: 'phone' }).ratio }; }
},
/* ---------- 第八批:color 冷暖色温 ---------- */
{
id: 'poster-temperature', name: '冷暖色温', category: 'color', emoji: '🌡️',
scene: '按主题与受众判断海报应使用暖色系 / 冷色系 / 中性色',
summary: '解析主题描述中的情绪关键词,输出冷暖判定、推荐色系与典型 hex,并提醒同类色系内微调色温保持层次。',
params: { brief: '主题/受众描述', explicit: '直接指定:warm/cool/neutral(可选,优先)' },
source: '自研(色彩温度感知规则)',
run(args) {
const b = String(args.brief || '');
const ex = String(args.explicit || '');
const warm = /餐饮|美食|促销|狂欢|节日|母婴|火锅|圣诞|新春|温暖|热情/.test(b);
const cool = /科技|医疗|金融|办公|专业|理性|冷静|夏季|海洋/.test(b);
const mood = ex === 'warm' ? 'warm' : (ex === 'cool' ? 'cool' : (ex === 'neutral' ? 'neutral' : (warm && !cool ? 'warm' : (cool && !warm ? 'cool' : 'neutral'))));
const table = {
warm: { name: '暖色系', palette: ['#E8590C 橙', '#D9480F 深橙', '#F5B041 金黄', '#B91C1C 红'], tip: '暖色增强食欲与亲近感;大面积用中高明度暖色,点缀用深暖色压场' },
cool: { name: '冷色系', palette: ['#1B4F72 深蓝', '#2E86C1 天蓝', '#6C3483 紫', '#148F77 青'], tip: '冷色传递专业与科技感;避免纯冷色堆砌,加 10% 中性灰平衡' },
neutral: { name: '中性色系', palette: ['#FFFFFF 白', '#E0E0E0 浅灰', '#616161 深灰', '#212121 黑'], tip: '中性色干净高级,适合极简/时尚/高端;用 1 个低饱和主色作强调' }
};
const t = table[mood];
return { ok: true, mood: t.name, palette: t.palette, clue: b ? '判定依据:' + b : '未提供描述', tip: t.tip };
},
test() { return { warm: this.run({ brief: '秋季餐饮促销' }).mood, cool: this.run({ brief: '企业金融年会' }).mood }; }
},
/* ---------- 第八批:typography 避头尾标点 ---------- */
{
id: 'poster-punctuation', name: '避头尾标点', category: 'typography', emoji: '🀄',
scene: '中文排版禁则处理:行首/行尾不可出现的标点检查',
summary: '检查多行文字的行首/行尾是否出现禁则标点(行首禁:,。;:、!?)》等;行尾禁:『「(《等),输出违规位置与换行建议。',
params: { lines: '文字行数组(支持 \\n 自动拆分)' },
source: '自研(中文排版禁则规范)',
run(args) {
const src = Array.isArray(args.lines) ? args.lines : [String(args.lines || '')];
const lines = src.join('\n').split('\n').map(s => s.trim()).filter(Boolean);
if (!lines.length) return { ok: true, note: '无文字内容' };
const banStart = ',。;:、!?)》】』」…—';
const banEnd = '(《【『「·—';
const issues = [];
lines.forEach((ln, i) => {
const first = ln[0], last = ln[ln.length - 1];
if (banStart.includes(first)) issues.push({ line: i + 1, pos: '行首', char: first });
if (banEnd.includes(last)) issues.push({ line: i + 1, pos: '行尾', char: last });
});
return {
ok: issues.length === 0, issues, lineCount: lines.length,
rule: '行首禁:' + banStart.split('').join(' ') + ';行尾禁:' + banEnd.split('').join(' '),
tip: issues.length ? '调整断行位置,把禁则标点让位到行尾或并入前一行;标题场景可手工插入软换行控制标点落位' : '标点落位符合中文禁则,排版规范'
};
},
test() { return { bad: this.run({ lines: ['。行首禁则测试', '行尾禁则测试('] }).issues.length, good: this.run({ lines: ['行首正常,行尾正常。'] }).ok }; }
},
/* ---------- 第八批:grid 网格对齐检查 ---------- */
{
id: 'poster-grid-align', name: '网格对齐', category: 'grid', emoji: '📏',
scene: '检查海报元素边缘是否对齐到网格线,消除悬空元素',
summary: '按分栏网格(列数/栏距/边距)计算网格线,检查元素左/右边缘是否贴齐网格线(容差 8px),输出未对齐元素清单。',
params: { elements: '元素数组(含 x/w)', grid: '{cols, gutter, margin, width}(可选,默认 3 栏)' },
source: '自研(网格对齐检查)',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0);
const g = args.grid || {};
const cols = Number(g.cols) || 3;
const gutter = Number(g.gutter) || 32;
const margin = Number(g.margin) || 48;
const width = Number(g.width) || 1000;
if (!els.length) return { ok: true, note: '无元素' };
const innerW = width - margin * 2;
const colW = (innerW - gutter * (cols - 1)) / cols;
const lines = [];
for (let i = 0; i <= cols; i++) lines.push(margin + i * (colW + gutter));
const near = (v, line) => Math.abs(v - line) <= 8;
const issues = [];
for (const e of els) {
const left = e.x, right = e.x + e.w;
const onGrid = lines.some(ln => near(left, ln)) || lines.some(ln => near(right, ln));
if (!onGrid) issues.push({ id: e.id || '元素', x: Math.round(left), tip: '左右边缘均未贴齐网格线,建议吸附到 ' + lines[0] + '~' + lines[lines.length - 1] + 'px 网格线' });
}
return { ok: issues.length === 0, issues, cols, colWidth: Math.round(colW), lines: lines.map(Math.round), tip: issues.length ? '元素悬空会显杂乱:让每个元素左或右边缘贴齐网格线(栏位起点),大元素跨栏时贴齐栏边界' : '元素均已对齐网格线,结构稳定' };
},
test() { return { bad: this.run({ elements: [{ id: 'a', x: 100, w: 300 }, { id: 'b', x: 570, w: 300 }] }).issues.length, ok: this.run({ elements: [{ id: 'a', x: 48, w: 288 }] }).ok }; }
},
/* ---------- 第八批:hierarchy CTA 引导 ---------- */
{
id: 'poster-cta-focus', name: 'CTA 引导', category: 'hierarchy', emoji: '🔘',
scene: '检查活动海报的行动号召(CTA)是否足够突出',
summary: '定位 CTA 元素(type=cta 或 ctaId 指定),将其视觉权重与最大非 CTA 元素对比:权重比 ≥ 1.6 才算突出,并校验 CTA 与底色对比。',
params: { elements: '元素数组(含 id/type/weight 或 fontSize/w/h)', ctaId: 'CTA 元素 id(可选,默认 type=cta)' },
source: '自研(行动号召突出度检查)',
run(args) {
const els = (args.elements || []).filter(e => e.w > 0 || e.fontSize || e.weight);
const ctaId = String(args.ctaId || '');
const score = e => (e.weight || 0) || (e.fontSize || 0) || (e.w * e.h);
const cta = ctaId ? els.find(e => String(e.id) === ctaId) : els.find(e => e.type === 'cta');
if (!cta) return { ok: false, note: '未找到 CTA 元素:请传入 ctaId 或给元素标 type=cta', tip: '活动海报必须有一个明确的行动号召(扫码/购票/报名/领券),否则转化率大降' };
const ctaScore = score(cta);
const others = els.filter(e => e !== cta).map(score);
const maxOther = others.length ? Math.max(...others) : 1;
const ratio = ctaScore / Math.max(maxOther, 1);
const ok = ratio >= 1.6;
return {
ok, ctaId: cta.id || 'CTA', ratio: Math.round(ratio * 100) / 100,
contrast: cta.contrast != null ? cta.contrast : '(未提供,建议 ≥ 3:1)',
tip: ok ? 'CTA 权重突出(' + ratio + ' 倍于次强元素),位置建议放视线终点(右下/居中偏下)' : 'CTA 权重仅 ' + ratio + ' 倍,建议放大字号/加深底色或周边留白,让行动号召一眼可见'
};
},
test() { return { ok: this.run({ elements: [{ id: 'title', fontSize: 90 }, { id: 'cta', type: 'cta', fontSize: 200 }] }).ok, weak: this.run({ elements: [{ id: 'title', fontSize: 180 }, { id: 'cta', type: 'cta', fontSize: 100 }] }).ok === false }; }
},
/* ---------- 第八批:print 专色印刷 ---------- */
{
id: 'poster-spot-color', name: '专色印刷', category: 'print', emoji: '🖍️',
scene: '判断品牌色是否该用专色印刷,及专色文件的制作要点',
summary: '按品牌色属性(大面积底色/极亮极暗色/品牌核心标识)输出是否启用专色(PMS)的建议,并给出专色通道、叠印与印刷厂沟通要点。',
params: { color: '品牌色 hex', scene: '使用场景:bg 大面积底色 / logo 标识 / text 文字(默认 logo)' },
source: '自研(专色 vs 四色印刷规范)',
run(args) {
const c = String(args.color || '').replace('#', '');
const scene = String(args.scene || 'logo');
const valid = /^[0-9a-fA-F]{6}$/.test(c);
// 内联亮度(海报技能不依赖 HGB_Skills.utils)
const l = valid ? (function () {
const r = parseInt(c.slice(0, 2), 16) / 255, g = parseInt(c.slice(2, 4), 16) / 255, b = parseInt(c.slice(4, 6), 16) / 255;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
})() : 0.5;
const needSpot = scene === 'bg' || l > 0.92 || l < 0.08;
return {
ok: true,
color: valid ? '#' + c : '(无效 hex)',
scene: scene === 'bg' ? '大面积底色' : (scene === 'text' ? '文字' : '品牌标识'),
useSpot: needSpot,
rule: needSpot ? '建议启用专色(PMS):大面积底色/极亮极暗色用四色叠印难稳定还原,专色可保证批量印刷一致' : '可用四色印刷(CMYK 模拟),品牌核心场景建议仍准备专色版本',
file: '① 专色单独建图层并命名 PMS 色号;② 大面积专色与底色叠印需做「叠印预览」检查;③ 专色另转灰度稿供低价印刷场景使用',
tip: '专色文件务必与印刷厂确认色号(Pantone 需附官方色卡编号,勿仅给 hex);金属色与高饱和橙红四色印刷易偏色,优先专色'
};
},
test() { return { bg: this.run({ color: '#E8590C', scene: 'bg' }).useSpot, logo: this.run({ color: '#E8590C', scene: 'logo' }).useSpot === false }; }
},
/* ---------- 第八批:creative 拼贴手法 ---------- */
{
id: 'poster-collage', name: '拼贴手法', category: 'creative', emoji: '🧻',
scene: '摄影/杂志/纸张拼贴海报的创意手法与构图建议',
summary: '按主题推荐拼贴风格(摄影拼贴/杂志拼贴/纸张拼贴),给出素材处理(裁切/叠加/混合模式)与构图要点。',
params: { theme: '海报主题', style: 'collage 风格:photo/magazine/paper(可选)' },
source: '自研(拼贴艺术手法)',
run(args) {
const t = String(args.theme || '');
const s = String(args.style || '');
const map = {
photo: { name: '摄影拼贴', fit: ['音乐节', '旅行', '潮流服饰'], tech: ['多图自由裁切堆叠', '混合模式 multiply/screen 制造层次', '描边或投影统一图片边界'] },
magazine: { name: '杂志拼贴', fit: ['时尚', '艺术展览', '文化沙龙'], tech: ['文字与图片穿插出血', '标题文字压在图片上', '复古半调网点/胶片颗粒'] },
paper: { name: '纸张拼贴', fit: ['儿童活动', '手作市集', '环保主题'], tech: ['不规则纸张裁切 + 阴影', '手写字体与涂鸦', '纸胶带/别针等道具元素'] }
};
const hit = map[s] || (t.includes('儿童') || t.includes('市集') ? map.paper : (t.includes('时尚') || t.includes('艺术') ? map.magazine : map.photo));
return { ok: true, theme: t || '(未提供)', style: hit.name, fit: hit.fit, techniques: hit.tech, tip: '拼贴的灵魂是「统一感」:先定 1 个主色调与 1 种图形语言(裁切方式/描边/纹理),再堆叠素材,避免素材风格打架' };
},
test() { return { paper: this.run({ theme: '儿童手作市集' }).style, magazine: this.run({ style: 'magazine' }).techniques.length }; }
},
/* ---------- 第八批:ai 海报文案框架 ---------- */
{
id: 'poster-copywriting', name: '海报文案框架', category: 'ai', emoji: '✍️',
scene: '为活动/促销海报生成标题、副标题、正文与行动号召的文案框架',
summary: '按「事件 + 利益 + 悬念」公式生成主标题候选,再补副标题/正文要点/CTA,输出可直接填内容的文案骨架。',
params: { theme: '活动主题/事件', benefit: '用户利益点(可选)', event: '活动类型:sale/event/launch(可选)' },
source: '自研(AIDA 文案框架)',
run(args) {
const theme = String(args.theme || '');
const benefit = String(args.benefit || '');
const event = String(args.event || '');
const ev = event === 'sale' ? '促销' : (event === 'launch' ? '新品发布' : '主题活动');
const b = benefit || '专属优惠';
return {
ok: true, theme: theme || '(未提供)', event: ev,
headline: theme ? [theme + '|' + b, b + ',' + theme + '限时开启', '错过再等一年:' + theme] : ['主标题(事件 + 利益)', '副标题(补足细节)'],
subhead: theme ? theme + ',' + b + ',即刻出发' : '副标题:一句话说清时间/地点/对象',
body: ['正文要点 1(时间地点)', '正文要点 2(活动内容)', '正文要点 3(规则说明)'],
cta: 'CTA:立即报名 / 扫码领券 / 马上抢购',
tip: '文案骨架按「标题抓眼球 → 副标题补信息 → 正文给细节 → CTA 促行动」四层;标题字数 ≤ 12 字(中文)避免换行破版'
};
},
test() { const r = this.run({ theme: '夏季美食节', benefit: '全场 5 折' }); return { headlines: r.headline.length, cta: r.cta.includes('CTA') }; }
}
]);