Files
drawlogo/js/core/aiDesigner.js
T

388 lines
21 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.
/**
* 画个标 · AI 智能设计器
* 核心:把品牌需求 + 内置资源库(图标/布局/配色/风格/字体)作为白名单交给 DeepSeek,
* AI 输出设计决策 JSON,系统校验后映射为完整方案(复用 generator 渲染)
*/
const HGB_AIDesigner = (function () {
'use strict';
/* ---------- 内置库解析 ---------- */
/** 从已加载的素材源按 "source:name" 解析图标对象 {id,name,svg,type}(懒加载适配:仅搜索已加载源,未加载返回 null) */
function resolveIcon(ref) {
if (!ref || typeof ref !== 'string') return null;
const hit = HGB_IconLib.findIcon(ref);
return hit ? { id: hit.id, name: hit.name, svg: hit.svg, type: hit.type } : null;
}
/** 预加载 AI 输出引用的素材源(懒加载适配:AI 图标 ref 带源前缀,先加载对应源文件再同步查找) */
async function preloadReferencedSources(data) {
const refs = (data.plans || []).map(p => p.icon).filter(Boolean);
const sids = [...new Set(refs.map(r => String(r).split(':')[0]))].filter(sid =>
typeof HGB_BuiltinIcons !== 'undefined' && !!(HGB_BuiltinIcons.sources || {})[sid]);
if (sids.length) await HGB_IconLib.preload(sids);
}
/** AI 可选的精选图标(带中文名,杜绝 AI 乱猜英文 id) */
const ICON_DEMO = [
['lucide:star', '星星'], ['lucide:rocket', '火箭'], ['lucide:globe', '地球'],
['lucide:home', '房子'], ['lucide:heart', '爱心'], ['lucide:book', '书本'],
['lucide:graduation-cap', '学士帽'], ['lucide:briefcase', '公文包'],
['lucide:building-2', '大楼'], ['lucide:shield', '盾牌'], ['lucide:crown', '皇冠'],
['lucide:diamond', '钻石'], ['lucide:target', '靶心'], ['lucide:sun', '太阳'],
['lucide:moon', '月亮'], ['lucide:flame', '火焰'], ['lucide:leaf', '叶子'],
['lucide:tree-pine', '松树'], ['lucide:cloud', '云朵'], ['lucide:droplet', '水滴'],
['lucide:zap', '闪电'], ['lucide:sparkles', '星光'], ['lucide:medal', '奖牌'],
['lucide:trophy', '奖杯'], ['lucide:award', '勋章'], ['lucide:plane', '飞机'],
['lucide:compass', '指南针'], ['lucide:map', '地图'], ['lucide:music', '音符'],
['lucide:coffee', '咖啡'], ['lucide:utensils', '餐具'], ['lucide:shopping-bag', '购物袋'],
['lucide:palette', '调色板'], ['lucide:pen-tool', '画笔'], ['lucide:laptop', '笔记本'],
['lucide:phone', '手机'], ['lucide:camera', '相机'], ['lucide:key', '钥匙'],
['lucide:lock', '锁'], ['lucide:eye', '眼睛'], ['lucide:bell', '铃铛'],
['lucide:gift', '礼物'], ['lucide:party-popper', '礼花'], ['lucide:hand-heart', '手心爱心'],
['tabler:building', '楼宇'], ['tabler:chart-bar', '柱状图'], ['tabler:currency-yuan', '货币'],
['tabler:school', '学校'], ['tabler:heartbeat', '心跳'], ['feather:activity', '活动波']
];
/** 规范化布局决策:非法修正 + 多样性(≥3 种布局、圆形环绕上限头像 4 / LOGO 2);头像模式布局池限定头像白名单 */
function normalizeLayoutDecisions(decisions, brand, avatar) {
// 图形标识(Mark)模板不再内嵌品牌名,任何品牌名长度都可用;
// 仅"环形环绕 Lockup"需要短品牌名 + 圆形 Mark
const ROUND = ['badge', 'seal', 'orb', 'duo', 'burst'];
const roundMax = avatar ? 4 : 2; // 头像模式允许更多圆形构图(差异化)
// 布局池:头像模式 = 头像白名单;LOGO 模式 = 全部布局排除头像专有
const pool = avatar
? HGB_AvatarGen.AVATAR_LAYOUTS
: HGB_Generator.LAYOUTS.filter(l => !l.avatarOnly).map(l => l.id);
// 1) 非法布局 → 默认(圆标容器 / 头像白名单首个)
for (const d of decisions) {
if (!pool.includes(d.layout)) {
d._layoutInvalid = true;
d.layout = avatar ? pool[0] : HGB_Generator.defaultLayout(brand).id;
}
}
// 2) 圆形 Mark 数 > 上限 → 多余的替换为其他布局(保证形状多样性)
const roundIdx = decisions.map((d, i) => ROUND.includes(d.layout) ? i : -1).filter(i => i >= 0);
if (roundIdx.length > roundMax) {
const others = pool.filter(id => !ROUND.includes(id));
for (const i of roundIdx.slice(roundMax)) {
decisions[i]._layoutInvalid = true;
decisions[i].layout = others[i % others.length];
}
}
// 3) 布局种类 < 3 → 用未用过的布局补齐
const uniq = new Set(decisions.map(d => d.layout));
if (uniq.size < 3 && decisions.length >= 3) {
const used = [...uniq];
const avail = pool.filter(id => !used.includes(id));
for (const d of decisions) {
if (!avail.length) break;
if (d.layout === used[0] && uniq.size < 3) {
d._layoutInvalid = true;
d.layout = avail.shift();
uniq.add(d.layout);
}
}
}
}
/* ---------- Prompt 组装 ---------- */
function buildPrompt(opt) {
const ind = opt.industry;
const avatar = !!opt.avatar; // 头像模式:纯图形、无文字、布局限定头像白名单
const brand = (opt.brand || '').trim();
const sub = (opt.sub || '').trim();
const desc = (opt.desc || '').trim();
const userIcons = (opt.icons || []).map(i => i.id).join('、') || '(未指定)';
// 本次启用的技能(用户勾选):把技能标识信息注入,AI 须按技能规范设计
const skillList = (opt.skills || []).map(id => {
const s = HGB_Skills.get(id);
return s ? `${s.emoji} ${s.name}(${s.scene})` : null;
}).filter(Boolean).join('\n') || '(未启用特定技能,按行业最佳实践设计)';
// 布局/风格/字体白名单(头像模式:布局只列头像白名单纯图形布局;LOGO 模式排除头像专有布局)
const layoutPool = avatar
? HGB_Generator.LAYOUTS.filter(l => HGB_AvatarGen.isAvatarLayout(l.id))
: HGB_Generator.LAYOUTS.filter(l => !l.avatarOnly);
const layoutList = layoutPool.map(l => `${l.id}:${l.name}(${l.desc})`).join('\n');
const lockupList = HGB_Generator.LOCKUP_POS.map(lp => `${lp.pos}:${lp.name}`).join('\n');
// 风格白名单:用户指定风格方向时仅列出所选风格
const userStyles = (opt.styleIds || []).filter(id => HGB_Styles.some(s => s.id === id));
const styleList = HGB_Styles.filter(s => !userStyles.length || userStyles.includes(s.id))
.map(s => `${s.id}:${s.name}(设计运动:${s.movement}|${s.desc};气质:${s.moods.join('/')})`).join('\n');
const stylePref = userStyles.length
? `用户指定了风格方向:${HGB_Styles.filter(s => userStyles.includes(s.id)).map(s => `${s.emoji}${s.name}`).join('、')}。styleId 必须从这些风格中选择;多套方案尽量覆盖不同的风格,展示该方向下的多种可能。`
: '(用户未限定风格方向,请在全部风格中挑选契合行业与品牌描述的风格,并让多套方案使用不同的风格方向)';
const fontList = (ind.fontFamilies || []).join('、') +
';通用中文字体:SimHei、Microsoft YaHei、SimSun、KaiTi;通用英文字体:Arial、Georgia、Impact、Verdana、Montserrat、Playfair Display';
// 图标白名单:精选(带中文名)+ 当前行业图标 + 用户已选图标
const iconWhitelist = [
...ICON_DEMO.map(([id, zh]) => `${id}(${zh})`),
...(ind.icons || []).map(i => `${ind.id}:${i.id}(行业图标)`),
...(opt.icons || []).map(i => `${i.id}(用户倾向图标)`)
].join('\n');
const sys = avatar ? `你是顶级的品牌头像设计师。请基于用户的品牌信息,从"内置设计资源库"中选择素材,设计多套专业头像(Avatar)方案。
架构理念(必须严格遵守):
- 头像 = 纯图形标识(图标 + 容器/环/盾/几何),不含任何文字。头像用于微信/QQ/抖音/App 图标/网站 Favicon 等场景,小尺寸(64px)也必须清晰可辨。
设计原则(必须严格遵守):
1. 深刻理解品牌行业与描述,所选图标语义必须与品牌强相关(如餐饮用火焰/餐具,教育用书本/学士帽)。
2. 如果品牌描述指定色系/风格/气质,必须优先选择契合的配色与风格;如果用户明确指定了风格方向,必须只从指定风格中选择,不得越界。
3. 图标、布局、风格必须从各自白名单中选择,不得编造白名单之外的 id。
4. 每个方案给出贴切的中文方案名与设计理念(concept)。
5. 图标差异纪律:多套方案尽量使用不同的图标(可从白名单挑选语义相近但形态不同的图标),绝对避免所有方案共用同一个图标。
6. 布局纪律:${opt.count || 5} 套方案必须覆盖至少 3 种不同布局;头像圆形构图(badge/seal/orb/duo/burst)最多 4 套,优先使用头像专有布局(fullbleed 全出血大图标 / concentric 同心圆环 / centroid 中心单环)。
7. 构图方法论纪律:每套方案的图形标识必须建立在明确的几何构图框架上(容器/环/弧/角标/几何叠加),构图框架尽可能互不相同,禁止只靠"换颜色/换图标"制造差异。
8. 风格气韵纪律:所选 styleId 的设计运动(movement)与色系必须贯穿整套方案——主色/背景必须贴合风格气质(如 Art Deco 金黑对称、孟菲斯撞色拼贴、Swiss 黑白留白、霓虹深底荧光、包豪斯高饱和原色),禁止"风格与配色割裂"(如极简风配高饱和渐变)。系统会自动按 styleId 的色系渲染配色,无需在 JSON 中填写 paletteName。
9. 小尺寸可读性:图形轮廓简洁、对比清晰,避免过于纤细/密集的细节,保证缩到 64px(Favicon)仍可辨识。
只输出 JSON,不要输出任何其他文字。JSON 结构:
{
"overallConcept": "整体设计理念(一句话概括系列设计思路)",
"plans": [
{
"name": "方案一的中文名",
"concept": "这套方案的设计理念说明",
"icon": "lucide:graduation-cap",
"layout": "orb",
"styleId": "minimal"
}
]
}
其中 plans 数组包含 ${opt.count || 5} 个方案。icon 必须来自下方图标白名单;layout/styleId 分别来自对应白名单(配色由系统自动跟随 styleId 的风格色系,无需填写 paletteName)。` : `你是顶级的品牌 LOGO 设计师。请基于用户的品牌信息,从"内置设计资源库"中选择素材,设计多套专业 LOGO 方案。
架构理念(必须严格遵守):
- LOGO 分两层:图形标识(Mark)= 纯图形(图标+容器+环),不含任何文字;品牌组合(Lockup)= Mark + 品牌名,品牌名只摆放在图形周边(下方/右侧/上方/环绕),绝不融进图形内部。
设计原则(必须严格遵守):
1. 深刻理解品牌行业与描述,所选图标语义必须与品牌强相关(如餐饮用火焰/餐具,教育用书本/学士帽)。
2. 如果品牌描述指定色系/风格/气质,必须优先选择契合的配色与风格;如果用户明确指定了风格方向,必须只从指定风格中选择,不得越界。
3. 图标、布局、配色、风格、字体、lockup 必须从各自白名单中选择,不得编造白名单之外的 id。
4. 每个方案给出贴切的中文方案名与设计理念(concept)。
5. 图标差异纪律:多套方案之间尽量使用不同的图标(可从白名单挑选语义相近但形态不同的图标),绝对避免所有方案共用同一个图标。
6. 布局纪律:${opt.count || 5} 套方案必须覆盖至少 3 种不同 Mark 布局;圆形 Mark(badge/seal/orb/duo/burst)最多 2 套。
7. lockup 纪律:品牌名摆放位置多样化,多套方案使用不同的 lockup 位置(如 2 套下方、1 套右侧、1 套上方);环形环绕(wrap)仅用于圆形 Mark 且品牌名 ≤6 中文字或 ≤12 西文字符。
8. 构图方法论纪律:每套方案的 Mark 必须建立在明确的几何构图框架上(容器/环/弧/绶带/角标/几何叠加),构图框架尽可能互不相同——借鉴"精确几何构图":圆与三角的数学比例、对称平衡、网格对齐;禁止只靠"换颜色/换图标/换位置"来制造差异。
9. 差异度纪律:多套方案必须在构图方式、视觉层次(环/盾/容器/绶带/角标/射线/拱门/几何叠加等)、配色、风格上有明显差异,呈现不同设计方向,而不是"换个颜色换个位置"。
10. 换色纪律:同一布局若复用,必须变换配色与风格;尽可能让每套方案的构图框架(layout)不同。
11. 风格气韵纪律:所选 styleId 的设计运动(movement)与色系必须贯穿整套方案——配色的主色/背景必须贴合风格气质(如 Art Deco 金黑对称、孟菲斯撞色拼贴、Swiss 黑白留白网格、霓虹深底荧光、包豪斯高饱和原色),禁止出现"风格与配色割裂"(如极简风配高饱和渐变)。系统会自动按 styleId 的色系渲染配色,无需在 JSON 中填写 paletteName。
只输出 JSON,不要输出任何其他文字。JSON 结构:
{
"overallConcept": "整体设计理念(一句话概括系列设计思路)",
"plans": [
{
"name": "方案一的中文名",
"concept": "这套方案的设计理念说明",
"icon": "lucide:graduation-cap",
"layout": "orb",
"lockup": "below",
"styleId": "minimal",
"fontCn": "SimHei",
"fontEn": "Georgia"
}
]
}
其中 plans 数组包含 ${opt.count || 5} 个方案。icon 必须来自下方图标白名单;layout/styleId 分别来自对应白名单(配色由系统自动跟随 styleId 的风格色系,无需填写 paletteName);lockup 来自品牌组合位置白名单;fontCn/fontEn 来自字体白名单。`;
const user = avatar ? `【品牌信息】
- 行业:${ind.name}(${ind.desc})
- 品牌名:${brand}
- 品牌描述:${desc || '(无,请根据行业特点设计)'}
- 用户倾向使用的图标:${userIcons}
【本次启用技能】(设计时须遵循这些技能的规范)
${skillList}
【风格方向要求】
${stylePref}
【图标白名单】(icon 字段从这里选,格式为 源:名称)
${iconWhitelist}
【布局模板白名单】(layout 字段,仅限适合头像的纯图形布局,不含文字)
${layoutList}
【设计风格白名单】(styleId 字段;配色自动跟随所选风格的色系)
${styleList}
【字体白名单】(仅供方案理念参考;头像为纯图形,不渲染任何文字)
${fontList}
请设计 ${opt.count || 5} 套头像方案,每套都要从白名单选择,并附上设计理念。` : `【品牌信息】
- 行业:${ind.name}(${ind.desc})
- 品牌名:${brand}
- 副标题:${sub || '(无)'}
- 品牌描述:${desc || '(无,请根据行业特点设计)'}
- 用户倾向使用的图标:${userIcons}
【本次启用技能】(设计时须遵循这些技能的规范)
${skillList}
【风格方向要求】
${stylePref}
【图标白名单】(icon 字段从这里选,格式为 源:名称)
${iconWhitelist}
【布局模板白名单】(layout 字段)
${layoutList}
【品牌组合位置白名单】(lockup 字段,品牌名在图形周边的摆放方式)
${lockupList}
【设计风格白名单】(styleId 字段;配色自动跟随所选风格的色系)
${styleList}
【字体白名单】(fontCn / fontEn 字段)
${fontList}
请设计 ${opt.count || 5} 套方案,每套都要从白名单选择,并附上设计理念。`;
return [
{ role: 'system', content: sys },
{ role: 'user', content: user }
];
}
/* ---------- 解析与校验 ---------- */
function extractJson(content) {
let text = (content || '').trim();
// 剥 markdown 围栏
text = text.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '');
const start = text.indexOf('{');
const end = text.lastIndexOf('}');
if (start < 0 || end < start) throw new Error('AI 输出不是有效 JSON');
return JSON.parse(text.slice(start, end + 1));
}
/** 校验并规范化 AI 输出:非法字段显式标记,布局统一规范 */
function validate(data, opt) {
const ind = opt.industry;
if (!data || !Array.isArray(data.plans) || !data.plans.length) {
throw new Error('AI 未返回方案');
}
const plans = data.plans.slice(0, 20);
for (const p of plans) {
// 图标:白名单解析,失败标记(映射时回退行业/用户图标)
if (p.icon && !resolveIcon(p.icon)) p._iconInvalid = true;
// 布局/风格:不在白名单标记(配色自动跟随风格,无需校验 paletteName)
const layoutOk = opt.avatar
? HGB_AvatarGen.isAvatarLayout(p.layout)
: HGB_Generator.LAYOUTS.some(l => l.id === p.layout && !l.avatarOnly);
if (!layoutOk) p._layoutInvalid = true;
if (!HGB_Styles.some(s => s.id === p.styleId)) p._styleInvalid = true;
}
// 布局纪律:非法修正 + 多样性保障(≥3 种布局、圆环类 ≤2)
normalizeLayoutDecisions(plans, (opt.brand || '').trim() || HGB_Generator.sampleName(ind), !!opt.avatar);
return data;
}
/** 颜色亮度(0-1) */
function lum(hex) {
const c = String(hex || '#FFFFFF').replace('#', '');
if (c.length !== 6) return 0.9;
const r = parseInt(c.slice(0, 2), 16) / 255;
const g = parseInt(c.slice(2, 4), 16) / 255;
const b = parseInt(c.slice(4, 6), 16) / 255;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
/** 对比度修正:深色主色 + 深色背景 → 背景自动调亮 */
function fixContrast(plan) {
const text = plan.elements.find(el => el.type === 'text');
const icon = plan.elements.find(el => el.type === 'icon' && el.iconId !== 'ring' && el.iconId !== 'deco-bar');
const main = (text && text.fill) || (icon && icon.stroke);
if (!main || !plan.bg) return;
if (lum(plan.bg) < 0.42 && lum(main) < 0.45) {
plan.bg = '#FFFFFF';
plan._bgFixed = true;
}
}
/** 把 AI 决策映射为完整方案(非法字段显式修正并记录警告) */
function mapToPlan(p, opt, idx) {
const ind = opt.industry;
const avatar = !!opt.avatar;
const brand = (opt.brand || '').trim() || HGB_Generator.sampleName(ind);
const sub = (opt.sub || '').trim();
const userIcons = (opt.icons || []).filter(Boolean);
const aiIcon = resolveIcon(p.icon);
// 图标容错:AI 图标非法时,按方案序号轮换用户/行业图标,避免"全套同一图标"
const fallbackPool = userIcons.length ? userIcons : ((ind.icons || []));
const icons = aiIcon
? [aiIcon]
: (fallbackPool.length ? [fallbackPool[idx % fallbackPool.length]] : []);
// 显式修正非法字段(不做随机回退);头像模式布局限定头像白名单
const layoutOk = avatar
? HGB_AvatarGen.isAvatarLayout(p.layout)
: HGB_Generator.LAYOUTS.some(l => l.id === p.layout && !l.avatarOnly);
const layoutId = layoutOk
? p.layout
: (avatar ? HGB_AvatarGen.AVATAR_LAYOUTS[0] : HGB_Generator.defaultLayout(brand).id);
// 风格:非法时优先回退用户指定风格池,其次行业推荐
const userStyleIds = (opt.styleIds || []).filter(id => HGB_Styles.some(s => s.id === id));
const styleId = HGB_Styles.some(s => s.id === p.styleId)
? p.styleId
: (userStyleIds.length
? userStyleIds[idx % userStyleIds.length]
: ((ind.styleIds || [])[0] || HGB_Styles[0].id));
const variant = HGB_Generator.generateVariant({
industry: ind,
brand,
sub,
icons,
layoutId,
styleId, // 配色由 generator 自动跟随 styleId 色系
lockup: avatar ? '' : p.lockup, // 头像为纯图形,无品牌名组合
// AI 字体决策生效(非法字体由 fontFor 自动回退)
fontFavs: [p.fontCn, p.fontEn].filter(Boolean)
});
variant.id = 'ai_plan_' + Date.now().toString(36) + idx.toString(36);
variant.name = (p.name || '').trim() || variant.name;
variant.concept = (p.concept || '').trim();
variant.aiIcon = aiIcon ? aiIcon.name : '';
variant.ai = true;
// 对比度自动修正
fixContrast(variant);
// 收集自动修正警告,供卡片展示
const warnings = [];
if (p._layoutInvalid) warnings.push('布局已自动调整');
if (p._iconInvalid) warnings.push('图标已替换');
if (p._styleInvalid) warnings.push('风格已调整');
if (variant._bgFixed) warnings.push('背景已调亮');
variant.warnings = warnings;
return variant;
}
/* ---------- 主入口 ---------- */
/**
* AI 生成方案
* @param {object} opt { industry, brand, sub, desc, icons, count }
* @returns {Promise<{plans:Array, overallConcept:string}>}
*/
async function generatePlansAI(opt) {
const messages = buildPrompt(opt);
const content = await HGB_AI.chat(messages, { json: true, maxTokens: 8192 });
let data = extractJson(content);
// 懒加载适配:预加载 AI 引用素材源,确保 resolveIcon/findIcon 命中
await preloadReferencedSources(data);
data = validate(data, opt);
const plans = data.plans.map((p, i) => mapToPlan(p, opt, i));
return {
plans,
overallConcept: (data.overallConcept || '').trim(),
raw: content
};
}
return { generatePlansAI, resolveIcon, extractJson, validate, mapToPlan, buildPrompt };
})();