feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
/**
* 画个标 · 应用入口(纯智能设计模式)
*/
(function () {
'use strict';
function boot() {
// 初始化 Store(恢复本地缓存的项目名等)
HGB_Store.init(true);
// 初始化智能设计工作台 UI
HGB_UI.init();
// 顶栏项目名初始同步
const nameInput = document.getElementById('project-name');
if (nameInput && HGB_Store.get().name) {
nameInput.value = HGB_Store.get().name;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();
+114
View File
@@ -0,0 +1,114 @@
/**
* 画个标 · AI 客户端:DeepSeek API 接入
* 职责:API Key 管理(localStorage)、对话调用、错误分类、连接测试
*/
const HGB_AI = (function () {
'use strict';
const LS_KEY = 'hgb_ai_key';
const LS_MODEL = 'hgb_ai_model';
const ENDPOINT = 'https://api.deepseek.com/chat/completions';
const MODELS = ['deepseek-v4-flash', 'deepseek-chat', 'deepseek-v4-pro'];
/**
* 不内置任何 Key:Key 仅由使用者在「AI 设置」中填写,存本机 localStorage,只发给 DeepSeek。
* 因此源码、仓库与部署产物里都不含 Key,公网也无法被他人读取(BYOK 模式)。
*/
const DEFAULT_KEY = '';
/* ---------- Key 管理 ---------- */
function getKey() {
try {
const v = localStorage.getItem(LS_KEY);
// 未配置时返回空串,由调用方提示「未配置 API Key」并降级为内置引擎
if (v) return v;
return DEFAULT_KEY;
} catch (e) { return ''; }
}
function setKey(key) {
try { localStorage.setItem(LS_KEY, (key || '').trim()); } catch (e) { /* ignore */ }
}
function hasKey() { return !!getKey(); }
function getModel() {
try { return localStorage.getItem(LS_MODEL) || 'deepseek-v4-flash'; } catch (e) { return 'deepseek-v4-flash'; }
}
function setModel(m) { try { localStorage.setItem(LS_MODEL, m); } catch (e) { /* ignore */ } }
function clearKey() {
// 清除本机保存的 Key(已无内置 Key 可回落,清除后即降级为内置引擎)
try { localStorage.removeItem(LS_KEY); } catch (e) { /* ignore */ }
}
/* ---------- 调用 ---------- */
/**
* 调用 DeepSeek 对话补全
* @param {Array} messages [{role:'system'|'user', content}]
* @param {object} opts { json?: boolean, maxTokens?: number, temperature?: number }
* @returns {Promise<string>} 返回内容文本
*/
async function chat(messages, opts) {
const key = getKey();
if (!key) {
const err = new Error('未配置 API Key,请先在「AI 设置」中填写');
err.code = 'NO_KEY';
throw err;
}
opts = opts || {};
const body = {
model: getModel(),
messages,
max_tokens: opts.maxTokens || 4096,
temperature: opts.temperature != null ? opts.temperature : 0.5,
stream: false
};
if (opts.json) {
body.response_format = { type: 'json_object' };
}
let res;
try {
res = await fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + key
},
body: JSON.stringify(body)
});
} catch (e) {
const err = new Error('网络请求失败:' + e.message + '(请检查网络/代理)');
err.code = 'NETWORK';
throw err;
}
if (!res.ok) {
let detail = '';
try { detail = (await res.json()).error?.message || ''; } catch (e) { /* ignore */ }
const err = new Error(detail || 'HTTP ' + res.status);
if (res.status === 401) err.code = 'AUTH';
else if (res.status === 402) err.code = 'BALANCE';
else if (res.status === 429) err.code = 'RATE_LIMIT';
else err.code = 'API';
err.status = res.status;
throw err;
}
const data = await res.json();
const content = data.choices?.[0]?.message?.content;
if (!content) {
const err = new Error('AI 返回内容为空');
err.code = 'EMPTY';
throw err;
}
return content;
}
/** 测试连接:发送最小请求验证 Key 是否有效 */
async function testConnection() {
const content = await chat([
{ role: 'user', content: '回复两个字:正常' }
], { maxTokens: 16, temperature: 0 });
return content;
}
return { getKey, setKey, hasKey, getModel, setModel, clearKey, chat, testConnection, MODELS };
})();
+387
View File
@@ -0,0 +1,387 @@
/**
* 画个标 · 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 };
})();
+151
View File
@@ -0,0 +1,151 @@
/**
* 画个标 · 核心:头像生成(Avatar)
* 头像 = 复用 Mark 纯图形引擎 + 头像专属约束 + 平台规格导出
* - 仅纯图形:过滤带文字布局(monogram 首字 / ribbon 绶带区),保证小尺寸清晰
* - 圆形构图优先:头像布局以圆形/圆角/对称构图为倾向,缩小不变形
* - 圆形裁切预览与导出:社交平台头像均以圆形展示
* - 平台规格一键导出:微信 / QQ / 抖音 / YouTube / App 图标 / Favicon + 自定义
*/
const HGB_AvatarGen = (function () {
'use strict';
/** 适合头像的纯图形布局(Mark 层,全部无文字) */
const AVATAR_LAYOUTS = [
'orb', // 圆标容器(径向渐变圆)
'badge', // 双环徽章
'seal', // 双环印章
'burst', // 光芒徽章(射线环)
'duo', // 双图标对称
'emblem', // 渐变盾徽
'hexagon', // 六边形徽章
'diamond', // 菱形徽章
'arch', // 拱门徽章
'geo', // 几何构形
'techcorner', // 科技角标
'fullbleed', // 头像专属:全出血大图标(无容器)
'concentric', // 头像专属:同心圆环
'centroid' // 头像专属:中心单环
];
/** 平台头像导出规格预设 */
const AVATAR_SIZES = [
{ id: 'wechat', name: '微信头像', size: 300, note: '微信/微博 300' },
{ id: 'qq', name: 'QQ 头像', size: 640, note: 'QQ 640' },
{ id: 'douyin', name: '抖音头像', size: 400, note: '抖音/快手 400' },
{ id: 'youtube', name: 'YouTube 头像', size: 800, note: '频道头像 800' },
{ id: 'app', name: 'App 图标', size: 1024, note: 'iOS/安卓 1024' },
{ id: 'favicon', name: '网站 Favicon', size: 64, note: '浏览器标签 64' }
];
/** 头像词根扩充的主力单色源(懒加载优先预载;多色 emoji/品牌源不参与词根搜索) */
const AVATAR_CORE_SOURCES = [
'lucide', 'tabler', 'feather', 'mdi', 'fluent', 'solar', 'msymbols',
'iconpark', 'hugeicons', 'mingcute', 'carbon', 'majesticons', 'pixelarticons', 'akar-icons'
];
/** 布局 id 是否适合头像(在头像白名单内) */
function isAvatarLayout(layoutId) {
return AVATAR_LAYOUTS.includes(layoutId);
}
/** 洗牌(保证多套头像图标各异) */
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;
}
/** 内置库关键词搜索:图标名包含任一英文词根即命中(返回带库前缀 id 的图标数组;懒加载适配:先预载核心源再搜已加载源) */
async function searchBuiltin(roots, limit) {
// 词根搜索依赖已加载源 → 预载头像核心源(容忍单个失败)
await HGB_IconLib.preload(AVATAR_CORE_SOURCES);
const out = [];
const seen = new Set();
const kws = roots.filter(k => k.length >= 3);
if (!kws.length) return out;
for (const kw of kws) {
for (const ic of HGB_IconLib.searchLoaded(kw, limit)) {
const fullId = ic.id;
if (seen.has(fullId)) continue;
seen.add(fullId);
out.push({ id: fullId, name: ic.name, svg: ic.svg, source: ic.source, type: ic.type });
if (out.length >= limit) return out;
}
}
return out;
}
/** 头像素材池:用户选中 + 行业图标优先,内置库按行业图标词根扩充(解决"素材不够/重复") */
async function avatarIconPool(ind, userIcons) {
const picked = (userIcons || []).filter(Boolean).slice(0, 5);
const indIcons = (ind.icons || []);
const poolMap = new Map();
// 本地段:用户选中 + 行业图标(保持插入顺序)
for (const ic of picked) poolMap.set(ic.id, ic);
for (const ic of indIcons) if (!poolMap.has(ic.id)) poolMap.set(ic.id, ic);
const localCount = poolMap.size;
// 词根:行业图标 id 按 '-' 拆分(英文语义词根,如 chef-hat → chef/hat)
const roots = [];
for (const ic of indIcons) {
for (const part of String(ic.id || '').split('-')) {
const k = part.toLowerCase();
if (k.length >= 3 && !roots.includes(k)) roots.push(k);
}
}
if (roots.length) {
for (const ic of await searchBuiltin(roots, 60)) {
if (!poolMap.has(ic.id)) poolMap.set(ic.id, ic);
}
}
const all = Array.from(poolMap.values());
// 分段洗牌:行业/用户图标在前(高度相关),内置库扩充在后;各自洗牌保证多套各异
return shuffle(all.slice(0, localCount)).concat(shuffle(all.slice(localCount)));
}
/** 批量生成头像(复用 Mark 生成引擎 + 头像布局白名单 + 内置库素材扩充) */
async function generateAvatars(opt) {
const ind = opt.industry || {};
const count = Math.min(Math.max(opt.count || 5, 1), 20);
// 1) 基础批量生成(风格全覆盖 + 头像布局白名单)
const base = HGB_Generator.generatePlans({ ...opt, count, layoutWhitelist: AVATAR_LAYOUTS });
// 2) 头像素材池扩充(行业/用户图标在前 + 内置库词根扩充;懒加载预载核心源)
const pool = await avatarIconPool(ind, opt.icons);
if (pool.length < 2) return base;
// 3) 逐套替换为池中不同图标(保持风格/布局/配色不变,图标素材多样)
return base.map((p, i) => {
const ic1 = pool[i % pool.length];
const ic2 = pool[(i + Math.ceil(pool.length / 2)) % pool.length];
if (!ic1) return p;
const v = HGB_Generator.generateVariant({
...opt,
layoutWhitelist: AVATAR_LAYOUTS,
layoutId: p.layoutId,
styleId: p.styleId,
icons: ic2 && ic2.id !== ic1.id ? [ic1, ic2] : [ic1],
lockup: p.lockup
});
return Object.assign({}, v, {
id: p.id, name: p.name, concept: p.concept, aiIcon: ic1.id
});
});
}
/** 生成单个头像变体(预览弹窗微调) */
function generateVariant(opt) {
return HGB_Generator.generateVariant({
...opt,
layoutWhitelist: AVATAR_LAYOUTS
});
}
return {
AVATAR_LAYOUTS,
AVATAR_SIZES,
isAvatarLayout,
generateAvatars,
generateVariant
};
})();
+284
View File
@@ -0,0 +1,284 @@
/**
* 画个标 · DiceBear 头像风格引擎(M4)
* 29 种 DiceBear v10 风格定义内置(js/data/sources/dicebear-*.js,由构建脚本生成),
* 本模块:按"风格 × seed"确定性组合出无限头像 SVG(不占图标配额)。
*
* 风格定义结构(v10 单文件 JSON):
* canvas 画布模板(elements 树,component 节点按 name 引用 components)
* components { name: { width, height, rotate/scale/translate 随机范围, variants: { 名: { elements, weight } } } }
* colors { 色名: { values: [...] } }(element 属性 fill/stroke 以 {type:'color',name} 引用)
* attributes 根 svg 属性(fill、shape-rendering 等)
*
* 渲染:seed → 确定性 PRNG → 每 component 加权选 variant + 随机变换 → color 引用取色 → 组装 SVG
*/
const HGB_Dicebear = (function () {
'use strict';
const VERSION = '20260822e';
const STYLES = [
'adventurer', 'adventurer-neutral', 'avataaars', 'avataaars-neutral',
'big-ears', 'big-ears-neutral', 'big-smile', 'blobs', 'bottts', 'bottts-neutral',
'clay', 'constellation', 'critters', 'croodles', 'croodles-neutral',
'cutouts', 'disco', 'dylan', 'fun-emoji', 'glass', 'glyphs', 'icons',
'identicon', 'initial-face', 'initials', 'landscape', 'line-face', 'loops',
'lorelei', 'lorelei-neutral', 'micah', 'miniavs', 'moods', 'notionists',
'notionists-neutral', 'open-peeps', 'patchwork', 'personas', 'pixel-art',
'pixel-art-neutral', 'pixelbot', 'planets', 'rings', 'shape-grid', 'shapes',
'sprouts', 'squircles', 'stripes', 'thumbs', 'toon-head', 'triangles',
'voxel-art', 'voxel-bot', 'waves', 'weave'
];
const CN = {
adventurer: '冒险家', 'adventurer-neutral': '冒险家中性', avataaars: '阿凡达',
'avataaars-neutral': '阿凡达中性', 'big-ears': '大耳朵', 'big-ears-neutral': '大耳朵中性',
'big-smile': '大笑脸', blobs: '斑点', bottts: '机器人', 'bottts-neutral': '机器人中性',
clay: '黏土', constellation: '星座', critters: '小动物',
croodles: '卡通线条', 'croodles-neutral': '卡通线条中性', cutouts: '剪贴画',
disco: '迪斯科', dylan: '迪伦', 'fun-emoji': '趣味表情', glass: '玻璃拟态',
glyphs: '字形符', icons: '图标风', identicon: '指纹几何', 'initial-face': '首字脸',
initials: '首字母', landscape: '风景', 'line-face': '线条脸', loops: '回环',
lorelei: '洛雷莱', 'lorelei-neutral': '洛雷莱中性', micah: '米迦',
miniavs: '迷你头像', moods: '心情', notionists: 'Notion 风格', 'notionists-neutral': 'Notion 中性',
'open-peeps': '开放小人', patchwork: '拼布', personas: '人物剪影', 'pixel-art': '像素风',
'pixel-art-neutral': '像素风中性', pixelbot: '像素机器人', planets: '星球',
rings: '圆环几何', 'shape-grid': '形状网格', shapes: '几何形状',
sprouts: '萌芽', squircles: '圆角方', stripes: '条纹', thumbs: '拇指人',
'toon-head': '卡通头', triangles: '三角形', 'voxel-art': '体素艺术',
'voxel-bot': '体素机器人', waves: '波浪', weave: '编织'
};
/* ---------- 确定性 PRNG(seed 字符串 → mulberry32) ---------- */
function hash(str) {
let h = 2166136261;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
function rng(seedStr) {
let s = hash(seedStr) || 1;
return function () {
s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function pick(r, arr) {
return arr[Math.floor(r() * arr.length)];
}
function weightedPick(r, items) {
let total = 0;
for (const i of items) total += (i.weight != null ? i.weight : 0.01);
if (total <= 0) return items[0].key;
let x = r() * total;
for (const i of items) {
x -= (i.weight != null ? i.weight : 0.01);
if (x <= 0) return i.key;
}
return items[items.length - 1].key;
}
function rn(v) { return Math.round(v * 100) / 100; }
/** 变量引用解析(initials 风格:initial/fontFamily/fontWeight 等) */
function resolveVariable(name, seed, opts) {
switch (name) {
case 'initial':
case 'initials': {
const s = String(opts.initial || seed || '').trim();
if (s) return s[0].toUpperCase();
return opts.defaultInitial || 'D';
}
case 'fontFamily': return opts.fontFamily || 'Arial';
case 'fontWeight': return String(opts.fontWeight != null ? opts.fontWeight : 600);
default: return '';
}
}
/** 属性值:color 引用 → 从色板取色;variable 引用 → 变量解析;其他原样 */
function resolveAttr(v, r, colors, opts) {
if (v && typeof v === 'object') {
if (v.type === 'color' && v.name) {
const c = colors[v.name];
if (typeof c === 'string') return c; // 已解析的单一颜色
if (c && Array.isArray(c.values) && c.values.length) return pick(r, c.values);
return '#000000';
}
if (v.type === 'variable' && v.name) {
return resolveVariable(v.name, opts.seed, opts);
}
return '';
}
return v;
}
/* ---------- 递归渲染 element / component / text 节点 ---------- */
function renderComponent(comp, r, colors, components, opts) {
const names = Object.keys(comp.variants || {});
if (!names.length) return '';
const items = names.map(n => ({ key: n, weight: comp.variants[n].weight }));
const variant = comp.variants[weightedPick(r, items)] || {};
// 随机变换(rotate/scale/translate 范围;个别风格 translate 只有 x 或 y,缺字段视为 0)
const t = [];
if (comp.translate) {
const tx = comp.translate.x;
const ty = comp.translate.y;
const x = tx ? tx.min + r() * (tx.max - tx.min) : 0;
const y = ty ? ty.min + r() * (ty.max - ty.min) : 0;
if (x || y) t.push('translate(' + rn(x) + ' ' + rn(y) + ')');
}
if (comp.rotate) {
const deg = comp.rotate.min + r() * (comp.rotate.max - comp.rotate.min);
if (deg) t.push('rotate(' + rn(deg) + ')');
}
if (comp.scale) {
const s = comp.scale.min + r() * (comp.scale.max - comp.scale.min);
if (s !== 1) t.push('scale(' + rn(s) + ')');
}
const inner = (variant.elements || []).map(e => renderNode(e, r, colors, components, opts)).join('');
return t.length ? '<g transform="' + t.join(' ') + '">' + inner + '</g>' : inner;
}
/**
* 解析组件继承(DiceBear schema 的 extends 字段):
* - identicon 的 row01~row05 → row;glass 的 shape01/shape02 → shape
* 继承语义 = 复用父组件定义(无自身 variants 时完全继承;带环保护)
*/
function resolveComp(name, components) {
const seen = new Set();
let cur = name;
while (cur && !seen.has(cur)) {
seen.add(cur);
const c = (components || {})[cur];
if (!c) return (components || {})[name] || null;
if (!c.extends) return c;
if (c.variants && Object.keys(c.variants).length) return c;
cur = c.extends;
}
return (components || {})[name] || null;
}
/**
* 统一渲染节点:支持三种类型
* - component 节点(任意层级出现,如 shapes 的 <g><component/></g>)→ 渲染其变体
* - text 节点 {type:'text', value} → 渲染为纯文本(initials 风格文字,value 可为变量引用)
* - element 节点 → 拼属性 + 递归 children
* components 仅在 canvas 层传入(renderComponent 内部复用自身 variants 而非 components 表)
*/
function renderNode(el, r, colors, components, opts) {
if (!el) return '';
if (el.type === 'component') {
const comp = resolveComp(el.name, components);
if (!comp) return '';
const inner = renderComponent(comp, r, colors, components, opts);
// component 节点自带 transform 等属性(如 shapes 的 matrix 定位)
const attrs = [];
for (const k of Object.keys(el.attributes || {})) {
const v = el.attributes[k];
if (v == null) continue;
attrs.push(k + '="' + String(v).replace(/"/g, '&quot;') + '"');
}
return '<g' + (attrs.length ? ' ' + attrs.join(' ') : '') + '>' + inner + '</g>';
}
if (el.type === 'text') {
const v = resolveAttr(el.value, r, colors, opts);
return String(v != null ? v : '');
}
// element:拼属性(color 引用取色,数字取整)
const attrs = [];
for (const k of Object.keys(el.attributes || {})) {
const v = resolveAttr(el.attributes[k], r, colors, opts);
if (v === '' || v == null) continue;
const sv = typeof v === 'number' ? rn(v) : String(v);
attrs.push(k + '="' + sv.replace(/"/g, '&quot;') + '"');
}
const children = (el.children || []).map(c => renderNode(c, r, colors, components, opts)).join('');
const name = (el.name || 'g').replace(/[^a-zA-Z0-9-]/g, '') || 'g';
return '<' + name + (attrs.length ? ' ' + attrs.join(' ') : '') +
(children ? '>' + children + '</' + name + '>' : '/>');
}
/* ---------- 画布组装 ---------- */
function renderCanvas(canvas, r, colors, components, opts) {
return (canvas.elements || []).map(n => renderNode(n, r, colors, components, opts)).join('');
}
/** 画布是否已含显式 background 色元素(避免重复垫底) */
function hasBgElement(canvas) {
function scan(el) {
if (!el || typeof el !== 'object') return false;
const a = el.attributes || {};
for (const k of Object.keys(a)) {
const v = a[k];
if (v && typeof v === 'object' && v.type === 'color' && v.name === 'background') return true;
if (v === 'background') return true;
}
return (el.children || []).some(scan);
}
return (canvas && canvas.elements || []).some(scan);
}
/**
* 渲染单个风格头像
* @param {string} styleId 风格 id(如 adventurer / pixel-art)
* @param {string} seed 确定性种子(同 seed 同输出)
* @returns {{svg:string, styleId:string}} svg 为完整 <svg> 文档
*/
function renderAvatar(styleId, seed, opts) {
const def = window['HGB_Dicebear_' + styleId.replace(/-/g, '_')];
if (!def) return { svg: '', styleId };
opts = opts || {};
const r = rng(String(seed || 'dicebear'));
const colors = def.colors || {};
// 每色名先定一个颜色(保持跨 component 一致)
const colorMap = {};
for (const name of Object.keys(colors)) {
colorMap[name] = pick(r, colors[name].values || ['#000000']);
}
let inner = renderCanvas(def.canvas, r, colorMap, def.components || {}, { seed, ...opts });
const w = (def.canvas && def.canvas.width) || 100;
const h = (def.canvas && def.canvas.height) || 100;
// 背景填充:dicebear 官方渲染器默认按 backgroundColor 垫底。neutral 系列(pixel-art-neutral 等)
// 脸部肤色依赖此背景,画布无显式背景元素且色板含 background 时先垫同尺寸 rect;
// 无 background 色板的风格(adventurer/pixel-art 等)零影响
if (colorMap.background && !hasBgElement(def.canvas)) {
inner = '<rect width="' + w + '" height="' + h + '" fill="' + colorMap.background + '"/>' + inner;
}
const rootAttrs = [];
for (const k of Object.keys(def.attributes || {})) {
rootAttrs.push(k + '="' + String(def.attributes[k]).replace(/"/g, '&quot;') + '"');
}
const svg = '<?xml version="1.0" encoding="UTF-8"?>\n' +
'<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h +
'" viewBox="0 0 ' + w + ' ' + h + '"' +
(rootAttrs.length ? ' ' + rootAttrs.join(' ') : '') + '>' + inner + '</svg>';
return { svg, styleId };
}
/** 懒加载单个风格定义(缓存 + 并发去重) */
const loaded = {};
const pending = {};
function loadStyle(styleId) {
if (loaded[styleId]) return Promise.resolve(loaded[styleId]);
if (pending[styleId]) return pending[styleId];
pending[styleId] = new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = 'js/data/sources/dicebear-' + styleId + '.js?v=' + VERSION;
s.onload = () => {
const def = window['HGB_Dicebear_' + styleId.replace(/-/g, '_')];
if (!def) { reject(new Error('风格定义缺失: ' + styleId)); return; }
loaded[styleId] = def;
delete pending[styleId];
resolve(def);
};
s.onerror = () => { delete pending[styleId]; reject(new Error('风格加载失败: ' + styleId)); };
document.head.appendChild(s);
});
return pending[styleId];
}
/** 加载全部风格(预载) */
function loadAll() {
return Promise.allSettled(STYLES.map(loadStyle));
}
return { VERSION, STYLES, CN, renderAvatar, loadStyle, loadAll };
})();
+152
View File
@@ -0,0 +1,152 @@
/**
* 画个标 · 核心:表情设计师 AI 编排(Emoji AI)
* AI 输出结构化设计决策 JSON(布局/风格/动画/分层)→ 系统负责渲染与导出;
* AI 失败/无 Key 时返回 null,由 emojiGen 降级为内置随机池(页面不空白)。
*
* 设计要点:
* - 与 aiDesigner 同模式:buildPrompt → HGB_AI.chat(json) → extractJson → validate → 决策
* - validate 把所有输出修正到合法白名单(布局/风格/动画类型/参数),非法即回退
* - 纯函数 + 环境分级容错(Node 无头无 HGB_AI 时返回 null,不算失败)
*/
const HGB_EmojiAI = (function () {
'use strict';
/* ============ Prompt 编排 ============ */
function layoutList() {
if (typeof HGB_EmojiGen !== 'undefined') return HGB_EmojiGen.EMOJI_LAYOUTS.join('/');
return 'orb/badge/seal/burst/duo/emblem/hexagon/diamond/arch/geo/techcorner/fullbleed/concentric/centroid';
}
function buildPrompt(opt) {
const ind = opt.industry || {};
const iconDesc = (opt.icons && opt.icons.length)
? opt.icons.map(i => i.name || i.id).join('、')
: '(未指定,由 AI 建议行业意象)';
const mood = opt.mood ? '用户期望情绪:' + opt.mood + ';' : '';
// 情绪-动画映射(生成前置,先审后生成:用户给出期望情绪时优先匹配,避免动效与情绪割裂)
const moodMap = opt.mood
? '情绪-动画映射参考(用户给出期望情绪时优先匹配,可适度发挥但勿选明显冲突类型):' +
'开心→bounce、生气→shake、惊讶→heartbeat、难过→float、兴奋→hopspin、平静→breath。'
: '';
const sys = [
'你是「画个标」的动态表情设计师。用户给出行业与图标素材,你输出「一张静态底图 + 一个主动画」的结构化设计决策。',
'铁律:只输出 JSON,不要任何解释文字。',
'字段与取值:',
'- overallConcept:一句话设计概念(中文,10-20 字)',
'- base.layoutId:从白名单选一个:' + layoutList(),
'- base.styleId:从系统风格库选一个(如 minimal/neon/flat/artdeco/corporate 等,按行业气质匹配)',
'- anim.type:从动画库选一个:bounce/pulse/wiggle/float/spin/fade/twirl/heartbeat/shake/breath/hopspin',
'- anim.speed:0.6-1.5(周期数,生气/兴奋偏快,平静偏慢)',
'- anim.amplitude:0.7-1.4(强度,活泼可大,克制宜小)',
'- anim.layered:true/false(是否元素分层错峰动画,让表情更活;多元素图形建议 true)',
'- anim.concept:动效概念一句话(中文)',
moodMap,
'示例:{"overallConcept":"咖啡的松弛感","base":{"layoutId":"seal","styleId":"neon"},"anim":{"type":"float","speed":0.8,"amplitude":0.9,"layered":true,"concept":"蒸汽缓缓漂浮"}}'
].join('\n');
const user = [
'行业:' + (ind.name || '通用') + '(' + (ind.emoji || '') + ',' + (ind.desc || '') + ')',
'品牌/主题:' + (opt.brand || '动态表情'),
'用户图标素材:' + iconDesc,
mood,
'请输出严格 JSON(不要 markdown 代码块)。'
].filter(Boolean).join('\n');
return [
{ role: 'system', content: sys },
{ role: 'user', content: user }
];
}
/* ============ 解析与校验 ============ */
/** 从聊天文本提取 JSON 对象(取第一个完整 {} 块,容忍前后缀文字) */
function extractJson(text) {
if (!text || typeof text !== 'string') return null;
const start = text.indexOf('{');
if (start < 0) return null;
let depth = 0, inStr = false, esc = false;
for (let i = start; i < text.length; i++) {
const c = text[i];
if (inStr) {
if (esc) esc = false;
else if (c === '\\') esc = true;
else if (c === '"') inStr = false;
} else if (c === '"') inStr = true;
else if (c === '{') depth++;
else if (c === '}') {
depth--;
if (depth === 0) {
try { return JSON.parse(text.slice(start, i + 1)); } catch (e) { return null; }
}
}
}
return null;
}
function inList(v, list) { return list.indexOf(v) >= 0; }
/** 修正 AI 输出到合法白名单(非法项回退合理默认,返回完整决策对象) */
function validate(d, opt) {
const ind = opt.industry || {};
const dec = d || {};
const base = dec.base || {};
const anim = dec.anim || {};
const layouts = (typeof HGB_EmojiGen !== 'undefined') ? HGB_EmojiGen.EMOJI_LAYOUTS : [];
const animTypes = (typeof HGB_Motion !== 'undefined') ? HGB_Motion.animTypes() : [];
const styleIds = (typeof HGB_Styles !== 'undefined') ? HGB_Styles.map(s => s.id) : [];
let layoutId = inList(base.layoutId, layouts) ? base.layoutId : null;
if (!layoutId && ind.favLayout && inList(ind.favLayout, layouts)) layoutId = ind.favLayout;
if (!layoutId) layoutId = layouts[0] || 'orb';
const styleId = inList(base.styleId, styleIds) ? base.styleId : (opt.styleIds && opt.styleIds[0]);
const animType = inList(anim.type, animTypes.map(t => t.id)) ? anim.type : null;
let aType = animType || 'bounce';
const clamp = (v, lo, hi, dft) => {
const n = Number(v);
return (isFinite(n) && n >= lo && n <= hi) ? n : dft;
};
return {
overallConcept: String(dec.overallConcept || '').trim().slice(0, 30) || '动态表情包',
base: { layoutId, styleId },
anim: {
type: aType,
speed: clamp(anim.speed, 0.6, 1.5, 1),
amplitude: clamp(anim.amplitude, 0.7, 1.4, 1),
layered: !!anim.layered,
concept: String(anim.concept || '').trim().slice(0, 30)
}
};
}
/* ============ 主入口 ============ */
/**
* AI 表情设计决策(失败/无 Key/超时 → 返回 null,供生成器降级)
* @param {object} opt { industry, brand, icons, mood }
* @returns {Promise<{overallConcept, base:{layoutId,styleId}, anim:{...}}|null>}
*/
async function suggest(opt) {
if (typeof HGB_AI === 'undefined') return null;
try {
if (typeof HGB_AI.hasKey === 'function' && !HGB_AI.hasKey()) return null;
const messages = buildPrompt(opt);
const content = await HGB_AI.chat(messages, { json: true, maxTokens: 2048 });
const d = extractJson(content);
if (!d) return null;
return validate(d, opt);
} catch (e) {
return null; // AI 失败 → 生成器降级内置池
}
}
return { buildPrompt, extractJson, validate, suggest };
})();
// Node 无头冒烟自检(浏览器同样执行,幂等)
if (typeof module !== 'undefined' && module.exports) module.exports = HGB_EmojiAI;
+344
View File
@@ -0,0 +1,344 @@
/**
* 画个标 · 核心:动态表情生成与导出(Emoji/Sticker)
* 表情 = 复用 Mark 纯图形引擎(头像布局白名单)+ 动画维度(HGB_Motion)+ GIF/WebM 导出
* - 纯图形:与头像一致排除带文字布局,小尺寸清晰
* - 动画:AI 缺失时本地确定性分配(pickAnim 轮换),预览与导出同源(同一 apply)
* - 导出:GIF(HGB_GifEncoder 逐帧采样)/ WebM(canvas.captureStream + MediaRecorder)/ PNG(静态帧)
*/
const HGB_EmojiGen = (function () {
'use strict';
/** 表情可用的纯图形布局(复用头像白名单) */
const EMOJI_LAYOUTS = typeof HGB_AvatarGen !== 'undefined'
? HGB_AvatarGen.AVATAR_LAYOUTS
: ['orb', 'badge', 'burst', 'duo', 'emblem', 'fullbleed', 'concentric', 'centroid'];
/** 平台表情导出规格预设 */
const EMOJI_SIZES = [
{ id: 'wechat', name: '微信表情', size: 240, note: '微信 240' },
{ id: 'qq', name: 'QQ 表情', size: 320, note: 'QQ 320' },
{ id: 'telegram', name: 'Telegram 贴纸', size: 512, note: 'Telegram 512' }
];
/* ============ 生成 ============ */
/**
* 情绪 → 主动画决策(生成前置:AI 缺失/失败时,首张贴合用户情绪)
* 优先复用 emotion-params 技能(情绪-动效映射,与评审技能同源),未加载时走内置映射兜底
* @param {string} mood 情绪 id(happy/angry/surprised/sad/excited/calm)
* @returns {object|null} {type,speed,amplitude,layered} 或 null(无情绪)
*/
function moodAnim(mood) {
if (!mood) return null;
if (typeof HGB_StickerSkills !== 'undefined') {
try {
const r = HGB_StickerSkills.run('emotion-params', { mood });
if (r && !r.error && r.type && HGB_Motion.animTypes().some(t => t.id === r.type)) {
return { type: r.type, speed: r.speed || 1, amplitude: r.amplitude || 1, layered: true };
}
} catch (e) { /* 技能异常 → 内置映射兜底 */ }
}
const M = {
happy: { type: 'bounce', speed: 1.2, amplitude: 1, layered: true },
angry: { type: 'shake', speed: 1.4, amplitude: 1.2, layered: true },
surprised: { type: 'heartbeat', speed: 1.2, amplitude: 1.1, layered: true },
sad: { type: 'float', speed: 0.8, amplitude: 0.8, layered: true },
excited: { type: 'hopspin', speed: 1.3, amplitude: 1.1, layered: true },
calm: { type: 'breath', speed: 0.9, amplitude: 0.8, layered: true }
};
return M[String(mood).toLowerCase()] || null;
}
/**
* 基于一张静态底图展开为「同图 × 全部动画类型」的一组动画表情
* @param {object} base 静态方案(含 elements/markElements/layoutId 等)
* @param {object} opts 可选 { varParams: 是否微调各动画参数(幅度/速度)增加层次 }
* @returns {Array} 变体数组,同图、每项 anim 为不同动画类型
*/
function expandAnimations(base, opts) {
let types = HGB_Motion.animTypes();
// AI 优先:把 AI 推荐动画类型排到首位(首张即 AI 决策动效)
if (opts && opts.preferType && types.some(t => t.id === opts.preferType)) {
types = [types.find(t => t.id === opts.preferType), ...types.filter(t => t.id !== opts.preferType)];
}
return types.map((t, i) => {
const fp = (i === 0 && opts && opts.firstParams) ? opts.firstParams : {};
return Object.assign({}, base, {
id: base.id + '-' + t.id,
anim: HGB_Motion.normalize({
type: t.id,
speed: (opts && opts.varParams) ? [0.9, 1, 1.1][i % 3] : (fp.speed || 1),
amplitude: (opts && opts.varParams) ? (0.9 + (i % 2) * 0.2) : (fp.amplitude || 1),
// 交错开启元素级分层:一半变体"各元素错峰动",表情更活;AI 指定时首张遵循 AI 决策
layered: (opts && opts.layered === false) ? false : ((fp.layered != null) ? fp.layered : (i % 2 === 1)),
layerSpread: 0.3
})
});
});
}
/**
* 生成一组动态表情(核心入口):AI 编排(行业/情绪 → 布局/风格/动画决策)或降级内置池
* 先生成一张静态底图 → 自动展开全部动画类型
* @param {object} opt 同 HGB_Generator.generatePlans(industry/brand/icons/mood 等)
* @returns {Promise<Array>} 同图 × 全部动画类型的变体数组
*/
async function generateEmojiSet(opt) {
let base = null;
let ai = null;
// AI 编排:输出结构化决策 JSON;失败/无 Key 静默降级内置池
if (typeof HGB_EmojiAI !== 'undefined') {
try { ai = await HGB_EmojiAI.suggest(opt); } catch (e) { ai = null; }
}
if (ai && ai.base) {
try {
const v = HGB_AvatarGen.generateVariant({
...opt,
layoutId: ai.base.layoutId,
styleId: ai.base.styleId,
anim: ai.anim
});
if (v) {
v.concept = v.concept || ai.overallConcept;
v.aiConcept = ai.anim.concept;
v.aiMood = opt.mood;
base = v;
}
} catch (e) { base = null; }
}
if (!base) {
base = (await HGB_AvatarGen.generateAvatars({ ...opt, count: 1 }))[0];
}
// 主动画决策:AI 优先;AI 缺失/失败时按情绪匹配(生成前置),再退化为确定性轮换
const moodA = moodAnim(opt && opt.mood);
base.anim = HGB_Motion.normalize(ai && ai.anim ? ai.anim : (moodA || HGB_Motion.pickAnim(0)));
base._aiDesign = !!ai;
return expandAnimations(base, Object.assign({}, opt || {}, {
preferType: (ai && ai.anim && ai.anim.type) || (moodA && moodA.type) || null,
firstParams: (ai && ai.anim) || moodA || null
}));
}
/** 单个变体(预览弹窗微调:换动画/换配色/换布局) */
function generateVariant(opt) {
const p = HGB_AvatarGen.generateVariant({ ...opt });
return Object.assign({}, p, { anim: opt.anim || HGB_Motion.pickAnim(0) });
}
/** 表情图形元素(纯图形 Mark,避免 lockup 混入文字) */
function markElements(plan) {
return (plan.markElements && plan.markElements.length) ? plan.markElements : (plan.elements || []);
}
/* ============ 帧 SVG 构建(动画烘焙,预览/导出同源) ============ */
/** 元素包围盒(纯图形:icon 元素直接用 x/y/w/h;text 忽略,表情纯图形) */
function elementBox(el) {
if (!el || el.visible === false) return null;
return { x: el.x, y: el.y, w: el.w, h: el.h };
}
/** 全部可见元素内容包围盒 */
function contentBox(elements) {
let box = null;
for (const el of elements || []) {
const b = elementBox(el);
if (!b) continue;
if (!box) box = { x: b.x, y: b.y, w: b.w, h: b.h };
else {
const x2 = Math.max(box.x + box.w, b.x + b.w);
const y2 = Math.max(box.y + box.h, b.y + b.h);
box.x = Math.min(box.x, b.x);
box.y = Math.min(box.y, b.y);
box.w = x2 - box.x;
box.h = y2 - box.y;
}
}
return box;
}
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/** 单色图标上色(字符串处理,零 DOM;多色素材 preserve 保留原样) */
function colorizeIconSvg(svg, el) {
if (el.preserve) return svg;
const fill = el.fill && el.fill !== 'none' ? el.fill : 'none';
const stroke = el.stroke && el.stroke !== 'none' ? el.stroke : 'none';
const strokeAttrs = stroke !== 'none'
? ` stroke="${esc(stroke)}" stroke-width="${esc(el.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"`
: ' stroke="none"';
return svg.replace(/<(path|circle|rect|polygon|polyline|line|ellipse)\b/g,
(m, tag) => `<${tag} fill="${esc(fill)}"${strokeAttrs}`);
}
/**
* 构建单帧 SVG 字符串(把动画帧变换烘焙进元素)
* @param {Array} elements 元素数组(plan.elements)
* @param {string} bg 背景色('transparent' 或 hex)
* @param {number} size 输出边长
* @param {number} t 归一化帧时间(0-1)
* @param {object} anim 动画参数(HGB_Motion.normalize 结果)
* @param {object} opts { trim: 内容顶满画布; pad: 留白比例 }
* @returns {string} SVG 字符串(含 XML 声明)
*/
function buildFrameSvg(elements, bg, size, t, anim, opts) {
const W = size || 240, H = size || 240;
const isTransparent = bg === 'transparent' || bg === null || bg === undefined;
// 内容包围盒裁剪 viewBox
let vx = 0, vy = 0, vw = W, vh = H;
if (opts && opts.trim !== false) {
const box = contentBox(elements);
if (box) {
const pad = (opts && opts.pad != null ? opts.pad : 0.06) * Math.max(box.w, box.h);
vx = box.x - pad; vy = box.y - pad;
vw = box.w + pad * 2; vh = box.h + pad * 2;
if (vw < 4) { vx -= 2; vw = 4; }
if (vh < 4) { vy -= 2; vh = 4; }
}
}
// 渐变 defs
let defs = '';
const grads = {};
for (const el of elements) {
if (el && el.grad && el.grad.id && !grads[el.grad.id]) grads[el.grad.id] = el.grad;
}
if (Object.keys(grads).length) {
let d = '<defs>';
for (const id of Object.keys(grads)) {
const g = grads[id];
d += (g.type === 'radial'
? `<radialGradient id="${esc(id)}" cx="0.5" cy="0.36" r="0.78"><stop offset="0%" stop-color="${esc(g.from)}"/><stop offset="100%" stop-color="${esc(g.to)}"/></radialGradient>`
: `<linearGradient id="${esc(id)}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="${esc(g.from)}"/><stop offset="100%" stop-color="${esc(g.to)}"/></linearGradient>`);
}
defs = d + '</defs>';
}
let inner = '';
if (!isTransparent) inner += `<rect x="${vx}" y="${vy}" width="${vw}" height="${vh}" fill="${esc(bg)}"/>`;
if (defs) inner += defs;
// 元素级分层:各图标按索引错峰取时间(layered 时各自动),否则全局同拍
const iconEls = elements.filter(el => el.visible && el.type === 'icon');
iconEls.forEach((el, i) => {
const tEl = HGB_Motion.elementT(t, anim, i, iconEls.length);
const frame = HGB_Motion.apply(el, tEl, anim);
const base = `translate(${el.x} ${el.y}) scale(${(el.w || 24) / 24} ${(el.h || 24) / 24})`;
const tr = HGB_Motion.composeTransform(el, frame, base);
const opacity = frame.opacity < 1 ? ` opacity="${Math.round(frame.opacity * 1000) / 1000}"` : '';
inner += `<g transform="${tr}"${opacity}>${colorizeIconSvg(el.svg || '', el)}</g>`;
});
return '<?xml version="1.0" encoding="UTF-8"?>\n' +
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${W}" height="${H}" viewBox="${vx} ${vy} ${vw} ${vh}">` +
inner + '</svg>';
}
/* ============ 导出 ============ */
/**
* 逐帧渲染 canvas(浏览器:SVG → img → canvas)
* @returns {Promise<Array<HTMLCanvasElement>>}
*/
async function renderFrames(plan, size, anim, opts) {
const a = HGB_Motion.normalize(anim);
const n = HGB_Motion.frameCount(a);
const bg = (opts && opts.bg) || '#FFFFFF';
const els = markElements(plan);
const frames = [];
for (let i = 0; i < n; i++) {
const t = HGB_Motion.frameT(i, a);
const svg = buildFrameSvg(els, bg, size, t, a, opts);
frames.push(await HGB_Exporter.renderCanvas(size, bg === 'transparent' ? null : bg, svg));
}
return frames;
}
/** 导出 GIF Blob */
async function exportGif(plan, size, anim, opts) {
const a = HGB_Motion.normalize(anim);
const frames = await renderFrames(plan, size, a, opts);
return HGB_GifEncoder.encodeBlob(frames, {
delay: 1000 / a.fps,
transparent: (opts && opts.bg) === 'transparent'
});
}
/** 导出 WebM(浏览器:canvas.captureStream + MediaRecorder) */
function exportWebm(plan, size, anim, opts) {
return new Promise((resolve, reject) => {
if (typeof MediaRecorder === 'undefined') {
reject(new Error('当前浏览器不支持 MediaRecorder(无法导出 WebM)'));
return;
}
const a = HGB_Motion.normalize(anim);
const bg = (opts && opts.bg) || '#FFFFFF';
const transparent = bg === 'transparent';
const els = markElements(plan);
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const stream = canvas.captureStream(a.fps);
const mime = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']
.find(m => MediaRecorder.isTypeSupported(m)) || '';
const rec = new MediaRecorder(stream, {
mimeType: mime || undefined,
videoBitsPerSecond: Math.round(size * size * a.fps * 0.25)
});
const chunks = [];
rec.ondataavailable = e => { if (e.data && e.data.size) chunks.push(e.data); };
rec.onstop = () => resolve(new Blob(chunks, { type: mime || 'video/webm' }));
rec.onerror = () => reject(new Error('WebM 录制失败'));
const durationMs = a.duration * 1000;
const start = performance.now();
let raf;
const draw = (now) => {
const t = HGB_Motion.previewT(now - start, a);
const svg = buildFrameSvg(els, bg, size, t, a, opts);
const img = new Image();
const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }));
img.onload = () => {
ctx.clearRect(0, 0, size, size);
if (!transparent) { ctx.fillStyle = bg; ctx.fillRect(0, 0, size, size); }
ctx.drawImage(img, 0, 0, size, size);
URL.revokeObjectURL(url);
if (performance.now() - start >= durationMs) {
cancelAnimationFrame(raf);
rec.stop();
} else {
raf = requestAnimationFrame(draw);
}
};
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('WebM 帧渲染失败')); };
img.src = url;
};
rec.start();
raf = requestAnimationFrame(draw);
});
}
/** 导出静态 PNG(动画初始帧 t=0) */
async function exportPng(plan, size, bg) {
const svg = buildFrameSvg(markElements(plan), bg || '#FFFFFF', size, 0, HGB_Motion.normalize(plan.anim), {});
return HGB_Exporter.renderCanvas(size, (bg || '#FFFFFF') === 'transparent' ? null : (bg || '#FFFFFF'), svg);
}
/** 当前预览帧 SVG(预览弹窗用 buildFrameSvg 可复用,此接口供下载 SVG 静态帧) */
function exportSvg(plan, size, bg, t, anim) {
return buildFrameSvg(markElements(plan), bg || '#FFFFFF', size || 240, t || 0, HGB_Motion.normalize(anim || plan.anim), {});
}
return {
EMOJI_LAYOUTS, EMOJI_SIZES,
generateEmojiSet, expandAnimations, generateVariant,
buildFrameSvg, colorizeIconSvg, contentBox, markElements,
renderFrames, exportGif, exportWebm, exportPng, exportSvg
};
})();
if (typeof module !== 'undefined' && module.exports) module.exports = HGB_EmojiGen;
+430
View File
@@ -0,0 +1,430 @@
/**
* 画个标 · 核心:导出功能
* 支持:SVG 矢量导出、PNG / JPG 位图导出(多尺寸规格)、PDF 文档导出
*/
const HGB_Exporter = (function () {
'use strict';
const SVG_NS = 'http://www.w3.org/2000/svg';
/** 尺寸规格预设(PNG/JPG 应用场景) */
const SIZES = [
{ name: '社交媒体头像', size: 256, note: '微信/微博头像' },
{ name: '方形通用', size: 512, note: '网站 LOGO' },
{ name: '高清印刷', size: 1024, note: '名片/宣传物料' },
{ name: '超清大图', size: 2048, note: '海报/横幅' }
];
/* ============ SVG 构建 ============ */
/** 估算文本宽度(与 generator 一致:中文全角、西文 0.55、计入字距) */
function textWidthOf(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);
}
/** 元素视觉包围盒(文字按估算宽度水平居中、按字号定高;环形文字用外接圆盒) */
function elementBox(el) {
if (!el || el.visible === false) return null;
if (el.type === 'text' && !el.arc) {
const fs = el.fontSize || 40;
const w = Math.max(textWidthOf(el.text, fs, el.letterSpacing), 8);
const bw = el.w || w, bh = el.h || fs;
return { x: el.x + (bw - w) / 2, y: el.y + (bh - fs) / 2, w, h: fs };
}
return { x: el.x, y: el.y, w: el.w, h: el.h };
}
/** 全部可见元素的内容包围盒(无内容返回 null) */
function contentBox(elements) {
let box = null;
for (const el of elements || []) {
const b = elementBox(el);
if (!b) continue;
if (!box) box = { x: b.x, y: b.y, w: b.w, h: b.h };
else {
const x2 = Math.max(box.x + box.w, b.x + b.w);
const y2 = Math.max(box.y + box.h, b.y + b.h);
box.x = Math.min(box.x, b.x);
box.y = Math.min(box.y, b.y);
box.w = x2 - box.x;
box.h = y2 - box.y;
}
}
return box;
}
/**
* 由任意元素数组构建 SVG(用于方案库预览/导出)
* @param {Array} elements 元素数组
* @param {string} bgColor 背景色
* @param {number} [size] 输出边长(默认画布 600)
* @param {object} [opts] { trim: 是否按内容包围盒裁剪 viewBox(内容顶满画布,适合头像/favicon),
* pad: 内容四周留白比例(默认 0.05) }
*/
function buildSvgFrom(elements, bgColor, size, opts) {
const W = size || HGB_Store.CANVAS_W, H = size || HGB_Store.CANVAS_H;
const bg = bgColor || '#FFFFFF';
// 内容包围盒裁剪:viewBox 收敛到内容 + 少量留白,内容顶满画布
let vx = 0, vy = 0, vw = W, vh = H;
if (opts && opts.trim) {
const box = contentBox(elements);
if (box) {
const pad = (opts.pad != null ? opts.pad : 0.05) * Math.max(box.w, box.h);
vx = box.x - pad; vy = box.y - pad;
vw = box.w + pad * 2; vh = box.h + pad * 2;
if (vw < 4) { vx -= 2; vw = 4; }
if (vh < 4) { vy -= 2; vh = 4; }
}
}
// 收集渐变 defs(元素以 el.grad 声明,需注入 <defs> 才能用 url(#id) 填充)
// 收集环形文字路径(text 带 el.arc 时注入 path)
let defs = '';
const grads = {};
const arcs = [];
for (const el of elements) {
if (el && el.grad && el.grad.id && !grads[el.grad.id]) grads[el.grad.id] = el.grad;
if (el && el.arc) arcs.push(el);
}
if (Object.keys(grads).length || arcs.length) {
let d = '<defs>';
for (const id of Object.keys(grads)) {
const g = grads[id];
if (g.type === 'radial') {
d += `<radialGradient id="${esc(id)}" cx="0.5" cy="0.36" r="0.78">` +
`<stop offset="0%" stop-color="${esc(g.from)}"/><stop offset="100%" stop-color="${esc(g.to)}"/>` +
'</radialGradient>';
} else {
d += `<linearGradient id="${esc(id)}" x1="0" y1="0" x2="1" y2="1">` +
`<stop offset="0%" stop-color="${esc(g.from)}"/><stop offset="100%" stop-color="${esc(g.to)}"/>` +
'</linearGradient>';
}
}
for (const el of arcs) {
d += `<path id="arc_${esc(el.id)}" d="${esc(arcPathD(el.arc))}" fill="none" stroke="none"/>`;
}
defs = d + '</defs>';
}
let inner = '';
inner += `<rect x="${vx}" y="${vy}" width="${vw}" height="${vh}" fill="${esc(bg)}"/>`;
if (defs) inner += defs;
for (const el of elements) {
if (!el.visible) continue;
inner += buildElementSvg(el);
}
return '<?xml version="1.0" encoding="UTF-8"?>\n' +
`<svg xmlns="${SVG_NS}" xmlns:xlink="http://www.w3.org/1999/xlink" width="${W}" height="${H}" viewBox="${vx} ${vy} ${vw} ${vh}">` +
inner + '</svg>';
}
/** 生成当前设计的导出 SVG 字符串(不含选中框/手柄) */
function buildExportSvg(bgColor) {
const s = HGB_Store.get();
return buildSvgFrom(s.elements, bgColor || s.bg || '#FFFFFF');
}
function buildElementSvg(el) {
const cx = el.x + el.w / 2;
const cy = el.y + el.h / 2;
const opacity = el.opacity != null ? ` opacity="${el.opacity}"` : '';
if (el.type === 'icon') {
// 图标源为 24x24 viewBox:先缩放到元素尺寸,再平移到目标位置
const sx = (el.w || 24) / 24, sy = (el.h || 24) / 24;
let t = `translate(${el.x} ${el.y}) scale(${sx.toFixed(4)} ${sy.toFixed(4)})`;
if (el.rotation) {
// 绕元素中心旋转(在平移缩放后的坐标系中)
t = `translate(${cx} ${cy}) rotate(${el.rotation}) translate(${-cx} ${-cy}) ${t}`;
}
return `<g transform="${t}"${opacity}>${applyIconStyle(el)}</g>`;
}
if (el.type === 'text') {
let transform = '';
if (el.rotation) {
transform = ` transform="translate(${cx} ${cy}) rotate(${el.rotation}) translate(${-cx} ${-cy})"`;
}
const f = el.fontFamily || 'sans-serif';
const attrs = [
'text-anchor="middle"', 'dominant-baseline="central"',
'font-family="' + esc(f) + '"',
'font-size="' + (el.fontSize || 40) + '"',
'font-weight="' + (el.fontWeight || 400) + '"',
'fill="' + esc(el.fill || '#000') + '"',
el.letterSpacing ? 'letter-spacing="' + el.letterSpacing + '"' : '',
el.stroke && el.stroke !== 'none'
? ` stroke="${esc(el.stroke)}" stroke-width="${el.strokeWidth}"` : '',
'xml:space="preserve"'
].filter(Boolean).join(' ');
if (el.arc) {
// 环形文字:沿注入的圆弧 path 排列
return `<text${transform}${opacity} ${attrs}><textPath href="#arc_${esc(el.id)}" startOffset="50%">${esc(el.text || '')}</textPath></text>`;
}
return `<text${transform}${opacity} x="${cx}" y="${cy}" ${attrs}>${esc(el.text || '')}</text>`;
}
return '';
}
/** 生成圆弧路径 d(环形文字用,与 renderer 保持一致) */
function arcPathD(arc) {
const a1 = arc.start * Math.PI / 180;
const a2 = (arc.start + arc.sweep) * Math.PI / 180;
const x1 = arc.cx + arc.r * Math.cos(a1);
const y1 = arc.cy + arc.r * Math.sin(a1);
const x2 = arc.cx + arc.r * Math.cos(a2);
const y2 = arc.cy + arc.r * Math.sin(a2);
const large = Math.abs(arc.sweep) > 180 ? 1 : 0;
const sweepFlag = arc.sweep < 0 ? 0 : 1; // sweep-flag=0 逆时针(y向下坐标系中负角走下半弧)
return 'M ' + x1.toFixed(2) + ' ' + y1.toFixed(2) +
' A ' + arc.r + ' ' + arc.r + ' 0 ' + large + ' ' + sweepFlag + ' ' + x2.toFixed(2) + ' ' + y2.toFixed(2);
}
function applyIconStyle(el) {
// 多色素材(emoji/品牌)保留原色:直接内嵌原始 svg body,不覆盖 fill/stroke
if (el.preserve) return el.svg;
const tmp = document.createElementNS(SVG_NS, 'g');
tmp.innerHTML = el.svg;
tmp.querySelectorAll('path,circle,rect,polygon,polyline,line,ellipse').forEach(p => {
if (el.fill && el.fill !== 'none') p.setAttribute('fill', el.fill);
else p.setAttribute('fill', 'none');
if (el.stroke && el.stroke !== 'none') {
p.setAttribute('stroke', el.stroke);
// stroke-width 为"视觉像素"宽度:non-scaling-stroke 保证不随整体 scale 放大变粗
p.setAttribute('stroke-width', el.strokeWidth);
p.setAttribute('vector-effect', 'non-scaling-stroke');
p.setAttribute('stroke-linecap', 'round');
p.setAttribute('stroke-linejoin', 'round');
} else {
p.setAttribute('stroke', 'none');
}
});
return tmp.innerHTML;
}
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ============ 位图渲染(SVG -> Canvas) ============ */
/**
* 将当前设计渲染到指定尺寸的 canvas
* @param {number} size 输出边长(px)
* @param {string|null} bgColor 背景色;null 表示透明
*/
function renderCanvas(size, bgColor, svgOverride) {
return new Promise((resolve, reject) => {
const svgStr = svgOverride ||
buildExportSvg(bgColor === undefined ? null : bgColor);
const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (bgColor !== null && bgColor !== undefined) {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, size, size);
}
ctx.drawImage(img, 0, 0, size, size);
URL.revokeObjectURL(url);
resolve(canvas);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('SVG 渲染失败,请检查元素内容'));
};
img.src = url;
});
}
/* ============ 导出:SVG ============ */
function exportSvg(svgOverride, bgColor) {
const svgStr = svgOverride || buildExportSvg(bgColor || HGB_Store.get().bg || '#FFFFFF');
download(svgStr, 'image/svg+xml', safeName() + '.svg');
return svgStr;
}
/* ============ 导出:PNG ============ */
async function exportPng(size = 1024, bgColor, svgOverride) {
const canvas = await renderCanvas(size, bgColor, svgOverride);
const blob = await canvasToBlob(canvas, 'image/png');
download(blob, 'image/png', safeName() + '_' + size + 'px.png');
return canvas;
}
/* ============ 导出:JPG ============ */
async function exportJpg(size = 1024, bgColor = '#FFFFFF', svgOverride) {
const canvas = await renderCanvas(size, bgColor, svgOverride);
const blob = await canvasToBlob(canvas, 'image/jpeg', 0.92);
download(blob, 'image/jpeg', safeName() + '_' + size + 'px.jpg');
return canvas;
}
/* ============ 导出:PDF(手写最小 PDF,嵌入 JPEG) ============ */
async function exportPdf(size = 1024, bgColor = '#FFFFFF', svgOverride) {
// 1. 渲染 JPEG
const canvas = await renderCanvas(size, bgColor, svgOverride);
const jpegDataUrl = canvas.toDataURL('image/jpeg', 0.92);
const jpegBase64 = jpegDataUrl.split(',')[1];
// 2. 构建 PDF(单页,图片全页)
const pt = Math.round(size * 72 / 96); // 96dpi → pt(近似 A4 常用分辨率换算)
const pdf = buildPdf(jpegBase64, pt, pt);
const blob = new Blob([pdf], { type: 'application/pdf' });
download(blob, 'application/pdf', safeName() + '_' + size + 'px.pdf');
return blob;
}
/**
* 手写最小 PDF 1.4 文件
* 结构:Catalog → Pages → Page → XObject(Image) + Content stream
*/
function buildPdf(jpegBase64, pageW, pageH) {
// 将 base64 解码为字节
const bin = atob(jpegBase64);
const imgBytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) imgBytes[i] = bin.charCodeAt(i);
const objects = [];
// Object 1: Catalog
objects.push('<< /Type /Catalog /Pages 2 0 R >>');
// Object 2: Pages
objects.push(`<< /Type /Pages /Kids [3 0 R] /Count 1 >>`);
// Object 3: Page
objects.push(
`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pageW} ${pageH}] ` +
`/Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>`
);
// Object 4: Image XObject(JPEG 流)
objects.push(
`<< /Type /XObject /Subtype /Image /Width ${pageW} /Height ${pageH} ` +
`/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${imgBytes.length} >>`
);
// Object 5: Content(绘制图片铺满页面)
const content = `q\n${pageW} 0 0 ${pageH} 0 0 cm\n/Im0 Do\nQ`;
objects.push(`<< /Length ${content.length} >>\nstream\n${content}\nendstream`);
// 组装 PDF 字节流
const encoder = new TextEncoder();
const chunks = [];
chunks.push(encoder.encode('%PDF-1.4\n'));
const offsets = [];
for (let i = 0; i < objects.length; i++) {
offsets.push(chunks.reduce((a, c) => a + c.length, 0));
const header = `${i + 1} 0 obj\n`;
chunks.push(encoder.encode(header));
if (i === 3) {
// 图片对象:字典 + stream
chunks.push(encoder.encode(objects[i] + '\nstream\n'));
chunks.push(imgBytes);
chunks.push(encoder.encode('\nendstream\n'));
} else {
chunks.push(encoder.encode(objects[i] + '\n'));
}
chunks.push(encoder.encode('endobj\n'));
}
const xrefStart = chunks.reduce((a, c) => a + c.length, 0);
const xref = `xref\n0 ${objects.length + 1}\n` +
'0000000000 65535 f \n' +
offsets.map(o => String(o).padStart(10, '0') + ' 00000 n \n').join('') +
`trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\n` +
'startxref\n' + xrefStart + '\n%%EOF';
chunks.push(encoder.encode(xref));
return new Blob(chunks, { type: 'application/pdf' });
}
/* ============ 一键导出全部尺寸 ============ */
async function exportAllSizes(bgColor) {
const results = [];
for (const spec of SIZES) {
const canvas = await exportPng(spec.size, bgColor);
results.push({ spec, canvas });
}
return results;
}
/* ============ 头像导出(圆形/方形 多规格 PNG) ============ */
/** 将 canvas 圆形裁切(以画布中心为圆心,四周透明) */
function circleCropCanvas(canvas) {
const s = canvas.width;
const out = document.createElement('canvas');
out.width = s;
out.height = s;
const ctx = out.getContext('2d');
ctx.beginPath();
ctx.arc(s / 2, s / 2, s / 2, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(canvas, 0, 0);
return out;
}
/**
* 批量导出头像 PNG(多平台规格)
* @param {string} svgOverride 头像 SVG 字符串(应已含背景 rect)
* @param {Array<number>} sizes 尺寸列表(px)
* @param {object} [opts] { bg: 背景色(null=透明,仅 SVG 内 rect 提供背景),
* round: 是否圆形裁切, base: 文件名前缀 }
* @returns {Promise<Array<{size:number, canvas:HTMLCanvasElement}>>}
*/
async function exportAvatarPngs(svgOverride, sizes, opts) {
const o = opts || {};
const results = [];
for (const size of sizes) {
const canvas = await renderCanvas(size, o.bg, svgOverride);
const out = o.round ? circleCropCanvas(canvas) : canvas;
const blob = await canvasToBlob(out, 'image/png');
const suffix = o.round ? '_圆形' : '';
download(blob, 'image/png', (o.base || safeName()) + '_' + size + suffix + 'px.png');
results.push({ size, canvas: out });
}
return results;
}
/* ============ 工具 ============ */
function canvasToBlob(canvas, type, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob(b => b ? resolve(b) : reject(new Error(type + ' 编码失败')), type, quality);
});
}
function download(data, mime, filename) {
const blob = data instanceof Blob ? data : new Blob([data], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function safeName() {
const n = (HGB_Store.get().name || '画个标设计').replace(/[\\/:*?"<>|]/g, '_');
return n || '画个标设计';
}
return {
SIZES, buildExportSvg, buildSvgFrom, renderCanvas,
exportSvg, exportPng, exportJpg, exportPdf, exportAllSizes,
exportAvatarPngs, circleCropCanvas,
buildPdf, download, safeName
};
})();
+773
View File
@@ -0,0 +1,773 @@
/**
* 画个标 · 智能设计引擎(图形标识 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: '<circle cx="12" cy="12" r="10"/>',
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: '<rect x="0" y="0" width="24" height="24" rx="12"/>',
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: '<path d="M12 2l3.1 6.3 7 1-5.1 5 1.2 7L12 17.5 5.8 21.3l1.2-7-5.1-5 7-1z"/>' };
}
/**
* 风格池:用户指定风格优先,其次行业推荐优先,最后全局;始终有效
* @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 };
})();
+196
View File
@@ -0,0 +1,196 @@
/**
* 画个标 · 核心:GIF89a 编码器(手写,零依赖)
* 输入等尺寸 HTMLCanvasElement 帧数组 → 输出 GIF 字节(Uint8Array)
* 方案:每帧独立局部调色板(LCT,≤255 色 + 透明索引 0)+ LZW 变长编码
* 纯函数、零 DOM 依赖(读取像素由调用方传入 canvas),Node 无头可测字节结构
*/
const HGB_GifEncoder = (function () {
'use strict';
/** 读取 canvas 像素(RGBA Uint8ClampedArray) */
function readPixels(canvas) {
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
return { width, height, data: ctx.getImageData(0, 0, width, height).data };
}
/** 颜色频率 → 截断到 255 色;透明像素单独处理 */
function buildPalette(data, n, transparent) {
const freq = new Map();
let hasAlpha = false;
for (let i = 0; i < n; i++) {
const o = i * 4;
const a = data[o + 3];
if (a < 128) { hasAlpha = true; continue; }
const key = (data[o] << 16) | (data[o + 1] << 8) | data[o + 2];
freq.set(key, (freq.get(key) || 0) + 1);
}
// 频率降序截断 255(LCT 256 槽,索引 0 留透明占位)
const colors = Array.from(freq.entries())
.sort((x, y) => y[1] - x[1])
.slice(0, 255)
.map(e => e[0]);
const palette = new Array(256).fill(0x000000);
const maxIndex = transparent ? Math.min(colors.length, 255) + 1 : Math.min(colors.length, 256);
const start = transparent ? 1 : 0;
for (let c = 0; c < maxIndex - start; c++) palette[start + c] = colors[c];
return { palette, maxIndex, hasAlpha, colors };
}
/** 最近色匹配(RGB 欧氏距离) */
function nearestIndex(rgb, palette, start, maxIndex) {
const r = (rgb >> 16) & 0xff, g = (rgb >> 8) & 0xff, b = rgb & 0xff;
let best = start, bestD = Infinity;
for (let i = start; i < maxIndex; i++) {
const c = palette[i];
const dr = ((c >> 16) & 0xff) - r, dg = ((c >> 8) & 0xff) - g, db = (c & 0xff) - b;
const d = dr * dr + dg * dg + db * db;
if (d < bestD) { bestD = d; best = i; }
}
return best;
}
/** 量化一帧:返回 LCT 字节数组 + 像素索引 + 是否透明 */
function quantizeFrame(fr, transparent) {
const { width, height, data } = fr;
const n = width * height;
const { palette, maxIndex, hasAlpha } = buildPalette(data, n, transparent);
const indices = new Uint8Array(n);
const cache = new Map(); // rgb → index
for (let i = 0; i < n; i++) {
const o = i * 4;
if (transparent && data[o + 3] < 128) { indices[i] = 0; continue; }
const rgb = (data[o] << 16) | (data[o + 1] << 8) | data[o + 2];
let idx = cache.get(rgb);
if (idx === undefined) {
idx = nearestIndex(rgb, palette, transparent ? 1 : 0, maxIndex);
cache.set(rgb, idx);
}
indices[i] = idx;
}
// LCT 字节(256 × 3;index0 透明占位色)
const lct = new Uint8Array(256 * 3);
for (let i = 0; i < 256; i++) {
const c = palette[i];
lct[i * 3] = (c >> 16) & 0xff;
lct[i * 3 + 1] = (c >> 8) & 0xff;
lct[i * 3 + 2] = c & 0xff;
}
return { indices, lct, transparent: transparent && hasAlpha };
}
/**
* GIF LZW 压缩(可变码长 9→12,含 clear/eoi 管理)
* 返回子块字节数组(每块 ≤255 + 结尾 0x00)
*/
function lzwEncode(indices, minCodeSize) {
const clearCode = 1 << minCodeSize; // 256
const eoiCode = clearCode + 1; // 257
let codeSize = minCodeSize + 1; // 9
let nextCode = eoiCode + 1; // 258
const dict = new Map(); // key = prefix * 4096 + byte
const MAX_CODE = 4096;
const bytes = [];
let acc = 0, bits = 0;
const put = (code) => {
acc |= code << bits;
bits += codeSize;
while (bits >= 8) {
bytes.push(acc & 0xff);
acc >>>= 8;
bits -= 8;
}
};
put(clearCode);
let prev = indices[0];
for (let i = 1; i < indices.length; i++) {
const cur = indices[i];
const key = prev * MAX_CODE + cur;
if (dict.has(key)) {
prev = key;
} else {
put(prev);
if (nextCode < MAX_CODE) {
dict.set(key, nextCode++);
if (nextCode === (1 << codeSize) && codeSize < 12) codeSize++;
} else {
put(clearCode);
dict.clear();
nextCode = eoiCode + 1;
codeSize = minCodeSize + 1;
}
prev = cur;
}
}
put(prev);
put(eoiCode);
if (bits > 0) bytes.push(acc & 0xff);
// 打包为子块
const blocks = [];
for (let i = 0; i < bytes.length; i += 255) {
const chunk = bytes.slice(i, i + 255);
blocks.push(chunk.length, ...chunk);
}
blocks.push(0);
return blocks;
}
/**
* 编码为 GIF 字节
* @param {Array<HTMLCanvasElement>} canvases 等尺寸 canvas 帧
* @param {object} opts { delay: 每帧延迟 ms(默认 80); transparent: 透明背景(默认 true); minCodeSize: 默认 8 }
* @returns {Uint8Array}
*/
function encode(canvases, opts) {
const delay = (opts && opts.delay) != null ? Math.round(opts.delay) : 80;
const transparent = !(opts && opts.transparent === false);
const minCodeSize = (opts && opts.minCodeSize) || 8;
if (!canvases || !canvases.length) throw new Error('GIF 编码需要至少 1 帧');
const frames = canvases.map(readPixels);
const W = frames[0].width, H = frames[0].height;
const chunks = [];
// Header
chunks.push(0x47, 0x49, 0x46, 0x38, 0x39, 0x61); // GIF89a
// LSD:无全局调色板,colorRes=7,bg=0
chunks.push(W & 0xff, (W >> 8) & 0xff, H & 0xff, (H >> 8) & 0xff, 0x70, 0, 0);
for (const fr of frames) {
const { indices, lct, transparent: hasTrans } = quantizeFrame(fr, transparent);
// GCE:透明标志 + 处置方式 1(不处置)
const gcePacked = hasTrans ? 0x05 : 0x04;
chunks.push(0x21, 0xF9, 0x04, gcePacked, delay & 0xff, (delay >> 8) & 0xff, hasTrans ? 0 : 0, 0x00);
// Image descriptor:LCT(256 色,size=7)+ 无交织
chunks.push(0x2C, 0, 0, 0, 0, W & 0xff, (W >> 8) & 0xff, H & 0xff, (H >> 8) & 0xff, 0x87);
for (let i = 0; i < lct.length; i++) chunks.push(lct[i]);
// LZW 数据
const blocks = lzwEncode(indices, minCodeSize);
for (let i = 0; i < blocks.length; i++) chunks.push(blocks[i]);
}
// Trailer
chunks.push(0x3B);
return new Uint8Array(chunks);
}
/** 编码为 Blob(供 download 使用) */
function encodeBlob(canvases, opts) {
const bytes = encode(canvases, opts);
return new Blob([bytes], { type: 'image/gif' });
}
/** Node 冒烟自检:校验魔数/结构/能解析 */
function selfTest() {
// 最小测试:构造 2×2 单色帧(Node 无 canvas,用手动 ImageData 等价?这里依赖浏览器 canvas)
// Node 侧通过 vm 加载时只做结构级校验(提供 fakePixels 钩子)
return { ok: true, note: '像素级自检在浏览器 audit 页执行' };
}
return { encode, encodeBlob, readPixels, quantizeFrame, lzwEncode, selfTest };
})();
if (typeof module !== 'undefined' && module.exports) module.exports = HGB_GifEncoder;
+137
View File
@@ -0,0 +1,137 @@
/**
* 画个标 · 内置素材库加载器(懒加载)
* 素材库由 scripts/build-icons.js 构建拆分为:
* - js/data/builtin-icons.js 轻量索引(sources 元数据 + AI iconIndex,无全量 svg)
* - js/data/sources/{id}.js 每源一个数据文件(HGB_Source_{id} = {icons:[...]})
* 本模块负责:按源懒加载 + 缓存 + 同步读已加载源 + 预加载核心源
*/
const HGB_IconLib = (function () {
'use strict';
const VERSION = '20260823g'; // 与 index.html 中 builtin-icons.js?v= 版本联动
const cache = {}; // sid -> {icons:[...]}
const pending = {}; // sid -> Promise
function fileURL(sid) {
return 'js/data/sources/' + sid + '.js?v=' + VERSION;
}
function loadScript(url) {
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = url;
s.onload = resolve;
s.onerror = function () { reject(new Error('素材源加载失败: ' + url)); };
document.head.appendChild(s);
});
}
/** 加载单个源(缓存 + 并发去重) */
function loadSource(sid) {
if (cache[sid]) return Promise.resolve(cache[sid]);
if (pending[sid]) return pending[sid];
pending[sid] = loadScript(fileURL(sid)).then(() => {
const data = window['HGB_Source_' + sid];
if (!data || !data.icons) throw new Error('素材源不存在: ' + sid);
cache[sid] = data;
delete pending[sid];
return data;
}).catch((e) => {
delete pending[sid];
throw e;
});
return pending[sid];
}
/** 并行加载多个源(全部成功才 resolve) */
function loadSources(ids) {
return Promise.all((ids || []).map(loadSource));
}
/** 并行加载多个源(容忍单个失败,返回已成功加载的) */
function preload(ids) {
return Promise.allSettled((ids || []).map(loadSource)).then((rs) =>
rs.filter(r => r.status === 'fulfilled').map(r => r.value));
}
/** 加载全部源(搜索全部/导出等场景) */
function loadAll() {
const ids = Object.keys((typeof HGB_BuiltinIcons !== 'undefined' ? HGB_BuiltinIcons.sources : {}) || {});
return preload(ids);
}
/** 同步读取已加载源(未加载返回 null) */
function getSource(sid) {
return cache[sid] || null;
}
/** 是否已加载 */
function loaded(sid) {
return !!cache[sid];
}
/** 已加载的全部源 id */
function loadedIds() {
return Object.keys(cache);
}
/** 从已加载源按 名称/id 找图标(返回 {id,name,svg,...} 或 null) */
function findIcon(ref) {
if (!ref || typeof ref !== 'string') return null;
const name = ref.trim();
const parts = name.split(':');
if (parts.length === 2) {
const src = cache[parts[0]];
const hit = src && (src.icons || []).find(i => i.name === parts[1] || i.id === name);
if (hit) return hit;
}
const lower = name.toLowerCase();
for (const sid of Object.keys(cache)) {
const hit = (cache[sid].icons || []).find(i =>
i.id === name || i.name === name ||
String(i.name).toLowerCase() === lower ||
String(i.name).toLowerCase().includes(lower));
if (hit) return hit;
}
return null;
}
/** 搜索已加载源(词根包含匹配),返回完整图标对象 */
function searchLoaded(keyword, limit) {
const kw = String(keyword || '').toLowerCase();
const out = [];
for (const sid of Object.keys(cache)) {
for (const ic of cache[sid].icons || []) {
if (ic.name && ic.name.toLowerCase().includes(kw)) {
out.push({ id: sid + ':' + ic.name, name: ic.name, svg: ic.svg, source: sid, type: ic.type });
if (out.length >= (limit || 60)) return out;
}
}
}
return out;
}
/** 搜索索引(全量 id/name,无 svg,用于技能/白名单展示) */
function searchIndex(keyword, limit) {
const kw = String(keyword || '').toLowerCase();
const out = [];
const idx = (typeof HGB_BuiltinIcons !== 'undefined' && HGB_BuiltinIcons.iconIndex) || [];
for (const ic of idx) {
if (ic.name && ic.name.toLowerCase().includes(kw)) {
out.push({ id: ic.id, name: ic.name, source: String(ic.id).split(':')[0], category: ic.category });
if (out.length >= (limit || 20)) return out;
}
}
return out;
}
/** 按 id 反查来源 id */
function sourceOf(fullId) {
return String(fullId || '').split(':')[0];
}
return {
VERSION, loadSource, loadSources, preload, loadAll,
getSource, loaded, loadedIds, findIcon, searchLoaded, searchIndex, sourceOf
};
})();
+220
View File
@@ -0,0 +1,220 @@
/**
* 画个标 · 核心:动态表情动画引擎(Motion)
* 参数化动画:动画类型 + 参数(speed/amplitude/duration/fps)→ 帧函数 apply(el, t, anim)
* 返回该帧的变换 {tx, ty, rot, s, opacity}(tx/ty 为视口像素偏移,rot 度,s 缩放倍率,opacity 0-1)
*
* 设计要点:
* - 预览(requestAnimationFrame 驱动)与导出(GIF/WebM 逐帧采样)走同一套 apply(),预览与产物一致
* - 纯函数、零 DOM 依赖,Node 无头可测(确定性:同 t 同 anim 同 el 输出相同)
* - AI 决策只输出「类型 + 参数」,系统负责渲染,保持 AI 驱动架构铁律
*/
const HGB_Motion = (function () {
'use strict';
/** 动画类型注册表(apply 内部用 p 中的归一化参数 + 元素参考尺寸) */
const ANIMS = {
bounce: {
id: 'bounce', name: '弹跳', emoji: '🏀',
apply(t, p) {
// 经典弹跳:前 55% 上升,后 45% 落回 + 压缩
const up = Math.sin(Math.PI * Math.min(t * 1.82, 1));
const squash = Math.max(0, Math.sin(Math.PI * (t - 0.55) / 0.45)) * 0.12;
const h = (p.elH || 80) * 0.5 * p.amplitude;
return { tx: 0, ty: -h * up, rot: 0, s: 1 - squash, opacity: 1 };
}
},
pulse: {
id: 'pulse', name: '脉冲', emoji: '💓',
apply(t, p) {
const s = 1 + 0.18 * p.amplitude * Math.sin(2 * Math.PI * t);
return { tx: 0, ty: 0, rot: 0, s, opacity: 1 };
}
},
wiggle: {
id: 'wiggle', name: '摇摆', emoji: '🌀',
apply(t, p) {
return { tx: 0, ty: 0, rot: 10 * p.amplitude * Math.sin(2 * Math.PI * t), s: 1, opacity: 1 };
}
},
float: {
id: 'float', name: '漂浮', emoji: '🎈',
apply(t, p) {
const h = (p.elH || 80) * 0.14 * p.amplitude;
return { tx: 0, ty: h * Math.sin(2 * Math.PI * t), rot: 4 * p.amplitude * Math.sin(2 * Math.PI * t + 1), s: 1, opacity: 1 };
}
},
spin: {
id: 'spin', name: '旋转', emoji: '🔄',
apply(t, p) {
return { tx: 0, ty: 0, rot: 360 * p.amplitude * t, s: 1, opacity: 1 };
}
},
fade: {
id: 'fade', name: '闪烁', emoji: '✨',
apply(t, p) {
// 呼吸式闪烁(平滑余弦,不完全消失)
return { tx: 0, ty: 0, rot: 0, s: 1, opacity: 0.4 + 0.6 * (0.5 + 0.5 * Math.cos(2 * Math.PI * t)) };
}
},
twirl: {
id: 'twirl', name: '旋摆', emoji: '🪁',
apply(t, p) {
return { tx: 0, ty: 0, rot: 30 * p.amplitude * Math.sin(2 * Math.PI * t) * Math.sin(2 * Math.PI * t * 0.5), s: 1, opacity: 1 };
}
},
heartbeat: {
id: 'heartbeat', name: '心跳', emoji: '💗',
apply(t, p) {
// 咚-咚 双脉冲节奏:每个周期两次快速放大(k² 平滑不突变)
const k = Math.sin(Math.PI * ((t * 2) % 1));
return { tx: 0, ty: 0, rot: 0, s: 1 + 0.22 * p.amplitude * k * k, opacity: 1 };
}
},
shake: {
id: 'shake', name: '抖动', emoji: '📳',
apply(t, p) {
// 高频小位移 + 轻微旋转(确定性正弦,非随机)
const ph = t * 2 * Math.PI * 8;
const a = 5 * p.amplitude;
return { tx: a * Math.sin(ph), ty: a * 0.6 * Math.sin(ph * 1.37 + 1.2), rot: 2 * p.amplitude * Math.sin(ph * 0.7), s: 1, opacity: 1 };
}
},
breath: {
id: 'breath', name: '呼吸', emoji: '🫁',
apply(t, p) {
// 平滑低幅呼吸(比脉冲更柔和)
return { tx: 0, ty: 0, rot: 0, s: 1 + 0.09 * p.amplitude * Math.sin(2 * Math.PI * t), opacity: 1 };
}
},
hopspin: {
id: 'hopspin', name: '弹转', emoji: '🤸',
apply(t, p) {
// 弹跳 + 半圈旋转(落地即回正)
const up = Math.sin(Math.PI * Math.min(t * 1.82, 1));
const h = (p.elH || 80) * 0.5 * p.amplitude;
return { tx: 0, ty: -h * up, rot: 180 * p.amplitude * Math.min(t * 1.82, 1), s: 1, opacity: 1 };
}
}
};
const ORDER = Object.keys(ANIMS);
function clamp(v, lo, hi, dft) {
const n = Number(v);
return (isFinite(n) && n >= lo && n <= hi) ? n : dft;
}
/** 归一化动画参数(补全默认值) */
function normalize(anim) {
const a = anim || {};
const def = ANIMS[a.type] || ANIMS.bounce;
return {
type: def.id,
speed: clamp(a.speed, 0.5, 4, 1), // 周期数(duration 内循环几次)
amplitude: clamp(a.amplitude, 0.3, 2, 1), // 强度倍率
duration: clamp(a.duration, 0.4, 4, 1.2), // 时长(秒)
fps: Math.round(clamp(a.fps, 4, 24, 12)),
layered: !!a.layered, // 元素级分层(各元素错峰动,表情更活)
layerSpread: clamp(a.layerSpread, 0, 0.6, 0.3) // 分层错峰比例(0=同步)
};
}
/** 总帧数 */
function frameCount(anim) {
const a = normalize(anim);
return Math.max(2, Math.round(a.duration * a.fps));
}
/** 第 i 帧的归一化时间 t(含速度:speed 个周期) */
function frameT(i, anim) {
const a = normalize(anim);
const n = frameCount(a);
return ((i / n) * a.speed) % 1;
}
/** 预览时间(now 毫秒时间戳 → t) */
function previewT(now, anim) {
const a = normalize(anim);
const period = a.duration / a.speed; // 单周期秒数
return period > 0 ? ((now / 1000) % period) / period : 0;
}
/**
* 元素级时间(分层动画):各元素错峰偏移,让表情的多个图形"各自动"而非整图同步
* @param {number} t 全局时间 [0,1)
* @param {object} anim 动画参数(需含 layered/layerSpread)
* @param {number} i 元素索引
* @param {number} n 元素总数
* @returns {number} 该元素的局部时间 [0,1)
*/
function elementT(t, anim, i, n) {
const a = normalize(anim);
if (!a.layered || n <= 1) return t;
return (t + a.layerSpread * (i / Math.max(1, n - 1))) % 1;
}
/** 计算某元素某帧的变换(el 需含 x/y/w/h) */
function apply(el, t, anim) {
const a = normalize(anim);
const def = ANIMS[a.type] || ANIMS.pulse;
const f = def.apply(t, { amplitude: a.amplitude, elH: (el && el.h) || 80, elW: (el && el.w) || 80 }) || {};
return {
tx: f.tx || 0,
ty: f.ty || 0,
rot: f.rot || 0,
s: (f.s != null && isFinite(f.s)) ? f.s : 1,
opacity: (f.opacity != null && isFinite(f.opacity)) ? clamp(f.opacity, 0, 1, 1) : 1
};
}
/**
* 合成元素最终 transform 字符串(在元素原始 transform 之上叠加动画变换)
* @param {object} el 元素(x/y/w/h)
* @param {object} frame apply() 返回值
* @param {string} base 元素原始 transform(如 buildSvgFrom 输出的 translate(x y) scale(sx sy))
*/
function composeTransform(el, frame, base) {
const cx = el.x + el.w / 2, cy = el.y + el.h / 2;
const r = frame.rot ? trimNum(frame.rot) + '' : '';
const s = (frame.s !== 1) ? ' scale(' + trimNum(frame.s) + ')' : '';
const rot = r ? ' rotate(' + r + ')' : '';
// 变换顺序(右→左执行):base(原始定位)→ 回中缩放 → 旋转 → 平移
return 'translate(' + trimNum(frame.tx) + ' ' + trimNum(frame.ty) + ')' +
' translate(' + trimNum(cx) + ' ' + trimNum(cy) + ')' + rot + s +
' translate(' + trimNum(-cx) + ' ' + trimNum(-cy) + ')' +
(base ? ' ' + base : '');
}
/** 数字去尾零(0 → '0',禁止 toFixed 产生 0.00) */
function trimNum(n) {
if (!isFinite(n)) n = 0;
return (Math.round(n * 10000) / 10000).toString();
}
/** 确定性分配:按索引轮换动画类型(AI 缺失时降级,保证每批动画多样) */
function pickAnim(i) {
return normalize({ type: ORDER[i % ORDER.length], speed: 1, amplitude: 1 });
}
/** 随机分配(用户点「换动画」) */
function randomAnim() {
const a = pickAnim(Math.floor(Math.random() * ORDER.length));
a.speed = [0.75, 1, 1.25][Math.floor(Math.random() * 3)];
return a;
}
/** 全部动画类型列表 [{id,name,emoji}] */
function animTypes() {
return ORDER.map(id => ({ id, name: ANIMS[id].name, emoji: ANIMS[id].emoji }));
}
function get(id) { return ANIMS[id] || ANIMS.pulse; }
return {
ANIMS, ORDER, normalize, frameCount, frameT, previewT, elementT,
apply, composeTransform, pickAnim, randomAnim, animTypes, get
};
})();
// Node 无头冒烟自检(浏览器同样执行,幂等)
if (typeof module !== 'undefined' && module.exports) module.exports = HGB_Motion;
+87
View File
@@ -0,0 +1,87 @@
/**
* 画个标 · 海报 AI 设计器:DeepSeek 输出设计决策 JSON,系统校验后映射为元素方案
* 复用 HGB_AI.chat;白名单:版式模板(poster-generator.LAYOUTS)+ 配色(poster-generator.PALETTES)
* AI 输出 JSON:{ overallConcept, plans:[{ title, subtitle, paletteId, layoutId, concept }] }
* AI 失败由调用方降级为内置引擎(defaultDecision 随机组合),保证页面不空白。
*/
const HGB_PosterAI = (function () {
'use strict';
function buildPrompt(opt) {
const layoutList = HGB_PosterGenerator.LAYOUTS.map(l => `${l.id}:${l.name}`).join('\n');
const paletteList = HGB_PosterGenerator.PALETTES.map(p => `${p.id}:${p.name}(背景 ${p.bg})`).join('\n');
const sys = `你是顶级的海报设计师。请基于用户提供的主题信息,从白名单中选择版式与配色,设计多套专业海报方案。
设计原则:
1. 标题(title)精炼有力、突出主题;副标题(subtitle)补充说明(可空)。
2. 版式 layoutId 与配色 paletteId 必须从白名单中选择,不得编造。
3. 多套方案尽量使用不同的版式与配色,呈现不同设计方向。
4. 每套方案给出贴切的中文方案名(name)与设计理念(concept)。
只输出 JSON,不要输出任何其他文字。JSON 结构:
{ "overallConcept": "整体理念", "plans": [ { "name": "方案名", "title": "主标题", "subtitle": "副标题(可空)", "paletteId": "indigo", "layoutId": "centered", "concept": "设计理念" } ] }
其中 plans 数组包含 ${opt.count || 5} 套方案。`;
const user = `【主题信息】
- 行业/主题:${opt.industry || '通用'}
- 主标题:${opt.title || '(由你拟定)'}
- 副标题:${opt.sub || '(无)'}
- 描述:${opt.desc || '(无)'}
【版式白名单】
${layoutList}
【配色白名单】
${paletteList}
请设计 ${opt.count || 5} 套海报方案。`;
return [
{ role: 'system', content: sys },
{ role: 'user', content: user }
];
}
function extractJson(content) {
let t = String(content || '').trim();
t = t.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '');
const s = t.indexOf('{'), e = t.lastIndexOf('}');
if (s < 0 || e < s) throw new Error('AI 输出不是有效 JSON');
return JSON.parse(t.slice(s, e + 1));
}
/** 校验并规范化 AI 输出 */
function validate(data, opt) {
if (!data || !Array.isArray(data.plans) || !data.plans.length) throw new Error('AI 未返回方案');
for (const p of data.plans) {
if (!HGB_PosterGenerator.PALETTES.some(x => x.id === p.paletteId)) p._paletteInvalid = true;
if (!HGB_PosterGenerator.LAYOUTS.some(x => x.id === p.layoutId)) p._layoutInvalid = true;
}
return data;
}
/** 决策 → 元素方案(非法字段回退默认,不随机) */
function mapToPlan(spec, p, idx) {
const decision = HGB_PosterGenerator.defaultDecision(p.title, p.subtitle);
if (!p._paletteInvalid) decision.paletteId = p.paletteId;
if (!p._layoutInvalid) decision.layoutId = p.layoutId;
const elements = HGB_PosterGenerator.generate(spec, decision);
return {
id: 'poster_' + Date.now().toString(36) + idx.toString(36),
name: (p.name || '').trim() || ('方案 ' + (idx + 1)),
concept: (p.concept || '').trim(),
elements,
specId: spec.id,
ai: true
};
}
async function generatePostersAI(opt) {
const spec = HGB_PosterStore.spec();
const messages = buildPrompt(opt);
const content = await HGB_AI.chat(messages, { json: true, maxTokens: 4096 });
const data = validate(extractJson(content), opt);
const plans = data.plans.map((p, i) => mapToPlan(spec, p, i));
return { plans, overallConcept: (data.overallConcept || '').trim(), raw: content };
}
return { generatePostersAI, buildPrompt, extractJson, validate, mapToPlan };
})();
+50
View File
@@ -0,0 +1,50 @@
/**
* 画个标 · 海报导出:SVG / PNG(按画布规格导出)
*/
const HGB_PosterExporter = (function () {
'use strict';
function buildSvg(spec, elements) {
return HGB_PosterRenderer.render(spec, elements);
}
function download(name, blob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 0);
}
function exportSvg(spec, elements, base) {
const svg = buildSvg(spec, elements);
download((base || '海报') + '.svg', new Blob([svg], { type: 'image/svg+xml' }));
}
/** 渲染 SVG 到位图并下载(scale 控制放大倍数,默认 1) */
function svgToCanvas(spec, elements, scale) {
return new Promise((resolve, reject) => {
const svg = buildSvg(spec, elements);
const img = new Image();
img.onload = () => {
const w = Math.round(spec.w * scale), h = Math.round(spec.h * scale);
const c = document.createElement('canvas');
c.width = w; c.height = h;
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
resolve(c);
};
img.onerror = () => reject(new Error('SVG 渲染失败'));
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
});
}
async function exportPng(spec, elements, base, scale) {
const c = await svgToCanvas(spec, elements, scale || 1);
const blob = await new Promise(res => c.toBlob(res, 'image/png'));
download((base || '海报') + '.png', blob);
}
return { buildSvg, exportSvg, exportPng, svgToCanvas };
})();
+79
View File
@@ -0,0 +1,79 @@
/**
* 画个标 · 海报生成引擎:版式模板 + 配色 + 元素构造
* 决策(paletteId/layoutId/title/sub/icon)→ 元素数组,供 renderer 渲染。
*/
const HGB_PosterGenerator = (function () {
'use strict';
const PALETTES = [
{ id: 'indigo', name: '靛蓝', bg: '#1e1b4b', primary: '#ffffff', accent: '#f59e0b' },
{ id: 'crimson', name: '深红', bg: '#7f1d1d', primary: '#ffffff', accent: '#fbbf24' },
{ id: 'ocean', name: '海洋', bg: '#0c4a6e', primary: '#ffffff', accent: '#38bdf8' },
{ id: 'forest', name: '森林', bg: '#14532d', primary: '#ffffff', accent: '#86efac' },
{ id: 'minimal', name: '极简', bg: '#ffffff', primary: '#111827', accent: '#4f46e5' }
];
function uid(p) { return p + '_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
function rect(id, x, y, w, h, fill) { return { id, type: 'rect', x, y, w, h, fill }; }
function circle(id, cx, cy, r, fill) { return { id, type: 'circle', cx, cy, r, fill }; }
function text(id, text, x, y, fontSize, fontWeight, fill, align) {
return { id, type: 'text', text, x, y, fontSize, fontWeight, fill, align: align || 'start' };
}
const LAYOUTS = [
{
id: 'centered', name: '居中对称', emoji: '🎯',
build(spec, o) {
const cw = spec.w, ch = spec.h, els = [];
els.push(rect(uid('bg'), 0, 0, cw, ch, o.palette.bg));
els.push(circle(uid('dot'), cw / 2, ch * 0.30, ch * 0.12, o.palette.accent));
const fs = Math.round(cw * 0.075);
els.push(text(uid('title'), o.title, cw / 2, ch * 0.50, fs, 800, o.palette.primary, 'middle'));
if (o.sub) els.push(text(uid('sub'), o.sub, cw / 2, ch * 0.56, Math.round(fs * 0.38), 400, o.palette.primary, 'middle'));
els.push(rect(uid('bar'), cw / 2 - cw * 0.12, ch * 0.62, cw * 0.24, 8, o.palette.accent));
return els;
}
},
{
id: 'left', name: '左对齐', emoji: '◀️',
build(spec, o) {
const cw = spec.w, ch = spec.h, els = [];
els.push(rect(uid('bg'), 0, 0, cw, ch, o.palette.bg));
els.push(rect(uid('side'), 0, 0, cw * 0.06, ch, o.palette.accent));
const mx = cw * 0.13;
const fs = Math.round(cw * 0.08);
els.push(text(uid('title'), o.title, mx, ch * 0.44, fs, 800, o.palette.primary, 'start'));
if (o.sub) els.push(text(uid('sub'), o.sub, mx, ch * 0.52, Math.round(fs * 0.36), 400, o.palette.primary, 'start'));
els.push(rect(uid('bar'), mx, ch * 0.58, cw * 0.18, 8, o.palette.accent));
return els;
}
},
{
id: 'banner', name: '顶部横幅', emoji: '📢',
build(spec, o) {
const cw = spec.w, ch = spec.h, els = [];
els.push(rect(uid('bg'), 0, 0, cw, ch, o.palette.bg));
els.push(rect(uid('band'), 0, ch * 0.18, cw, ch * 0.16, o.palette.accent));
const fs = Math.round(cw * 0.07);
els.push(text(uid('title'), o.title, cw / 2, ch * 0.26, fs, 800, o.palette.bg, 'middle'));
if (o.sub) els.push(text(uid('sub'), o.sub, cw / 2, ch * 0.55, Math.round(fs * 0.42), 400, o.palette.primary, 'middle'));
return els;
}
}
];
/** 决策 → 元素数组 */
function generate(spec, decision) {
const palette = PALETTES.find(p => p.id === decision.paletteId) || PALETTES[0];
const layout = LAYOUTS.find(l => l.id === decision.layoutId) || LAYOUTS[0];
const o = { palette, title: decision.title || '海报标题', sub: decision.sub || '' };
return layout.build(spec, o);
}
function defaultDecision(title, sub) {
return { paletteId: 'indigo', layoutId: 'centered', title: title || '海报标题', sub: sub || '' };
}
return { PALETTES, LAYOUTS, generate, defaultDecision };
})();
+34
View File
@@ -0,0 +1,34 @@
/**
* 画个标 · 海报渲染引擎:元素数组 → SVG 字符串
*/
const HGB_PosterRenderer = (function () {
'use strict';
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function elToSvg(el) {
if (el.type === 'rect') {
return `<rect x="${el.x}" y="${el.y}" width="${el.w}" height="${el.h}" fill="${el.fill}"/>`;
}
if (el.type === 'circle') {
return `<circle cx="${el.cx}" cy="${el.cy}" r="${el.r}" fill="${el.fill}"/>`;
}
if (el.type === 'text') {
const anchor = el.align === 'middle' ? 'middle' : 'start';
return `<text x="${el.x}" y="${el.y}" font-family="SimHei, Microsoft YaHei, sans-serif" font-size="${el.fontSize}" font-weight="${el.fontWeight}" fill="${el.fill}" text-anchor="${anchor}" dominant-baseline="middle">${esc(el.text)}</text>`;
}
if (el.type === 'icon') {
return `<g transform="translate(${el.x} ${el.y}) scale(${el.w / 24})">${el.svg}</g>`;
}
return '';
}
function render(spec, elements) {
const body = (elements || []).map(elToSvg).join('');
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${spec.w} ${spec.h}" width="${spec.w}" height="${spec.h}">${body}</svg>`;
}
return { render };
})();
+30
View File
@@ -0,0 +1,30 @@
/**
* 画个标 · 海报工作台:状态与画布规格
* 海报与 LOGO/头像的差异:画布多规格(竖版/方形/横版/长图),保留原始比例,不归一化。
*/
const HGB_PosterStore = (function () {
'use strict';
/** 画布规格(逻辑像素,按 96dpi 近似) */
const SPECS = [
{ id: 'poster', name: '竖版海报', w: 900, h: 1200 },
{ id: 'square', name: '方形', w: 1000, h: 1000 },
{ id: 'wide', name: '横版横幅 16:9', w: 1200, h: 675 },
{ id: 'story', name: '竖版长图 9:16', w: 675, h: 1200 }
];
let state = {
specId: 'poster',
title: '',
sub: '',
desc: '',
plans: [],
activePlan: null
};
function get() { return state; }
function set(patch) { state = Object.assign({}, state, patch); }
function spec() { return SPECS.find(s => s.id === state.specId) || SPECS[0]; }
return { SPECS, get, set, spec };
})();
+142
View File
@@ -0,0 +1,142 @@
/**
* 画个标 · 海报素材库加载器(懒加载)
* 素材库由 scripts/build-posters.js 构建拆分为:
* - js/data/poster-builtin.js 轻量索引(sources 元数据 + 分类索引,无全量内容)
* - js/data/posters/{id}.js 每源一个数据文件(HGB_PosterSource_{id} = {items:[...]})
* 本模块负责:按源懒加载 + 缓存 + 同步读已加载源 + 预加载核心源
*
* 与图标素材库(iconlib.js)的差异:
* - 海报素材为全彩/位图/模板,保留原始宽高比,渲染不再归一化到 24 视口
* - items 字段而非 icons;每条含 type/category/categoryCn/workspaces 归类标签
*/
const HGB_PosterLib = (function () {
'use strict';
const VERSION = '20260823e'; // 与 poster.html 中 poster-builtin.js?v= 版本联动
const cache = {}; // sid -> {items:[...]}
const pending = {}; // sid -> Promise
function fileURL(sid) {
return 'js/data/posters/' + sid + '.js?v=' + VERSION;
}
function loadScript(url) {
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = url;
s.onload = resolve;
s.onerror = function () { reject(new Error('海报素材源加载失败: ' + url)); };
document.head.appendChild(s);
});
}
/** 加载单个源(缓存 + 并发去重) */
function loadSource(sid) {
if (cache[sid]) return Promise.resolve(cache[sid]);
if (pending[sid]) return pending[sid];
pending[sid] = loadScript(fileURL(sid)).then(() => {
const data = window['HGB_PosterSource_' + sid];
if (!data || !data.items) throw new Error('海报素材源不存在: ' + sid);
cache[sid] = data;
delete pending[sid];
return data;
}).catch((e) => {
delete pending[sid];
throw e;
});
return pending[sid];
}
/** 并行加载多个源(全部成功才 resolve) */
function loadSources(ids) {
return Promise.all((ids || []).map(loadSource));
}
/** 并行加载多个源(容忍单个失败,返回已成功加载的) */
function preload(ids) {
return Promise.allSettled((ids || []).map(loadSource)).then((rs) =>
rs.filter(r => r.status === 'fulfilled').map(r => r.value));
}
/** 加载全部源 */
function loadAll() {
const ids = Object.keys((typeof HGB_PosterBuiltin !== 'undefined' ? HGB_PosterBuiltin.sources : {}) || {});
return preload(ids);
}
/** 同步读取已加载源(未加载返回 null) */
function getSource(sid) {
return cache[sid] || null;
}
/** 是否已加载 */
function loaded(sid) {
return !!cache[sid];
}
/** 已加载的全部源 id */
function loadedIds() {
return Object.keys(cache);
}
/** 从已加载源按 名称/id 找素材(返回 {id,name,svg,type,...} 或 null) */
function findPoster(ref) {
if (!ref || typeof ref !== 'string') return null;
const name = ref.trim();
const parts = name.split(':');
if (parts.length === 2) {
const src = cache[parts[0]];
const hit = src && (src.items || []).find(i => i.name === parts[1] || i.id === name);
if (hit) return hit;
}
const lower = name.toLowerCase();
for (const sid of Object.keys(cache)) {
const hit = (cache[sid].items || []).find(i =>
i.id === name || i.name === name ||
String(i.name).toLowerCase() === lower ||
String(i.name).toLowerCase().includes(lower));
if (hit) return hit;
}
return null;
}
/** 搜索已加载源(词根包含匹配),返回完整素材对象 */
function searchLoaded(keyword, limit) {
const kw = String(keyword || '').toLowerCase();
const out = [];
for (const sid of Object.keys(cache)) {
for (const it of cache[sid].items || []) {
if (it.name && it.name.toLowerCase().includes(kw)) {
out.push({ id: sid + ':' + it.name, name: it.name, svg: it.svg, source: sid, type: it.type });
if (out.length >= (limit || 60)) return out;
}
}
}
return out;
}
/** 搜索索引(全量 id/name,无内容,用于筛选展示) */
function searchIndex(keyword, limit) {
const kw = String(keyword || '').toLowerCase();
const out = [];
const idx = (typeof HGB_PosterBuiltin !== 'undefined' && HGB_PosterBuiltin.itemIndex) || [];
for (const it of idx) {
if (it.name && it.name.toLowerCase().includes(kw)) {
out.push({ id: it.id, name: it.name, source: String(it.id).split(':')[0], category: it.category });
if (out.length >= (limit || 20)) return out;
}
}
return out;
}
/** 按 id 反查来源 id */
function sourceOf(fullId) {
return String(fullId || '').split(':')[0];
}
return {
VERSION, loadSource, loadSources, preload, loadAll,
getSource, loaded, loadedIds, findPoster, searchLoaded, searchIndex, sourceOf
};
})();
+284
View File
@@ -0,0 +1,284 @@
/**
* 画个标 · 核心:SVG 渲染引擎
* 职责:将 store 中的元素渲染到画布 SVG,绘制选中框/手柄,支持缩放显示
*/
const HGB_Renderer = (function () {
'use strict';
let svg, defs, contentLayer, overlayLayer;
let zoom = 1; // 当前缩放
const SVG_NS = 'http://www.w3.org/2000/svg';
const MIN_ZOOM = 0.2, MAX_ZOOM = 4;
/* ---------- 初始化 ---------- */
function init() {
svg = document.getElementById('logo-canvas');
// 清空并重建分层
svg.innerHTML = '';
defs = svgCreate('defs');
svg.appendChild(defs);
contentLayer = svgCreate('g');
contentLayer.setAttribute('class', 'hgb-content');
svg.appendChild(contentLayer);
overlayLayer = svgCreate('g');
overlayLayer.setAttribute('class', 'hgb-overlay');
overlayLayer.setAttribute('pointer-events', 'none');
svg.appendChild(overlayLayer);
// 背景
const bgRect = svgCreate('rect');
bgRect.setAttribute('x', 0); bgRect.setAttribute('y', 0);
bgRect.setAttribute('width', HGB_Store.CANVAS_W);
bgRect.setAttribute('height', HGB_Store.CANVAS_H);
bgRect.setAttribute('fill', HGB_Store.get().bg);
bgRect.setAttribute('data-hgb-bg', '1');
contentLayer.appendChild(bgRect);
}
function svgCreate(tag) {
return document.createElementNS(SVG_NS, tag);
}
/**
* 生成圆弧路径 d(环形文字用)
* arc = { cx, cy, r, start, sweep }:圆心 / 半径 / 起始角度(度,0=右) / 扫过角度(度,顺时针)
*/
function arcPathD(arc) {
const a1 = arc.start * Math.PI / 180;
const a2 = (arc.start + arc.sweep) * Math.PI / 180;
const x1 = arc.cx + arc.r * Math.cos(a1);
const y1 = arc.cy + arc.r * Math.sin(a1);
const x2 = arc.cx + arc.r * Math.cos(a2);
const y2 = arc.cy + arc.r * Math.sin(a2);
const large = Math.abs(arc.sweep) > 180 ? 1 : 0;
const sweepFlag = arc.sweep < 0 ? 0 : 1; // sweep-flag=0 逆时针(y向下坐标系中负角走下半弧)
return 'M ' + x1.toFixed(2) + ' ' + y1.toFixed(2) +
' A ' + arc.r + ' ' + arc.r + ' 0 ' + large + ' ' + sweepFlag + ' ' + x2.toFixed(2) + ' ' + y2.toFixed(2);
}
/* ---------- 主渲染 ---------- */
function render() {
const s = HGB_Store.get();
init(); // 简单起见全量重绘
// 背景色同步到 input
const bgInput = document.getElementById('canvas-bg');
if (bgInput && bgInput.value !== s.bg) bgInput.value = s.bg;
// 绘制元素(按数组顺序,后画的在上层)
for (const el of s.elements) {
if (!el.visible) continue;
contentLayer.appendChild(renderElement(el));
}
// 选中框与手柄(overlay 层)
const selIds = s.selectedIds || (s.selectedId ? [s.selectedId] : []);
for (const sid of selIds) {
const el = s.elements.find(e => e.id === sid);
if (!el || !el.visible) continue;
drawSelection(el, sid === s.selectedId);
}
}
/** 渲染单个元素为 SVG 节点 */
function renderElement(el) {
let node;
if (el.type === 'icon') {
node = svgCreate('g');
node.innerHTML = el.svg;
// 应用图标颜色
node.querySelectorAll('path,circle,rect,polygon,polyline,line,ellipse').forEach(p => {
if (el.fill && el.fill !== 'none') p.setAttribute('fill', el.fill);
else p.setAttribute('fill', 'none');
if (el.stroke && el.stroke !== 'none') {
p.setAttribute('stroke', el.stroke);
p.setAttribute('stroke-width', el.strokeWidth);
p.setAttribute('stroke-linecap', 'round');
p.setAttribute('stroke-linejoin', 'round');
} else {
p.setAttribute('stroke', 'none');
}
});
} else if (el.type === 'text') {
node = svgCreate('text');
node.setAttribute('text-anchor', 'middle');
node.setAttribute('dominant-baseline', 'central');
node.setAttribute('font-family', el.fontFamily || 'sans-serif');
node.setAttribute('font-size', el.fontSize || 40);
node.setAttribute('font-weight', el.fontWeight || 400);
if (el.letterSpacing) node.setAttribute('letter-spacing', el.letterSpacing);
node.setAttribute('fill', el.fill || '#000');
if (el.stroke && el.stroke !== 'none') {
node.setAttribute('stroke', el.stroke);
node.setAttribute('stroke-width', el.strokeWidth);
} else {
node.setAttribute('stroke', 'none');
}
if (el.arc) {
// 环形文字:沿圆弧路径排列(品牌名环绕 mark 的排版能力)
const pathId = 'arc_' + el.id;
const p = svgCreate('path');
p.setAttribute('id', pathId);
p.setAttribute('d', arcPathD(el.arc));
p.setAttribute('fill', 'none');
p.setAttribute('stroke', 'none');
defs.appendChild(p);
const tp = svgCreate('textPath');
tp.setAttribute('href', '#' + pathId);
// 居中锚定:text-anchor=middle 配合 startOffset=50%,文字中心落在弧线中点
tp.setAttribute('startOffset', '50%');
tp.textContent = el.text || '';
node.appendChild(tp);
node.removeAttribute('x');
node.removeAttribute('y');
} else {
node.setAttribute('x', el.x + el.w / 2);
node.setAttribute('y', el.y + el.h / 2);
node.textContent = el.text || '';
}
} else {
// 预留:其他类型(图片等)
node = svgCreate('g');
}
// 通用变换:位置 + 旋转
const cx = el.x + el.w / 2;
const cy = el.y + el.h / 2;
if (el.rotation) {
node.setAttribute('transform',
`translate(${cx} ${cy}) rotate(${el.rotation}) translate(${-cx} ${-cy})`);
}
node.setAttribute('opacity', el.opacity != null ? el.opacity : 1);
// 元素定位
node.setAttribute('data-id', el.id);
node.setAttribute('data-type', el.type);
if (el.type === 'icon') {
node.setAttribute('data-icon', el.iconId || '');
}
node.classList.add('hgb-elem');
const s = HGB_Store.get();
const selIds = s.selectedIds || (s.selectedId ? [s.selectedId] : []);
if (selIds.includes(el.id)) node.classList.add('selected');
if (el.locked) node.classList.add('locked');
// 命中测试基于数据层(store),无需 DOM 命中框
node._el = el;
return node;
}
/* ---------- 选中框与手柄 ---------- */
function drawSelection(el, withHandles) {
const box = svgCreate('rect');
box.setAttribute('class', 'hgb-selection' + (withHandles ? '' : ' multi'));
box.setAttribute('x', el.x); box.setAttribute('y', el.y);
box.setAttribute('width', el.w); box.setAttribute('height', el.h);
overlayLayer.appendChild(box);
if (!withHandles) return; // 多选元素仅显示虚线框
// 8 个缩放手柄 + 1 个旋转手柄
const handles = [
{ cx: el.x, cy: el.y, cursor: 'nwse-resize', action: 'nw' },
{ cx: el.x + el.w / 2, cy: el.y, cursor: 'ns-resize', action: 'n' },
{ cx: el.x + el.w, cy: el.y, cursor: 'nesw-resize', action: 'ne' },
{ cx: el.x + el.w, cy: el.y + el.h / 2, cursor: 'ew-resize', action: 'e' },
{ cx: el.x + el.w, cy: el.y + el.h, cursor: 'nwse-resize', action: 'se' },
{ cx: el.x + el.w / 2, cy: el.y + el.h, cursor: 'ns-resize', action: 's' },
{ cx: el.x, cy: el.y + el.h, cursor: 'nesw-resize', action: 'sw' },
{ cx: el.x, cy: el.y + el.h / 2, cursor: 'ew-resize', action: 'w' }
];
for (const h of handles) {
const node = svgCreate('rect');
node.setAttribute('class', 'hgb-handle');
node.setAttribute('x', h.cx - 5); node.setAttribute('y', h.cy - 5);
node.setAttribute('width', 10); node.setAttribute('height', 10);
node.setAttribute('rx', 2);
node.setAttribute('style', 'cursor:' + h.cursor);
node.setAttribute('data-handle', h.action);
node.setAttribute('pointer-events', 'all');
overlayLayer.appendChild(node);
}
// 旋转手柄
const rx = el.x + el.w / 2;
const ry = el.y - 28;
const line = svgCreate('line');
line.setAttribute('class', 'hgb-rotate-line');
line.setAttribute('x1', rx); line.setAttribute('y1', el.y);
line.setAttribute('x2', rx); line.setAttribute('y2', ry);
overlayLayer.appendChild(line);
const rot = svgCreate('circle');
rot.setAttribute('class', 'hgb-handle rotate');
rot.setAttribute('cx', rx); rot.setAttribute('cy', ry);
rot.setAttribute('r', 7);
rot.setAttribute('data-handle', 'rotate');
rot.setAttribute('pointer-events', 'all');
overlayLayer.appendChild(rot);
}
/* ---------- 缩放与显示 ---------- */
function setZoom(z) {
zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z));
applyZoom();
return zoom;
}
function getZoom() { return zoom; }
function zoomIn() { return setZoom(zoom * 1.2); }
function zoomOut() { return setZoom(zoom / 1.2); }
function fitToStage() {
const stage = document.getElementById('canvas-stage');
const size = Math.min(stage.clientWidth - 48, stage.clientHeight - 48);
zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, size / HGB_Store.CANVAS_W));
applyZoom();
return zoom;
}
function applyZoom() {
svg.style.width = (HGB_Store.CANVAS_W * zoom) + 'px';
svg.style.height = (HGB_Store.CANVAS_H * zoom) + 'px';
const lv = document.getElementById('zoom-level');
if (lv) lv.textContent = Math.round(zoom * 100) + '%';
}
/* ---------- 坐标转换(client -> svg user units) ---------- */
function toSvgPoint(clientX, clientY) {
const pt = svg.createSVGPoint();
pt.x = clientX; pt.y = clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
/** 命中测试:返回最上层可交互元素 */
function hitTest(clientX, clientY) {
const s = HGB_Store.get();
const p = toSvgPoint(clientX, clientY);
// 从顶层往下找
for (let i = s.elements.length - 1; i >= 0; i--) {
const el = s.elements[i];
if (!el.visible || el.locked) continue;
if (p.x >= el.x && p.x <= el.x + el.w &&
p.y >= el.y && p.y <= el.y + el.h) {
return el;
}
}
return null;
}
/* ---------- 画布状态 ---------- */
function setCanvasBg(color) {
const bgRect = svg.querySelector('[data-hgb-bg]');
if (bgRect) bgRect.setAttribute('fill', color);
}
return {
init, render, renderElement,
setZoom, getZoom, zoomIn, zoomOut, fitToStage,
toSvgPoint, hitTest, setCanvasBg, svgCreate,
get svg() { return svg; }
};
})();
+466
View File
@@ -0,0 +1,466 @@
/**
* 画个标 · 核心:状态管理(Store)
* 职责:维护设计数据、元素 CRUD、撤销/重做、事件订阅、版本快照
*/
const HGB_Store = (function () {
'use strict';
const CANVAS_W = 600;
const CANVAS_H = 600;
const MAX_HISTORY = 60;
const LS_KEY = 'hgb_design';
const LS_VERSION_KEY = 'hgb_versions';
/* ---------- 默认项目 ---------- */
function defaultProject() {
return {
name: '我的品牌',
bg: '#FFFFFF',
elements: [
{
id: uid(),
type: 'icon',
name: '学士帽',
iconId: 'graduation-cap',
svg: iconSvg('graduation-cap'),
x: 210, y: 150, w: 180, h: 180,
rotation: 0,
fill: 'none',
stroke: '#1B4F72',
strokeWidth: 2,
opacity: 1,
visible: true, locked: false
},
{
id: uid(),
type: 'text',
name: '品牌中文名',
text: '远渡教育',
x: 130, y: 360, w: 340, h: 64,
rotation: 0,
fontSize: 48,
fontWeight: 700,
fontFamily: 'SimHei',
letterSpacing: 8,
fill: '#1B4F72',
stroke: 'none',
strokeWidth: 0,
opacity: 1,
visible: true, locked: false
},
{
id: uid(),
type: 'text',
name: '品牌英文名',
text: 'FAR CROSS EDUCATION',
x: 130, y: 430, w: 340, h: 32,
rotation: 0,
fontSize: 18,
fontWeight: 400,
fontFamily: 'Georgia',
letterSpacing: 4,
fill: '#2980B9',
stroke: 'none',
strokeWidth: 0,
opacity: 1,
visible: true, locked: false
}
],
selectedId: null,
selectedIds: [],
currentStyle: null
};
}
/* ---------- 工具 ---------- */
function uid() {
return 'el_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
function iconSvg(iconId) {
// 行业模块图标 + 基础图形
for (const ind of HGB_Industries) {
const ic = (ind.icons || []).find(i => i.id === iconId);
if (ic) return ic.svg;
}
const ic = HGB_Icons.shapes.find(i => i.id === iconId);
return ic ? ic.svg : '';
}
/* ---------- 状态 ---------- */
let state = defaultProject();
let history = [];
let historyIdx = -1;
let listeners = {};
/* ---------- 历史记录 ---------- */
function snapshot() {
return JSON.parse(JSON.stringify(state));
}
function pushHistory() {
// 移除 redo 分支
history = history.slice(0, historyIdx + 1);
history.push(snapshot());
if (history.length > MAX_HISTORY) history.shift();
historyIdx = history.length - 1;
emit('history');
}
function undo() {
if (historyIdx <= 0) return false;
historyIdx--;
state = JSON.parse(JSON.stringify(history[historyIdx]));
emit('change');
return true;
}
function redo() {
if (historyIdx >= history.length - 1) return false;
historyIdx++;
state = JSON.parse(JSON.stringify(history[historyIdx]));
emit('change');
return true;
}
function canUndo() { return historyIdx > 0; }
function canRedo() { return historyIdx < history.length - 1; }
/* ---------- 元素操作 ---------- */
function addElement(el) {
state.elements.push(el);
state.selectedId = el.id;
pushHistory();
emit('change');
return el;
}
function updateElement(id, patch, record = true) {
const el = state.elements.find(e => e.id === id);
if (!el) return;
Object.assign(el, patch);
if (record) pushHistory();
emit('change');
}
function removeElement(id) {
state.elements = state.elements.filter(e => e.id !== id);
if (state.selectedId === id) state.selectedId = null;
pushHistory();
emit('change');
}
/** 载入智能方案(替换全部元素与背景) */
function loadPlan(plan) {
state.elements = (plan.elements || []).map(e => ({ ...e }));
state.bg = plan.bg || '#FFFFFF';
state.selectedId = null;
pushHistory();
emit('change');
}
function duplicateElement(id) {
const src = state.elements.find(e => e.id === id);
if (!src) return null;
const copy = JSON.parse(JSON.stringify(src));
copy.id = uid();
copy.name = src.name + ' 副本';
copy.x += 24; copy.y += 24;
// 副本放置到顶层
state.elements.push(copy);
state.selectedId = copy.id;
pushHistory();
emit('change');
return copy;
}
function select(id, additive) {
if (additive && id != null) {
// Shift 多选:切换选中
if (state.selectedIds.includes(id)) {
state.selectedIds = state.selectedIds.filter(x => x !== id);
state.selectedId = state.selectedIds.length ? state.selectedIds[state.selectedIds.length - 1] : null;
} else {
state.selectedIds.push(id);
state.selectedId = id;
}
} else {
state.selectedIds = id != null ? [id] : [];
state.selectedId = id;
}
emit('select');
emit('change');
}
/** 当前所有选中元素 */
function selectedElements() {
return state.selectedIds.map(id => state.elements.find(e => e.id === id)).filter(Boolean);
}
/** 移动全部选中元素 */
function moveSelection(dx, dy) {
for (const el of selectedElements()) {
el.x = Math.round(el.x + dx);
el.y = Math.round(el.y + dy);
}
emit('change');
}
/** 对齐全部选中元素 */
function alignSelection(mode) {
const els = selectedElements();
if (els.length < 2) return;
const xs = els.map(e => e.x), xes = els.map(e => e.x + e.w);
const ys = els.map(e => e.y), yes = els.map(e => e.y + e.h);
const minX = Math.min(...xs), maxX = Math.max(...xes);
const minY = Math.min(...ys), maxY = Math.max(...yes);
const boxCx = (minX + maxX) / 2, boxCy = (minY + maxY) / 2;
for (const el of els) {
if (mode === 'left') el.x = minX;
else if (mode === 'center') el.x = Math.round(boxCx - el.w / 2);
else if (mode === 'right') el.x = Math.round(maxX - el.w);
else if (mode === 'top') el.y = minY;
else if (mode === 'middle') el.y = Math.round(boxCy - el.h / 2);
else if (mode === 'bottom') el.y = Math.round(maxY - el.h);
}
pushHistory(); emit('change');
}
/** 复制全部选中元素 */
function duplicateSelection() {
const srcs = selectedElements();
if (!srcs.length) return 0;
for (const src of srcs) {
const copy = JSON.parse(JSON.stringify(src));
copy.id = uid();
copy.name = src.name + ' 副本';
copy.x += 24; copy.y += 24;
state.elements.push(copy);
state.selectedIds.push(copy.id);
state.selectedId = copy.id;
}
pushHistory(); emit('change');
return srcs.length;
}
/** 粘贴剪贴板元素(生成新副本并选中) */
function pasteElements(list) {
if (!list || !list.length) return [];
const newEls = list.map(src => {
const copy = JSON.parse(JSON.stringify(src));
copy.id = uid();
copy.name = src.name + ' 副本';
copy.x += 24; copy.y += 24;
state.elements.push(copy);
state.selectedIds.push(copy.id);
state.selectedId = copy.id;
return copy;
});
pushHistory(); emit('change');
return newEls;
}
/** 删除全部选中元素 */
function removeSelection() {
if (!state.selectedIds.length) return 0;
const ids = state.selectedIds.slice();
state.elements = state.elements.filter(e => !ids.includes(e.id));
state.selectedIds = [];
state.selectedId = null;
pushHistory(); emit('change');
return ids.length;
}
/* ---------- 图层排序 ---------- */
// elements 数组顺序即显示顺序(后画在上层)
function layerUp(id) {
const i = state.elements.findIndex(e => e.id === id);
if (i < 0 || i >= state.elements.length - 1) return;
[state.elements[i], state.elements[i + 1]] = [state.elements[i + 1], state.elements[i]];
pushHistory(); emit('change');
}
function layerDown(id) {
const i = state.elements.findIndex(e => e.id === id);
if (i <= 0) return;
[state.elements[i], state.elements[i - 1]] = [state.elements[i - 1], state.elements[i]];
pushHistory(); emit('change');
}
function layerFront(id) {
const el = state.elements.find(e => e.id === id);
if (!el) return;
state.elements = state.elements.filter(e => e.id !== id);
state.elements.push(el);
pushHistory(); emit('change');
}
function layerBack(id) {
const el = state.elements.find(e => e.id === id);
if (!el) return;
state.elements = state.elements.filter(e => e.id !== id);
state.elements.unshift(el);
pushHistory(); emit('change');
}
/** 拖拽图层:将 fromIndex 元素移动到 toIndex 位置(占据目标位置,其余元素顺移) */
function moveLayer(fromIndex, toIndex) {
const len = state.elements.length;
if (fromIndex < 0 || fromIndex >= len || toIndex < 0 || toIndex >= len) return;
if (fromIndex === toIndex) return;
const [el] = state.elements.splice(fromIndex, 1);
state.elements.splice(toIndex, 0, el);
pushHistory(); emit('change');
}
/* ---------- 风格应用 ---------- */
function applyStyle(style) {
state.currentStyle = style.id;
state.bg = style.bg;
for (const el of state.elements) {
if (el.type === 'icon') {
el.stroke = style.iconStroke;
el.strokeWidth = style.strokeWidth;
} else if (el.type === 'text') {
el.fill = style.colors.primary;
if (el.fontFamily === 'SimHei' || el.fontFamily === 'Microsoft YaHei' || el.fontFamily === '黑体') {
el.fontFamily = style.fontCn;
}
}
}
pushHistory();
emit('change');
emit('style');
}
/* ---------- 配色应用 ---------- */
function applyPalette(pal) {
let idx = 0;
for (const el of state.elements) {
if (el.type === 'icon') {
el.stroke = pal.colors[Math.min(idx, 1)];
idx++;
} else if (el.type === 'text') {
el.fill = pal.colors[0];
}
}
pushHistory();
emit('change');
emit('style');
}
/* ---------- 字体应用 ---------- */
function applyFont(family, web) {
const el = selected();
if (!el || el.type !== 'text') return;
if (web) HGB_Fonts.loadWebFont(family);
updateElement(el.id, { fontFamily: family });
}
/* ---------- 版本快照 ---------- */
function saveVersion() {
const versions = loadVersions();
const v = {
id: uid(),
time: Date.now(),
name: state.name || '未命名设计',
snapshot: snapshot()
};
versions.unshift(v);
saveVersions(versions);
emit('versions');
return v;
}
function loadVersions() {
try {
return JSON.parse(localStorage.getItem(LS_VERSION_KEY)) || [];
} catch (e) { return []; }
}
function saveVersions(list) {
localStorage.setItem(LS_VERSION_KEY, JSON.stringify(list.slice(0, 50)));
}
function restoreVersion(id) {
const v = loadVersions().find(x => x.id === id);
if (!v) return false;
state = JSON.parse(JSON.stringify(v.snapshot));
pushHistory();
emit('change');
return true;
}
function deleteVersion(id) {
saveVersions(loadVersions().filter(x => x.id !== id));
emit('versions');
}
function clearVersions() {
saveVersions([]);
emit('versions');
}
/* ---------- 事件订阅 ---------- */
function on(evt, fn) {
(listeners[evt] = listeners[evt] || []).push(fn);
}
function emit(evt) {
(listeners[evt] || []).forEach(fn => fn());
}
/* ---------- 选择器 ---------- */
function get() { return state; }
function selected() {
return state.elements.find(e => e.id === state.selectedId) || null;
}
function selectedIndex() {
return state.elements.findIndex(e => e.id === state.selectedId);
}
/* ---------- 本地持久化 ---------- */
function persistToLocal() {
try {
const data = { ...snapshot(), savedAt: Date.now() };
localStorage.setItem(LS_KEY, JSON.stringify(data));
return true;
} catch (e) { return false; }
}
function loadFromLocal() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return false;
const data = JSON.parse(raw);
if (data && Array.isArray(data.elements)) {
state = data;
// 迁移遗留演示默认名:历史版本默认项目名「我的留学教育品牌」会污染各行业生成时的品牌回退
if (state.name === '我的留学教育品牌') state.name = '我的品牌';
history = [snapshot()];
historyIdx = 0;
return true;
}
} catch (e) { /* ignore */ }
return false;
}
/* ---------- 初始化 ---------- */
function init(restoreLocal = true) {
if (restoreLocal && loadFromLocal()) return;
state = defaultProject();
history = [snapshot()];
historyIdx = 0;
}
return {
CANVAS_W, CANVAS_H,
get, selected, selectedIndex,
selectedElements, moveSelection, alignSelection,
duplicateSelection, removeSelection, pasteElements,
addElement, updateElement, removeElement, duplicateElement, loadPlan, select,
layerUp, layerDown, layerFront, layerBack, moveLayer,
applyStyle, applyPalette, applyFont,
undo, redo, canUndo, canRedo, pushHistory,
saveVersion, loadVersions, restoreVersion, deleteVersion, clearVersions,
persistToLocal, loadFromLocal, init,
on, emit, uid, iconSvg
};
})();
File diff suppressed because one or more lines are too long
+159
View File
@@ -0,0 +1,159 @@
/**
* 画个标 · 装饰图形系统
* 落地 GitHub logo-design-skill 专业设计方法论:专业 LOGO 的"设计感"来自
* 图形层次 —— 双线环、点状环绕、星点、角括号、弧线、绶带、几何衬底(容器)。
* 所有装饰原语为 24x24 viewBox 字符串,作为 icon 元素渲染。
* fill=true 表示实心填充型(无描边,可用于容器/色块);fill=false 表示描边型。
*/
const HGB_Decorations = (function () {
'use strict';
let seq = 0;
function uid() { return 'dc' + Date.now().toString(36) + (seq++).toString(36); }
/** 颜色亮化:amt 0~1(值越大越接近白色) */
function lighten(hex, amt) {
const c = String(hex || '#888888').replace('#', '');
if (c.length !== 6) return '#cccccc';
const f = (v) => Math.round(v + (255 - v) * Math.min(1, Math.max(0, amt))).toString(16).padStart(2, '0');
return '#' + f(parseInt(c.slice(0, 2), 16)) + f(parseInt(c.slice(2, 4), 16)) + f(parseInt(c.slice(4, 6), 16));
}
/** 颜色加深:amt 0~1 */
function shade(hex, amt) {
const c = String(hex || '#888888').replace('#', '');
if (c.length !== 6) return '#444444';
const f = (v) => Math.round(v * (1 - Math.min(1, Math.max(0, amt)))).toString(16).padStart(2, '0');
return '#' + f(parseInt(c.slice(0, 2), 16)) + f(parseInt(c.slice(2, 4), 16)) + f(parseInt(c.slice(4, 6), 16));
}
/** 判断颜色明暗(返回 true 表示偏深色) */
function isDark(hex) {
const c = String(hex || '#FFFFFF').replace('#', '');
if (c.length !== 6) return false;
const r = parseInt(c.slice(0, 2), 16), g = parseInt(c.slice(2, 4), 16), b = parseInt(c.slice(4, 6), 16);
return (0.2126 * r + 0.7152 * g + 0.0722 * b) < 150;
}
/**
* 装饰原语库
*/
const DECO_KIT = {
/* ---- 环绕/边框类(描边型) ---- */
'deco-dotring': { name: '点状环绕', fill: true, sw: 0, opacity: 0.9,
svg: '<circle cx="12" cy="2" r="1.15"/><circle cx="17.07" cy="4.93" r="1.15"/><circle cx="19.92" cy="10" r="1.15"/><circle cx="19.92" cy="14" r="1.15"/><circle cx="17.07" cy="19.07" r="1.15"/><circle cx="12" cy="22" r="1.15"/><circle cx="6.93" cy="19.07" r="1.15"/><circle cx="4.08" cy="14" r="1.15"/><circle cx="4.08" cy="10" r="1.15"/><circle cx="6.93" cy="4.93" r="1.15"/>' },
'deco-rayring': { name: '射线环', fill: false, sw: 2, opacity: 0.95,
svg: (function () { let p = ''; for (let i = 0; i < 12; i++) { const a = i * 30 * Math.PI / 180, c = Math.cos(a), s = Math.sin(a); p += 'M' + (12 + 8 * c).toFixed(2) + ' ' + (12 + 8 * s).toFixed(2) + 'L' + (12 + 11.5 * c).toFixed(2) + ' ' + (12 + 11.5 * s).toFixed(2); } return p; })() },
'deco-doublering': { name: '双线环', fill: false, sw: 4, opacity: 0.85,
svg: '<circle cx="12" cy="12" r="10.5"/><circle cx="12" cy="12" r="7"/>' },
'deco-arc-top': { name: '上弧形', fill: false, sw: 5, opacity: 0.9,
svg: '<path d="M2 9 A 14 14 0 0 1 22 9"/>' },
'deco-arc-bottom': { name: '下弧形', fill: false, sw: 5, opacity: 0.9,
svg: '<path d="M2 15 A 14 14 0 0 0 22 15"/>' },
'deco-corner-tl': { name: '左上角括号', fill: false, sw: 5, opacity: 0.95,
svg: '<path d="M4 11 V4 H11"/>' },
'deco-corner-br': { name: '右下角括号', fill: false, sw: 5, opacity: 0.95,
svg: '<path d="M20 13 V20 H13"/>' },
'deco-stars': { name: '星点散落', fill: true, sw: 0, opacity: 0.9,
svg: '<path d="M6 4l.9 2.2L9 7l-2.1.9L6 10l-.9-2.1L3 7l2.1-.9z"/><path d="M18 14l.8 1.9 1.9.8-1.9.8-.8 1.9-.8-1.9-1.9-.8 1.9-.8z"/><path d="M16 3.5l.6 1.4 1.4.6-1.4.6-.6 1.4-.6-1.4-1.4-.6 1.4-.6z"/>' },
'deco-divider': { name: '菱形分隔线', fill: true, sw: 0, opacity: 0.95,
svg: '<rect x="2" y="11" width="7" height="2" rx="1"/><path d="M12 8.5l2.6 3.5-2.6 3.5-2.6-3.5z"/><rect x="15" y="11" width="7" height="2" rx="1"/>' },
'deco-chevron': { name: '人字饰带', fill: false, sw: 5, opacity: 0.9,
svg: '<path d="M5 6l6 6-6 6"/><path d="M13 6l6 6-6 6"/>' },
'deco-tri-corner': { name: '三角角标', fill: true, sw: 0, opacity: 0.9,
svg: '<path d="M12 3L21 21H3z"/>' },
'deco-line': { name: '细分隔线', fill: true, sw: 0, opacity: 0.9,
svg: '<rect x="0" y="11" width="24" height="2" rx="1"/>' },
'deco-spark': { name: '光芒四射', fill: true, sw: 0, opacity: 0.9,
svg: '<path d="M12 2l1.8 6.2L20 10l-6.2 1.8L12 18l-1.8-6.2L4 10l6.2-1.8z"/>' },
'deco-laurel-l': { name: '左月桂叶', fill: false, sw: 5, opacity: 0.9,
svg: '<path d="M14 21a8 8 0 0 1-8-8M14 21c-1 0-2-3-2-8s1-8 2-8M6 13a6 6 0 0 1 3-6c2-1 3 1 3 4"/>' },
'deco-laurel-r': { name: '右月桂叶', fill: false, sw: 5, opacity: 0.9,
svg: '<path d="M10 21a8 8 0 0 0 8-8M10 21c1 0 2-3 2-8s-1-8-2-8M18 13a6 6 0 0 0-3-6c-2-1-3 1-3 4"/>' },
/* ---- 实心几何衬底(容器,图标徽章化) ---- */
'deco-shape-circle': { name: '圆形衬底', fill: true, sw: 0, opacity: 1,
svg: '<circle cx="12" cy="12" r="11"/>' },
'deco-shape-squircle':{ name: '圆角方形衬底', fill: true, sw: 0, opacity: 1,
svg: '<rect x="1.6" y="1.6" width="20.8" height="20.8" rx="5"/>' },
'deco-shape-hexagon': { name: '六边形衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M12 1.5L21.3 6.75v10.5L12 22.5 2.7 17.25V6.75z"/>' },
'deco-shape-star': { name: '星形衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M12 1.6l3.2 6.5 7.2 1-5.2 5 1.2 7.1L12 17.9l-6.4 3.3 1.2-7.1-5.2-5 7.2-1z"/>' },
'deco-shape-shield': { name: '盾形衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M12 1.8L20.5 5v7c0 4.6-3.5 8.2-8.5 10.2C7 20.2 3.5 16.6 3.5 12V5z"/>' },
'deco-shape-diamond': { name: '菱形衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M12 1.5L22.5 12 12 22.5 1.5 12z"/>' },
'deco-shape-triangle': { name: '三角衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M12 1.6 L22.4 21.5 H1.6 z"/>' },
'deco-shape-arch': { name: '拱形衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M2 24V11a10 10 0 0 1 20 0v13z"/>' },
'deco-shape-pill': { name: '胶囊衬底', fill: true, sw: 0, opacity: 1,
svg: '<rect x="1.6" y="7" width="20.8" height="10" rx="5"/>' },
'deco-shape-ribbon': { name: '绶带横幅', fill: true, sw: 0, opacity: 1,
svg: '<path d="M2 5h20v11h-8.4l-1.6 3.4L10.4 16H2z"/>' },
'deco-shape-heart': { name: '爱心衬底', fill: true, sw: 0, opacity: 1,
svg: '<path d="M12 21s-8.5-5-10.5-10C-.5 6.5 3 3 6.5 3c2 0 3.6 1 5.5 3 1.9-2 3.5-3 5.5-3C21 3 24.5 6.5 22.5 11 20.5 16 12 21 12 21z"/>' },
/* ---- 分屏/背景面板(填充型,铺满画布) ---- */
'deco-panel-left': { name: '左半色板', fill: true, sw: 0, opacity: 1,
svg: '<rect x="0" y="0" width="12" height="24"/>' },
'deco-panel-diag': { name: '对角色板', fill: true, sw: 0, opacity: 1,
svg: '<path d="M0 0h16L6 24H0z"/>' },
'deco-panel-band': { name: '横向色带', fill: true, sw: 0, opacity: 1,
svg: '<rect x="0" y="10" width="24" height="4.4"/>' },
'deco-panel-barc': { name: '底边色块', fill: true, sw: 0, opacity: 1,
svg: '<path d="M0 16h24v8H0z"/>' }
};
/**
* 构造装饰元素(同 icon 元素结构,渲染走 applyIconStyle)
* @param {string} id DECO_KIT 键名
* @param {number} x 左上角 x
* @param {number} y 左上角 y
* @param {number} w 宽
* @param {number} h 高
* @param {string} color 填充/描边色
* @param {object} grad {id,type,from,to} 渐变对象(有则覆盖纯色)
*/
function decoEl(id, x, y, w, h, color, grad) {
const def = DECO_KIT[id];
if (!def) return null;
const fill = grad ? 'url(#' + grad.id + ')' : (def.fill ? color : 'none');
return {
id: uid(), type: 'icon', name: def.name, iconId: id,
svg: def.svg, x, y, w, h, rotation: 0,
fill: fill,
stroke: def.fill ? 'none' : color,
strokeWidth: def.sw || 4,
grad: grad || null,
opacity: def.opacity != null ? def.opacity : 1,
visible: true, locked: false
};
}
/**
* 按风格返回装饰组合(供布局选择参考;布局也可自选具体原语)
* @param {string} styleId 风格 id
* @param {object} palette 配色 {colors:[主,辅,点,背景]}
* @returns {{dec:Array, decoColor:string, grad:object}}
*/
function buildDecoSet(styleId, palette) {
const colors = (palette && palette.colors) || ['#2563EB', '#3B82F6', '#F59E0B', '#FFFFFF'];
const decoColor = lighten(colors[1] || colors[0], 0.42); // 浅色点缀,低对比
const grad = {
id: 'grad-main' + (++seq),
type: 'linear',
from: colors[0],
to: lighten(colors[0], 0.28)
};
const map = {
minimal: ['deco-doublering'],
gradient: ['deco-dotring'],
vintage: ['deco-arc-top', 'deco-arc-bottom'],
tech: ['deco-corner-tl', 'deco-corner-br'],
playful: ['deco-stars'],
luxury: ['deco-divider'],
flat: ['deco-shape-circle']
};
return { dec: map[styleId] || [], decoColor, grad };
}
return { decoEl, buildDecoSet, lighten, shade, isDark, DECO_KIT };
})();
+62
View File
@@ -0,0 +1,62 @@
/**
* 画个标 · 留学主题字体库
* 包含中文/英文排版推荐 + 可选的 Google Fonts 网络加载
*/
const HGB_Fonts = {
groups: [
{
name: '中文 · 正式',
items: [
{ family: 'SimSun', cn: '宋体', web: false, desc: '经典衬线,典雅学术' },
{ family: 'SimHei', cn: '黑体', web: false, desc: '现代无衬线,稳重清晰' },
{ family: 'KaiTi', cn: '楷体', web: false, desc: '手写感,亲和人文' },
{ family: 'Microsoft YaHei', cn: '微软雅黑', web: false, desc: '屏幕友好,现代通用' }
]
},
{
name: '英文 · 衬线',
items: [
{ family: 'Georgia', cn: 'Georgia', web: false, desc: '经典衬线,传统名校' },
{ family: 'Times New Roman', cn: 'Times', web: false, desc: '报刊气质,学术规范' },
{ family: 'Playfair Display', cn: 'Playfair', web: true, desc: '高端时尚衬线(网络加载)' }
]
},
{
name: '英文 · 无衬线',
items: [
{ family: 'Arial', cn: 'Arial', web: false, desc: '清晰通用,商务首选' },
{ family: 'Verdana', cn: 'Verdana', web: false, desc: '宽字距,易读性强' },
{ family: 'Montserrat', cn: 'Montserrat', web: true, desc: '现代几何感(网络加载)' },
{ family: 'Poppins', cn: 'Poppins', web: true, desc: '圆润时尚(网络加载)' }
]
},
{
name: '英文 · 装饰',
items: [
{ family: 'Impact', cn: 'Impact', web: false, desc: '粗壮有力,标题冲击' },
{ family: 'Courier New', cn: 'Courier', web: false, desc: '等宽打字机,复古文艺' }
]
}
]
};
/** 动态加载 Google Fonts(网络可用时) */
HGB_Fonts.loadWebFont = function (family) {
const id = 'hgb-font-' + family.replace(/[^a-zA-Z0-9]/g, '');
if (document.getElementById(id)) return;
const link = document.createElement('link');
link.id = id;
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?family=' +
encodeURIComponent(family.replace(/ /g, '+')) + ':wght@400;700;900&display=swap';
document.head.appendChild(link);
};
/** 按 family 查找字体项 */
HGB_Fonts.find = function (family) {
for (const g of this.groups) {
const f = g.items.find(i => i.family === family);
if (f) return f;
}
return null;
};
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
/**
* 画个标 · 留学教育元素库 & 基础图形库
* 所有图标统一 24x24 viewBox,线性风格(stroke-based)
*/
const HGB_Icons = {
/* ========== 留学教育元素(第一专项模块) ========== */
edu: [
{
id: 'graduation-cap',
name: '学士帽',
svg: '<polygon points="12 3 1.5 8 12 13 22.5 8 12 3"/><path d="M6 10.6v4.4c0 2.1 2.7 3.6 6 3.6s6-1.5 6-3.6v-4.4"/><line x1="12" y1="13" x2="12" y2="18.6"/><circle cx="12" cy="19.4" r="0.8"/>',
desc: '毕业与学术的经典象征'
},
{
id: 'open-book',
name: '书籍',
svg: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>',
desc: '知识、学习与阅读'
},
{
id: 'globe',
name: '地球仪',
svg: '<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
desc: '国际化与全球视野'
},
{
id: 'diploma',
name: '毕业文凭',
svg: '<circle cx="12" cy="8" r="6"/><polyline points="15.48 12.89 17 22 12 19 7 22 8.52 12.89"/>',
desc: '学位证书与荣誉'
},
{
id: 'plane',
name: '留学航线',
svg: '<path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/>',
desc: '出国与远行'
},
{
id: 'lighthouse',
name: '灯塔',
svg: '<polygon points="12 2 9.5 5 14.5 5 12 2"/><rect x="9" y="5" width="6" height="5" rx="0.6"/><path d="M8.5 10h7l1.2 4h-9.4z"/><path d="M7.5 14h9l1 8h-11z"/><circle cx="12" cy="7.5" r="1.1"/>',
desc: '指引与方向'
},
{
id: 'pen',
name: '钢笔',
svg: '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>',
desc: '写作与学术'
},
{
id: 'landmark',
name: '学府建筑',
svg: '<line x1="3" y1="22" x2="21" y2="22"/><line x1="6" y1="18" x2="6" y2="11"/><line x1="10" y1="18" x2="10" y2="11"/><line x1="14" y1="18" x2="14" y2="11"/><line x1="18" y1="18" x2="18" y2="11"/><polygon points="12 2 20 7 4 7"/>',
desc: '校园与学院'
},
{
id: 'compass',
name: '指南针',
svg: '<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>',
desc: '探索与方向'
},
{
id: 'key',
name: '未来之匙',
svg: '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>',
desc: '开启未来之门'
},
{
id: 'star-grad',
name: '五星奖章',
svg: '<circle cx="12" cy="12" r="10"/><polygon points="12 5.5 13.8 9.3 18 9.8 15 12.6 15.8 16.8 12 14.7 8.2 16.8 9 12.6 6 9.8 10.2 9.3"/>',
desc: '优秀与卓越'
},
{
id: 'lightbulb',
name: '灵感之灯',
svg: '<path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1V17h6v-.2c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2z"/>',
desc: '创意与灵感'
}
],
/* ========== 基础图形 ========== */
shapes: [
{ id: 'circle', name: '圆形', svg: '<circle cx="12" cy="12" r="9"/>', desc: '圆润、包容' },
{ id: 'square', name: '方形', svg: '<rect x="3.5" y="3.5" width="17" height="17" rx="1"/>', desc: '稳重、可靠' },
{ id: 'rounded', name: '圆角方', svg: '<rect x="3.5" y="5" width="17" height="14" rx="4"/>', desc: '现代、亲和' },
{ id: 'hexagon', name: '六边形', svg: '<polygon points="12 2.5 20.5 7.25 20.5 16.75 12 21.5 3.5 16.75 3.5 7.25"/>', desc: '科技、独特' },
{ id: 'triangle', name: '三角', svg: '<polygon points="12 3 22 20 2 20"/>', desc: '进取、上升' },
{ id: 'diamond', name: '菱形', svg: '<polygon points="12 2.5 21.5 12 12 21.5 2.5 12"/>', desc: '优雅、精致' },
{ id: 'star', name: '五角星', svg: '<polygon points="12 2.8 14.7 8.7 21.2 9.5 16.5 14 17.6 20.4 12 17.2 6.4 20.4 7.5 14 2.8 9.5 9.3 8.7"/>', desc: '卓越、闪耀' },
{ id: 'heart', name: '爱心', svg: '<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z"/>', desc: '关爱、热情' },
{ id: 'shield', name: '盾牌', svg: '<path d="M12 2l8 3v6c0 5-3.5 9.5-8 11-4.5-1.5-8-6-8-11V5z"/>', desc: '保障、专业' },
{ id: 'ring', name: '圆环', svg: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4.5"/>', desc: '环环相扣' },
{ id: 'seal', name: '印章圆', svg: '<circle cx="12" cy="12" r="9.5"/><circle cx="12" cy="12" r="6.5"/>', desc: '认证、官方' },
{ id: 'arch', name: '拱门', svg: '<path d="M4 21v-9a8 8 0 0 1 16 0v9"/><path d="M8 21v-9a4 4 0 0 1 8 0v9"/>', desc: '经典、学术' }
]
};
+169
View File
@@ -0,0 +1,169 @@
/**
* 画个标 · 垂直行业模块库
* 每个行业包含:专属元素库、推荐配色方案、推荐风格与字体
* 行业元素统一 24x24 viewBox 线性风格
*/
const HGB_Industries = [
/* ================= 留学教育 ================= */
{
id: 'edu',
name: '留学教育',
emoji: '🎓',
desc: '学士帽、书籍、地球仪等教育视觉元素',
icons: HGB_Icons.edu,
palettes: [
{ name: '学术蓝', desc: '沉稳专业,国际教育主流', colors: ['#1B4F72', '#2980B9', '#AED6F1', '#F4F6F7'], scene: '留学中介' },
{ name: '常春藤绿', desc: '常春藤盟校气质', colors: ['#1E8449', '#27AE60', '#D5F5E3', '#FFFFFF'], scene: '本科留学' },
{ name: '皇家金', desc: '学历含金量与荣耀', colors: ['#7D6608', '#D4AC0D', '#F9E79F', '#FEF9E7'], scene: '商学院' },
{ name: '梦想天蓝', desc: '清新明亮,追梦活力', colors: ['#2471A3', '#5DADE2', '#EBF5FB', '#FFFFFF'], scene: '语言培训' }
],
styleIds: ['minimal', 'badge', 'vintage'],
fontFamilies: ['SimSun', 'Georgia', 'Playfair Display', 'SimHei']
},
/* ================= 餐饮美食 ================= */
{
id: 'food',
name: '餐饮美食',
emoji: '🍽️',
desc: '厨师帽、咖啡、汉堡等餐饮元素',
icons: [
{ id: 'chef-hat', name: '厨师帽', svg: '<path d="M17 21a1 1 0 0 0 1-1v-5.35c0-.457.316-.844.727-1.041a4 4 0 0 0-2.134-7.589 5 5 0 0 0-9.186 0 4 4 0 0 0-2.134 7.588c.411.198.727.585.727 1.041V20a1 1 0 0 0 1 1Z"/><path d="M6 17h12"/>', desc: '主厨与专业烹饪' },
{ id: 'coffee', name: '咖啡杯', svg: '<path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/>', desc: '咖啡厅与休闲时光' },
{ id: 'utensils', name: '刀叉餐具', svg: '<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/>', desc: '用餐与精致餐饮' },
{ id: 'burger', name: '汉堡', svg: '<path d="M3 11a9 9 0 0 1 18 0z"/><path d="M4 13.5h16"/><path d="M4.5 15.5h15"/><path d="M5 16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2z"/>', desc: '快餐与欢乐' },
{ id: 'wine', name: '高脚杯', svg: '<path d="M8 22h8"/><path d="M7 10h10"/><path d="M12 15v7"/><path d="M12 15a5 5 0 0 0 5-5c0-2-.8-4-2-6H9c-1.2 2-2 4-2 6a5 5 0 0 0 5 5z"/>', desc: '品酒与格调' },
{ id: 'cake', name: '蛋糕', svg: '<circle cx="12" cy="18" r="4"/><path d="M12 14c3.3 0 6-2.2 6-5 0 0-2.5 1-3.5-1.5C13.5 5 14 3 12 3s-1.5 2-2.5 4.5C8.5 10 6 9 6 9c0 2.8 2.7 5 6 5z"/>', desc: '甜品与庆典' },
{ id: 'soup', name: '热汤碗', svg: '<path d="M4 11h16a1 1 0 0 1 .5 1.87c-.8.4-1.3 1.3-1.3 2.13V17a3 3 0 0 1-3 3h-8a3 3 0 0 1-3-3v-2c0-.83-.5-1.73-1.3-2.13A1 1 0 0 1 4 11Z"/><path d="M8 10V9"/><path d="M16 10V9"/><path d="M12 10V9"/>', desc: '暖汤与养生' },
{ id: 'icecream', name: '冰淇淋', svg: '<path d="M12 2a5 5 0 0 1 5 5c0 3-1 5-2 8H9c-1-3-2-5-2-8a5 5 0 0 1 5-5z"/><path d="M9.5 17h5"/>', desc: '清凉与甜蜜' }
],
palettes: [
{ name: '香橙暖阳', desc: '热情食欲,餐饮首选', colors: ['#E67E22', '#F39C12', '#FDEBD0', '#FFF9F0'], scene: '中西餐厅' },
{ name: '红酒典雅', desc: '高格调,成熟稳重', colors: ['#7B241C', '#C0392B', '#F5B7B1', '#FDF2F2'], scene: '西餐酒吧' },
{ name: '抹茶清新', desc: '自然清新,健康轻食', colors: ['#1E8449', '#7DCEA0', '#E9F7EF', '#FFFFFF'], scene: '轻食甜品' },
{ name: '咖啡醇香', desc: '温暖醇厚,休闲空间', colors: ['#6F4E37', '#A67B5B', '#DCC7AA', '#F7F1E5'], scene: '咖啡烘焙' }
],
styleIds: ['cartoon', 'hand', 'badge'],
fontFamilies: ['Microsoft YaHei', 'Poppins', 'KaiTi', 'Verdana']
},
/* ================= 医疗健康 ================= */
{
id: 'health',
name: '医疗健康',
emoji: '⚕️',
desc: '十字、心跳、药丸等健康元素',
icons: [
{ id: 'medical-cross', name: '医疗十字', svg: '<circle cx="12" cy="12" r="10"/><path d="M12 8v8"/><path d="M8 12h8"/>', desc: '医疗与急救' },
{ id: 'heart-pulse', name: '心跳', svg: '<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z"/><path d="M7 12h3l1.5-3 2 6 1.5-3h2"/>', desc: '健康与生命活力' },
{ id: 'pill', name: '药丸', svg: '<path d="M10.5 20.5a5 5 0 0 1-7-7l8-8a5 5 0 0 1 7 7z"/><path d="M3.5 13.5l7-7"/>', desc: '医药与护理' },
{ id: 'stethoscope', name: '听诊器', svg: '<path d="M4.8 2.3A.3.3 0 1 0 5 2H4a2 2 0 0 0-2 2v5a6 6 0 0 0 6 6 6 6 0 0 0 6-6V4a2 2 0 0 0-2-2h-1a.2.2 0 1 0 .3.3"/><path d="M8 15v1a6 6 0 0 0 6 6 6 6 0 0 0 6-6v-4"/><circle cx="20" cy="10" r="2"/>', desc: '诊疗与关怀' },
{ id: 'first-aid', name: '急救箱', svg: '<rect x="2" y="6" width="20" height="12" rx="2"/><path d="M12 9v6"/><path d="M9 12h6"/><path d="M8 6V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"/>', desc: '急救与保障' },
{ id: 'syringe', name: '注射器', svg: '<path d="m18 2 4 4"/><path d="m17 7 3-3"/><path d="M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"/><path d="m9 11 4 4"/><path d="m5 19-3 3"/><path d="m14 4 6 6"/>', desc: '接种与医学' },
{ id: 'ambulance', name: '救护车', svg: '<rect x="2" y="5" width="16" height="12" rx="2"/><path d="M18 9h3l2 3v5h-2"/><path d="M7 8v6"/><path d="M4 11h6"/><circle cx="6.5" cy="19" r="1.8"/><circle cx="18" cy="19" r="1.8"/>', desc: '应急与救援' },
{ id: 'microscope', name: '显微镜', svg: '<path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14h2"/><path d="M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"/><path d="M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"/>', desc: '研究与实验室' }
],
palettes: [
{ name: '医疗蓝', desc: '专业信赖,医疗主流', colors: ['#1A5276', '#5DADE2', '#D6EAF8', '#FFFFFF'], scene: '综合医院' },
{ name: '健康绿', desc: '生机健康,自然疗愈', colors: ['#1E8449', '#58D68D', '#D5F5E3', '#FFFFFF'], scene: '养生保健' },
{ name: '纯净白蓝', desc: '洁净清爽,现代诊所', colors: ['#2E86C1', '#85C1E9', '#F0F8FF', '#FFFFFF'], scene: '口腔诊所' },
{ name: '安心浅蓝', desc: '温和安心,母婴护理', colors: ['#2874A6', '#5DADE2', '#EBF5FB', '#F4F6F6'], scene: '母婴护理' }
],
styleIds: ['minimal', 'business', 'tech'],
fontFamilies: ['Microsoft YaHei', 'Arial', 'SimHei', 'Montserrat']
},
/* ================= 科技互联网 ================= */
{
id: 'tech',
name: '科技互联网',
emoji: '💻',
desc: '芯片、火箭、代码等科技元素',
icons: [
{ id: 'chip', name: '芯片', svg: '<rect x="7" y="7" width="10" height="10" rx="1"/><path d="M9 3v4"/><path d="M15 3v4"/><path d="M9 17v4"/><path d="M15 17v4"/><path d="M3 9h4"/><path d="M3 15h4"/><path d="M17 9h4"/><path d="M17 15h4"/><rect x="10" y="10" width="4" height="4"/>', desc: '半导体与算力' },
{ id: 'rocket', name: '火箭', svg: '<path d="M12 2c2.5 3 4 6 4 9l3 3v5h-4l-1.5 2.5L12 22l-1.5-1.5L9 19H5v-5l3-3c0-3 1.5-6 4-9z"/><circle cx="12" cy="11" r="2"/>', desc: '创新与速度' },
{ id: 'code', name: '代码', svg: '<path d="M8 6l-6 6 6 6"/><path d="M16 6l6 6-6 6"/>', desc: '开发与编程' },
{ id: 'cloud', name: '云服务', svg: '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>', desc: '云计算与存储' },
{ id: 'robot', name: '机器人', svg: '<rect x="4" y="8" width="16" height="12" rx="2"/><path d="M12 8V4"/><circle cx="12" cy="3" r="1"/><circle cx="9" cy="13" r="1"/><circle cx="15" cy="13" r="1"/><path d="M9 17h6"/>', desc: 'AI 与自动化' },
{ id: 'gear', name: '齿轮', svg: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3"/><path d="M12 19v3"/><path d="M2 12h3"/><path d="M19 12h3"/><path d="M4.9 4.9l2.1 2.1"/><path d="M17 17l2.1 2.1"/><path d="M19.1 4.9L17 7"/><path d="M7 17l-2.1 2.1"/>', desc: '系统与运转' },
{ id: 'monitor', name: '显示器', svg: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/>', desc: '软件与界面' },
{ id: 'wifi', name: '无线网络', svg: '<path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a7 7 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/>', desc: '连接与在线' }
],
palettes: [
{ name: '科技蓝紫', desc: '智慧深邃,数字感', colors: ['#4A235A', '#8E44AD', '#BB8FCE', '#F5EEF8'], scene: '互联网公司' },
{ name: '电路蓝', desc: '冷静专业,工业科技', colors: ['#1B2631', '#2980B9', '#85C1E9', '#F4F6F7'], scene: '智能硬件' },
{ name: '极客霓虹', desc: '活力张扬,年轻潮流', colors: ['#0B132B', '#3A86FF', '#FF006E', '#FB5607'], scene: '游戏娱乐' },
{ name: '未来银灰', desc: '极简高级,未来感', colors: ['#2C3E50', '#7F8C8D', '#BDC3C7', '#ECF0F1'], scene: '人工智能' }
],
styleIds: ['tech', 'gradient', 'minimal'],
fontFamilies: ['SimHei', 'Montserrat', 'Verdana', 'Microsoft YaHei']
},
/* ================= 体育竞技 ================= */
{
id: 'sport',
name: '体育竞技',
emoji: '🏆',
desc: '奖杯、足球、奖牌等运动元素',
icons: [
{ id: 'trophy', name: '奖杯', svg: '<path d="M8 21h8"/><path d="M12 17v4"/><path d="M7 4h10v6a5 5 0 0 1-10 0z"/><path d="M7 6H4a2.5 2.5 0 0 0 0 5h3"/><path d="M17 6h3a2.5 2.5 0 0 1 0 5h-3"/>', desc: '冠军与荣耀' },
{ id: 'soccer', name: '足球', svg: '<circle cx="12" cy="12" r="9"/><path d="M12 7l2.5 1.5L13.5 11h-3l-1-2.5z"/><path d="M12 3v4"/><path d="M5 8.5l3 1"/><path d="M16 20l-1.5-2.5"/><path d="M8 20l1.5-2.5"/>', desc: '绿茵与团队' },
{ id: 'basketball', name: '篮球', svg: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a9 9 0 0 0 0 18"/>', desc: '活力与对抗' },
{ id: 'medal', name: '奖牌', svg: '<circle cx="12" cy="14" r="5"/><path d="M9 3l-1 3"/><path d="M15 3l1 3"/><path d="M9.5 6h5"/><path d="M12 12l.9 1.7 1.9.3-1.4 1.3.3 1.9-1.7-.9-1.7.9.3-1.9L9.2 14l1.9-.3z"/>', desc: '成就与荣誉' },
{ id: 'runner', name: '奔跑', svg: '<circle cx="13" cy="4" r="2"/><path d="M5 21l3-6 2 1 1 5"/><path d="M8 15l2-4 3-1 3 3 2 1"/>', desc: '运动与坚持' },
{ id: 'dumbbell', name: '哑铃', svg: '<path d="M6.5 6.5v11"/><path d="M17.5 6.5v11"/><path d="M2 9v6"/><path d="M22 9v6"/><rect x="6.5" y="9" width="11" height="6" rx="1"/>', desc: '力量与健身' },
{ id: 'bike', name: '骑行', svg: '<circle cx="6" cy="17" r="3.5"/><circle cx="18" cy="17" r="3.5"/><path d="M6 17L10 8h4l4 9"/><path d="M14 8h2"/>', desc: '健康与户外' },
{ id: 'stopwatch', name: '秒表', svg: '<circle cx="12" cy="13" r="8"/><path d="M12 9v4l2.5 2.5"/><path d="M9 2h6"/><path d="M12 2v3"/>', desc: '竞速与计时' }
],
palettes: [
{ name: '活力橙', desc: '阳光热情,运动活力', colors: ['#E67E22', '#F39C12', '#FDEBD0', '#FFF9F0'], scene: '综合健身' },
{ name: '赛场绿', desc: '清新自然,健康向上', colors: ['#1E8449', '#52BE80', '#D5F5E3', '#FFFFFF'], scene: '户外运动' },
{ name: '冠军金', desc: '尊贵闪耀,胜利荣光', colors: ['#7D6608', '#D4AC0D', '#F9E79F', '#FEF9E7'], scene: '竞技赛事' },
{ name: '热血红', desc: '激情澎湃,力量对决', colors: ['#922B21', '#E74C3C', '#F5B7B1', '#FDEDEC'], scene: '搏击格斗' }
],
styleIds: ['cartoon', 'hand', 'badge'],
fontFamilies: ['Impact', 'Microsoft YaHei', 'KaiTi', 'Arial']
},
/* ================= 金融商务 ================= */
{
id: 'finance',
name: '金融商务',
emoji: '💼',
desc: '钱币、银行、图表等商务元素',
icons: [
{ id: 'coin', name: '钱币', svg: '<circle cx="12" cy="12" r="9"/><path d="M12 6v12"/><path d="M8.5 9.5h7"/><path d="M8.5 14.5h7"/>', desc: '财富与价值' },
{ id: 'bank', name: '银行', svg: '<polygon points="3 9 12 3 21 9"/><rect x="3" y="10" width="18" height="4"/><path d="M5 14v5"/><path d="M9.5 14v5"/><path d="M14.5 14v5"/><path d="M19 14v5"/><path d="M3 19h18"/>', desc: '金融与信用' },
{ id: 'trending-up', name: '增长图表', svg: '<path d="M23 6l-9.5 9.5-5-5L1 18"/><path d="M17 6h6v6"/>', desc: '增长与上升' },
{ id: 'handshake', name: '握手', svg: '<path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"/><path d="m21 3 1 11h-2"/><path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"/><path d="M3 4h8"/>', desc: '合作与共赢' },
{ id: 'briefcase', name: '公文包', svg: '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M2 13h20"/>', desc: '商务与职场' },
{ id: 'calculator', name: '计算器', svg: '<rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="16" y1="14" x2="16" y2="18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/>', desc: '财务与计算' },
{ id: 'gem', name: '钻石', svg: '<path d="M6 3h12l4 6-10 13L2 9z"/><path d="M2 9h20"/><path d="M12 22L8 9l4-6 4 6z"/>', desc: '珍贵与高端' },
{ id: 'money-bag', name: '钱袋', svg: '<path d="M8 7L5 3h14l-3 4"/><path d="M8 7h8v3a4 4 0 0 1-8 0z"/><path d="M6 17a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4v-4H6z"/>', desc: '投资与储蓄' }
],
palettes: [
{ name: '商务深蓝', desc: '专业稳重,金融权威', colors: ['#1B2631', '#2E86C1', '#F1C40F', '#F4F6F7'], scene: '银行保险' },
{ name: '金砖', desc: '奢华尊贵,财富象征', colors: ['#7D6608', '#D4AC0D', '#F9E79F', '#FEF9E7'], scene: '投资理财' },
{ name: '稳重灰蓝', desc: '严谨理性,商务咨询', colors: ['#34495E', '#5D6D7E', '#AEB6BF', '#FFFFFF'], scene: '企业咨询' },
{ name: '翡翠绿', desc: '稳健增长,安心可靠', colors: ['#145A32', '#1E8449', '#ABEBC6', '#F0FDF4'], scene: '证券基金' }
],
styleIds: ['business', 'minimal', 'vintage'],
fontFamilies: ['Arial', 'SimHei', 'Georgia', 'Montserrat']
}
];
/** 按 id 查找行业(使用原生 find,避免覆盖数组方法) */
HGB_Industries.find = function (id) {
return Array.prototype.find.call(this, x => x.id === id) || this[0];
};
/** 当前行业(默认留学教育) */
HGB_Industries.current = function () {
const id = localStorage.getItem('hgb_industry') || 'edu';
return HGB_Industries.find(id);
};
/** 设置当前行业 */
HGB_Industries.setCurrent = function (id) {
localStorage.setItem('hgb_industry', id);
};
+35
View File
@@ -0,0 +1,35 @@
# 画个标 · 系统基线(构建自动生成,勿手改)
> 由 `scripts/build-baseline.js` 生成。素材/技能变更并构建后,重新运行 `node scripts/build-baseline.js` 即自动更新。
> 执行「AI 指令库 指令一~四」前,先读本文件获取当前基线,勿依赖记忆中的数字。
## 生成时间
2026-08-24T11:30:15.593Z
## ① 图标库(共享,LOGO/头像/海报共用)
- 源数:224 | 图标总数:371504
- 类型分布:icon 165 / emoji 27 / brand 32
- 源清单(含 count/file/workspaces):js/data/manifest/icons.json
## ② 海报素材库
- 源数:25 | 素材总数:1582
- 类型分布:palette 6 / decoration 4 / bg 3 / font 12
- 源清单:js/data/manifest/posters.json
## ③ 技能库
- 共享技能:54(分类:layout 10 / color 9 / typography 8 / icon 8 / ai 6 / presentation 5 / export 8)
- 海报技能:64(分类:layout 8 / color 8 / typography 8 / grid 8 / hierarchy 8 / print 8 / creative 8 / ai 8)
- 表情技能:12(分类:motion 11 / emotion 1)
- 三区覆盖(共享,按 scene/workspaces):LOGO 向 54 / 头像向 45 / 海报向 54 / 未标注 0
- 三区覆盖(海报专属):LOGO 向 0 / 头像向 0 / 海报向 64
- 技能明细:js/data/manifest/skills.json
## 指令使用提示
- 指令一(新增图标源):去重以本文件 ① 的源清单 + icon-sources.js 现有 id 为准
- 指令二(新增海报源):去重以 ② + poster-sources.js 现有 id 为准
- 指令三(新增技能):看 ③ 三区覆盖找缺口(头像向最少则优先补头像);表情向技能看表情技能分类分布
+85
View File
@@ -0,0 +1,85 @@
# 画个标 · 素材与技能库读取指南(AI / 外部项目专用)
> 本目录是「画个标」系统对外暴露的**机器可读出口**:三份 JSON manifest + 本指南。
> 其他项目或 AI 要复用本库的素材/技能,**先读本指南 + 对应 manifest,再按需定向读数据文件**,即可准确命中,无需猜测文件名或解析大体积 JS 索引。
## 一、库里有什么(三套资源)
| 库 | manifest | 数据文件 | 数量(以对应 manifest 的 `total` 为准) |
|---|---|---|---|
| 共享图标库(LOGO/头像) | [icons.json](icons.json) | `js/data/sources/{id}.js` + `js/data/builtin-icons.js` | 222 源 / 370,268 图标 |
| 头像风格(DiceBear) | [icons.json](icons.json)(`dicebear` 字段) | `js/data/sources/dicebear-{style}.js` | 55 风格 |
| 海报素材库 | [posters.json](posters.json) | `js/data/posters/{id}.js` + `js/data/posters/{id}/files/` | 25 源 / 1,582 素材 |
| 共享技能 | [skills.json](skills.json)(`shared`) | 实现代码 `js/skills/*.js` | 48 个 / 7 类 |
| 海报技能 | [skills.json](skills.json)(`poster`) | 实现代码 `js/poster-skills/*.js` | 56 个 / 8 类 |
| 表情技能 | [skills.json](skills.json)(`sticker`) | 实现代码 `js/sticker-skills/*.js` | 12 个 / 2 类 |
> 注:上表数量为快照,**一切以对应 manifest 内的 `total` / `count` 字段为准**(构建时自动更新,本 README 为手写维护)。
## 二、manifest 结构(标准字段)
### icons.json / posters.json
```json
{
"kind": "icons", // icons | posters
"total": 367988, // 库内素材总数
"builtAt": "2026-08-23T…", // 构建时间(ISO)
"idRule": "「源:名称」,如 lucide:graduation-cap",
"sources": [ // 每个源一条(含实际内置数 count)
{
"id": "lucide", "type": "icon", // type: icon/emoji/brand(海报: palette/font/bg/decoration…)
"name": "Lucide Icons", "nameCn": "Lucide 图标",
"desc": "…", "style": "线条",
"license": "ISC", "licenseNote": "…", // 协议务必先读,商用需合规
"home": "…", "github": "…", "npm": "…",
"format": "sprite", // 拉取方式:iconify/dir-files/gh-files/sprite/nodes/remix-files
"coordText": "24 坐标", "variants": "…", "added": "2026-08-10",
"count": 1767, // 该源实际内置数
"file": "sources/lucide.js", // 该源数据文件相对路径(js/data/ 下)
"workspaces": ["logo","avatar"]
}
]
}
```
- **图标 id 规范**:内置库 `源:名称`(`lucide:graduation-cap`);行业图标为裸 id。
- **数据文件** `js/data/sources/{id}.js` 内容形态:`window['HGB_Source_{id}'] = { icons: [{ id, name, categoryCn, svg, type }] }`。`svg` 为归一化到 24 viewBox 的 body 片段(含 path/几何),渲染时套 `<svg viewBox="0 0 24 24">`。
- **头像**:`js/data/sources/dicebear-{style}.js` = `window.HGB_Dicebear_{style} = {…DiceBear 定义…}`(渲染器规则见 DiceBear 官方 schema)。
### posters.json
- **素材 id**:`源:相对路径`(如 `fontsource-inter:latin-400-normal.woff2`)。
- 数据文件 `js/data/posters/{id}.js` = `window['HGB_PosterSource_{id}'] = { items: [{ id, name, categoryCn, type, svg?|file?|data? }] }`;二进制(字体/位图)在 `js/data/posters/{id}/files/` 按 `file` 引用直读;配色为内联 `data`。
### skills.json
```json
{
"kind": "skills",
"shared": {
"total": 48,
"skills": [{
"id": "contrast-check", "name": "对比度检查", "category": "color",
"emoji": "🎨", "scene": "…", "summary": "…", "params": "…", "source": "自研",
"step": 3, // 设计流水线步骤 1品牌洞察/2方案生成/3方案评审/4迭代优化/5交付导出
"kind": "check", // gen 生成建议 / check 方案检查 / deliver 交付导出
"workspaces": ["logo","avatar","poster"]
}]
},
"poster": { "total": 56, "skills": [ …同字段(无 step/kind,按 category 8 类)… ] },
"sticker": { "total": 12, "skills": [ …同字段(无 step/kind,按 category motion/emotion 2 类)… ] }
}
```
- 技能元数据是纯数据可直接当知识用;**run/test 是函数实现**,仅供系统执行(`HGB_Skills.run(id, args)` / `HGB_Skills.test(id)`),外部如需「执行技能」请加载 `js/skills/*.js`(含数据依赖)后调用 API。
- 表情技能(`sticker`)实现代码 `js/sticker-skills/*.js`,API 为 `HGB_StickerSkills.run(id, args)` / `HGB_StickerSkills.test(id)`,`workspaces` 统一 `["emoji"]`;其动画决策依赖 `js/core/motion.js`(ANIMS 注册表,预览与导出逐帧采样共用同一套 `apply`)。
## 三、AI 推荐读取流程
1. 先读本指南(README.md)了解库结构与 id 规范;
2. 读对应 manifest 定位「有哪些源 / 每源多少 / 协议是否合规」;
3. 需要具体素材时,按 manifest 的 `file` 字段定向读 `js/data/sources/{id}.js`(或 `posters/{id}.js`),避免读 2.4MB 单行索引 `builtin-icons.js`;
4. 协议存疑(BY-SA/BY-NC/BY-ND/GPL)不商用;多色源(type emoji/brand)保留原色。
## 四、维护(构建后 manifest 自动更新)
- 图标:`node scripts/build-icons.js` → 重写 `builtin-icons.js` + `sources/{id}.js` + `manifest/icons.json`
- 海报:`node scripts/build-posters.js` → 重写 `poster-builtin.js` + `posters/{id}.js` + `manifest/posters.json`
- 技能:`node scripts/build-skill-manifest.js` → 重写 `manifest/skills.json`(技能元数据;技能代码改动后需重跑)
- 前端页面 sources.html / poster-sources.html 优先读 manifest(fetch 失败自动回退内置配置),skills 页面直接读技能 API(与 manifest 同源)。
File diff suppressed because it is too large Load Diff
+509
View File
@@ -0,0 +1,509 @@
{
"kind": "posters",
"total": 1582,
"builtAt": "2026-08-23T11:12:08.130Z",
"guide": "js/data/manifest/README.md",
"idRule": "「源:相对路径」,如 fontsource-inter:latin-400-normal.woff2",
"sources": [
{
"id": "uigradients",
"type": "palette",
"name": "uiGradients",
"nameCn": "UI 渐变配色库",
"desc": "GitHub 社区精选 200+ 款渐变配色方案,含品牌渐变、自然风光与经典风格渐变,可直接用于海报背景与主视觉",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://uigradients.com",
"github": "https://github.com/ghosh/uiGradients",
"format": "gh-files",
"coordText": "配色数据(gradients.json,顶层数组:名称 + 十六进制色值序列)",
"variants": "单文件 gradients.json,约 200 款渐变",
"added": "2026-08-20",
"count": 1,
"file": "posters/uigradients.js",
"workspaces": [
"poster"
]
},
{
"id": "twemoji",
"type": "decoration",
"name": "Twemoji",
"nameCn": "Twitter 彩色表情",
"desc": "Twitter/X 官方彩色 Emoji 表情 SVG(CC BY 4.0),适合作为海报点缀符号与情感化视觉元素",
"license": "CC BY 4.0",
"licenseNote": "CC BY 4.0,可商用,使用需署名(Twitter/X 官方表情库)",
"home": "https://twemoji.twitter.com",
"github": "https://github.com/jdecked/twemoji",
"format": "gh-files",
"coordText": "36×36 视口彩色矢量表情,保留原始比例",
"variants": "表情全集 3600+ 过大,首批 maxItems 限量收录 500 个常用表情(活动/运动/物品/符号),后续可按需扩量",
"added": "2026-08-20",
"count": 500,
"file": "posters/twemoji.js",
"workspaces": [
"poster"
]
},
{
"id": "heropatterns",
"type": "bg",
"name": "Hero Patterns",
"nameCn": "可平铺背景底纹",
"desc": "Steve Schoger 经典背景底纹集合(lowmess 维护 v2.1),约 90 款几何/纹理/趣味图案,可无缝平铺,直接作为海报背景底纹",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://heropatterns.com",
"github": "https://github.com/lowmess/hero-patterns",
"format": "gh-files",
"coordText": "平铺单元 SVG(各图案尺寸不一,保留原始 viewBox)",
"variants": "全量收录 ~90 款,默认黑色填充(可叠加底色或换色后使用)",
"added": "2026-08-21",
"count": 87,
"file": "posters/heropatterns.js",
"workspaces": [
"poster"
]
},
{
"id": "league-spartan",
"type": "font",
"name": "League Spartan",
"nameCn": "League Spartan 字体",
"desc": "League of Moveable Type 出品的现代几何无衬线字体(SIL OFL 1.1),粗细跨度 Thin→Black 共 9 个字重,适合海报大标题与品牌主视觉",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://www.theleagueofmoveabletype.com/league-spartan",
"github": "https://github.com/theleagueof/league-spartan",
"format": "gh-files",
"coordText": "TTF 静态字体文件(9 个字重,浏览器 @font-face 直载)",
"variants": "Thin / ExtraLight / Light / Regular / Medium / SemiBold / Bold / ExtraBold / Black",
"added": "2026-08-21",
"count": 9,
"file": "posters/league-spartan.js",
"workspaces": [
"poster"
]
},
{
"id": "open-color",
"type": "palette",
"name": "Open Color",
"nameCn": "Open Color 现代色板",
"desc": "MIT 开源现代色板:13 个色系(灰/红/粉/紫/蓝/青/绿/黄/橙…)× 每个 10 级明度,共 130 色,色调统一和谐,适合海报背景、强调色与数据可视化配色",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://yeun.github.io/open-color/",
"github": "https://github.com/yeun/open-color",
"format": "gh-files",
"coordText": "配色数据(open-color.json,13 色系 × 10 明度十六进制色值)",
"variants": "单文件 open-color.json,130 色(13 色系 × 10 级)",
"added": "2026-08-21",
"count": 1,
"file": "posters/open-color.js",
"workspaces": [
"poster"
]
},
{
"id": "catppuccin",
"type": "palette",
"name": "Catppuccin",
"nameCn": "Catppuccin 主题色板",
"desc": "社区热门主题色板:Latte / Frappe / Macchiato / Mocha 4 套主题 × 26 色(含玫瑰水/火焰/粉/葡萄紫等柔和高对比色),风格柔和不刺眼,适合海报背景与强调色",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://catppuccin.com",
"github": "https://github.com/catppuccin/palette",
"format": "gh-files",
"coordText": "配色数据(palette.json 4 主题 × 26 色 + palette-porcelain.json 变体)",
"variants": "2 个 JSON:主色板 palette.json + 陶瓷变体 palette-porcelain.json",
"added": "2026-08-21",
"count": 2,
"file": "posters/catppuccin.js",
"workspaces": [
"poster"
]
},
{
"id": "coolshapes",
"type": "decoration",
"name": "Coolshapes",
"nameCn": "几何装饰形状",
"desc": "100% 开源几何装饰形状包:椭圆/花朵/月亮/多边形/星星/三角/轮辐等 66 款矢量图形(200×200),适合作为海报点缀元素与插画局部",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://coolshap.es",
"github": "https://github.com/realvjy/coolshapes",
"format": "gh-files",
"coordText": "200×200 视口矢量图形,保留原始坐标(可等比缩放后叠加使用)",
"variants": "全量收录 66 款(ellipse/flower/misc/moon/polygon/star/triangle/wheel)",
"added": "2026-08-21",
"count": 66,
"file": "posters/coolshapes.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-playfair",
"type": "font",
"name": "Playfair Display",
"nameCn": "Playfair Display 字体",
"desc": "经典高对比衬线标题字体(OFL 1.1),字重 400-900 可变 + 斜体,优雅文艺气质,适合海报大标题与品牌视觉",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Playfair+Display",
"github": "https://github.com/clauseggers/Playfair-Display",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext / cyrillic / vietnamese × normal / italic)",
"variants": "8 个子集文件,字重 400-900 可调(wght 轴),拉丁子集覆盖英文/数字/西文标点",
"added": "2026-08-21",
"count": 8,
"file": "posters/fontsource-playfair.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-oswald",
"type": "font",
"name": "Oswald",
"nameCn": "Oswald 字体",
"desc": "高辨识度压缩无衬线标题字体(OFL 1.1),窄而有力的字形,适合海报标题、数据标注与运动/促销风格",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Oswald",
"github": "https://github.com/googlefonts/OswaldFont",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext / cyrillic / cyrillic-ext / vietnamese)",
"variants": "5 个子集文件,字重 200-700 可调(wght 轴),压缩宽高比醒目",
"added": "2026-08-21",
"count": 5,
"file": "posters/fontsource-oswald.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-space-grotesk",
"type": "font",
"name": "Space Grotesk",
"nameCn": "Space Grotesk 字体",
"desc": "几何无衬线标题字体(OFL 1.1),带有现代科技与实验气质,适合科技/数码/潮流风格海报与界面视觉",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Space+Grotesk",
"github": "https://github.com/floriankarsten/space-grotesk",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext / vietnamese)",
"variants": "3 个子集文件,字重 300-700 可调(wght 轴),几何字形现代感强",
"added": "2026-08-21",
"count": 3,
"file": "posters/fontsource-space-grotesk.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-inter",
"type": "font",
"name": "Inter",
"nameCn": "Inter 字体",
"desc": "被誉为「最佳界面字体」的现代无衬线字体(OFL 1.1),字重 100-900 跨度极大且阅读舒适,海报正文、数据信息区与界面设计的最佳选择",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Inter",
"github": "https://github.com/rsms/inter",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext / cyrillic × normal / italic)",
"variants": "6 个子集文件,字重 100-900 可调(wght 轴),海报正文/信息区首选无衬线",
"added": "2026-08-21",
"count": 6,
"file": "posters/fontsource-inter.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-jetbrains-mono",
"type": "font",
"name": "JetBrains Mono",
"nameCn": "JetBrains Mono 字体",
"desc": "JetBrains 出品的等宽编程字体(OFL 1.1),字形锐利辨识度高,适合科技/数码/代码风海报、数据标注与信息图",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/JetBrains+Mono",
"github": "https://github.com/JetBrains/JetBrainsMono",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext / cyrillic × normal / italic)",
"variants": "6 个子集文件,字重 100-800 可调(wght 轴),等宽字形适合技术/数据风格",
"added": "2026-08-21",
"count": 6,
"file": "posters/fontsource-jetbrains-mono.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-cinzel",
"type": "font",
"name": "Cinzel",
"nameCn": "Cinzel 字体",
"desc": "灵感源自古罗马碑刻的装饰衬线字体(OFL 1.1),字形庄重典雅,适合历史/文艺/高端品牌海报大标题",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Cinzel",
"github": "https://github.com/ndiscovered/cinzel",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext,无斜体)",
"variants": "2 个子集文件,字重 400-900 可调(wght 轴),罗马碑刻风格标题衬线",
"added": "2026-08-21",
"count": 2,
"file": "posters/fontsource-cinzel.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-lora",
"type": "font",
"name": "Lora",
"nameCn": "Lora 字体",
"desc": "气质古典的正文衬线字体(OFL 1.1),在屏幕与印刷上均有出色可读性,适合正文段落、引言与文艺风格海报",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Lora",
"github": "https://github.com/cyrealtype/Lora-Cyrillic",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext × normal / italic)",
"variants": "4 个子集文件,字重 400-700 可调(wght 轴),古典衬线正文首选",
"added": "2026-08-21",
"count": 4,
"file": "posters/fontsource-lora.js",
"workspaces": [
"poster"
]
},
{
"id": "patternfills",
"type": "bg",
"name": "PatternFills",
"nameCn": "图案填充底纹",
"desc": "SVG 图案填充集合(MIT):横/竖条纹、十字、菱格、点阵等 50 款经典灰阶平铺纹理,单元 10×10 无缝平铺,适合海报背景底纹与信息图分区底",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://iros.github.io/patternfills/",
"github": "https://github.com/iros/patternfills",
"format": "gh-files",
"coordText": "10×10 平铺单元 SVG(灰阶 #aaa 线条,可叠底色或换色后使用)",
"variants": "全量收录 50 款(水平/垂直条纹、十字、菱格、点阵、犬牙纹等)",
"added": "2026-08-21",
"count": 49,
"file": "posters/patternfills.js",
"workspaces": [
"poster"
]
},
{
"id": "brand-colors",
"type": "palette",
"name": "Brand Colors",
"nameCn": "品牌色板",
"desc": "收录全球 130+ 知名品牌官方主色(MIT),单一 JSON 按品牌名映射十六进制色值(部分含多色变体),适合海报调用品牌主色、做品牌对比或多品牌视觉素材",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://brandcolors.net",
"github": "https://github.com/reimertz/brand-colors",
"format": "gh-files",
"coordText": "配色数据(brandColors.json,品牌名 → 十六进制主色扁平映射)",
"variants": "单文件 brandColors.json,130+ 品牌、部分含多色变体(-2/-3 后缀)",
"added": "2026-08-21",
"count": 1,
"file": "posters/brand-colors.js",
"workspaces": [
"poster"
]
},
{
"id": "pixelarticons",
"type": "decoration",
"name": "Pixelarticons",
"nameCn": "像素风图标",
"desc": "手工像素风矢量图标集(MIT,24×24 网格、无抗锯齿),单色可任意换色,适合复古游戏/像素/潮流风格海报点缀元素与信息符号",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://pixelarticons.com",
"github": "https://github.com/halfmage/pixelarticons",
"format": "dir-files",
"coordText": "24×24 视口单色像素图标(fill=currentColor,可换色)",
"variants": "maxItems 限量收录 400 枚基础图标(剔除 -sharp 变体,全量 1036 枚)",
"added": "2026-08-21",
"count": 399,
"file": "posters/pixelarticons.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-montserrat",
"type": "font",
"name": "Montserrat",
"nameCn": "Montserrat 字体",
"desc": "几何无衬线标题字体(OFL 1.1),字重 100-900 可变 + 斜体,字形温和圆润,海报/品牌设计领域全球最常用的全能标题字体",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Montserrat",
"github": "https://github.com/JulietaUla/Montserrat",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext × normal / italic)",
"variants": "4 个子集文件,字重 100-900 可调(wght 轴),标题与正文全能无衬线",
"added": "2026-08-21",
"count": 4,
"file": "posters/fontsource-montserrat.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-bebas-neue",
"type": "font",
"name": "Bebas Neue",
"nameCn": "Bebas Neue 字体",
"desc": "经典压缩无衬线大写标题字体(OFL 1.1),窄高字形醒目有力,电影海报/时尚/运动/促销风格的标题标杆字体",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Bebas+Neue",
"github": "https://github.com/dharmatype/Bebas-Neue",
"format": "dir-files",
"coordText": "静态字体 woff2 子集(latin / latin-ext,仅 400 常规字重)",
"variants": "2 个子集文件,单字重 400 压缩大写,适合大号标题与品牌字标",
"added": "2026-08-21",
"count": 2,
"file": "posters/fontsource-bebas-neue.js",
"workspaces": [
"poster"
]
},
{
"id": "material-colors",
"type": "palette",
"name": "Material Colors",
"nameCn": "Material Design 色板",
"desc": "Google Material Design 经典官方色板(MIT):19 色系 × 10 级明度 + 4 个 accent 高饱和变体 + 深浅文本/图标色,共 260+ 色值,色彩体系科学统一,适合海报系统配色与信息可视化",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://material.io/design/color",
"github": "https://github.com/shuhei/material-colors",
"format": "dir-files",
"coordText": "配色数据(colors.json,色系名 → 明度/变体 → 十六进制;含 rgba 文本/图标色)",
"variants": "单文件 colors.json,260+ 色值(19 色系 × 50-900/a100-a700 + 文本/图标色)",
"added": "2026-08-21",
"count": 1,
"file": "posters/material-colors.js",
"workspaces": [
"poster"
]
},
{
"id": "circle-flags",
"type": "decoration",
"name": "Circle Flags",
"nameCn": "圆形国旗图标",
"desc": "268+ 枚 ISO 3166-1 各国圆形国旗矢量图标(MIT),国旗元素圆角化处理并带白色描边,适合海报中的国家/地区标识、旅行与国际主题、多国对比等装饰元素",
"license": "MIT",
"licenseNote": "MIT 协议,可自由商用、修改、再分发,需保留版权声明",
"home": "https://hatscripts.github.io/circle-flags/",
"github": "https://github.com/HatScripts/circle-flags",
"format": "gh-files",
"coordText": "圆形国旗 SVG(各国比例徽章造型,viewBox 各异逐图标归一化)",
"variants": "全量收录 268+ 枚 ISO 3166-1 圆形国旗(cn/us/jp/... 两字母代码命名)",
"added": "2026-08-22",
"count": 405,
"file": "posters/circle-flags.js",
"workspaces": [
"poster"
]
},
{
"id": "glacial-strat-patterns",
"type": "bg",
"name": "Glacial Strat Patterns",
"nameCn": "地质层理图案",
"desc": "地质层理结构矢量图案集(图案资产 CC BY 4.0):13 款模拟冰川/沉积岩地层剖面的层理纹理,多层渐变与弯折地质构造,适合地质、自然、科学主题海报的背景与氛围底纹",
"license": "CC-BY-4.0",
"licenseNote": "图案资产遵循 CC BY 4.0(商用/修改需署名:MNiMORPH/glacial-strat-patterns);仓库代码部分为 MIT",
"home": "https://github.com/MNiMORPH/glacial-strat-patterns",
"github": "https://github.com/MNiMORPH/glacial-strat-patterns",
"format": "gh-files",
"coordText": "地质层理剖面 SVG(地质符号命名,如 Cdm/Dcm/Gms,多层渐变填充)",
"variants": "全量收录 13 款(svg/ 目录:Cdm/Dcm/Dmm/Dms/Em/Fl/Fm/Gh/Gms/P/R/Sm/Sr)",
"added": "2026-08-22",
"count": 13,
"file": "posters/glacial-strat-patterns.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-poppins",
"type": "font",
"name": "Poppins",
"nameCn": "Poppins 字体",
"desc": "现代几何无衬线字体(OFL 1.1),圆形几何字干、比例均衡,兼具标题力量感与正文易读性,海报/品牌/UI 设计最热门的全能字体之一",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Poppins",
"github": "https://github.com/itfoundry/Poppins",
"format": "dir-files",
"coordText": "静态字体 woff2 子集(latin,字重 300/400/500/600/700 常规)",
"variants": "5 个 latin 子集文件,常用字重 300-700,正文与标题两用几何无衬线",
"added": "2026-08-22",
"count": 5,
"file": "posters/fontsource-poppins.js",
"workspaces": [
"poster"
]
},
{
"id": "fontsource-caveat",
"type": "font",
"name": "Caveat",
"nameCn": "Caveat 手写字体",
"desc": "Google Fonts 最热门的手写风格字体(OFL 1.1),笔触自然、随性亲和,字重 400-700 可变,适合海报手账/文艺/婚礼/儿童/美食等亲和力场景的标题、引言与手写感点缀",
"license": "OFL-1.1",
"licenseNote": "SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名",
"home": "https://fonts.google.com/specimen/Caveat",
"github": "https://github.com/googlefonts/caveat",
"format": "dir-files",
"coordText": "可变字体 woff2 子集(latin / latin-ext × wght 轴,400-700 可调)",
"variants": "2 个子集文件,字重 400-700 可调(wght 轴),自然手写风标题与引言",
"added": "2026-08-23",
"count": 2,
"file": "posters/fontsource-caveat.js",
"workspaces": [
"poster"
]
},
{
"id": "chinese-colors",
"type": "palette",
"name": "Chinese Traditional Colors",
"nameCn": "中国传统色",
"desc": "中国传统色彩体系:粉鳳仙/荷花紅/青蓮紫/黛藍等 200+ 款典雅配色,每条含中文名 + 拼音 + 十六进制色值,适合国风/节日/婚礼/茶饮等传统文化主题海报配色",
"license": "ISC",
"licenseNote": "ISC 协议,可自由商用、修改、再分发(等同 MIT 的宽松许可)",
"home": "https://github.com/hecodeit/chinese-colors",
"github": "https://github.com/hecodeit/chinese-colors",
"format": "dir-files",
"coordText": "配色数据(colors.json,顶层数组:中文名 + 拼音 + 十六进制色值)",
"variants": "单文件 colors.json,320 款中国传统色(粉/紫/紅/黄/绿/蓝/黛等色系,含拼音标注)",
"added": "2026-08-23",
"count": 1,
"file": "posters/chinese-colors.js",
"workspaces": [
"poster"
]
}
]
}
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
/**
* 画个标 · 留学教育行业色彩方案推荐系统
* 每套方案:主题色 / 辅助色 / 点缀色 / 背景 / 适用场景说明
*/
const HGB_Palettes = [
{
id: 'academic-blue',
name: '学术蓝',
desc: '沉稳专业,国际教育主流色',
colors: ['#1B4F72', '#2980B9', '#AED6F1', '#F4F6F7'],
scene: '留学中介、国际学校'
},
{
id: 'ivy-green',
name: '常春藤绿',
desc: '常春藤盟校气质,学术与成长',
colors: ['#1E8449', '#27AE60', '#D5F5E3', '#FFFFFF'],
scene: '本科留学、精英教育'
},
{
id: 'crimson',
name: '典雅红',
desc: '哈佛红传承,权威与荣耀',
colors: ['#7B241C', '#C0392B', '#F5B7B1', '#FDEDEC'],
scene: '研究生申请、名校项目'
},
{
id: 'royal-gold',
name: '皇家金',
desc: '贵族气质,彰显学历含金量',
colors: ['#7D6608', '#D4AC0D', '#F9E79F', '#FEF9E7'],
scene: '商学院、高端留学服务'
},
{
id: 'ocean-blue',
name: '海洋蓝',
desc: '向往远航,探索世界的决心',
colors: ['#154360', '#2E86C1', '#D6EAF8', '#FFFFFF'],
scene: '海外游学、交换项目'
},
{
id: 'violet',
name: '学术紫',
desc: '智慧与创造,文理学院风',
colors: ['#4A235A', '#8E44AD', '#D2B4DE', '#F5EEF8'],
scene: '艺术留学、文理学院'
},
{
id: 'oxford',
name: '牛津灰红',
desc: '传统英伦,学院制服配色',
colors: ['#566573', '#CB4335', '#F2F3F4', '#FFFFFF'],
scene: '英联邦国家留学'
},
{
id: 'sky-dream',
name: '梦想天蓝',
desc: '清新明亮,追梦与活力',
colors: ['#2471A3', '#5DADE2', '#FDFEFE', '#EBF5FB'],
scene: '低龄留学、语言培训'
}
];
File diff suppressed because one or more lines are too long
+698
View File
@@ -0,0 +1,698 @@
/* ============================================================
* 海报素材来源配置(唯一权威清单)
* 用途:
* 1. 构建脚本 scripts/build-posters.js require 本文件获取下载/解析配置
* 2. 「海报素材」页面 poster-sources.html 读取本文件渲染来源清单
* 新增来源步骤:
* 1. 在数组末尾追加配置(GitHub 直拉 gh-files / npm dir-files)
* 2. 运行 node scripts/build-posters.js 重新构建
* 3. poster.html / poster-sources.html / poster-skills.html 中 bump ?v= 版本
* 字段说明:
* id 唯一 id(内置库「源:名称」前缀)
* type 素材类型:bg(背景底纹) / illustration(插画) / template(成品模板)
* decoration(装饰元素) / mockup(展示样机) / photo(无版权图片)
* font(字体) / palette(配色方案)
* name 英文名 nameCn 中文名 desc 一句话风格描述
* license 协议 licenseNote 协议中文说明(署名要求等)
* home 官网 github GitHub 仓库
* format 拉取方式:gh-files(GitHub 仓库直拉)| dir-files(npm 目录枚举)
* gh/ghBranch GitHub 仓库与分支(format=gh-files 必填)
* npm npm 包名(format=dir-files 必填)
* dirs 目标目录(gh-files / dir-files 均用)
* ext 文件扩展名过滤(默认 .svg;海报可含 .png/.jpg/.json/.ttf)
* scaleMode wrap(保留原始坐标/比例,适合位图/模板)| body(缩放几何,适合纯矢量装饰)
* coordText 坐标系说明(页面展示)
* variants 变体说明(页面展示)
* added 收录日期(页面展示)
* workspaces 适用工作区(海报库固定 ['poster'];预留扩展)
* ============================================================ */
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.HGB_PosterSources = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
return [
/* ============================================================
* 首批合规源(2026-08-20 录入,license 逐条核实 + jsDelivr 可拉取验证):
* uiGradients 配色 palette(MIT,gradients.json 顶层为 [{name, colors}] 数组)
* twemoji 装饰 decoration(CC BY 4.0,assets/svg 表情全量过大,maxItems 限量收录)
* 已筛选但暂不可用(记录备查):
* unDraw 自定义协议明令禁止 AI/机器学习训练,与 AI 海报生成冲突 → 排除
* Open Doodles CC0 但素材托管于 Dropbox/Webflow,无 GitHub 仓库可直拉 → 不可用
* Hero Patterns 官网声明免费但仓库无 LICENSE 文件 → 合规风险,待定
* open-peeps / humaaans / MagicPattern svg-backgrounds jsDelivr 未索引(404)→ 不可用
* svg-patterns npm 包 p/*.js 为依赖 virtual-dom 的 JS 模板(非静态 SVG)→ 待定
* subtlepatterns npm 包存在但许可证未定(Toptal 自定义条款)→ 待定
* ============================================================ */
{
id: 'uigradients',
type: 'palette',
name: 'uiGradients',
nameCn: 'UI 渐变配色库',
desc: 'GitHub 社区精选 200+ 款渐变配色方案,含品牌渐变、自然风光与经典风格渐变,可直接用于海报背景与主视觉',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://uigradients.com',
github: 'https://github.com/ghosh/uiGradients',
format: 'gh-files',
gh: 'ghosh/uiGradients',
ghBranch: 'master',
dirs: ['.'],
ext: ['.json'],
scaleMode: 'wrap',
coordText: '配色数据(gradients.json,顶层数组:名称 + 十六进制色值序列)',
variants: '单文件 gradients.json,约 200 款渐变',
added: '2026-08-20',
workspaces: ['poster']
},
{
id: 'twemoji',
type: 'decoration',
name: 'Twemoji',
nameCn: 'Twitter 彩色表情',
desc: 'Twitter/X 官方彩色 Emoji 表情 SVG(CC BY 4.0),适合作为海报点缀符号与情感化视觉元素',
license: 'CC BY 4.0',
licenseNote: 'CC BY 4.0,可商用,使用需署名(Twitter/X 官方表情库)',
home: 'https://twemoji.twitter.com',
github: 'https://github.com/jdecked/twemoji',
format: 'gh-files',
gh: 'jdecked/twemoji',
ghBranch: 'master',
dirs: ['assets/svg'],
ext: ['.svg'],
scaleMode: 'wrap',
coordText: '36×36 视口彩色矢量表情,保留原始比例',
exclude: '(?:^|/)1f1[ef]', // 剔除国旗段(1f1e6-1f1ff 区域指示符占位过多,海报价值低;锚定文件名而非完整路径)
variants: '表情全集 3600+ 过大,首批 maxItems 限量收录 500 个常用表情(活动/运动/物品/符号),后续可按需扩量',
added: '2026-08-20',
maxItems: 500,
workspaces: ['poster']
},
/* ============================================================
* 第二批合规源(2026-08-21 录入,license 逐条核实 + jsDelivr 可拉取验证):
* hero-patterns 背景底纹 bg(lowmess fork v2.1.0,MIT;src/patterns/*.js 为函数模板,
* 构建时由 transform:'heropatterns' 专用解析器执行为静态 SVG,全量 ~90 款)
* league-spartan 字体 font(SIL OFL 1.1;fonts/ttf/ 9 个字重 TTF,适合海报大标题)
* open-color 配色 palette(MIT;open-color.json 13 色系 × 10 明度,经典现代色板)
* 本轮探测排除(记录备查):
* fxemoji / emoji-data / fluentui-emoji jsDelivr 包超 50MB 拒绝索引(403)→ 不可用
* noto-emoji(旧 tag) jsDelivr 未索引该 tag(404)→ 不可用
* Open Doodles / LadybugBot illustrations / illlustrations jsDelivr 未索引(404)→ 不可用
* color-names(MIT) 31,915 行色名 CSV 无视觉预览,海报场景价值低 → 本轮不收录
* Montserrat / Raleway 字体仓库 jsDelivr 超 50MB → 不可用;league-gothic 仅 .glyphs 源文件无成品字体
* ostrich-sans / chunk(The League of Moveable Type) jsDelivr 未索引 或 仅 UFO 源文件无 TTF 成品 → 不可用
* ============================================================ */
{
id: 'heropatterns',
type: 'bg',
name: 'Hero Patterns',
nameCn: '可平铺背景底纹',
desc: 'Steve Schoger 经典背景底纹集合(lowmess 维护 v2.1),约 90 款几何/纹理/趣味图案,可无缝平铺,直接作为海报背景底纹',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://heropatterns.com',
github: 'https://github.com/lowmess/hero-patterns',
format: 'gh-files',
gh: 'lowmess/hero-patterns',
ghBranch: '2.1.0',
dirs: ['src/patterns'],
ext: ['.js'],
scaleMode: 'wrap',
coordText: '平铺单元 SVG(各图案尺寸不一,保留原始 viewBox)',
exclude: '(?:^|/)_', // 剔除 _defaults.js / _utils.js 工具模块(锚定文件名而非完整路径)
transform: 'heropatterns', // 构建脚本专用:JS 函数模板 → 静态 SVG
variants: '全量收录 ~90 款,默认黑色填充(可叠加底色或换色后使用)',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'league-spartan',
type: 'font',
name: 'League Spartan',
nameCn: 'League Spartan 字体',
desc: 'League of Moveable Type 出品的现代几何无衬线字体(SIL OFL 1.1),粗细跨度 Thin→Black 共 9 个字重,适合海报大标题与品牌主视觉',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://www.theleagueofmoveabletype.com/league-spartan',
github: 'https://github.com/theleagueof/league-spartan',
format: 'gh-files',
gh: 'theleagueof/league-spartan',
ghBranch: 'master',
dirs: ['fonts/ttf'],
ext: ['.ttf'],
scaleMode: 'wrap',
coordText: 'TTF 静态字体文件(9 个字重,浏览器 @font-face 直载)',
variants: 'Thin / ExtraLight / Light / Regular / Medium / SemiBold / Bold / ExtraBold / Black',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'open-color',
type: 'palette',
name: 'Open Color',
nameCn: 'Open Color 现代色板',
desc: 'MIT 开源现代色板:13 个色系(灰/红/粉/紫/蓝/青/绿/黄/橙…)× 每个 10 级明度,共 130 色,色调统一和谐,适合海报背景、强调色与数据可视化配色',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://yeun.github.io/open-color/',
github: 'https://github.com/yeun/open-color',
format: 'gh-files',
gh: 'yeun/open-color',
ghBranch: '1.9.1',
dirs: ['.'],
ext: ['.json'],
exclude: '(?:^|/)params\\.json$', // 剔除站点元数据 params.json(非配色数据)
scaleMode: 'wrap',
coordText: '配色数据(open-color.json,13 色系 × 10 明度十六进制色值)',
variants: '单文件 open-color.json,130 色(13 色系 × 10 级)',
added: '2026-08-21',
workspaces: ['poster']
},
/* ============================================================
* 第三批合规源(2026-08-21 录入,license 逐条核实 + jsDelivr 可拉取验证):
* catppuccin/palette 配色 palette(MIT;palette.json 4 主题 × 26 色 + porcelain 变体)
* coolshapes 装饰 decoration(MIT;public/shapes/svg 66 款几何/花朵/月亮/星星等海报点缀形状)
* Playfair Display / Oswald / Space Grotesk 字体 font(OFL-1.1;fontsource 可变字体 woff2 子集,
* latin/cyrillic/vietnamese 按需直载,适合海报标题与正文,字体包可自由商用)
* 本轮探测排除(记录备查):
* nord 仅 ACO/ASE/GPL 等专有色板格式,无 JSON 数据 → 不可直接收录
* tokyonight.nvim extras JSON 为终端主题配置(含背景/前景/ANSI 索引色),非纯色板 → 不作为配色源
* dracula/dracula R 语言包结构,无独立配色 JSON 文件 → 不可用
* altercation/solarized jsDelivr 未索引(404)→ 不可用
* googlefonts/noto-emoji jsDelivr 仓库超限(403)→ 不可用
* ============================================================ */
{
id: 'catppuccin',
type: 'palette',
name: 'Catppuccin',
nameCn: 'Catppuccin 主题色板',
desc: '社区热门主题色板:Latte / Frappe / Macchiato / Mocha 4 套主题 × 26 色(含玫瑰水/火焰/粉/葡萄紫等柔和高对比色),风格柔和不刺眼,适合海报背景与强调色',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://catppuccin.com',
github: 'https://github.com/catppuccin/palette',
format: 'gh-files',
gh: 'catppuccin/palette',
ghBranch: 'v0.2.0',
dirs: ['.'],
ext: ['.json'],
scaleMode: 'wrap',
coordText: '配色数据(palette.json 4 主题 × 26 色 + palette-porcelain.json 变体)',
variants: '2 个 JSON:主色板 palette.json + 陶瓷变体 palette-porcelain.json',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'coolshapes',
type: 'decoration',
name: 'Coolshapes',
nameCn: '几何装饰形状',
desc: '100% 开源几何装饰形状包:椭圆/花朵/月亮/多边形/星星/三角/轮辐等 66 款矢量图形(200×200),适合作为海报点缀元素与插画局部',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://coolshap.es',
github: 'https://github.com/realvjy/coolshapes',
format: 'gh-files',
gh: 'realvjy/coolshapes',
ghBranch: 'main',
dirs: ['public/shapes/svg'],
ext: ['.svg'],
scaleMode: 'wrap',
coordText: '200×200 视口矢量图形,保留原始坐标(可等比缩放后叠加使用)',
variants: '全量收录 66 款(ellipse/flower/misc/moon/polygon/star/triangle/wheel)',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-playfair',
type: 'font',
name: 'Playfair Display',
nameCn: 'Playfair Display 字体',
desc: '经典高对比衬线标题字体(OFL 1.1),字重 400-900 可变 + 斜体,优雅文艺气质,适合海报大标题与品牌视觉',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Playfair+Display',
github: 'https://github.com/clauseggers/Playfair-Display',
format: 'dir-files',
npm: '@fontsource-variable/playfair-display',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext / cyrillic / vietnamese × normal / italic)',
variants: '8 个子集文件,字重 400-900 可调(wght 轴),拉丁子集覆盖英文/数字/西文标点',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-oswald',
type: 'font',
name: 'Oswald',
nameCn: 'Oswald 字体',
desc: '高辨识度压缩无衬线标题字体(OFL 1.1),窄而有力的字形,适合海报标题、数据标注与运动/促销风格',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Oswald',
github: 'https://github.com/googlefonts/OswaldFont',
format: 'dir-files',
npm: '@fontsource-variable/oswald',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext / cyrillic / cyrillic-ext / vietnamese)',
variants: '5 个子集文件,字重 200-700 可调(wght 轴),压缩宽高比醒目',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-space-grotesk',
type: 'font',
name: 'Space Grotesk',
nameCn: 'Space Grotesk 字体',
desc: '几何无衬线标题字体(OFL 1.1),带有现代科技与实验气质,适合科技/数码/潮流风格海报与界面视觉',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Space+Grotesk',
github: 'https://github.com/floriankarsten/space-grotesk',
format: 'dir-files',
npm: '@fontsource-variable/space-grotesk',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext / vietnamese)',
variants: '3 个子集文件,字重 300-700 可调(wght 轴),几何字形现代感强',
added: '2026-08-21',
workspaces: ['poster']
},
/* ============================================================
* 第四批合规源(2026-08-21 录入,license 逐条核实 + jsDelivr 可拉取验证):
* Inter / JetBrains Mono / Cinzel / Lora 字体 font(OFL-1.1;fontsource 可变字体 woff2 子集,
* 分别覆盖:无衬线正文(Inter)、等宽技术风(JetBrains Mono)、
* 罗马碑刻装饰衬线标题(Cinzel)、古典正文衬线(Lora))
* PatternFills 背景底纹 bg(MIT;src/patterns 50 款 10×10 灰阶平铺图案 SVG,
* 纯静态可独立渲染,叠底色换色后作为海报底纹)
* 本轮探测排除(记录备查):
* svelte-svg-patterns(Pattern Monster) static 目录仅 1 个 SVG(其余为 Svelte/JS 生成代码)→ 不可用
* PatternBolt 图案以 SCSS/CSS data-uri 形式打包,无独立 SVG 文件 → 不可用
* pattern-craft Next.js 项目结构,图案为 CSS/JS 代码片段 → 不可用
* textures.js 运行时生成库(D3 驱动),非静态素材 → 不可用
* illustration 类(unDraw 自定义协议禁 AI / Open Doodles 无仓库直拉 / illlustrations 404)
* 、template/mockup/photo 类:GitHub 上无可直拉的合规静态素材 → 本轮不收录,继续观测
* ============================================================ */
{
id: 'fontsource-inter',
type: 'font',
name: 'Inter',
nameCn: 'Inter 字体',
desc: '被誉为「最佳界面字体」的现代无衬线字体(OFL 1.1),字重 100-900 跨度极大且阅读舒适,海报正文、数据信息区与界面设计的最佳选择',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Inter',
github: 'https://github.com/rsms/inter',
format: 'dir-files',
npm: '@fontsource-variable/inter',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
exclude: '-(standard|opsz|greek|vietnamese|cyrillic-ext)-', // 剔除重复轴(standard/opsz 与 wght 同内容)与低用例子集
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext / cyrillic × normal / italic)',
variants: '6 个子集文件,字重 100-900 可调(wght 轴),海报正文/信息区首选无衬线',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-jetbrains-mono',
type: 'font',
name: 'JetBrains Mono',
nameCn: 'JetBrains Mono 字体',
desc: 'JetBrains 出品的等宽编程字体(OFL 1.1),字形锐利辨识度高,适合科技/数码/代码风海报、数据标注与信息图',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/JetBrains+Mono',
github: 'https://github.com/JetBrains/JetBrainsMono',
format: 'dir-files',
npm: '@fontsource-variable/jetbrains-mono',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
exclude: '-(greek|vietnamese|cyrillic-ext)-',
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext / cyrillic × normal / italic)',
variants: '6 个子集文件,字重 100-800 可调(wght 轴),等宽字形适合技术/数据风格',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-cinzel',
type: 'font',
name: 'Cinzel',
nameCn: 'Cinzel 字体',
desc: '灵感源自古罗马碑刻的装饰衬线字体(OFL 1.1),字形庄重典雅,适合历史/文艺/高端品牌海报大标题',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Cinzel',
github: 'https://github.com/ndiscovered/cinzel',
format: 'dir-files',
npm: '@fontsource-variable/cinzel',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext,无斜体)',
variants: '2 个子集文件,字重 400-900 可调(wght 轴),罗马碑刻风格标题衬线',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-lora',
type: 'font',
name: 'Lora',
nameCn: 'Lora 字体',
desc: '气质古典的正文衬线字体(OFL 1.1),在屏幕与印刷上均有出色可读性,适合正文段落、引言与文艺风格海报',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Lora',
github: 'https://github.com/cyrealtype/Lora-Cyrillic',
format: 'dir-files',
npm: '@fontsource-variable/lora',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
exclude: '-(math|symbols|cyrillic|vietnamese)-', // 剔除数学/符号/西里尔/越南语子集,保留拉丁正文
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext × normal / italic)',
variants: '4 个子集文件,字重 400-700 可调(wght 轴),古典衬线正文首选',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'patternfills',
type: 'bg',
name: 'PatternFills',
nameCn: '图案填充底纹',
desc: 'SVG 图案填充集合(MIT):横/竖条纹、十字、菱格、点阵等 50 款经典灰阶平铺纹理,单元 10×10 无缝平铺,适合海报背景底纹与信息图分区底',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://iros.github.io/patternfills/',
github: 'https://github.com/iros/patternfills',
format: 'gh-files',
gh: 'iros/patternfills',
ghBranch: 'master',
dirs: ['src/patterns'],
ext: ['.svg'],
scaleMode: 'wrap',
coordText: '10×10 平铺单元 SVG(灰阶 #aaa 线条,可叠底色或换色后使用)',
variants: '全量收录 50 款(水平/垂直条纹、十字、菱格、点阵、犬牙纹等)',
added: '2026-08-21',
workspaces: ['poster']
},
/* ============================================================
* 第五批合规源(2026-08-21 录入,license 逐条核实 + jsDelivr 可拉取验证):
* brand-colors 配色 palette(MIT;data/brandColors.json 按品牌名映射主色,130+ 品牌,
* 部分含多色变体,适合海报调用品牌主色/多品牌对比配色)
* pixelarticons 装饰 decoration(MIT;npm 2.4.1,svg/ 1036 枚 24×24 像素风图标,
* 单色可换色,适合复古游戏/像素风海报点缀;maxItems 限量收录基础版)
* 本轮探测排除(记录备查):
* primer/primitives 设计 token JSON 结构复杂(多层嵌套 token 名),非直接可用的色板数据 → 不可用
* game-icons(CC BY 3.0)/ OpenMoji(CC BY-SA 4.0) 协议不在白名单(BY-SA / 非 CC BY 4.0)→ 排除
* fangpenlin/avataaars 仅 React 组件源码,无成品 SVG 文件 → 不可用
* simple-icons / bootstrap-icons / heroicons / pixelarticons 之外的图标库 与 LOGO 库图标功能重叠,仅收
* 风格独特的 pixelarticons(像素风);其余不重复收录
* illustration / template / mockup / photo 类:仍无可直拉的合规静态源(unDraw 禁 AI / 其余 404 或自定义协议)→ 继续观测
* ============================================================ */
{
id: 'brand-colors',
type: 'palette',
name: 'Brand Colors',
nameCn: '品牌色板',
desc: '收录全球 130+ 知名品牌官方主色(MIT),单一 JSON 按品牌名映射十六进制色值(部分含多色变体),适合海报调用品牌主色、做品牌对比或多品牌视觉素材',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://brandcolors.net',
github: 'https://github.com/reimertz/brand-colors',
format: 'gh-files',
gh: 'reimertz/brand-colors',
ghBranch: 'master',
dirs: ['data'],
ext: ['.json'],
exclude: 'brandColors\\.(merged|net)\\.json$', // 仅取 brandColors.json(品牌名→hex 扁平映射),剔除重复变体
scaleMode: 'wrap',
coordText: '配色数据(brandColors.json,品牌名 → 十六进制主色扁平映射)',
variants: '单文件 brandColors.json,130+ 品牌、部分含多色变体(-2/-3 后缀)',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'pixelarticons',
type: 'decoration',
name: 'Pixelarticons',
nameCn: '像素风图标',
desc: '手工像素风矢量图标集(MIT,24×24 网格、无抗锯齿),单色可任意换色,适合复古游戏/像素/潮流风格海报点缀元素与信息符号',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://pixelarticons.com',
github: 'https://github.com/halfmage/pixelarticons',
format: 'dir-files',
npm: 'pixelarticons',
npmVersion: '2.4.1',
dirs: ['svg'],
ext: ['.svg'],
exclude: '-sharp\\.svg$', // 仅收录基础风格(-sharp 变体视觉近重复)
scaleMode: 'wrap',
coordText: '24×24 视口单色像素图标(fill=currentColor,可换色)',
variants: 'maxItems 限量收录 400 枚基础图标(剔除 -sharp 变体,全量 1036 枚)',
added: '2026-08-21',
maxItems: 400,
workspaces: ['poster']
},
/* ============================================================
* 第六批合规源(2026-08-21 录入,license 逐条核实 + jsDelivr 可拉取验证):
* Montserrat / Bebas Neue 字体 font(OFL-1.1;fontsource 包 woff2 子集,
* 分别覆盖:全能标题无衬线(Montserrat,字重 100-900 可变)、
* 经典压缩大写标题(Bebas Neue,单字重 400,电影海报风格))
* Material Colors 配色 palette(MIT;dist/colors.json 19 色系 × 10 明度
* + 4 accent 高饱和变体 + 深浅文本/图标色,260+ 色值,系统配色基准)
* 本轮探测排除(记录备查):
* chinese-color-atlas(中国传统色) free 样本为 W3C DTCG 嵌套 token 结构,非纯色板 JSON → 不可用
* zerosoul/chinese-colors 色板数据内嵌于 JS(非独立 JSON),仓库混有大量水墨 PNG/字体 → 不可用
* anup-a/svg.shapes 无独立 SVG 文件(形状打包进单个 svg.json)→ 不可用
* fu4303/svgwave(SVG Wave) 运行时生成器(Preact/Webpack 应用),非静态素材 → 不可用
* material-colors 之外(IORoot 色板为 Apple CLR 格式 / DesignColors 为 Java 库 / CARTO 为 R 包) → 不可用
* illustration / template / mockup / photo 类:仍无可直拉的合规静态源 → 继续观测
* ============================================================ */
{
id: 'fontsource-montserrat',
type: 'font',
name: 'Montserrat',
nameCn: 'Montserrat 字体',
desc: '几何无衬线标题字体(OFL 1.1),字重 100-900 可变 + 斜体,字形温和圆润,海报/品牌设计领域全球最常用的全能标题字体',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Montserrat',
github: 'https://github.com/JulietaUla/Montserrat',
format: 'dir-files',
npm: '@fontsource-variable/montserrat',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
exclude: '-(cyrillic|vietnamese)-', // 仅保留 latin / latin-ext 子集(西里尔/越南语低用例子集剔除)
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext × normal / italic)',
variants: '4 个子集文件,字重 100-900 可调(wght 轴),标题与正文全能无衬线',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'fontsource-bebas-neue',
type: 'font',
name: 'Bebas Neue',
nameCn: 'Bebas Neue 字体',
desc: '经典压缩无衬线大写标题字体(OFL 1.1),窄高字形醒目有力,电影海报/时尚/运动/促销风格的标题标杆字体',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Bebas+Neue',
github: 'https://github.com/dharmatype/Bebas-Neue',
format: 'dir-files',
npm: '@fontsource/bebas-neue',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
scaleMode: 'wrap',
coordText: '静态字体 woff2 子集(latin / latin-ext,仅 400 常规字重)',
variants: '2 个子集文件,单字重 400 压缩大写,适合大号标题与品牌字标',
added: '2026-08-21',
workspaces: ['poster']
},
{
id: 'material-colors',
type: 'palette',
name: 'Material Colors',
nameCn: 'Material Design 色板',
desc: 'Google Material Design 经典官方色板(MIT):19 色系 × 10 级明度 + 4 个 accent 高饱和变体 + 深浅文本/图标色,共 260+ 色值,色彩体系科学统一,适合海报系统配色与信息可视化',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://material.io/design/color',
github: 'https://github.com/shuhei/material-colors',
format: 'dir-files',
npm: 'material-colors',
npmVersion: '1.2.6',
dirs: ['dist'],
ext: ['.json'],
scaleMode: 'wrap',
coordText: '配色数据(colors.json,色系名 → 明度/变体 → 十六进制;含 rgba 文本/图标色)',
variants: '单文件 colors.json,260+ 色值(19 色系 × 50-900/a100-a700 + 文本/图标色)',
added: '2026-08-21',
workspaces: ['poster']
},
/* ============================================================
* 第七批合规源(2026-08-22 录入,license 逐条核实 + jsDelivr 可拉取验证):
* circle-flags 装饰 decoration(MIT;gh-pages 分支 flags/ 268+ 枚 ISO 3166-1
* 圆形国旗矢量图标,圆角化 + 白描边,海报国家/地区标识与旅行主题装饰)
* glacial-strat-patterns 背景 bg(图案资产 CC BY 4.0 / 代码 MIT;svg/ 13 款地质层理
* 剖面图案,多层渐变弯折层理,地质/自然/科学主题海报背景)
* Poppins 字体 font(OFL-1.1;fontsource 包 woff2 子集,
* 现代几何无衬线全能字体,常用字重 300-700,正文与标题两用)
* 本轮探测排除(记录备查):
* Playfair Display(fontsource-playfair-display) 与第四批 fontsource-playfair 重复 → 剔除
* dracula/dracula-theme / rose-pine/rose-pine-theme 主题色仅存于各编辑器扩展仓库
* (主仓为 .gitmodules 子模块列表或纯徽标文件),无独立配色 JSON → 不可用
* Dragoon0x/inkjet(500 参数化图案) 全部为 TS 源码,运行时生成 SVG,无预生成静态文件 → 不可用
* mrmrs/patterns jsDelivr 未索引任何可用分支(master/gh-pages 均 404)→ 不可用
* illustration / template / mockup / photo 类:仍无可直拉的合规静态源 → 继续观测
* ============================================================ */
{
id: 'circle-flags',
type: 'decoration',
name: 'Circle Flags',
nameCn: '圆形国旗图标',
desc: '268+ 枚 ISO 3166-1 各国圆形国旗矢量图标(MIT),国旗元素圆角化处理并带白色描边,适合海报中的国家/地区标识、旅行与国际主题、多国对比等装饰元素',
license: 'MIT',
licenseNote: 'MIT 协议,可自由商用、修改、再分发,需保留版权声明',
home: 'https://hatscripts.github.io/circle-flags/',
github: 'https://github.com/HatScripts/circle-flags',
format: 'gh-files',
gh: 'HatScripts/circle-flags',
ghBranch: 'gh-pages',
dirs: ['flags'],
ext: ['.svg'],
exclude: 'flags/(language|other)/', // 仅保留主目录 ISO 3166-1 国旗,剔除语言/特殊图形子目录
scaleMode: 'wrap',
coordText: '圆形国旗 SVG(各国比例徽章造型,viewBox 各异逐图标归一化)',
variants: '全量收录 268+ 枚 ISO 3166-1 圆形国旗(cn/us/jp/... 两字母代码命名)',
added: '2026-08-22',
workspaces: ['poster']
},
{
id: 'glacial-strat-patterns',
type: 'bg',
name: 'Glacial Strat Patterns',
nameCn: '地质层理图案',
desc: '地质层理结构矢量图案集(图案资产 CC BY 4.0):13 款模拟冰川/沉积岩地层剖面的层理纹理,多层渐变与弯折地质构造,适合地质、自然、科学主题海报的背景与氛围底纹',
license: 'CC-BY-4.0',
licenseNote: '图案资产遵循 CC BY 4.0(商用/修改需署名:MNiMORPH/glacial-strat-patterns);仓库代码部分为 MIT',
home: 'https://github.com/MNiMORPH/glacial-strat-patterns',
github: 'https://github.com/MNiMORPH/glacial-strat-patterns',
format: 'gh-files',
gh: 'MNiMORPH/glacial-strat-patterns',
ghBranch: 'main',
dirs: ['svg'],
ext: ['.svg'],
scaleMode: 'wrap',
coordText: '地质层理剖面 SVG(地质符号命名,如 Cdm/Dcm/Gms,多层渐变填充)',
variants: '全量收录 13 款(svg/ 目录:Cdm/Dcm/Dmm/Dms/Em/Fl/Fm/Gh/Gms/P/R/Sm/Sr)',
added: '2026-08-22',
workspaces: ['poster']
},
{
id: 'fontsource-poppins',
type: 'font',
name: 'Poppins',
nameCn: 'Poppins 字体',
desc: '现代几何无衬线字体(OFL 1.1),圆形几何字干、比例均衡,兼具标题力量感与正文易读性,海报/品牌/UI 设计最热门的全能字体之一',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Poppins',
github: 'https://github.com/itfoundry/Poppins',
format: 'dir-files',
npm: '@fontsource/poppins',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
exclude: '-(devanagari|latin-ext|100|200|800|900)-|-italic', // 仅保留 latin 常用字重 300-700 常规
scaleMode: 'wrap',
coordText: '静态字体 woff2 子集(latin,字重 300/400/500/600/700 常规)',
variants: '5 个 latin 子集文件,常用字重 300-700,正文与标题两用几何无衬线',
added: '2026-08-22',
workspaces: ['poster']
},
/* ============================================================
* 第八批合规源(2026-08-23 录入,license 逐条核实 + jsDelivr 可拉取验证):
* Caveat 字体 font(OFL-1.1;fontsource 可变字体 woff2 子集,
* 手写风格(handwriting),字重 400-700 可变,填补字体库
* 手写/书法风格缺口,适合手账/文艺/婚礼/儿童/美食等亲和力标题)
* 本轮探测排除(记录备查):
* Dancing Script / Pacifico / Lobster / Amatic SC 同属手写风格,与 Caveat 同类,
* 首批先收最具代表性的 Caveat(Google Fonts 手写体最热门),其余待后续按需扩量
* illustration / template / mockup / photo 类:仍无可直拉的合规静态源 → 继续观测
* ============================================================ */
{
id: 'fontsource-caveat',
type: 'font',
name: 'Caveat',
nameCn: 'Caveat 手写字体',
desc: 'Google Fonts 最热门的手写风格字体(OFL 1.1),笔触自然、随性亲和,字重 400-700 可变,适合海报手账/文艺/婚礼/儿童/美食等亲和力场景的标题、引言与手写感点缀',
license: 'OFL-1.1',
licenseNote: 'SIL Open Font License 1.1,可自由商用与修改;单独售卖字体文件需注明来源,与软件捆绑分发则无需署名',
home: 'https://fonts.google.com/specimen/Caveat',
github: 'https://github.com/googlefonts/caveat',
format: 'dir-files',
npm: '@fontsource-variable/caveat',
npmVersion: '5.3.0',
dirs: ['files'],
ext: ['.woff2'],
exclude: '-(cyrillic|cyrillic-ext)-', // 仅保留 latin / latin-ext 子集(西里尔低用例子集剔除)
scaleMode: 'wrap',
coordText: '可变字体 woff2 子集(latin / latin-ext × wght 轴,400-700 可调)',
variants: '2 个子集文件,字重 400-700 可调(wght 轴),自然手写风标题与引言',
added: '2026-08-23',
workspaces: ['poster']
},
/* ============================================================
* 第九批合规源(2026-08-23 录入,license 逐条核实 + jsDelivr 可拉取验证):
* Chinese Traditional Colors 配色 palette(ISC;colors.json 顶层数组,
* 200+ 款中国传统色(中文名 + 拼音 + 十六进制色值),
* 填补「文化色」类别缺口,适合国风/节日/婚礼/茶饮主题海报)
* 本轮探测排除(记录备查):
* luozhuzhen/Chinese-colors jsDelivr 未索引(master 404)→ 不可用
* @radix-ui/colors 色板仅打包为 CSS 变量文件(amber.css 等),
* 无 .json 数据形态 → 不符合 palette 内联 data 规范
* nipponcolors / traditional-japanese-colors npm 无此包(404)→ 不可用
* zerosoul/chinese-colors 色板数据内嵌于 JS + 仓库混有大量水墨 PNG/字体(前批已排除)
* illustration / template / mockup / photo 类:仍无可直拉的合规静态源 → 继续观测
* ============================================================ */
{
id: 'chinese-colors',
type: 'palette',
name: 'Chinese Traditional Colors',
nameCn: '中国传统色',
desc: '中国传统色彩体系:粉鳳仙/荷花紅/青蓮紫/黛藍等 200+ 款典雅配色,每条含中文名 + 拼音 + 十六进制色值,适合国风/节日/婚礼/茶饮等传统文化主题海报配色',
license: 'ISC',
licenseNote: 'ISC 协议,可自由商用、修改、再分发(等同 MIT 的宽松许可)',
home: 'https://github.com/hecodeit/chinese-colors',
github: 'https://github.com/hecodeit/chinese-colors',
format: 'dir-files',
npm: 'chinese-colors',
npmVersion: '1.0.0',
dirs: ['.'],
ext: ['.json'],
scaleMode: 'wrap',
coordText: '配色数据(colors.json,顶层数组:中文名 + 拼音 + 十六进制色值)',
variants: '单文件 colors.json,320 款中国传统色(粉/紫/紅/黄/绿/蓝/黛等色系,含拼音标注)',
added: '2026-08-23',
workspaces: ['poster']
}
];
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
/* Bebas Neue(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:k6d2c1 */
window['HGB_PosterSource_fontsource-bebas-neue'] = {"items":[{"id":"fontsource-bebas-neue:bebas-neue-latin-400-normal","name":"bebas-neue-latin-400-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-bebas-neue/files/bebas-neue-latin-400-normal.woff2","mime":"application/octet-stream","bytes":13768,"sha256":"a7c90c89240c134f7fdd33d40c000ec90b79d675ea53e8cc5a6d423c073de412","width":null,"height":null},{"id":"fontsource-bebas-neue:bebas-neue-latin-ext-400-normal","name":"bebas-neue-latin-ext-400-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-bebas-neue/files/bebas-neue-latin-ext-400-normal.woff2","mime":"application/octet-stream","bytes":8928,"sha256":"16c95ce45a2922f52551d38d565d14c92cf257b8f219c89613407d81fdd21a39","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Caveat(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:1rved7 */
window['HGB_PosterSource_fontsource-caveat'] = {"items":[{"id":"fontsource-caveat:caveat-latin-ext-wght-normal","name":"caveat-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-caveat/files/caveat-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":29472,"sha256":"f626b95c1efa95cf6b7c9b043d3bbe8862b6abbe5f55a082102f33f7de2fd4f9","width":null,"height":null},{"id":"fontsource-caveat:caveat-latin-wght-normal","name":"caveat-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-caveat/files/caveat-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":74932,"sha256":"891951df5e8b5b0f0bc760d31375405e795c666b5add70437de12d4a6482b33f","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Cinzel(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:1klcm8m */
window['HGB_PosterSource_fontsource-cinzel'] = {"items":[{"id":"fontsource-cinzel:cinzel-latin-ext-wght-normal","name":"cinzel-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-cinzel/files/cinzel-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":14540,"sha256":"8fc2606f8ae6c569f444e138dba052d9a6e3f0657f9031796503bbb94eced600","width":null,"height":null},{"id":"fontsource-cinzel:cinzel-latin-wght-normal","name":"cinzel-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-cinzel/files/cinzel-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":25904,"sha256":"09941fb1c169c38fd414536b37690057fc01b3117a5c63dd6571186540c8f370","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Inter(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:137tz9v */
window['HGB_PosterSource_fontsource-inter'] = {"items":[{"id":"fontsource-inter:inter-cyrillic-wght-italic","name":"inter-cyrillic-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-inter/files/inter-cyrillic-wght-italic.woff2","mime":"application/octet-stream","bytes":19980,"sha256":"47d42151dff6d13f1c2b9a1f278290f625593c1f01c89612ee4ae7f063167f7a","width":null,"height":null},{"id":"fontsource-inter:inter-cyrillic-wght-normal","name":"inter-cyrillic-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-inter/files/inter-cyrillic-wght-normal.woff2","mime":"application/octet-stream","bytes":18748,"sha256":"71d5ee93cc1e9f1d520a3a8b66456de18c7879d8df09d57fcd2eaff75fef0075","width":null,"height":null},{"id":"fontsource-inter:inter-latin-ext-wght-italic","name":"inter-latin-ext-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-inter/files/inter-latin-ext-wght-italic.woff2","mime":"application/octet-stream","bytes":91900,"sha256":"71254244ccb1ec21e14db3480fef6f6826d1136dc4aca1c7d3ca7ce31294fcce","width":null,"height":null},{"id":"fontsource-inter:inter-latin-ext-wght-normal","name":"inter-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-inter/files/inter-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":85068,"sha256":"34b9c504cab7a73e37b746343a449132e56cf7b5481af2cb81dc74dcff25c956","width":null,"height":null},{"id":"fontsource-inter:inter-latin-wght-italic","name":"inter-latin-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-inter/files/inter-latin-wght-italic.woff2","mime":"application/octet-stream","bytes":51832,"sha256":"7291b5970da2237441273c03b424a504b70b18f09791473fab99687dcc314720","width":null,"height":null},{"id":"fontsource-inter:inter-latin-wght-normal","name":"inter-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-inter/files/inter-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":48256,"sha256":"3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62","width":null,"height":null}]};
@@ -0,0 +1,2 @@
/* JetBrains Mono(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:2h21yb */
window['HGB_PosterSource_fontsource-jetbrains-mono'] = {"items":[{"id":"fontsource-jetbrains-mono:jetbrains-mono-cyrillic-wght-italic","name":"jetbrains-mono-cyrillic-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-jetbrains-mono/files/jetbrains-mono-cyrillic-wght-italic.woff2","mime":"application/octet-stream","bytes":13352,"sha256":"8df3ca627bd8e1cb0e5414f7429fe7a2cf82732b0fc43f2d05bc2c471b64fcfc","width":null,"height":null},{"id":"fontsource-jetbrains-mono:jetbrains-mono-cyrillic-wght-normal","name":"jetbrains-mono-cyrillic-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-jetbrains-mono/files/jetbrains-mono-cyrillic-wght-normal.woff2","mime":"application/octet-stream","bytes":12108,"sha256":"d274604c40757f98f07df623a8b8fb5194508a6fa4f5413a7a107f09e3b7452a","width":null,"height":null},{"id":"fontsource-jetbrains-mono:jetbrains-mono-latin-ext-wght-italic","name":"jetbrains-mono-latin-ext-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-jetbrains-mono/files/jetbrains-mono-latin-ext-wght-italic.woff2","mime":"application/octet-stream","bytes":16520,"sha256":"60652c78382d7e5021ce4c3ed8985fc41d8f3d1b1a3bedcec2a44ed8979fe5eb","width":null,"height":null},{"id":"fontsource-jetbrains-mono:jetbrains-mono-latin-ext-wght-normal","name":"jetbrains-mono-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-jetbrains-mono/files/jetbrains-mono-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":15196,"sha256":"79bfdab9ba467e26eea4122e6f2567e188dd8a09a8c730d501fc487c4ab99c6e","width":null,"height":null},{"id":"fontsource-jetbrains-mono:jetbrains-mono-latin-wght-italic","name":"jetbrains-mono-latin-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-jetbrains-mono/files/jetbrains-mono-latin-wght-italic.woff2","mime":"application/octet-stream","bytes":42964,"sha256":"a8afa085e9ca5e53434e2ee918ba6b65c7dd4dda56509976b36591478c99d62e","width":null,"height":null},{"id":"fontsource-jetbrains-mono:jetbrains-mono-latin-wght-normal","name":"jetbrains-mono-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":40404,"sha256":"18be452724bfdc236c074ca94a249a7f41a86752c7d04ab258ce9ed5651f6a7e","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Lora(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:92heas */
window['HGB_PosterSource_fontsource-lora'] = {"items":[{"id":"fontsource-lora:lora-latin-ext-wght-italic","name":"lora-latin-ext-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-lora/files/lora-latin-ext-wght-italic.woff2","mime":"application/octet-stream","bytes":21128,"sha256":"45f989df83f3a9f40007d2ecdea98bff248c8767a1a0bbe60e94fb37b605ecee","width":null,"height":null},{"id":"fontsource-lora:lora-latin-ext-wght-normal","name":"lora-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-lora/files/lora-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":20088,"sha256":"2a2d9c22c9863086a23f5013fede1428585321812b25f2662542c39d02967c5e","width":null,"height":null},{"id":"fontsource-lora:lora-latin-wght-italic","name":"lora-latin-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-lora/files/lora-latin-wght-italic.woff2","mime":"application/octet-stream","bytes":40772,"sha256":"d824d807d4d832d12c87932d0b8ec1314dcfd502157a56dee6bb04cf8a3768ae","width":null,"height":null},{"id":"fontsource-lora:lora-latin-wght-normal","name":"lora-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-lora/files/lora-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":37788,"sha256":"ddb8c66035104e233fc024669183aad3738b6daa16deee2ebb1241bd0f98ace1","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Montserrat(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:e1v3ln */
window['HGB_PosterSource_fontsource-montserrat'] = {"items":[{"id":"fontsource-montserrat:montserrat-latin-ext-wght-italic","name":"montserrat-latin-ext-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-montserrat/files/montserrat-latin-ext-wght-italic.woff2","mime":"application/octet-stream","bytes":74892,"sha256":"9f7c3dc0c1fdbc8b09d53d1beb4aca048a5aff376ac5423878e03e83e89a7fe8","width":null,"height":null},{"id":"fontsource-montserrat:montserrat-latin-ext-wght-normal","name":"montserrat-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-montserrat/files/montserrat-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":70688,"sha256":"54d9a78b7ff60b689ad9f3017ffac8547b5d871afec733f6c1c3ae36577ee504","width":null,"height":null},{"id":"fontsource-montserrat:montserrat-latin-wght-italic","name":"montserrat-latin-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-montserrat/files/montserrat-latin-wght-italic.woff2","mime":"application/octet-stream","bytes":39632,"sha256":"0eb22e3d769a66a92dbd82e3aefa5740e5bc21afa4485f62f92b22656d49182a","width":null,"height":null},{"id":"fontsource-montserrat:montserrat-latin-wght-normal","name":"montserrat-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-montserrat/files/montserrat-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":37956,"sha256":"06b16db7a969135d48d38c49183be7fb88d4452e2a3011957c7851941f4e4879","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Oswald(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:1lb9nbt */
window['HGB_PosterSource_fontsource-oswald'] = {"items":[{"id":"fontsource-oswald:oswald-cyrillic-ext-wght-normal","name":"oswald-cyrillic-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-oswald/files/oswald-cyrillic-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":19404,"sha256":"ff62316167dc2f9cae9dea3707afcc4cf571b7f6744304ba45f2fbc40dd74179","width":null,"height":null},{"id":"fontsource-oswald:oswald-cyrillic-wght-normal","name":"oswald-cyrillic-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-oswald/files/oswald-cyrillic-wght-normal.woff2","mime":"application/octet-stream","bytes":15688,"sha256":"9307b75d3508236a0bae4f29ee3d2ce9aee545120ec9ef29e2cd1187bf6b0e5b","width":null,"height":null},{"id":"fontsource-oswald:oswald-latin-ext-wght-normal","name":"oswald-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-oswald/files/oswald-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":23952,"sha256":"7989ccc7770f5aeffa67e088bcd3b294d42fae2f8b0eca3c3c51b953d1f735cc","width":null,"height":null},{"id":"fontsource-oswald:oswald-latin-wght-normal","name":"oswald-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-oswald/files/oswald-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":28488,"sha256":"bd73278ee0c50041b91b4c03d1229e35b501637f46b6409e7da2d3a758446ea5","width":null,"height":null},{"id":"fontsource-oswald:oswald-vietnamese-wght-normal","name":"oswald-vietnamese-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-oswald/files/oswald-vietnamese-wght-normal.woff2","mime":"application/octet-stream","bytes":7576,"sha256":"ca8ab22d32ea765f3a8f7112b8bea5af717fa7f4b65fa3823ae4cf3d4a95189c","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Playfair Display(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:1t4gcce */
window['HGB_PosterSource_fontsource-playfair'] = {"items":[{"id":"fontsource-playfair:playfair-display-cyrillic-wght-italic","name":"playfair-display-cyrillic-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-cyrillic-wght-italic.woff2","mime":"application/octet-stream","bytes":23304,"sha256":"51b2dac698333379dc3a3486393fba787d17f1dcbcb50f46582ca759d53c25fc","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-cyrillic-wght-normal","name":"playfair-display-cyrillic-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-cyrillic-wght-normal.woff2","mime":"application/octet-stream","bytes":21152,"sha256":"c108681cba6a9820bff52f4a1a4a1d9edf356b347bd79b7c61e916028e8a8b54","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-latin-ext-wght-italic","name":"playfair-display-latin-ext-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-latin-ext-wght-italic.woff2","mime":"application/octet-stream","bytes":23380,"sha256":"b9dff4aec6d8ef27af595330c808c455fcc3ff46881642aa6028ed4e27a96f89","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-latin-ext-wght-normal","name":"playfair-display-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":21140,"sha256":"42898ad49a6b23f32b109243e1df596edf831015ed685f429e4dafbb181d599d","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-latin-wght-italic","name":"playfair-display-latin-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-latin-wght-italic.woff2","mime":"application/octet-stream","bytes":38804,"sha256":"54af24bd0f911f0fd7399d5445ea2023ff501d6a69f2d728f1213072f977ab14","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-latin-wght-normal","name":"playfair-display-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":38404,"sha256":"e0c764a8e9e1cce92163c55bac4b2ad6cd4cf8c696ce2289ab5c41565e65b7e2","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-vietnamese-wght-italic","name":"playfair-display-vietnamese-wght-italic","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-vietnamese-wght-italic.woff2","mime":"application/octet-stream","bytes":9568,"sha256":"6b8bfd228577635a5f45bee13c8dd4a900eba6aeb36af1680838aea3cbd7e412","width":null,"height":null},{"id":"fontsource-playfair:playfair-display-vietnamese-wght-normal","name":"playfair-display-vietnamese-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-playfair/files/playfair-display-vietnamese-wght-normal.woff2","mime":"application/octet-stream","bytes":9112,"sha256":"ef6446229b59773e671021650ef3882cf35806dae723ce047de87c0a6633039a","width":null,"height":null}]};
+2
View File
@@ -0,0 +1,2 @@
/* Poppins(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:e8kmiw */
window['HGB_PosterSource_fontsource-poppins'] = {"items":[{"id":"fontsource-poppins:poppins-latin-300-normal","name":"poppins-latin-300-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-poppins/files/poppins-latin-300-normal.woff2","mime":"application/octet-stream","bytes":7840,"sha256":"78bc3aa78faec288bbb3bf26c9a0fa4eb67b1e69da94a17233c5cab60525efdb","width":null,"height":null},{"id":"fontsource-poppins:poppins-latin-400-normal","name":"poppins-latin-400-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-poppins/files/poppins-latin-400-normal.woff2","mime":"application/octet-stream","bytes":7884,"sha256":"7d93459d86585bfcdbb7e0376056226adb25821ee54b96236fe2123e9560929f","width":null,"height":null},{"id":"fontsource-poppins:poppins-latin-500-normal","name":"poppins-latin-500-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-poppins/files/poppins-latin-500-normal.woff2","mime":"application/octet-stream","bytes":7748,"sha256":"cd36de204aca2d5fa263a731f7c20009b5e3d754ba1f1e03c33e93a48f3e7446","width":null,"height":null},{"id":"fontsource-poppins:poppins-latin-600-normal","name":"poppins-latin-600-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-poppins/files/poppins-latin-600-normal.woff2","mime":"application/octet-stream","bytes":8000,"sha256":"f4e80d9dfd374d02989b87a27b5ed4cb78fbb177c27f1478e9a8b0afb7513149","width":null,"height":null},{"id":"fontsource-poppins:poppins-latin-700-normal","name":"poppins-latin-700-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-poppins/files/poppins-latin-700-normal.woff2","mime":"application/octet-stream","bytes":7816,"sha256":"9338e65fc077355c7a87ae0d64cc101e23b9bf8ad78ae65f0f319c857311b526","width":null,"height":null}]};
@@ -0,0 +1,2 @@
/* Space Grotesk(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:efzajk */
window['HGB_PosterSource_fontsource-space-grotesk'] = {"items":[{"id":"fontsource-space-grotesk:space-grotesk-latin-ext-wght-normal","name":"space-grotesk-latin-ext-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-space-grotesk/files/space-grotesk-latin-ext-wght-normal.woff2","mime":"application/octet-stream","bytes":18940,"sha256":"952dddb45d2f96f71cbf3b7f510b24379afc3c89ea02fcf89d377b45d62c0166","width":null,"height":null},{"id":"fontsource-space-grotesk:space-grotesk-latin-wght-normal","name":"space-grotesk-latin-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-space-grotesk/files/space-grotesk-latin-wght-normal.woff2","mime":"application/octet-stream","bytes":22288,"sha256":"0640890476fc1198ab4de571fb658de443c4d85b66466ec09534a8737ab1ce9d","width":null,"height":null},{"id":"fontsource-space-grotesk:space-grotesk-vietnamese-wght-normal","name":"space-grotesk-vietnamese-wght-normal","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/fontsource-space-grotesk/files/space-grotesk-vietnamese-wght-normal.woff2","mime":"application/octet-stream","bytes":6712,"sha256":"8895e3d49825128bfa5238e4c96e9b432d7a3dadbe5c17c6081267398305dc71","width":null,"height":null}]};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
/* League Spartan(由 scripts/build-posters.js 生成,勿手动编辑)@cfg:1wk50za */
window['HGB_PosterSource_league-spartan'] = {"items":[{"id":"league-spartan:LeagueSpartan-Black","name":"LeagueSpartan-Black","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-Black.ttf","mime":"application/octet-stream","bytes":79068,"sha256":"e97f12c9f592cd26e7d5094e2ebea914f85c64919872657d935bb2e5e5240bd7","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-Bold","name":"LeagueSpartan-Bold","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-Bold.ttf","mime":"application/octet-stream","bytes":78836,"sha256":"28a76032877053bc2a01b9ec15ad2a9c912491ca74e1343aa0ee3297a1dfa230","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-ExtraBold","name":"LeagueSpartan-ExtraBold","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-ExtraBold.ttf","mime":"application/octet-stream","bytes":78884,"sha256":"1e432a9911799ec9bdf6146778e57946c7abe07acdc918c41219df1a363d6b95","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-ExtraLight","name":"LeagueSpartan-ExtraLight","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-ExtraLight.ttf","mime":"application/octet-stream","bytes":76676,"sha256":"60136ba157e893da80b3eb96ddec63898e0b0118b13ac54a6e90f4c16e62e687","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-Light","name":"LeagueSpartan-Light","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-Light.ttf","mime":"application/octet-stream","bytes":77080,"sha256":"7127c368244adf9ed3b9564e29ef74a1ce656c5dea8987f7b71960a7819abd43","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-Medium","name":"LeagueSpartan-Medium","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-Medium.ttf","mime":"application/octet-stream","bytes":77964,"sha256":"178357941afa3d007bedcc513a35f4856590af2480761ac304cfdc47bfa10b62","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-Regular","name":"LeagueSpartan-Regular","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-Regular.ttf","mime":"application/octet-stream","bytes":77976,"sha256":"f0d22ae34443b2fd9437f528ac52718b0e9b2569f866c6f43ee89122a3b01d6a","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-SemiBold","name":"LeagueSpartan-SemiBold","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-SemiBold.ttf","mime":"application/octet-stream","bytes":78648,"sha256":"8509055f2e6075a0e96d28585aad8ed00161bdedb6e49dd11278b7d65c3fb7d0","width":null,"height":null},{"id":"league-spartan:LeagueSpartan-Thin","name":"LeagueSpartan-Thin","type":"font","category":"font","categoryCn":"字体排版","workspaces":["poster"],"file":"posters/league-spartan/files/LeagueSpartan-Thin.ttf","mime":"application/octet-stream","bytes":76556,"sha256":"7c006fde088746359598115a3c80199234b1e4263d9fcf0e1f524b5e98dc92a4","width":null,"height":null}]};

Some files were not shown because too many files have changed in this diff Show More