Files

269 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 画个标 · 表情技能(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' }); }
}
]);