/** * 画个标 · 智能设计引擎(图形标识 Mark + 品牌组合 Lockup 双层架构) * 核心理念: * - 图形标识(Mark):纯图形设计(容器/环/图标/装饰),不含任何文字 —— 这是"LOGO 设计"本身 * - 品牌组合(Lockup):在 Mark 完成之后,把品牌名摆放到图形周边(下方/右侧/上方/环绕) * - 品牌名绝不"融"进图形内部,只作为周边排版层存在 * 落地 GitHub logo-design-skill 设计方法论: * - 元素坐标一律以"中心点"传递,杜绝文字/图标偏离画布中心轴 * - 布局 = 图形容器/环/弧/绶带 + 图标的三层构图,而非"换个位置" * - 深色主色填充的容器(盾/圆/方)承载白色图标,形成徽章化视觉 */ const HGB_Generator = (function () { 'use strict'; const CW = 600, CH = 600; let seq = 0; /* ============ 字体选择 ============ */ // 覆盖风格库 font/fontCn 的全部取值,保证风格字体真正生效(否则 fontFor 回退默认) const CN_FONTS = ['SimHei', 'Microsoft YaHei', 'SimSun', 'KaiTi', '宋体', '微软雅黑', '楷体', '黑体']; const EN_FONTS = ['Arial', 'Georgia', 'Impact', 'Verdana', 'Montserrat', 'Playfair Display', 'Comic Sans MS', 'Times New Roman', 'Bradley Hand', 'Arial Black']; /** 根据文本内容与行业推荐字体,选择合适字体 */ function fontFor(text, favs) { const hasCn = /[\u4e00-\u9fa5]/.test(text || ''); const pool = hasCn ? CN_FONTS : EN_FONTS; const fav = (favs || []).find(f => pool.includes(f)); return fav || (hasCn ? 'SimHei' : 'Arial'); } /* ============ 文字测量与自适应(防"破版"核心) ============ */ /** 估算文本渲染宽度:中文全角、西文按 0.55 倍、计入字距 */ function textWidth(text, fontSize, spacing) { const t = String(text || ''); const cn = (t.match(/[\u4e00-\u9fa5]/g) || []).length; const other = t.length - cn; return cn * fontSize + other * fontSize * 0.55 + Math.max(0, t.length - 1) * (spacing || 0); } /** 从基准字号向下收缩,直到文本能放进 maxWidth(下限 min) */ function fitFontSize(text, maxWidth, base, spacing, min) { const t = String(text || ''); if (!t) return base; const mn = min || 18; let size = base; while (size > mn && textWidth(t, size, spacing || 0) > maxWidth) size -= 2; return Math.round(size); } /** 超宽文本截断加省略号 */ function clipText(text, maxWidth, size, spacing) { const t = String(text || ''); if (!t || textWidth(t, size, spacing || 0) <= maxWidth) return t; let cut = t; while (cut.length > 1 && textWidth(cut + '…', size, spacing || 0) > maxWidth) cut = cut.slice(0, -1); return cut + '…'; } /** 品牌名是否适合环形环绕等紧凑排版(中文 ≤6 字或西文 ≤12 字符) */ function isShort(brand) { const t = String(brand || '').trim(); const cn = (t.match(/[\u4e00-\u9fa5]/g) || []).length; return cn ? cn <= 6 : t.length <= 12; } /** 按品牌名长度选择默认 Mark 布局 */ function defaultLayout(brand) { return LAYOUTS.find(l => l.id === 'orb') || LAYOUTS[0]; } /** 风格倾向布局:取风格 layouts 列表第一个有效布局(可再按白名单过滤;无则 null) */ function favLayout(style, whitelist) { let favs = (style && style.layouts || []).filter(id => LAYOUTS.some(l => l.id === id)); if (whitelist && whitelist.length) favs = favs.filter(id => whitelist.includes(id)); // LOGO 模式(无白名单)排除头像专有布局 else favs = favs.filter(id => !(LAYOUTS.find(l => l.id === id) || {}).avatarOnly); if (!favs.length) return null; return LAYOUTS.find(l => l.id === favs[0]) || null; } /** 布局兜底:白名单优先轮换,否则全局轮换(排除头像专有布局) */ function layoutFallback(i, whitelist) { if (whitelist && whitelist.length) { const l = LAYOUTS.find(x => x.id === whitelist[i % whitelist.length]); if (l) return l; } const pool = LAYOUTS.filter(l => !l.avatarOnly); return pool[i % pool.length]; } /* ============ 元素构造(一律以"中心点"定位) ============ */ function iconEl(ic, cx, cy, w, h, stroke, strokeW) { return { id: uid('ic'), type: 'icon', name: (ic && ic.name) || '图标', iconId: (ic && ic.id) || '', svg: (ic && ic.svg) || '', // 多色素材(emoji/品牌):preserve=true 时导出/预览保留原始颜色,不按 stroke/fill 覆盖 preserve: !!(ic && ic.type && ic.type !== 'icon'), x: Math.round(cx - w / 2), y: Math.round(cy - h / 2), w, h, rotation: 0, fill: 'none', stroke, strokeWidth: strokeW, opacity: 1, visible: true, locked: false }; } function ringEl(cx, cy, w, h, stroke, strokeW) { return { id: uid('rg'), type: 'icon', name: '圆环', iconId: 'ring', svg: '', x: Math.round(cx - w / 2), y: Math.round(cy - h / 2), w, h, rotation: 0, fill: 'none', stroke, strokeWidth: strokeW, opacity: 1, visible: true, locked: false }; } function textEl(text, cx, cy, w, h, fontFamily, fontSize, fontWeight, fill, letterSpacing) { return { id: uid('tx'), type: 'text', name: '文字', text, x: Math.round(cx - w / 2), y: Math.round(cy - h / 2), w, h, rotation: 0, fontSize, fontWeight, fontFamily, letterSpacing, fill, stroke: 'none', strokeWidth: 0, opacity: 1, visible: true, locked: false }; } /** 环形文字(textPath,品牌名环绕 Mark 用) */ function arcTextEl(text, arc, fontFamily, fontSize, fontWeight, fill, letterSpacing) { return { id: uid('at'), type: 'text', name: '环形文字', text, x: Math.round(arc.cx - arc.r), y: Math.round(arc.cy - arc.r), w: Math.round(arc.r * 2), h: Math.round(arc.r * 2), rotation: 0, fontSize, fontWeight, fontFamily, letterSpacing, fill, stroke: 'none', strokeWidth: 0, arc, opacity: 1, visible: true, locked: false }; } /** 装饰原语(中心定位包装) */ function decoAt(decId, cx, cy, w, h, color, grad) { return HGB_Decorations.decoEl(decId, Math.round(cx - w / 2), Math.round(cy - h / 2), w, h, color, grad); } /** 色条:铺满元素的圆角矩形 */ function barEl(cx, cy, w, h, color, grad) { return { id: uid('br'), type: 'icon', name: '色条', iconId: 'deco-bar', svg: '', x: Math.round(cx - w / 2), y: Math.round(cy - h / 2), w, h, rotation: 0, fill: grad ? 'url(#' + grad.id + ')' : color, stroke: 'none', strokeWidth: 0, grad: grad || null, opacity: 1, visible: true, locked: false }; } function uid(prefix) { return prefix + '_' + Date.now().toString(36) + (seq++).toString(36); } /* ============ 渐变 ============ */ /** 主色线性渐变(容器填充用) */ function mainGrad(palette) { return { id: 'g_' + Date.now().toString(36) + (seq++).toString(36), type: 'linear', from: palette.colors[0], to: HGB_Decorations.lighten(palette.colors[0], 0.30) }; } /** 径向渐变(圆标用,中心亮边缘深) */ function radialGrad(palette) { return { id: 'g_' + Date.now().toString(36) + (seq++).toString(36), type: 'radial', from: HGB_Decorations.lighten(palette.colors[0], 0.42), to: palette.colors[0] }; } /** 首字符(字标用):中文取首字,西文取大写首字母 */ function initialOf(text) { const t = String(text || '').trim(); if (!t) return 'A'; const ch = t[0]; return /[\u4e00-\u9fa5]/.test(ch) ? ch : ch.toUpperCase(); } /* ============ 图形标识模板(Mark,只出纯图形,无文字) ============ */ /** * 每个模板 build(opts) 返回 mark 图形元素数组 * opts = { brand, sub, icon, icon2, palette, style, fontCn, fontEn, strokeW, cx, cy } * cx/cy = 图形中心(默认 300,300);palette.colors = [主色, 辅助色, 点缀色, 背景] * round = true 表示圆形构图(可配环形环绕 Lockup) */ const LAYOUTS = [ { id: 'badge', name: '双环徽章', emoji: '🎖️', round: true, desc: '外环+内双线环承载图标,权威与荣耀', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c0 = palette.colors[0], c1 = palette.colors[1]; const els = []; els.push(ringEl(cx0, cy0, 380, 380, c1, strokeW * 1.5)); // 外环 r=190 els.push(decoAt('deco-doublering', cx0, cy0, 280, 280, c1, null)); // 内双线环 r≈98/65 els.push(iconEl(icon, cx0, cy0, 128, 128, c0, strokeW)); // 图标居中 return els; } }, { id: 'seal', name: '双环印章', emoji: '🕹️', round: true, desc: '官方认证感,双环+星点', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c0 = palette.colors[0], c1 = palette.colors[1], c2 = palette.colors[2]; const els = []; els.push(ringEl(cx0, cy0, 400, 400, c1, strokeW * 2)); // 外环 r=200 els.push(ringEl(cx0, cy0, 320, 320, c1, strokeW)); // 内环 r=160 els.push(decoAt('deco-shape-diamond', cx0, cy0 + 150, 26, 26, c2, null)); // 底部星点 els.push(iconEl(icon, cx0, cy0 - 10, 110, 106, c0, strokeW)); return els; } }, { id: 'emblem', name: '渐变盾徽', emoji: '🛡️', desc: '渐变盾形承载白色图标,学院派权威', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const g = mainGrad(palette); const els = []; els.push(decoAt('deco-shape-shield', cx0, cy0, 360, 360, null, g)); // 渐变盾形 els.push(iconEl(icon, cx0, cy0, 126, 126, '#FFFFFF', strokeW)); // 白色图标 return els; } }, { id: 'orb', name: '圆标容器', emoji: '🔮', round: true, desc: '径向渐变圆承载白色图标,现代应用图标风', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1]; const g = radialGrad(palette); const els = []; els.push(decoAt('deco-shape-circle', cx0, cy0, 320, 320, null, g)); // 径向渐变圆 r=160 els.push(iconEl(icon, cx0, cy0, 130, 130, '#FFFFFF', strokeW * 1.2)); els.push(decoAt('deco-dotring', cx0, cy0, 380, 380, c1, null)); // 点状光环 r≈170 return els; } }, { id: 'monogram', name: '首字字标', emoji: '🔠', desc: '渐变圆角容器承载品牌首字,字标风', build(opts) { const { brand, palette, fontCn, fontEn, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c2 = palette.colors[2]; const g = mainGrad(palette); const letter = initialOf(brand); const els = []; els.push(decoAt('deco-shape-squircle', cx0, cy0, 280, 280, null, g)); // 渐变圆角容器 const lf = /[\u4e00-\u9fa5]/.test(letter) ? fontFor(brand, [fontCn, fontEn]) : fontFor(brand, [fontEn, fontCn]); const lfs = fitFontSize(letter, 200, 158, 0, 64); els.push(textEl(letter, cx0, cy0, 200, 190, lf, lfs, 800, '#FFFFFF', 0)); // 白色大字 els.push(decoAt('deco-shape-diamond', cx0, cy0 + 132, 30, 30, c2, null)); // 底部点缀菱形 return els; } }, { id: 'ribbon', name: '绶带横幅', emoji: '🎗️', desc: '主色绶带+图标,庆典仪式感', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c0 = palette.colors[0], c2 = palette.colors[2]; const els = []; els.push(decoAt('deco-arc-top', cx0, cy0 - 62, 280, 160, c2, null)); // 顶部弧线 els.push(iconEl(icon, cx0, cy0 - 68, 140, 140, c0, strokeW * 1.3)); els.push(decoAt('deco-shape-ribbon', cx0, cy0 + 82, 460, 150, c0, null)); // 绶带(主色填充,无文字) els.push(decoAt('deco-shape-pill', cx0, cy0 + 176, 210, 40, c2, null)); // 下方点缀胶囊 return els; } }, { id: 'hexagon', name: '六边形徽章', emoji: '⬡', desc: '渐变六边形承载白色图标,秩序感', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const g = mainGrad(palette); const c2 = palette.colors[2]; const els = []; els.push(decoAt('deco-shape-hexagon', cx0, cy0, 320, 320, null, g)); els.push(iconEl(icon, cx0, cy0, 126, 126, '#FFFFFF', strokeW * 1.1)); els.push(decoAt('deco-shape-diamond', cx0, cy0 + 156, 22, 22, c2, null)); return els; } }, { id: 'diamond', name: '菱形徽章', emoji: '💎', desc: '渐变菱形承载白色图标,精品感', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const g = mainGrad(palette); const c2 = palette.colors[2]; const els = []; els.push(decoAt('deco-shape-diamond', cx0, cy0, 320, 320, null, g)); els.push(iconEl(icon, cx0, cy0, 120, 120, '#FFFFFF', strokeW * 1.1)); els.push(decoAt('deco-shape-circle', cx0, cy0 + 152, 18, 18, c2, null)); return els; } }, { id: 'techcorner', name: '科技角标', emoji: '🪐', desc: '对角括号框定图标+光芒,科技秩序感', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1], c2 = palette.colors[2]; const els = []; els.push(decoAt('deco-corner-tl', cx0 - 230, cy0 - 230, 140, 140, c1, null)); els.push(decoAt('deco-corner-br', cx0 + 230, cy0 + 230, 140, 140, c1, null)); els.push(decoAt('deco-spark', cx0 + 168, cy0 - 182, 48, 48, c2, null)); els.push(iconEl(icon, cx0, cy0, 140, 140, c1, strokeW * 1.2)); return els; } }, { id: 'duo', name: '双图标对称', emoji: '🪞', round: true, desc: '双图标对称弧拱,均衡呼应', build(opts) { const { icon, icon2, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1], c2 = palette.colors[2]; const els = []; els.push(decoAt('deco-arc-top', cx0, cy0 - 88, 320, 170, c1, null)); // 顶部连接弧 els.push(iconEl(icon, cx0 - 152, cy0 + 18, 124, 124, c1, strokeW)); els.push(iconEl(icon2 || icon, cx0 + 152, cy0 + 18, 124, 124, c1, strokeW)); els.push(decoAt('deco-shape-diamond', cx0, cy0 - 176, 30, 30, c2, null)); return els; } }, { id: 'arch', name: '拱门徽章', emoji: '🏛️', desc: '渐变拱门承载白色图标,学院殿堂感', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const g = mainGrad(palette); const c2 = palette.colors[2]; const els = []; els.push(decoAt('deco-shape-arch', cx0, cy0, 300, 330, null, g)); // 渐变拱形衬底 els.push(iconEl(icon, cx0, cy0 + 16, 118, 118, '#FFFFFF', strokeW * 1.1)); els.push(decoAt('deco-shape-diamond', cx0, cy0 + 152, 26, 26, c2, null)); // 底部点缀 return els; } }, { id: 'burst', name: '光芒徽章', emoji: '☀️', round: true, desc: '射线环+渐变圆承载图标,光芒与能量', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1]; const g = radialGrad(palette); const els = []; els.push(decoAt('deco-rayring', cx0, cy0, 396, 396, c1, null)); // 12 根射线环 r≈198 els.push(decoAt('deco-shape-circle', cx0, cy0, 264, 264, null, g)); // 径向渐变圆 r=132 els.push(iconEl(icon, cx0, cy0, 116, 116, '#FFFFFF', strokeW * 1.1)); return els; } }, { id: 'geo', name: '几何构形', emoji: '📐', desc: '圆环+渐变三角黄金叠加,精确几何感', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1], c2 = palette.colors[2]; const g = mainGrad(palette); const els = []; els.push(ringEl(cx0, cy0, 400, 400, c1, strokeW * 1.8)); // 外圆环 r=200 els.push(decoAt('deco-shape-triangle', cx0, cy0 + 12, 300, 280, null, g)); // 渐变三角衬底 els.push(iconEl(icon, cx0, cy0 + 8, 116, 116, '#FFFFFF', strokeW * 1.1)); els.push(decoAt('deco-shape-diamond', cx0, cy0 + 116, 22, 22, c2, null)); return els; } }, /* ===== 头像专有布局(avatarOnly:仅头像模式白名单使用,LOGO 模式全局池排除) ===== */ { id: 'fullbleed', name: '全出血图标', emoji: '🖼️', avatarOnly: true, desc: '无容器,图标直接顶满画布,纯图形大图标头像', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c0 = palette.colors[0]; const els = []; els.push(iconEl(icon, cx0, cy0, 232, 232, c0, strokeW * 2)); // 大图标占 77% return els; } }, { id: 'concentric', name: '同心圆环', emoji: '🌀', avatarOnly: true, desc: '三重同心圆环+中心图标,韵律感头像', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1], c2 = palette.colors[2]; const els = []; els.push(ringEl(cx0, cy0, 400, 400, c1, strokeW * 1.2)); // 外环 r=200 els.push(ringEl(cx0, cy0, 330, 330, c1, strokeW * 0.8)); // 中环 r=165 els.push(ringEl(cx0, cy0, 240, 240, c2, strokeW)); // 内环 r=120 els.push(iconEl(icon, cx0, cy0, 120, 120, c1, strokeW * 1.1)); return els; } }, { id: 'centroid', name: '中心单环', emoji: '⭕', avatarOnly: true, desc: '细圆环+居中大图标,简洁单图标头像', build(opts) { const { icon, palette, strokeW, cx, cy } = opts; const cx0 = cx || 300, cy0 = cy || 300; const c1 = palette.colors[1]; const els = []; els.push(ringEl(cx0, cy0, 372, 372, c1, strokeW * 1.3)); // 外环 r=186 els.push(iconEl(icon, cx0, cy0, 158, 158, c1, strokeW * 1.5)); // 大图标 return els; } } ]; /* ============ 品牌组合位置(Lockup) ============ */ /** * 品牌名在 Mark 图形周边的摆放方式 * pos: below(下方居中) / right(右侧并排) / top(上方居中) / wrap(环形环绕) */ const LOCKUP_POS = [ { pos: 'below', name: '下方居中' }, { pos: 'right', name: '右侧并排' }, { pos: 'top', name: '上方居中' }, { pos: 'wrap', name: '环形环绕' } ]; /** 计算 Mark 的图形元素(以指定中心生成一次,供 Lockup 复用) */ function buildMark(layout, opts, cx, cy) { return layout.build({ ...opts, cx, cy }); } /** * 生成单个 Lockup 组合(Mark + 品牌名在 pos 位置) * @returns {Array} 完整元素数组 */ function buildLockup(layout, pos, opts) { const brand = opts.brand; const sub = opts.sub; const palette = opts.palette; const c0 = palette.colors[0], c1 = palette.colors[1], c2 = palette.colors[2]; const fontCn = opts.fontCn, fontEn = opts.fontEn; const els = []; if (pos === 'wrap') { // 环形环绕:仅圆形 Mark + 短品牌名 els.push(...buildMark(layout, opts, 300, 300)); const rOut = 214; const fs = fitFontSize(brand, rOut * 2, 36, 3, 18); els.push(arcTextEl(brand, { cx: 300, cy: 300, r: rOut, start: 180, sweep: 180 }, fontFor(brand, [fontCn, fontEn]), fs, 700, c0, 3)); if (sub) { const sfs = fitFontSize(sub, rOut * 1.6, 15, 3, 11); // 副标题走"下半弧":start=180 起、逆时针(负sweep)经过底部到 0°,文字沿底部自左向右正立 els.push(arcTextEl(clipText(sub, rOut * 1.6, sfs, 3), { cx: 300, cy: 300, r: rOut, start: 180, sweep: -180 }, fontFor(sub, [fontEn, fontCn]), sfs, 400, c1, 3)); } return els; } if (pos === 'below') { // Mark 偏上居中,品牌名在正下方 els.push(...buildMark(layout, opts, 300, 246)); els.push(barEl(300, 470, 120, 8, c2)); const MAXW = 460; const fs = fitFontSize(brand, MAXW, 46, 3, 22); els.push(textEl(brand, 300, 512, MAXW, 62, fontFor(brand, [fontCn, fontEn]), fs, 700, c0, 3)); if (sub) { const sfs = fitFontSize(sub, 380, 16, 3, 12); els.push(textEl(clipText(sub, 380, sfs, 3), 300, 560, 380, 24, fontFor(sub, [fontEn, fontCn]), sfs, 400, c1, 3)); } return els; } if (pos === 'right') { // Mark 居左,品牌名右侧左对齐 els.push(...buildMark(layout, opts, 232, 300)); const MAXW = 150; const fs = fitFontSize(brand, MAXW, 42, 1, 18); const tw = textWidth(brand, fs, 1); const cxT = 430 + tw / 2; els.push(textEl(brand, cxT, 286, MAXW, 58, fontFor(brand, [fontCn, fontEn]), fs, 700, c0, 1)); els.push(barEl(cxT, 352, 60, 7, c2)); if (sub) { const sfs = fitFontSize(sub, MAXW, 14, 2, 11); els.push(textEl(clipText(sub, MAXW, sfs, 2), cxT, 392, MAXW, 22, fontFor(sub, [fontEn, fontCn]), sfs, 400, c1, 2)); } return els; } // top:Mark 偏下居中,品牌名在正上方 els.push(...buildMark(layout, opts, 300, 356)); els.push(barEl(300, 222, 120, 8, c2)); const MAXW = 460; const fs = fitFontSize(brand, MAXW, 42, 3, 22); els.push(textEl(brand, 300, 178, MAXW, 58, fontFor(brand, [fontCn, fontEn]), fs, 700, c0, 3)); if (sub) { const sfs = fitFontSize(sub, 380, 15, 3, 12); els.push(textEl(clipText(sub, 380, sfs, 3), 300, 132, 380, 22, fontFor(sub, [fontEn, fontCn]), sfs, 400, c1, 3)); } return els; } /* ============ 方案生成 ============ */ /** * 组合单个方案(供批量生成与变体微调复用) * @returns {{id,name,layoutId,layoutName,paletteName,styleId,bg, * markElements:Array, lockups:Array, elements:Array}} */ function composePlan(opt, layout, palette, style) { const brand = opt.brand; const sub = opt.sub; const icon1 = opt.icon1; const icon2 = opt.icon2; const fontFavs = opt.fontFavs || []; // 视觉描边宽度(屏幕像素):配合 exporter 的 non-scaling-stroke 恒定不随放大变粗 const strokeW = Math.max(3, Math.round(((style ? style.strokeWidth : 1.6) || 1.6) * 2)); const mkOpts = { brand, sub, icon: icon1, icon2: icon2, palette, style, // 风格字体优先(风格自包含),行业/AI 字体建议兜底 fontCn: (style && style.fontCn && CN_FONTS.includes(style.fontCn)) ? style.fontCn : (fontFavs.find(f => CN_FONTS.includes(f)) || (style && style.fontCn) || 'SimHei'), fontEn: (style && style.font && EN_FONTS.includes(style.font)) ? style.font : (fontFavs.find(f => EN_FONTS.includes(f)) || (style && style.font) || 'Arial'), strokeW }; // 图形标识(Mark):纯图形,居中展示 const markElements = buildMark(layout, mkOpts, 300, 300); // 品牌组合(Lockup):品牌名在图形周边各位置摆放 const lockups = LOCKUP_POS.filter(lp => lp.pos !== 'wrap' || (layout.round && isShort(brand)) ).map(lp => ({ pos: lp.pos, name: lp.name, elements: buildLockup(layout, lp.pos, mkOpts) })); return { id: 'plan_' + Date.now().toString(36) + (seq++).toString(36), name: `${layout.name} · ${palette.name}`, layoutId: layout.id, layoutName: layout.name, paletteName: palette.name, styleId: style ? style.id : '', bg: palette.colors[3] || '#FFFFFF', markElements, // 区域一:图形标识(纯图形) lockups, // 区域二:品牌组合(各位置) elements: lockups.length ? lockups[0].elements : markElements // 默认下方组合 }; } /** 收集选中图标池(去重,含行业图标;供批量生成轮换使用) */ function pickIcons(opt, ind) { const picked = (opt.icons || []).filter(Boolean).slice(0, 5); const indIcons = (ind.icons || []).filter(i => !picked.some(p => p.id === i.id)); const pool = [...picked, ...indIcons]; const iconPool = pool.filter((v, i) => v && pool.findIndex(x => x && x.id === v.id) === i); return { icon1: iconPool[0], icon2: iconPool[1] || iconPool[0], iconPool: iconPool.length ? iconPool : [icon1Fallback(ind)] }; } /** 行业图标兜底(找不到任何图标时) */ function icon1Fallback(ind) { return (ind.icons || [])[0] || { id: 'star', name: '星形', svg: '' }; } /** * 风格池:用户指定风格优先,其次行业推荐优先,最后全局;始终有效 * @param {object} ind 行业对象(含 styleIds) * @param {Array} [styleIds] 用户选中的风格 id(空/无效则全部) */ function stylePoolFor(ind, styleIds) { const indIds = (ind.styleIds || []); let pool = HGB_Styles.slice(); if (styleIds && styleIds.length) { const wanted = pool.filter(s => styleIds.includes(s.id)); if (wanted.length) pool = wanted; // 全部无效则回退全部风格 } // 行业推荐优先排序 return pool.sort((a, b) => (indIds.includes(b.id) ? 1 : 0) - (indIds.includes(a.id) ? 1 : 0)); } /** * 批量生成方案 * @param {object} opt * industry: 行业对象(含 palettes/styleIds/fontFamilies/icons) * brand: 品牌名(默认取行业默认名称或用户输入) * sub: 副标题(可空) * icons: 用户选中的图标数组 [{id,name,svg}],可为空(用行业图标轮换) * styleIds: 用户选中的风格方向 id(空 = 全部风格) * iconLocked: 图标驱动模式——每个选中图标固定作为核心图形,各产出 count 套(总方案 = 图标数 × count) * count: 生成数量(图标驱动时 = 每个图标的套数) * @returns {Array} 方案数组 */ function generatePlans(opt) { const ind = opt.industry || HGB_Industries[0]; const brand = (opt.brand || '').trim() || sampleName(ind); const sub = (opt.sub || '').trim(); const count = Math.max(1, Math.min(20, opt.count || 6)); // 风格池:用户指定 > 行业推荐 > 全局 const stylePool = stylePoolFor(ind, opt.styleIds); // 风格顺序洗牌:默认 5 套尽量覆盖不同风格,避免"同风格扎堆" const styleOrder = shuffle(stylePool.slice()); // 布局白名单(可选):限定 Mark 布局范围(头像模式排除带文字的 monogram/ribbon) const layoutWhitelist = (opt.layoutWhitelist || []).filter(id => LAYOUTS.some(l => l.id === id)); // 每风格的倾向布局列表(预洗牌,保证每次生成构图有变化) const styleLayouts = {}; for (const s of stylePool) { let favs = (s.layouts || []).filter(id => LAYOUTS.some(l => l.id === id)); if (layoutWhitelist.length) favs = favs.filter(id => layoutWhitelist.includes(id)); // LOGO 模式(无白名单)排除头像专有布局 else favs = favs.filter(id => !(LAYOUTS.find(l => l.id === id) || {}).avatarOnly); styleLayouts[s.id] = shuffle(favs); } const fontFavs = ind.fontFamilies || []; // 图标驱动模式:每个选中图标固定作为核心图形,各产出 count 套(icon1=icon2=该图标) const lockedIcons = opt.iconLocked ? (opt.icons || []).filter(Boolean).slice(0, 5) : []; if (lockedIcons.length) { const plans = []; for (const locked of lockedIcons) { for (let j = 0; j < count; j++) { const i = plans.length; const style = styleOrder[i % styleOrder.length]; // 配色始终跟随风格色系(消除"极简风配行业蓝"式冲淡) const palette = { name: style.name, colors: [style.colors.primary, style.colors.secondary, style.colors.accent, style.colors.bg] }; // 布局:有白名单时按白名单全集轮换;否则按风格倾向优先轮换 const favs = styleLayouts[style.id]; const layout = layoutWhitelist.length ? layoutFallback(i, layoutWhitelist) : (favs.length ? (LAYOUTS.find(l => l.id === favs[i % favs.length]) || layoutFallback(i, layoutWhitelist)) : layoutFallback(i, layoutWhitelist)); const plan = composePlan({ brand, sub, icon1: locked, icon2: locked, fontFavs }, layout, palette, style); // 记录锁定图标:变体微调(换配色/换布局)时保持同一图标 plan.lockedIcon = { id: locked.id, name: locked.name, svg: locked.svg, source: locked.source || '' }; plans.push(plan); } } return plans; } const { iconPool } = pickIcons(opt, ind); const plans = []; for (let i = 0; i < count; i++) { // 风格优先:每套方案由风格决定配色/背景/线宽/字体/构图 const style = styleOrder[i % styleOrder.length]; // 配色始终跟随风格色系(消除"极简风配行业蓝"式冲淡) const palette = { name: style.name, colors: [style.colors.primary, style.colors.secondary, style.colors.accent, style.colors.bg] }; // 布局:有白名单时按白名单全集轮换(保证头像专有布局被覆盖); // 否则按风格倾向优先轮换,无倾向则从全局轮换 const favs = styleLayouts[style.id]; const layout = layoutWhitelist.length ? layoutFallback(i, layoutWhitelist) : (favs.length ? (LAYOUTS.find(l => l.id === favs[i % favs.length]) || layoutFallback(i, layoutWhitelist)) : layoutFallback(i, layoutWhitelist)); // 图标轮换:每套方案使用不同图标,避免"全套一个图标"的雷同感 const a = iconPool[i % iconPool.length]; const b = iconPool[(i + 1) % iconPool.length]; plans.push(composePlan({ brand, sub, icon1: a, icon2: b, fontFavs }, layout, palette, style)); } return plans; } /** * 生成单个变体(预览弹窗内微调用) * @param {object} opt 与 generatePlans 相同,额外支持: * layoutId: 指定 Mark 模板 id(不传则取风格倾向布局) * styleId: 指定风格 id(不传则行业优先) * 说明:配色不再独立传入,始终跟随 styleId 对应风格的色系(风格自包含) * @returns {object} 单个方案 */ function generateVariant(opt) { const ind = opt.industry || HGB_Industries[0]; const brand = (opt.brand || '').trim() || sampleName(ind); const sub = (opt.sub || '').trim(); const { icon1, icon2 } = pickIcons(opt, ind); // 字体偏好:AI 决策优先,其次行业推荐 const fontFavs = (opt.fontFavs && opt.fontFavs.length) ? opt.fontFavs : (ind.fontFamilies || []); // 风格池:用户指定 > 行业推荐 > 全局 const stylePool = stylePoolFor(ind, opt.styleIds); // 目标风格:指定或行业优先 const style = opt.styleId ? (stylePool.find(s => s.id === opt.styleId) || stylePool[0]) : stylePool[0]; // 配色始终跟随风格色系(风格自包含:颜色/背景/字重/线宽随风格整体切换) const palette = { name: style.name, colors: [style.colors.primary, style.colors.secondary, style.colors.accent, style.colors.bg] }; // 目标布局:指定且合法,否则取风格倾向布局(受白名单约束) const layoutWhitelist = (opt.layoutWhitelist || []).filter(id => LAYOUTS.some(l => l.id === id)); const layout = opt.layoutId ? (LAYOUTS.find(l => l.id === opt.layoutId) || layoutFallback(0, layoutWhitelist)) : (favLayout(style, layoutWhitelist) || layoutFallback(0, layoutWhitelist)); const plan = composePlan({ brand, sub, icon1, icon2, fontFavs }, layout, palette, style); // 应用指定 Lockup 位置(默认 elements 为第一个位置) if (opt.lockup) { const hit = plan.lockups.find(lp => lp.pos === opt.lockup); if (hit) plan.elements = hit.elements; } return plan; } /** 行业默认示例品牌名 */ function sampleName(ind) { const map = { edu: 'EDUCATION', food: '美食工坊', health: '仁心医疗', tech: 'NEXUS', sport: '锐动体育', finance: '恒信资本' }; return map[ind.id] || ind.name || '我的品牌'; } /** 洗牌 */ function shuffle(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } return { generatePlans, generateVariant, sampleName, defaultLayout, isShort, LAYOUTS, LOCKUP_POS, CW, CH }; })();