feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 画个标 · 表情技能(12 个:motion 11 / emotion 1)
|
||||
* 全部为纯函数:run(args) 返回 {ok/tip/...},test() 可自跑。
|
||||
* 用于表情生成后的评审/优化建议(动态节奏/物理/循环/体积/平台/情绪)。
|
||||
* 来源标注「自研」或公开设计原则。
|
||||
*/
|
||||
HGB_StickerSkills.register([
|
||||
/* ---------- motion 动画节奏 ---------- */
|
||||
{
|
||||
id: 'motion-timing', name: '节奏合理性', category: 'motion', emoji: '⏱',
|
||||
scene: '评估动画时长与帧率是否合理(过短看不清、过长拖沓、帧率过低卡顿)',
|
||||
summary: '按动画类型建议合适时长与帧率:表情通常 0.8-2s,弹跳/闪烁宜短促,漂浮/旋转可稍长。',
|
||||
params: { duration: '时长秒(默认 1.2)', fps: '帧率(默认 12)', type: '动画类型 id' },
|
||||
source: '自研(动态表情通用规范)',
|
||||
run(args) {
|
||||
const d = Number(args.duration) || 1.2;
|
||||
const fps = Number(args.fps) || 12;
|
||||
const type = args.type || 'bounce';
|
||||
const fastTypes = ['bounce', 'wiggle', 'fade'];
|
||||
const ideal = fastTypes.includes(type) ? 1.0 : 1.5;
|
||||
const fpsOk = fps >= 8;
|
||||
const durOk = Math.abs(d - ideal) <= 0.6;
|
||||
const tips = [];
|
||||
if (!fpsOk) tips.push('帧率过低(<8fps)会卡顿,建议 12fps');
|
||||
if (!durOk) tips.push(d < ideal - 0.6 ? '时长过短,动作看不清,建议 ' + ideal + 's' : '时长偏长,动作拖沓,建议 ' + ideal + 's');
|
||||
return {
|
||||
ok: fpsOk && durOk,
|
||||
fps, duration: d, idealDuration: ideal,
|
||||
tip: tips.length ? tips.join(';') : '节奏合理(' + d + 's × ' + fps + 'fps)'
|
||||
};
|
||||
},
|
||||
test() { return this.run({ duration: 1.2, fps: 12, type: 'bounce' }); }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'bounce-physics', name: '弹跳物理', category: 'motion', emoji: '🏀',
|
||||
scene: '检查弹跳动画的落点与压缩是否符合物理直觉',
|
||||
summary: '弹跳应在上升段减速、下落段加速,触底时有轻微压缩(squash)并回弹,幅度不溢出画面。',
|
||||
params: { amplitude: '强度倍率(0.3-2)', elH: '元素高度 px' },
|
||||
source: '自研(弹跳 12 原则)',
|
||||
run(args) {
|
||||
const amp = Number(args.amplitude) || 1;
|
||||
const elH = Number(args.elH) || 80;
|
||||
const jump = elH * 0.5 * amp;
|
||||
const overflows = jump > elH * 0.9;
|
||||
return {
|
||||
ok: amp <= 1.2 && !overflows,
|
||||
jumpHeight: Math.round(jump),
|
||||
tip: overflows ? '弹跳幅度过大,位移可能超出表情画面,建议 amplitude ≤ 1.2' : (amp <= 1.2 ? '弹跳物理合理(高度 ≈ 元素高的 ' + Math.round(50 * amp) + '%)' : '幅度偏大,建议 0.8-1.2 之间')
|
||||
};
|
||||
},
|
||||
test() { return this.run({ amplitude: 1, elH: 80 }); }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'loop-seamless', name: '循环无缝', category: 'motion', emoji: '🔁',
|
||||
scene: '检查动画首尾帧是否平滑衔接(循环无跳变)',
|
||||
summary: '正弦/余弦类动画天然无缝;线性动画(旋转/位移动画)需确认首尾状态一致。',
|
||||
params: { type: '动画类型 id' },
|
||||
source: '自研(循环动画原则)',
|
||||
run(args) {
|
||||
const t = args.type || 'bounce';
|
||||
const seamless = ['pulse', 'wiggle', 'float', 'fade', 'spin', 'twirl'].includes(t);
|
||||
return {
|
||||
ok: seamless,
|
||||
tip: seamless ? '该类型使用正弦/整周期驱动,首尾自然衔接' : '该类型首尾状态可能跳变,建议确认结束帧与起始帧一致或使用整周期时长'
|
||||
};
|
||||
},
|
||||
test() { return this.run({ type: 'pulse' }); }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'motion-simplicity', name: '动画克制性', category: 'motion', emoji: '🧘',
|
||||
scene: '检查表情动画是否过于花哨(多种动画叠加/幅度过大)',
|
||||
summary: '单个表情建议只做一种主动画(或主+极轻微辅助),幅度适中,避免炫技影响识别。',
|
||||
params: { animCount: '同时叠加的动画层数(默认 1)', amplitude: '强度倍率(默认 1)' },
|
||||
source: '自研(动态设计克制原则)',
|
||||
run(args) {
|
||||
const n = Number(args.animCount) || 1;
|
||||
const amp = Number(args.amplitude) || 1;
|
||||
const ok = n <= 1 && amp <= 1.4;
|
||||
return {
|
||||
ok,
|
||||
tip: ok ? '动画克制,主次分明' : (n > 1 ? '同时叠加 ' + n + ' 层动画,建议只保留主动画' : '幅度过大,建议降低到 1.4 以内')
|
||||
};
|
||||
},
|
||||
test() { return this.run({ animCount: 1, amplitude: 1 }); }
|
||||
},
|
||||
|
||||
/* ---------- emotion 情绪传达 ---------- */
|
||||
{
|
||||
id: 'emotion-fit', name: '情绪传达', category: 'motion', emoji: '😄',
|
||||
scene: '检查动画类型与表情主题情绪是否匹配(开心/生气/惊讶/难过/兴奋/平静/鼓励/警示等)',
|
||||
summary: '开心→弹跳/摇摆;生气→抖动;惊讶→心跳/脉冲;难过→漂浮;兴奋→弹跳旋转;平静→呼吸;鼓励→漂浮;警示→闪烁。',
|
||||
params: { keyword: '主题/场景关键词(中文)', type: '动画类型 id' },
|
||||
source: '自研(情绪-动作映射,与 emotion-params 生成决策同源)',
|
||||
run(args) {
|
||||
const kw = String(args.keyword || '');
|
||||
const type = args.type || 'bounce';
|
||||
const map = [
|
||||
{ re: /开心|欢乐|庆祝|成功|胜利|点赞/, types: ['bounce', 'wiggle', 'spin'], hint: '弹跳/摇摆/旋转' },
|
||||
{ re: /生气|愤怒|暴躁/, types: ['shake'], hint: '高频抖动' },
|
||||
{ re: /惊讶|震惊|惊喜|哇/, types: ['heartbeat', 'pulse'], hint: '心跳脉冲' },
|
||||
{ re: /难过|伤心|失落/, types: ['float'], hint: '低落漂浮' },
|
||||
{ re: /兴奋|激动|high/, types: ['hopspin', 'bounce'], hint: '弹跳旋转' },
|
||||
{ re: /平静|舒缓|放松/, types: ['breath', 'float'], hint: '柔和呼吸' },
|
||||
{ re: /鼓励|加油|支持|打气/, types: ['float'], hint: '上下浮动' },
|
||||
{ re: /警示|提醒|注意|重要/, types: ['fade', 'pulse'], hint: '闪烁/脉冲' }
|
||||
];
|
||||
const hit = kw ? map.find(m => m.re.test(kw)) : null;
|
||||
if (!hit) return { ok: true, tip: '未识别情绪关键词,保持默认动画即可' };
|
||||
const ok = hit.types.includes(type);
|
||||
return { ok, tip: ok ? '动画与「' + kw + '」情绪匹配' : '「' + kw + '」更建议使用 ' + hit.hint + ' 表达' };
|
||||
},
|
||||
test() { return this.run({ keyword: '庆祝', type: 'bounce' }); }
|
||||
},
|
||||
|
||||
/* ---------- style 风格匹配 ---------- */
|
||||
{
|
||||
id: 'motion-style-match', name: '动画与风格匹配', category: 'motion', emoji: '🎭',
|
||||
scene: '检查动画风格与视觉风格是否统一(拟物→弹性动画,极简→克制位移)',
|
||||
summary: '圆润/拟物风格适合弹性夸张动画;极简/几何风格适合小幅线性动画;像素风格适合定格跳帧。',
|
||||
params: { styleId: '风格 id', type: '动画类型 id' },
|
||||
source: '自研(风格-动画匹配)',
|
||||
run(args) {
|
||||
const sid = args.styleId || '';
|
||||
const type = args.type || 'bounce';
|
||||
const linear = ['minimal', 'swiss', 'flat'].some(k => sid.includes(k));
|
||||
const pixel = sid.includes('pixel');
|
||||
const playful = ['neon', 'cartoon', 'vibrant', 'fun'].some(k => sid.includes(k));
|
||||
let tip = '动画与风格匹配';
|
||||
let ok = true;
|
||||
if (linear && ['wiggle', 'spin'].includes(type)) { ok = false; tip = '极简/几何风格建议克制位移(小幅漂浮/脉冲),摇摆旋转偏花哨'; }
|
||||
if (pixel && ['float', 'wiggle'].includes(type)) { ok = false; tip = '像素风格建议定格跳帧动画,平滑漂浮/摇摆不协调'; }
|
||||
if (playful && ['fade'].includes(type)) { ok = false; tip = '活泼风格建议弹性动画(弹跳/摇摆),闪烁过于安静'; }
|
||||
return { ok, tip };
|
||||
},
|
||||
test() { return this.run({ styleId: 'minimal', type: 'bounce' }); }
|
||||
},
|
||||
|
||||
/* ---------- export 导出工程 ---------- */
|
||||
{
|
||||
id: 'gif-volume', name: 'GIF 体积控制', category: 'motion', emoji: '📦',
|
||||
scene: '评估导出 GIF 体积是否在平台限制内(微信动图建议 <500KB)',
|
||||
summary: '体积 ≈ 尺寸² × 帧数 × 色彩复杂度;超限时建议降尺寸/降帧率/减少帧数。',
|
||||
params: { size: '边长 px(默认 240)', fps: '帧率(默认 12)', duration: '时长秒(默认 1.2)' },
|
||||
source: '自研(GIF 工程经验)',
|
||||
run(args) {
|
||||
const size = Number(args.size) || 240;
|
||||
const fps = Number(args.fps) || 12;
|
||||
const d = Number(args.duration) || 1.2;
|
||||
const frames = Math.round(fps * d);
|
||||
const estKb = Math.round(size * size * frames * 0.035 / 1024);
|
||||
const ok = estKb <= 500;
|
||||
const tips = [];
|
||||
if (estKb > 500) {
|
||||
if (size > 240) tips.push('降到 240px');
|
||||
if (fps > 10) tips.push('帧率降到 8-10fps');
|
||||
if (d > 1.2) tips.push('时长缩到 1.2s');
|
||||
}
|
||||
return { ok, estimatedKb: estKb, tip: ok ? '预计体积约 ' + estKb + 'KB,平台内合规' : '预计体积约 ' + estKb + 'KB 超限,建议:' + (tips.join('、') || '减少色彩数') };
|
||||
},
|
||||
test() { return this.run({ size: 240, fps: 12, duration: 1.2 }); }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'platform-spec', name: '平台尺寸规范', category: 'motion', emoji: '📱',
|
||||
scene: '按目标平台校验表情尺寸/格式合规性(微信/QQ/Telegram)',
|
||||
summary: '微信动图 240px、QQ 表情 320px、Telegram 贴纸 512px;超限会压缩失真。',
|
||||
params: { platform: '平台 id(wechat/qq/telegram/other)', size: '边长 px' },
|
||||
source: '自研(平台规范)',
|
||||
run(args) {
|
||||
const spec = {
|
||||
wechat: { size: 240, note: '微信动图/自定义表情 240' },
|
||||
qq: { size: 320, note: 'QQ 表情 320' },
|
||||
telegram: { size: 512, note: 'Telegram 贴纸 512' }
|
||||
};
|
||||
const p = spec[args.platform];
|
||||
if (!p) return { ok: true, tip: '自定义尺寸 ' + (args.size || '?') + 'px 可直接使用' };
|
||||
const size = Number(args.size) || 0;
|
||||
const ok = size === p.size;
|
||||
return { ok, tip: ok ? '符合' + p.note + '规格' : '建议导出为 ' + p.size + 'px(' + p.note + '),当前 ' + size + 'px 会被平台压缩' };
|
||||
},
|
||||
test() { return this.run({ platform: 'wechat', size: 240 }); }
|
||||
},
|
||||
|
||||
/* ---------- emotion 情绪驱动(gen:情绪 → 动画决策) ---------- */
|
||||
{
|
||||
id: 'emotion-params', name: '情绪-动画映射', category: 'emotion', emoji: '😊',
|
||||
scene: '根据目标情绪给出动画类型与参数建议(开心/生气/惊讶/难过/兴奋/平静)',
|
||||
summary: '情绪决定动效语言:开心→轻快弹跳、生气→高频抖动、惊讶→心跳脉冲、难过→低落漂浮、兴奋→弹跳旋转、平静→柔和呼吸。',
|
||||
params: { mood: '情绪词(happy/angry/surprised/sad/excited/calm)', type: '当前动画类型(可选)' },
|
||||
source: '自研(情绪-动效语言映射)',
|
||||
run(args) {
|
||||
const map = {
|
||||
happy: { type: 'bounce', speed: 1.2, amplitude: 1, note: '开心:轻快弹跳,节奏明快' },
|
||||
angry: { type: 'shake', speed: 1.4, amplitude: 1.2, note: '生气:高频抖动,幅度略大' },
|
||||
surprised: { type: 'heartbeat', speed: 1.2, amplitude: 1.1, note: '惊讶:心跳脉冲,节奏感强' },
|
||||
sad: { type: 'float', speed: 0.8, amplitude: 0.8, note: '难过:低落漂浮,慢而轻' },
|
||||
excited: { type: 'hopspin', speed: 1.3, amplitude: 1.1, note: '兴奋:弹跳旋转,活力四射' },
|
||||
calm: { type: 'breath', speed: 0.9, amplitude: 0.8, note: '平静:柔和呼吸,舒缓' }
|
||||
};
|
||||
const m = map[String(args.mood || 'happy').toLowerCase()];
|
||||
if (!m) return { ok: false, tip: '未识别情绪,可用:' + Object.keys(map).join('/') };
|
||||
const matched = !args.type || args.type === m.type;
|
||||
return Object.assign({ ok: matched, mood: m.type, tip: matched ? '推荐「' + m.note + '」' : '当前 ' + args.type + ' 与情绪不符,建议改用 ' + m.type + '(' + m.note + ')' }, m);
|
||||
},
|
||||
test() { return this.run({ mood: 'happy' }); }
|
||||
},
|
||||
|
||||
/* ---------- motion 分层与幅度评审 ---------- */
|
||||
{
|
||||
id: 'layer-harmony', name: '分层动画协调', category: 'motion', emoji: '🎼',
|
||||
scene: '检查分层动画(元素错峰)是否协调,不被打乱节奏',
|
||||
summary: '分层错峰应在 0-0.5 周期内:主导元素同拍或先动,辅助元素错峰跟随;超过 0.5 会"各动各的"。',
|
||||
params: { layered: '是否分层', layerSpread: '错峰比例 0-0.6', elementCount: '元素数' },
|
||||
source: '自研(分层动画编排)',
|
||||
run(args) {
|
||||
const layered = !!args.layered;
|
||||
const spread = Number(args.layerSpread) || 0;
|
||||
const n = Number(args.elementCount) || 1;
|
||||
if (!layered || n <= 1) return { ok: true, tip: '单拍动画,无需协调' };
|
||||
const ok = spread >= 0 && spread <= 0.5;
|
||||
return { ok, tip: ok ? '错峰 ' + spread + ' 周期,分层协调有层次' : '错峰比例超 0.5 会各动各的,建议 0.2-0.4' };
|
||||
},
|
||||
test() { return this.run({ layered: true, layerSpread: 0.3, elementCount: 3 }); }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'motion-range', name: '幅度不溢出', category: 'motion', emoji: '📐',
|
||||
scene: '检查动画位移/缩放是否超出表情画布(边缘被裁切)',
|
||||
summary: '位移与缩放总和应在画布内:弹跳高度 ≤ 元素高 50%,缩放 ≤ 1.35,抖动位移 ≤ 8px。',
|
||||
params: { type: '动画类型', amplitude: '强度倍率', elH: '元素高 px', size: '画布边长 px' },
|
||||
source: '自研(画布安全域)',
|
||||
run(args) {
|
||||
const amp = Number(args.amplitude) || 1;
|
||||
const elH = Number(args.elH) || 80;
|
||||
const size = Number(args.size) || 240;
|
||||
const t = args.type || 'bounce';
|
||||
let maxShift = 0, maxScale = 1;
|
||||
if (t === 'bounce' || t === 'hopspin') maxShift = elH * 0.5 * amp;
|
||||
else if (t === 'shake') maxShift = 5 * amp;
|
||||
else if (t === 'pulse') maxScale = 1 + 0.18 * amp;
|
||||
else if (t === 'heartbeat') maxScale = 1 + 0.22 * amp;
|
||||
else if (t === 'breath') maxScale = 1 + 0.09 * amp;
|
||||
const margin = (size - elH) / 2;
|
||||
const ok = maxShift <= margin + elH * 0.1 && maxScale <= 1.5;
|
||||
return { ok, maxShift: Math.round(maxShift * 10) / 10, maxScale,
|
||||
tip: ok ? '位移 ' + Math.round(maxShift) + 'px / 缩放 ' + Math.round(maxScale * 100) + '%,画布内安全' : '动画幅度可能溢出画布,建议降低 amplitude 或扩大安全边距' };
|
||||
},
|
||||
test() { return this.run({ type: 'bounce', amplitude: 1, elH: 80, size: 240 }); }
|
||||
},
|
||||
|
||||
{
|
||||
id: 'motion-ease', name: '缓动曲线', category: 'motion', emoji: '📈',
|
||||
scene: '检查动画是否平滑缓动(无生硬突变)',
|
||||
summary: '正弦/余弦类平滑;弹跳应有减速段;线性匀速位移显得生硬。',
|
||||
params: { type: '动画类型 id' },
|
||||
source: '自研(缓动 12 原则)',
|
||||
run(args) {
|
||||
const t = args.type || 'bounce';
|
||||
const smooth = ['pulse', 'float', 'fade', 'breath', 'heartbeat', 'twirl', 'wiggle'].includes(t);
|
||||
const eased = ['bounce', 'hopspin'].includes(t);
|
||||
return { ok: smooth || eased, tip: (smooth ? '正弦驱动,平滑自然' : eased ? '含减速/压缩段,物理自然' : '线性驱动,建议叠加缓动曲线') + '(' + t + ')' };
|
||||
},
|
||||
test() { return this.run({ type: 'bounce' }); }
|
||||
}
|
||||
]);
|
||||
Reference in New Issue
Block a user