558 lines
14 KiB
JavaScript
558 lines
14 KiB
JavaScript
|
|
// 动画管理器 - 微信小程序专用
|
|||
|
|
// 提供统一的动画效果和页面过渡管理
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 动画管理器
|
|||
|
|
* 功能:
|
|||
|
|
* 1. 页面过渡动画
|
|||
|
|
* 2. 组件动画效果
|
|||
|
|
* 3. 交互反馈动画
|
|||
|
|
* 4. 加载状态动画
|
|||
|
|
* 5. 手势动画
|
|||
|
|
* 6. 性能优化动画
|
|||
|
|
*/
|
|||
|
|
class AnimationManager {
|
|||
|
|
constructor() {
|
|||
|
|
this.isInitialized = false;
|
|||
|
|
|
|||
|
|
// 动画配置
|
|||
|
|
this.config = {
|
|||
|
|
// 动画开关
|
|||
|
|
enabled: true,
|
|||
|
|
|
|||
|
|
// 性能模式
|
|||
|
|
performanceMode: 'auto', // auto, high, low
|
|||
|
|
|
|||
|
|
// 动画时长配置
|
|||
|
|
durations: {
|
|||
|
|
fast: 200,
|
|||
|
|
normal: 300,
|
|||
|
|
slow: 500,
|
|||
|
|
page: 400
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 缓动函数配置
|
|||
|
|
easings: {
|
|||
|
|
ease: 'ease',
|
|||
|
|
easeIn: 'ease-in',
|
|||
|
|
easeOut: 'ease-out',
|
|||
|
|
easeInOut: 'ease-in-out',
|
|||
|
|
linear: 'linear',
|
|||
|
|
spring: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
|||
|
|
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 页面过渡配置
|
|||
|
|
pageTransitions: {
|
|||
|
|
slideLeft: {
|
|||
|
|
enter: { transform: 'translateX(100%)', opacity: 0 },
|
|||
|
|
active: { transform: 'translateX(0)', opacity: 1 },
|
|||
|
|
leave: { transform: 'translateX(-100%)', opacity: 0 }
|
|||
|
|
},
|
|||
|
|
slideRight: {
|
|||
|
|
enter: { transform: 'translateX(-100%)', opacity: 0 },
|
|||
|
|
active: { transform: 'translateX(0)', opacity: 1 },
|
|||
|
|
leave: { transform: 'translateX(100%)', opacity: 0 }
|
|||
|
|
},
|
|||
|
|
slideUp: {
|
|||
|
|
enter: { transform: 'translateY(100%)', opacity: 0 },
|
|||
|
|
active: { transform: 'translateY(0)', opacity: 1 },
|
|||
|
|
leave: { transform: 'translateY(-100%)', opacity: 0 }
|
|||
|
|
},
|
|||
|
|
slideDown: {
|
|||
|
|
enter: { transform: 'translateY(-100%)', opacity: 0 },
|
|||
|
|
active: { transform: 'translateY(0)', opacity: 1 },
|
|||
|
|
leave: { transform: 'translateY(100%)', opacity: 0 }
|
|||
|
|
},
|
|||
|
|
fade: {
|
|||
|
|
enter: { opacity: 0 },
|
|||
|
|
active: { opacity: 1 },
|
|||
|
|
leave: { opacity: 0 }
|
|||
|
|
},
|
|||
|
|
scale: {
|
|||
|
|
enter: { transform: 'scale(0.8)', opacity: 0 },
|
|||
|
|
active: { transform: 'scale(1)', opacity: 1 },
|
|||
|
|
leave: { transform: 'scale(1.2)', opacity: 0 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 动画实例缓存
|
|||
|
|
this.animationCache = new Map();
|
|||
|
|
|
|||
|
|
// 性能监控
|
|||
|
|
this.performanceStats = {
|
|||
|
|
animationCount: 0,
|
|||
|
|
averageDuration: 0,
|
|||
|
|
droppedFrames: 0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化动画管理器
|
|||
|
|
init() {
|
|||
|
|
if (this.isInitialized) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 检测设备性能
|
|||
|
|
this.detectPerformance();
|
|||
|
|
|
|||
|
|
// 设置全局动画样式
|
|||
|
|
this.setupGlobalStyles();
|
|||
|
|
|
|||
|
|
this.isInitialized = true;
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 动画管理器初始化失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🎭 ===== 基础动画 =====
|
|||
|
|
|
|||
|
|
// 创建动画实例
|
|||
|
|
createAnimation(options = {}) {
|
|||
|
|
const defaultOptions = {
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeOut,
|
|||
|
|
delay: 0,
|
|||
|
|
transformOrigin: '50% 50% 0'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const animationOptions = { ...defaultOptions, ...options };
|
|||
|
|
|
|||
|
|
// 根据性能模式调整动画
|
|||
|
|
if (this.config.performanceMode === 'low') {
|
|||
|
|
animationOptions.duration = Math.min(animationOptions.duration, 200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const animation = wx.createAnimation(animationOptions);
|
|||
|
|
|
|||
|
|
// 缓存动画实例
|
|||
|
|
const animationId = this.generateAnimationId();
|
|||
|
|
this.animationCache.set(animationId, {
|
|||
|
|
animation: animation,
|
|||
|
|
options: animationOptions,
|
|||
|
|
createTime: Date.now()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return { animation, animationId };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 淡入动画
|
|||
|
|
fadeIn(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation.opacity(1).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 淡出动画
|
|||
|
|
fadeOut(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeIn,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation.opacity(0).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 滑入动画
|
|||
|
|
slideIn(direction = 'left', options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.spring,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const transforms = {
|
|||
|
|
left: () => animation.translateX(0),
|
|||
|
|
right: () => animation.translateX(0),
|
|||
|
|
up: () => animation.translateY(0),
|
|||
|
|
down: () => animation.translateY(0)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return transforms[direction]().opacity(1).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 滑出动画
|
|||
|
|
slideOut(direction = 'left', distance = '100%', options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeIn,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const transforms = {
|
|||
|
|
left: () => animation.translateX(`-${distance}`),
|
|||
|
|
right: () => animation.translateX(distance),
|
|||
|
|
up: () => animation.translateY(`-${distance}`),
|
|||
|
|
down: () => animation.translateY(distance)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return transforms[direction]().opacity(0).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 缩放动画
|
|||
|
|
scale(scale = 1, options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.spring,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation.scale(scale).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 旋转动画
|
|||
|
|
rotate(angle = 360, options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.slow,
|
|||
|
|
timingFunction: this.config.easings.linear,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation.rotate(angle).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🎪 ===== 组合动画 =====
|
|||
|
|
|
|||
|
|
// 弹跳进入动画
|
|||
|
|
bounceIn(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.slow,
|
|||
|
|
timingFunction: this.config.easings.bounce,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.scale(0.3).opacity(0).step({ duration: 0 })
|
|||
|
|
.scale(1.05).opacity(1).step({ duration: 200 })
|
|||
|
|
.scale(0.95).step({ duration: 100 })
|
|||
|
|
.scale(1).step({ duration: 100 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 弹跳退出动画
|
|||
|
|
bounceOut(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeIn,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.scale(1.1).step({ duration: 100 })
|
|||
|
|
.scale(0).opacity(0).step({ duration: 200 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 摇摆动画
|
|||
|
|
shake(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.fast,
|
|||
|
|
timingFunction: this.config.easings.linear,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.translateX(-10).step({ duration: 50 })
|
|||
|
|
.translateX(10).step({ duration: 50 })
|
|||
|
|
.translateX(-10).step({ duration: 50 })
|
|||
|
|
.translateX(10).step({ duration: 50 })
|
|||
|
|
.translateX(0).step({ duration: 50 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 脉冲动画
|
|||
|
|
pulse(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeInOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.scale(1).step({ duration: 0 })
|
|||
|
|
.scale(1.05).step({ duration: 150 })
|
|||
|
|
.scale(1).step({ duration: 150 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🎬 ===== 页面过渡 =====
|
|||
|
|
|
|||
|
|
// 页面进入动画
|
|||
|
|
pageEnter(transitionType = 'slideLeft', options = {}) {
|
|||
|
|
const transition = this.config.pageTransitions[transitionType];
|
|||
|
|
if (!transition) {
|
|||
|
|
console.warn('未知的页面过渡类型:', transitionType);
|
|||
|
|
return this.fadeIn(options);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.page,
|
|||
|
|
timingFunction: this.config.easings.easeOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 设置初始状态
|
|||
|
|
this.applyTransform(animation, transition.enter);
|
|||
|
|
animation.step({ duration: 0 });
|
|||
|
|
|
|||
|
|
// 执行进入动画
|
|||
|
|
this.applyTransform(animation, transition.active);
|
|||
|
|
return animation.step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 页面退出动画
|
|||
|
|
pageLeave(transitionType = 'slideLeft', options = {}) {
|
|||
|
|
const transition = this.config.pageTransitions[transitionType];
|
|||
|
|
if (!transition) {
|
|||
|
|
console.warn('未知的页面过渡类型:', transitionType);
|
|||
|
|
return this.fadeOut(options);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.page,
|
|||
|
|
timingFunction: this.config.easings.easeIn,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 执行退出动画
|
|||
|
|
this.applyTransform(animation, transition.leave);
|
|||
|
|
return animation.step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🎯 ===== 交互动画 =====
|
|||
|
|
|
|||
|
|
// 按钮点击动画
|
|||
|
|
buttonPress(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.fast,
|
|||
|
|
timingFunction: this.config.easings.easeOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.scale(0.95).step({ duration: 100 })
|
|||
|
|
.scale(1).step({ duration: 100 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 卡片悬停动画
|
|||
|
|
cardHover(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.scale(1.02)
|
|||
|
|
.translateY(-2)
|
|||
|
|
.step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 列表项滑动动画
|
|||
|
|
listItemSlide(direction = 'left', options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: this.config.durations.normal,
|
|||
|
|
timingFunction: this.config.easings.easeOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const distance = direction === 'left' ? -80 : 80;
|
|||
|
|
return animation.translateX(distance).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🔄 ===== 加载动画 =====
|
|||
|
|
|
|||
|
|
// 旋转加载动画
|
|||
|
|
loadingSpinner(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: 1000,
|
|||
|
|
timingFunction: this.config.easings.linear,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation.rotate(360).step();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 脉冲加载动画
|
|||
|
|
loadingPulse(options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: 1000,
|
|||
|
|
timingFunction: this.config.easings.easeInOut,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.opacity(0.3).step({ duration: 500 })
|
|||
|
|
.opacity(1).step({ duration: 500 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 波浪加载动画
|
|||
|
|
loadingWave(delay = 0, options = {}) {
|
|||
|
|
const { animation } = this.createAnimation({
|
|||
|
|
duration: 600,
|
|||
|
|
timingFunction: this.config.easings.easeInOut,
|
|||
|
|
delay: delay,
|
|||
|
|
...options
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return animation
|
|||
|
|
.translateY(-10).step({ duration: 300 })
|
|||
|
|
.translateY(0).step({ duration: 300 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🎨 ===== 工具方法 =====
|
|||
|
|
|
|||
|
|
// 应用变换
|
|||
|
|
applyTransform(animation, transform) {
|
|||
|
|
Object.keys(transform).forEach(property => {
|
|||
|
|
const value = transform[property];
|
|||
|
|
|
|||
|
|
switch (property) {
|
|||
|
|
case 'opacity':
|
|||
|
|
animation.opacity(parseFloat(value));
|
|||
|
|
break;
|
|||
|
|
case 'transform':
|
|||
|
|
// 解析transform值
|
|||
|
|
this.parseTransform(animation, value);
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.warn('不支持的动画属性:', property);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析transform值
|
|||
|
|
parseTransform(animation, transformValue) {
|
|||
|
|
const transforms = transformValue.split(' ');
|
|||
|
|
|
|||
|
|
transforms.forEach(transform => {
|
|||
|
|
if (transform.includes('translateX')) {
|
|||
|
|
const value = this.extractValue(transform);
|
|||
|
|
animation.translateX(value);
|
|||
|
|
} else if (transform.includes('translateY')) {
|
|||
|
|
const value = this.extractValue(transform);
|
|||
|
|
animation.translateY(value);
|
|||
|
|
} else if (transform.includes('scale')) {
|
|||
|
|
const value = parseFloat(this.extractValue(transform));
|
|||
|
|
animation.scale(value);
|
|||
|
|
} else if (transform.includes('rotate')) {
|
|||
|
|
const value = this.extractValue(transform);
|
|||
|
|
animation.rotate(parseFloat(value));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提取变换值
|
|||
|
|
extractValue(transform) {
|
|||
|
|
const match = transform.match(/\(([^)]+)\)/);
|
|||
|
|
return match ? match[1] : '0';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测设备性能
|
|||
|
|
detectPerformance() {
|
|||
|
|
try {
|
|||
|
|
// 使用新的API替代已弃用的wx.getSystemInfoSync
|
|||
|
|
const deviceInfo = wx.getDeviceInfo();
|
|||
|
|
const { platform, system, model } = deviceInfo;
|
|||
|
|
|
|||
|
|
// 简单的性能评估
|
|||
|
|
let performanceScore = 100;
|
|||
|
|
|
|||
|
|
// 根据平台调整
|
|||
|
|
if (platform === 'android') {
|
|||
|
|
performanceScore -= 10;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据系统版本调整
|
|||
|
|
const systemVersion = parseFloat(system.match(/[\d.]+/)?.[0] || '0');
|
|||
|
|
if (systemVersion < 10) {
|
|||
|
|
performanceScore -= 20;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据机型调整(简化判断)
|
|||
|
|
if (model && model.toLowerCase().includes('redmi')) {
|
|||
|
|
performanceScore -= 15;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置性能模式
|
|||
|
|
if (performanceScore >= 80) {
|
|||
|
|
this.config.performanceMode = 'high';
|
|||
|
|
} else if (performanceScore >= 60) {
|
|||
|
|
this.config.performanceMode = 'auto';
|
|||
|
|
} else {
|
|||
|
|
this.config.performanceMode = 'low';
|
|||
|
|
// 低性能模式下禁用复杂动画
|
|||
|
|
this.config.durations.normal = 200;
|
|||
|
|
this.config.durations.slow = 300;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 性能检测失败:', error);
|
|||
|
|
this.config.performanceMode = 'auto';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置全局动画样式
|
|||
|
|
setupGlobalStyles() {
|
|||
|
|
// 这里可以设置一些全局的CSS动画类
|
|||
|
|
// 微信小程序中主要通过WXSS实现
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成动画ID
|
|||
|
|
generateAnimationId() {
|
|||
|
|
return `anim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理动画缓存
|
|||
|
|
cleanupAnimationCache() {
|
|||
|
|
const now = Date.now();
|
|||
|
|
const expireTime = 5 * 60 * 1000; // 5分钟
|
|||
|
|
|
|||
|
|
for (const [id, cache] of this.animationCache) {
|
|||
|
|
if (now - cache.createTime > expireTime) {
|
|||
|
|
this.animationCache.delete(id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取性能统计
|
|||
|
|
getPerformanceStats() {
|
|||
|
|
return {
|
|||
|
|
...this.performanceStats,
|
|||
|
|
cacheSize: this.animationCache.size,
|
|||
|
|
performanceMode: this.config.performanceMode
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置动画开关
|
|||
|
|
setEnabled(enabled) {
|
|||
|
|
this.config.enabled = enabled;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置性能模式
|
|||
|
|
setPerformanceMode(mode) {
|
|||
|
|
if (['auto', 'high', 'low'].includes(mode)) {
|
|||
|
|
this.config.performanceMode = mode;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 销毁动画管理器
|
|||
|
|
destroy() {
|
|||
|
|
this.animationCache.clear();
|
|||
|
|
this.isInitialized = false;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建全局实例
|
|||
|
|
const animationManager = new AnimationManager();
|
|||
|
|
|
|||
|
|
module.exports = animationManager;
|
|||
|
|
|