Files

346 lines
18 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.
/**
* 画个标 · 技能库:配色类(color)
* 注册:HGB_Skills.register([...])
*/
(function () {
'use strict';
const U = HGB_Skills.utils;
const { lum, contrast } = U;
HGB_Skills.register([
{
id: 'contrast-check',
name: '对比度校验',
category: 'color',
emoji: '🎨',
scene: '配色合规检查(WCAG 2.1 AA)',
summary: '计算主色与背景的对比度并对照 WCAG AA(正文 ≥4.5:1,大文本/UI ≥3:1),深色主色配深色背景时给出调亮建议。',
params: [
{ name: 'foreground', type: 'string', required: true, desc: '前景色(主色)HEX' },
{ name: 'background', type: 'string', required: true, desc: '背景色 HEX' }
],
source: 'GitHub: stagius/flowize DESIGN_SYSTEM.md(WCAG AA 体系)',
run: function (args) {
const fg = args.foreground || '#1B4F72';
const bg = args.background || '#FFFFFF';
const ratio = contrast(fg, bg);
return {
foreground: fg, background: bg, ratio,
passLarge: ratio >= 3, passText: ratio >= 4.5,
verdict: ratio >= 4.5 ? 'AA 达标(正文可用)' : (ratio >= 3 ? '仅限大文本/UI' : '不达标,建议加深前景或调亮背景')
};
},
test: function () {
return {
light: this.run({ foreground: '#1B4F72', background: '#FFFFFF' }),
dark: this.run({ foreground: '#0F172A', background: '#0F172A' })
};
}
},
{
id: 'color-harmony',
name: '行业配色推荐',
category: 'color',
emoji: '🌈',
scene: '按行业与品牌气质推荐和谐配色',
summary: '从行业推荐配色与全局配色库中筛选贴合品牌描述的方案,返回主色/辅色/点缀色与适用场景。',
params: [
{ name: 'industry', type: 'string', required: true, desc: '行业 id(edu/food/health/tech/sport/finance)' },
{ name: 'desc', type: 'string', required: false, desc: '品牌描述(可含色系偏好)' }
],
source: 'GitHub: design-resources-for-developers(配色资源索引)+ 自研行业库',
run: function (args) {
const ind = HGB_Industries.find(x => x.id === args.industry) || HGB_Industries[0];
const desc = String(args.desc || '');
const pool = [...(ind.palettes || []), ...HGB_Palettes];
const keywordMap = [
{ kw: ['蓝', '科技', '理性'], field: 'colors', idx: 0, hint: '科技蓝系' },
{ kw: ['橙', '暖', '美食'], field: 'colors', idx: 0, hint: '温暖暖色系' },
{ kw: ['绿', '健康', '自然'], field: 'colors', idx: 0, hint: '清新绿系' }
];
const matched = [];
for (const m of keywordMap) {
if (m.kw.some(k => desc.includes(k))) {
const pal = pool.find(p => p.colors && p.colors[0] && lum(p.colors[0]) > 0.05) || pool[0];
if (pal && !matched.includes(pal.name)) matched.push(pal.name + '(' + m.hint + ')');
}
}
return {
industry: ind.name,
desc: desc || '(无偏好)',
recommend: pool.slice(0, 3).map(p => p.name),
match: matched
};
},
test: function () {
return this.run({ industry: 'food', desc: '温暖治愈的橙色系烘焙品牌' });
}
},
{
id: 'palette-build',
name: '品牌色板生成',
category: 'color',
emoji: '🖌️',
scene: '从品牌描述生成主/辅/点缀三级色板',
summary: '解析品牌描述中的色系关键词,从配色库提取一套 主色+辅色+点缀色 的完整色板,并附明暗度与搭配说明。',
params: [
{ name: 'desc', type: 'string', required: true, desc: '品牌描述(含色系/气质偏好)' },
{ name: 'industry', type: 'string', required: false, desc: '行业 id' }
],
source: '自研(基于行业色板 + 关键词色系匹配)',
run: function (args) {
const desc = String(args.desc || '');
const ind = HGB_Industries.find(x => x.id === args.industry);
const pool = [...(ind ? ind.palettes || [] : []), ...HGB_Palettes];
const toneMap = [
{ kw: ['蓝', '科技', '理性', '沉稳'], idx: 0 },
{ kw: ['橙', '暖', '美食', '活力'], idx: 0 },
{ kw: ['绿', '健康', '自然', '环保'], idx: 0 },
{ kw: ['红', '热烈', '喜庆', '激情'], idx: 0 },
{ kw: ['紫', '优雅', '神秘', '高端'], idx: 0 },
{ kw: ['金', '贵气', '奢华', '复古'], idx: 0 }
];
let pal = null;
for (const t of toneMap) {
if (t.kw.some(k => desc.includes(k))) {
pal = pool.find(p => p.colors && p.colors.length >= 3);
if (pal) break;
}
}
pal = pal || pool.find(p => p.colors && p.colors.length >= 3) || pool[0] || { colors: ['#1B4F72', '#2E86C1', '#F5B041'] };
const c = pal.colors;
const palette = { primary: c[0], secondary: c[1] || c[0], accent: c[2] || c[1] || c[0] };
const bright = lum(palette.primary) > 0.5;
return {
desc: desc || '(无偏好)',
palette,
contrast: Math.round(contrast(palette.primary, '#FFFFFF') * 100) / 100,
usage: `${palette.primary} 主色(品牌核心)/ ${palette.secondary} 辅色(次层级)/ ${palette.accent} 点缀色(CTA/高亮)`,
tone: bright ? '亮色系' : '深色系',
note: bright ? '建议搭配深色文字保证对比度' : '建议搭配浅色背景凸显品牌色'
};
},
test: function () {
return this.run({ desc: '科技感十足的蓝色软件品牌' });
}
},
{
id: 'gradient-suggest',
name: '渐变搭配建议',
category: 'color',
emoji: '🌅',
scene: '品牌色渐变色相搭配推荐',
summary: '基于品牌主色推荐和谐渐变色相(同色系渐变/互补渐变/邻近渐变三类),并给出渐变方向与使用场景。',
params: [
{ name: 'color', type: 'string', required: true, desc: '主色 HEX' }
],
source: '自研(基于色相环渐变理论)',
run: function (args) {
const color = String(args.color || '#1B4F72').replace('#', '');
if (color.length !== 6) return { error: '需提供 6 位 HEX 色值' };
const r = parseInt(color.slice(0, 2), 16), g = parseInt(color.slice(2, 4), 16), b = parseInt(color.slice(4, 6), 16);
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const l = (max + min) / 2 / 255;
let h = 0;
if (max !== min) {
const d = max - min;
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
else if (max === g) h = ((b - r) / d + 2) * 60;
else h = ((r - g) / d + 4) * 60;
}
const variants = [
{ name: '同色系渐变', hue: h, note: '由深到浅同色相,稳重安全,适合企业品牌' },
{ name: '互补渐变', hue: (h + 180) % 360, note: '与主色互补,视觉冲击强,适合活动/电商' },
{ name: '邻近渐变', hue: (h + 30) % 360, note: '邻近色相过渡自然,柔和现代,适合科技品牌' }
];
return { color: '#' + color, hue: Math.round(h), lightness: Math.round(l * 100) + '%', variants };
},
test: function () {
return this.run({ color: '#1B4F72' });
}
},
{
id: 'color-blind-check',
name: '色盲友好检查',
category: 'color',
emoji: '🧑‍🦱',
scene: '红绿色盲场景下的主色与背景可区分度检查',
summary: '基于常见红绿色盲(约 8% 男性)感知模型,检查前景与背景是否同属红绿系且对比不足,给出改蓝/紫或加大明度差的建议。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {bg, elements}' }
],
source: '自研(红绿色盲感知启发式)',
run: function (args) {
const plan = args.plan || {};
const bg = plan.bg || '#FFFFFF';
const els = plan.elements || [];
const icon = els.find(el => el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId));
const fg = (icon && icon.stroke) || '#333333';
const hueOf = (hex) => {
const c = String(hex || '#FFF').replace('#', '');
if (c.length !== 6) return -1;
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;
const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
if (d === 0) return -1;
let h;
if (max === r) h = ((g - b) / d) % 6;
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
return ((h * 60) % 360 + 360) % 360;
};
const inRG = (h) => h >= 0 && (h <= 60 || (h >= 75 && h <= 170) || h >= 320); // 红/橙/绿近似区
const h1 = hueOf(fg), h2 = hueOf(bg);
const bothRG = inRG(h1) && inRG(h2);
const ratio = contrast(fg, bg);
const risky = bothRG && ratio < 3;
return {
pass: !risky,
foreground: fg, background: bg,
fgFamily: h1 < 0 ? '中性色' : (inRG(h1) ? '红绿系' : '非红绿系'),
contrast: ratio,
verdict: risky ? '前景与背景同属红绿系且对比不足(' + ratio + ':1),红绿色盲人群难以区分' : (bothRG ? '虽同为红绿系,但明度对比足够(' + ratio + ':1)' : '前景非红绿系或与背景区分良好'),
suggest: risky ? '点缀/主色建议改用蓝/紫色系,或加大明度差至 3:1 以上' : '无需调整'
};
},
test: function () {
const plan = HGB_Generator.generateVariant({ industry: HGB_Industries[0], brand: '色盲测试' });
return { safe: this.run({ plan: { bg: '#FFFFFF', elements: [{ type: 'icon', iconId: 'x', stroke: '#1B4F72' }] } }).pass, risky: this.run({ plan: { bg: '#2E7D32', elements: [{ type: 'icon', iconId: 'x', stroke: '#B71C1C' }] } }).pass };
}
},
{
id: 'color-psychology',
name: '色彩心理学推荐',
category: 'color',
emoji: '🧠',
scene: '按行业与情感目标推荐品牌色彩心理学配色',
summary: '借鉴色彩心理学情感色系(红=热情/蓝=专业/绿=自然/金=高端等),按行业推荐最能传达品牌情感的主色与情感标签。',
params: [
{ name: 'industry', type: 'string', required: false, desc: '行业 id' }
],
source: 'GitHub: color-psychology 色彩心理学资料 + 自研行业映射',
run: function (args) {
const ind = HGB_Industries.find(x => x.id === args.industry) || HGB_Industries[0];
const map = {
edu: [{ color: '#2F5FE3', emotion: '专业与信赖', hex: '蓝色' }],
food: [{ color: '#E8590C', emotion: '食欲与热情', hex: '橙色' }],
health: [{ color: '#2FA36B', emotion: '健康与治愈', hex: '绿色' }],
tech: [{ color: '#1F7A8C', emotion: '创新与科技', hex: '青蓝' }],
sport: [{ color: '#E63946', emotion: '活力与冲击', hex: '红色' }],
finance: [{ color: '#1B4F72', emotion: '稳重与财富', hex: '深蓝' }]
};
const hit = map[ind.id] || [{ color: '#5B21B6', emotion: '创意与神秘', hex: '紫色' }];
return {
industry: ind.name,
primary: hit[0].color, emotion: hit[0].emotion, family: hit[0].hex,
note: '「主色 + 中性灰 + 少量点缀色」的三角配色最稳妥,避免满屏高饱和'
};
},
test: function () {
return { food: this.run({ industry: 'food' }).primary, tech: this.run({ industry: 'tech' }).emotion };
}
},
{
id: 'monochrome-check',
name: '单色灰度适配',
category: 'color',
emoji: '⬛',
scene: 'LOGO 转为单色(黑白/传真/印章)后的辨识度检查',
summary: '用亮度对比判断主体与背景在灰度下是否仍可辨识:单色印刷、传真、水印场景需要足够的明暗对比。',
params: [
{ name: 'plan', type: 'object', required: true, desc: '方案对象 {elements, bg}' }
],
source: '自研(灰度对比度规则)',
run: function (args) {
const plan = args.plan || {};
const bg = plan.bg || '#FFFFFF';
const els = plan.elements || [];
const fgEl = els.find(el => (el.type === 'icon' && el.iconId && !['ring', 'deco-bar'].includes(el.iconId)) || (el.type === 'text'));
const fg = (fgEl && (fgEl.stroke || fgEl.fill)) || '#333333';
const c = contrast(fg, bg);
const pass = c >= 3;
return {
pass, contrast: c,
grayLum: Math.round(lum(fg) * 100) + '%',
verdict: pass ? '灰度对比度 ' + c + ':1,单色场景可辨识' : '灰度对比度 ' + c + ':1,单色场景易糊,建议加大明暗差',
suggest: pass ? '无需调整' : '主体与背景明暗相近,单色印刷时建议加深主体或换浅色背景'
};
},
test: function () {
return { dark: this.run({ plan: { bg: '#FFFFFF', elements: [{ type: 'icon', iconId: 'x', stroke: '#111111' }] } }).pass, light: this.run({ plan: { bg: '#FFFFFF', elements: [{ type: 'icon', iconId: 'x', stroke: '#EEEEEE' }] } }).pass };
}
},
{
id: 'wcag-contrast',
name: 'WCAG 无障碍对比度',
category: 'color',
emoji: '♿',
scene: '品牌色与背景的 WCAG 无障碍对比度评级',
summary: '计算前景/背景颜色的 WCAG 对比度并评级:普通文本需 ≥4.5:1(AA)、大字号 ≥3:1,AAA 需 7:1/4.5:1,输出达标级别与调色方向。',
params: [
{ name: 'fg', type: 'string', required: true, desc: '前景色(#RRGGBB)' },
{ name: 'bg', type: 'string', required: true, desc: '背景色(#RRGGBB)' },
{ name: 'size', type: 'string', required: false, desc: 'text 普通文本 / large 大字号(≥24px 或 18.66px 加粗)' }
],
source: 'W3C WCAG 2.1 对比度规范 + 自研评级',
run: function (args) {
const fg = args.fg || '#333333', bg = args.bg || '#FFFFFF';
const c = contrast(fg, bg);
const large = args.size === 'large';
const need = large ? { aa: 3, aaa: 4.5 } : { aa: 4.5, aaa: 7 };
const level = c >= need.aaa ? 'AAA' : (c >= need.aa ? 'AA' : '不达标');
const pass = c >= need.aa;
return {
pass, ratio: c, level, large,
verdict: '对比度 ' + c + ':1,' + (large ? '大字号' : '普通文本') + ' 达' + level + (pass ? '' : ',未达 AA(需 ' + need.aa + ':1)'),
suggest: pass ? '对比度符合 WCAG,无障碍友好' : '建议加深前景或提亮背景,拉开明暗差(当前 ' + c + ':1)'
};
},
test: function () {
return { good: this.run({ fg: '#111111', bg: '#FFFFFF' }).level, bad: this.run({ fg: '#CCCCCC', bg: '#FFFFFF' }).pass, large: this.run({ fg: '#777777', bg: '#FFFFFF', size: 'large' }).pass };
}
},
{
id: 'neutral-palette',
name: '中性色系统',
category: 'color',
emoji: '⬜',
scene: '为 LOGO/VI 推荐黑白灰中性色系统(单色/双色场景)',
summary: '基于品牌主色亮度生成配套中性灰阶(黑/深灰/中灰/浅灰/白),并给出双色与单色场景的搭档建议:主色 + 中性色构成极简双色系统,单色印刷仍保留明暗层次。',
params: [
{ name: 'color', type: 'string', required: false, desc: '品牌主色 HEX(默认 #1B4F72)' },
{ name: 'usage', type: 'string', required: false, desc: 'mono 单色 / duo 双色 / full 完整系统(默认 duo)' }
],
source: '自研(中性色阶 + 明度对比规范)',
run: function (args) {
const c = String(args.color || '#1B4F72').replace('#', '');
const base = /^[0-9a-fA-F]{6}$/.test(c) ? c : '1B4F72';
const usage = String(args.usage || 'duo');
const l = lum('#' + base);
const scale = [
{ name: '纯黑', hex: '#000000' },
{ name: '深灰', hex: '#333333' },
{ name: '中灰', hex: '#757575' },
{ name: '浅灰', hex: '#E0E0E0' },
{ name: '纯白', hex: '#FFFFFF' }
];
const mates = scale.filter(s => contrast(s.hex, '#' + base) >= 3);
const duo = mates.length ? mates : scale;
const primary = '#' + base;
return {
primary, tone: l > 0.5 ? '亮色系' : '深色系',
scale,
duo: { primary, neutral: duo[0].hex, note: '主色 + 「' + duo[0].name + '」构成双色系统,单色印刷/传真场景主体依然清晰' },
mono: { note: '单色场景用纯黑/纯白表现明暗层次,主色转灰度时保留 ≥3:1 明暗差(配合「单色灰度适配」技能复核)' },
full: { note: '完整系统 = 主色 + 5 级灰阶 + 黑/白:灰阶用于次层级信息,主色仅作强调,避免满屏高饱和' },
usage: usage === 'mono' ? '单色' : (usage === 'duo' ? '双色' : '完整系统'),
tip: '中性色是品牌色的「背景板」:浅灰作底色、深灰作正文、黑/白作极简标识,主色只出现在关键图形上'
};
},
test: function () {
const r = this.run({ color: '#1B4F72', usage: 'duo' });
return { scale: r.scale.length, duo: r.duo.neutral, mono: r.mono.note !== '' };
}
}
]);
})();