/** * 画个标 · 核心:动态表情动画引擎(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;