/** * AnimationManager - 动画管理器 (性能优化版) * * ============================================================================ * 🚨 CRITICAL PRINCIPLE: DATA-FIRST, UI-DEFERRED (数据优先,界面延后) * ============================================================================ * * ALL animations in this project MUST follow the "Data-First, UI-Deferred" pattern: * * 1. UPDATE DATA FIRST (Before Animation) * - Update the model/state to reflect the FINAL expected state * - Do NOT update the UI at this point * - This ensures data integrity even if animation is interrupted or skipped * * 2. PLAY ANIMATION (Visual Transition) * - Animation is purely a visual representation of the state change * - Animation MUST NOT modify game data or state * - Animation can be skipped/cancelled without affecting game logic * * 3. UPDATE UI AFTER ANIMATION (On Complete Callback) * - Refresh the UI to reflect the already-updated data * - This synchronizes the visual display with the data state * * EXAMPLE - Draw Card Flow: * Step 1: playerHands[playerIndex].push(card) // Update data * Step 2: playDrawCardAnimation(...) // Play animation * Step 3: (onComplete) _updatePlayerHand(...) // Update UI * * EXAMPLE - Insert Card into Hand Flow: * Step 1: Sort hand including new card (data) // Update data * Step 2: playDrawCardInsertAnimation(...) // Play animation * Step 3: (onComplete) _updatePlayerHand(...) // Update UI * * WHY THIS MATTERS: * - Animation is just a presentation layer, it should never affect data correctness * - If animation fails/skips, the game state remains consistent * - Supports instant mode (no animation) without code changes * - Prevents race conditions between animations and game logic * * ⚠️ WARNING: Violating this principle will cause data inconsistency bugs that * are extremely difficult to debug. All developers and AI assistants MUST * follow this pattern when implementing any animation-related code. * * ============================================================================ * 🚨 核心原则:数据优先,界面延后(中文说明) * ============================================================================ * * 本项目所有动画必须遵循「数据优先,界面延后」模式: * * 1. 先更新数据(动画播放前) * - 将模型/状态更新为动画完成后的最终预期状态 * - 此时不要更新界面 * - 这确保即使动画被中断或跳过,数据仍然正确 * * 2. 播放动画(视觉过渡) * - 动画只是状态变化的视觉表现 * - 动画绝对不能修改游戏数据或状态 * - 动画可以被跳过/取消而不影响游戏逻辑 * * 3. 动画完成后更新界面(在完成回调中) * - 刷新界面以反映已更新的数据 * - 使视觉显示与数据状态同步 * * 示例 - 摸牌流程: * 第1步: playerHands[playerIndex].push(card) // 更新数据 * 第2步: playDrawCardAnimation(...) // 播放动画 * 第3步: (onComplete) _updatePlayerHand(...) // 更新界面 * * 示例 - 摸牌插入手牌流程: * 第1步: 对包含新牌的手牌进行排序(数据层面) // 更新数据 * 第2步: playDrawCardInsertAnimation(...) // 播放动画 * 第3步: (onComplete) _updatePlayerHand(...) // 更新界面 * * 为什么这很重要: * - 动画只是表现层,绝不能影响数据的正确性 * - 如果动画失败/跳过,游戏状态仍保持一致 * - 无需修改代码即可支持无动画模式(即时模式) * - 防止动画与游戏逻辑之间的竞态条件 * * ⚠️ 警告:违反此原则将导致极难调试的数据不一致bug。 * 所有开发人员和AI助手在实现任何动画相关代码时必须遵循此模式。 * * ============================================================================ * * 功能: * 1. 管理所有游戏动画 * 2. 提供基础动画(移动/缩放/旋转/淡入淡出/帧动画) * 3. 支持动画队列(串行/并行) * 4. 支持缓动函数 * 5. 动画生命周期管理 * * 性能优化: * - 使用 SpriteManager API 统一管理精灵属性 * - 使用数组存储活动动画,避免 for...in 遍历 * - 缓存缓动函数引用,避免每帧字符串查找 * - 活动动画计数器,O(1) 判断是否需要循环 * - 预计算差值,减少每帧计算量 * - 帧动画优化:只在帧变化时更新 * * 依赖: * - SpriteManager - 精灵属性管理器 (必须先加载) * * ============================================================================ * @guide 开发指南 - AnimationManager 使用规范 * ============================================================================ * * 【初始化(UIBootstrap 中调用一次)】 * AnimationManager.init(); * * 【核心原则:数据优先,界面延后(必须遵守)】 * * 错误写法(先更新UI再改数据): * SpriteManager.setPosition(spriteId, targetX, targetY); // ❌ 先改UI * gameData.push(card); // ❌ 后改数据 * * 正确写法(先改数据,动画完成后再刷新UI): * // Step 1: 先更新数据层 * gameData.push(card); * * // Step 2: 播放动画 * AnimationManager.move(spriteId, fromX, fromY, targetX, targetY, 300, 'easeOut', * function() { * // Step 3: 动画完成回调中刷新UI * self._updatePlayerHandDisplay(); * } * ); * * 【移动动画】 * // 精灵从当前位置移动到目标位置,300ms,缓出 * AnimationManager.move(spriteId, startX, startY, endX, endY, 300, 'easeOut', callback); * * 【缩放动画(突出显示效果)】 * // 先放大后缩回(弹出效果) * AnimationManager.scale(spriteId, 1.0, 1.3, 150, 'easeOut', function() { * AnimationManager.scale(spriteId, 1.3, 1.0, 150, 'easeIn', null); * }); * * 【透明度动画(淡入淡出)】 * // 淡出 * AnimationManager.opacity(spriteId, 1.0, 0.0, 200, 'linear', function() { * SpriteManager.hide(spriteId); * }); * * 【帧动画(翻牌等序列帧)】 * // 从第0帧播放到第7帧,每帧60ms * AnimationManager.frame(spriteId, 0, 7, 60, false, callback); * * 【停止/取消动画】 * var animId = AnimationManager.move(...); * AnimationManager.stop(animId); // 停止单个动画 * AnimationManager.stopAll(); // 停止所有动画(游戏结束时) * * 【缓动函数可选值】 * 'linear' 匀速 * 'easeIn' 加速 * 'easeOut' 减速(最常用) * 'easeInOut' 先加速后减速 * * @version 2.0.0 * @date 2025-12-14 */ /* global SpriteManager */ var AnimationManager = AnimationManager || {}; (function() { 'use strict'; // ============================================================================ // 私有变量 (性能优化) // ============================================================================ var _initialized = false; var _animationList = []; // 活动动画数组(遍历更快) var _animationMap = {}; // ID到数组索引的映射(快速查找/删除) var _spriteAnimMap = {}; // 精灵ID+动画类型 到 动画ID的映射(防止重复动画冲突) var _queues = {}; // 动画队列 var _nextAnimationId = 1; var _nextQueueId = 1; var _rafId = null; var _activeCount = 0; // 活动动画计数(避免遍历判断) // 属性码常量(避免重复查找 GameABCUtils.PropertyCodes) var PROP_X = 18; var PROP_Y = 19; var PROP_SCALE = 33; var PROP_ROTATION = 34; var PROP_OPACITY = 35; var PROP_FRAME = 43; // ============================================================================ // 缓动函数 (内联优化) // ============================================================================ var Easing = { linear: function(t) { return t; }, easeIn: function(t) { return t * t; }, easeOut: function(t) { return t * (2 - t); }, easeInOut: function(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }, easeInCubic: function(t) { return t * t * t; }, easeOutCubic: function(t) { var t1 = t - 1; return t1 * t1 * t1 + 1; }, easeInOutCubic: function(t) { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; }, elastic: function(t) { return t === 0 || t === 1 ? t : -Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI); }, bounce: function(t) { if (t < 1 / 2.75) { return 7.5625 * t * t; } if (t < 2 / 2.75) { t -= 1.5 / 2.75; return 7.5625 * t * t + 0.75; } if (t < 2.5 / 2.75) { t -= 2.25 / 2.75; return 7.5625 * t * t + 0.9375; } t -= 2.625 / 2.75; return 7.5625 * t * t + 0.984375; } }; // 缓动函数缓存(启动时建立,避免每帧查找) var _easingCache = {}; for (var key in Easing) { if (Easing.hasOwnProperty(key)) { _easingCache[key] = Easing[key]; } } // ============================================================================ // 动画管理(优化版) // ============================================================================ /** * 生成精灵动画唯一标识 (精灵ID + 动画类型) * @private */ function _getSpriteAnimKey(spriteId, animType) { return spriteId + '_' + animType; } /** * 停止精灵的同类型动画 (防止重复动画冲突) * @private */ function _stopExistingSpriteAnimation(spriteId, animType) { var key = _getSpriteAnimKey(spriteId, animType); var existingAnimId = _spriteAnimMap[key]; if (existingAnimId !== undefined) { // 停止并移除旧动画 var index = _animationMap[existingAnimId]; if (index !== undefined) { _animationList[index].stop(); _removeAnimation(existingAnimId); } } } /** * 添加动画到活动列表 * @private */ function _addAnimation(animation) { // 如果同一精灵已有同类型动画在播放,先停止旧动画 if (animation.spriteId && animation.animType) { _stopExistingSpriteAnimation(animation.spriteId, animation.animType); // 记录新动画 var key = _getSpriteAnimKey(animation.spriteId, animation.animType); _spriteAnimMap[key] = animation.id; } var index = _animationList.length; _animationList.push(animation); _animationMap[animation.id] = index; _activeCount++; } /** * 从活动列表移除动画(使用交换删除法避免数组移位) * @private */ function _removeAnimation(animationId) { var index = _animationMap[animationId]; if (index === undefined) return; // 清理精灵动画映射 var animation = _animationList[index]; if (animation && animation.spriteId && animation.animType) { var key = _getSpriteAnimKey(animation.spriteId, animation.animType); if (_spriteAnimMap[key] === animationId) { delete _spriteAnimMap[key]; } } var lastIndex = _animationList.length - 1; // 如果不是最后一个,用最后一个交换到当前位置 if (index !== lastIndex) { var lastAnimation = _animationList[lastIndex]; _animationList[index] = lastAnimation; _animationMap[lastAnimation.id] = index; } // 移除最后一个 _animationList.pop(); delete _animationMap[animationId]; _activeCount--; } // ============================================================================ // 动画基类 (优化版) // ============================================================================ /** * 动画基类 * @constructor */ function Animation(config) { this.id = _nextAnimationId++; this.spriteId = config.spriteId; this.animType = config.animType || 'generic'; // 动画类型标识,用于防止重复动画 this.duration = config.duration || 300; this.callback = config.callback; this.startTime = 0; this.elapsedTime = 0; this.isPlaying = false; this.isPaused = false; this.isComplete = false; // 缓存缓动函数引用(避免每帧字符串查找) this._easingFunc = _easingCache[config.easing] || Easing.linear; } Animation.prototype.start = function() { this.startTime = Date.now(); this.isPlaying = true; this.isPaused = false; this.isComplete = false; }; Animation.prototype.update = function(currentTime) { if (!this.isPlaying || this.isPaused || this.isComplete) { return; } this.elapsedTime = currentTime - this.startTime; var progress = this.elapsedTime / this.duration; if (progress >= 1) { // 直接调用缓存的函数引用 this.animate(this._easingFunc(1)); this.complete(); } else { this.animate(this._easingFunc(progress)); } }; Animation.prototype.animate = function(progress) { // 子类实现 }; Animation.prototype.complete = function() { this.isComplete = true; this.isPlaying = false; if (this.callback) { this.callback(); } }; Animation.prototype.stop = function() { this.isPlaying = false; this.isPaused = false; this.isComplete = true; }; Animation.prototype.pause = function() { if (this.isPlaying && !this.isPaused) { this.isPaused = true; } }; Animation.prototype.resume = function() { if (this.isPaused) { this.isPaused = false; this.startTime = Date.now() - this.elapsedTime; } }; // ============================================================================ // 移动动画 (优化版 - 预计算差值) // ============================================================================ /** * 移动动画 * @constructor * @extends Animation */ function MoveAnimation(config) { config.animType = 'move'; // 设置动画类型 Animation.call(this, config); this.fromX = config.fromX; this.fromY = config.fromY; this.toX = config.toX; this.toY = config.toY; // 预计算差值(避免每帧计算) this._deltaX = this.toX - this.fromX; this._deltaY = this.toY - this.fromY; } MoveAnimation.prototype = Object.create(Animation.prototype); MoveAnimation.prototype.constructor = MoveAnimation; MoveAnimation.prototype.animate = function(progress) { SpriteManager.setPosition( this.spriteId, this.fromX + this._deltaX * progress, this.fromY + this._deltaY * progress ); }; // ============================================================================ // 缩放动画 (优化版) // ============================================================================ /** * 缩放动画 * @constructor * @extends Animation */ function ScaleAnimation(config) { config.animType = 'scale'; // 设置动画类型 Animation.call(this, config); this.fromScale = config.fromScale !== undefined ? config.fromScale : 1; this.toScale = config.toScale !== undefined ? config.toScale : 1; // 预计算差值 this._deltaScale = this.toScale - this.fromScale; } ScaleAnimation.prototype = Object.create(Animation.prototype); ScaleAnimation.prototype.constructor = ScaleAnimation; ScaleAnimation.prototype.animate = function(progress) { var scale = this.fromScale + this._deltaScale * progress; SpriteManager.setScale(this.spriteId, scale); }; // ============================================================================ // 旋转动画 (优化版) // ============================================================================ /** * 旋转动画 * @constructor * @extends Animation */ function RotateAnimation(config) { config.animType = 'rotate'; // 设置动画类型 Animation.call(this, config); this.fromAngle = config.fromAngle || 0; this.toAngle = config.toAngle || 0; this._deltaAngle = this.toAngle - this.fromAngle; } RotateAnimation.prototype = Object.create(Animation.prototype); RotateAnimation.prototype.constructor = RotateAnimation; RotateAnimation.prototype.animate = function(progress) { var angle = this.fromAngle + this._deltaAngle * progress; SpriteManager.setRotation(this.spriteId, angle); }; // ============================================================================ // 淡入淡出动画 (优化版) // ============================================================================ /** * 淡入淡出动画 * @constructor * @extends Animation */ function FadeAnimation(config) { config.animType = 'fade'; // 设置动画类型 Animation.call(this, config); this.fromAlpha = config.fromAlpha !== undefined ? config.fromAlpha : 1; this.toAlpha = config.toAlpha !== undefined ? config.toAlpha : 1; this._deltaAlpha = this.toAlpha - this.fromAlpha; } FadeAnimation.prototype = Object.create(Animation.prototype); FadeAnimation.prototype.constructor = FadeAnimation; FadeAnimation.prototype.animate = function(progress) { var alpha = this.fromAlpha + this._deltaAlpha * progress; SpriteManager.setOpacity(this.spriteId, alpha); }; // ============================================================================ // 帧动画 (优化版 - 只在帧变化时更新) // ============================================================================ /** * 帧动画 - 用于播放多帧图片序列动画 * @constructor * @extends Animation * @param {Object} config - 配置对象 * @param {number} config.spriteId - 精灵ID * @param {number} config.startFrame - 起始帧(从1开始) * @param {number} config.endFrame - 结束帧 * @param {number} config.duration - 持续时间(ms),单次播放时间 * @param {boolean} [config.loop=false] - 是否循环播放 * @param {number} [config.loopCount=-1] - 循环次数,-1表示无限循环 * @param {boolean} [config.pingPong=false] - 是否往返播放(1-2-3-2-1) * @param {string} [config.easing='linear'] - 缓动函数 * @param {Function} [config.callback] - 完成回调 * @param {Function} [config.onFrame] - 帧变化回调 */ function FrameAnimation(config) { config.animType = 'frame'; // 设置动画类型 Animation.call(this, config); this.startFrame = config.startFrame || 1; this.endFrame = config.endFrame || 1; this.loop = config.loop || false; this.loopCount = config.loopCount !== undefined ? config.loopCount : -1; this.pingPong = config.pingPong || false; this.onFrame = config.onFrame; // 预计算(避免每帧计算) this._totalFrames = Math.abs(this.endFrame - this.startFrame) + 1; this._frameRange = this._totalFrames - 1; this._minFrame = Math.min(this.startFrame, this.endFrame); this._maxFrame = Math.max(this.startFrame, this.endFrame); this._lastFrame = -1; this._singleDuration = config.duration || 300; // 循环时重新计算总时长 if (this.loop && this.loopCount > 0) { this.duration = this._singleDuration * this.loopCount; if (this.pingPong) { this.duration = this.duration * 2; } } } FrameAnimation.prototype = Object.create(Animation.prototype); FrameAnimation.prototype.constructor = FrameAnimation; FrameAnimation.prototype.animate = function(progress) { var currentFrame; if (this.loop && this.loopCount < 0) { // 无限循环模式,使用循环进度 var elapsed = this.elapsedTime % this._singleDuration; var loopProgress = elapsed / this._singleDuration; if (this.pingPong) { var cycleTime = this._singleDuration * 2; var cycleElapsed = this.elapsedTime % cycleTime; if (cycleElapsed < this._singleDuration) { loopProgress = cycleElapsed / this._singleDuration; } else { loopProgress = 1 - (cycleElapsed - this._singleDuration) / this._singleDuration; } } currentFrame = Math.floor(this.startFrame + loopProgress * this._frameRange); } else if (this.loop && this.loopCount > 0) { // 有限循环模式 - 修复循环逻辑 var elapsed = this.elapsedTime % this._singleDuration; var loopProgress = elapsed / this._singleDuration; if (this.pingPong) { var cycleTime = this._singleDuration * 2; var cycleElapsed = this.elapsedTime % cycleTime; if (cycleElapsed < this._singleDuration) { loopProgress = cycleElapsed / this._singleDuration; } else { loopProgress = 1 - (cycleElapsed - this._singleDuration) / this._singleDuration; } } currentFrame = Math.floor(this.startFrame + loopProgress * this._frameRange); } else { // 非循环模式 if (this.pingPong) { if (progress < 0.5) { currentFrame = Math.floor(this.startFrame + (progress * 2) * this._frameRange); } else { currentFrame = Math.floor(this.endFrame - ((progress - 0.5) * 2) * this._frameRange); } } else { currentFrame = Math.floor(this.startFrame + progress * this._frameRange); } } // 确保帧号在范围内 if (currentFrame < this._minFrame) currentFrame = this._minFrame; if (currentFrame > this._maxFrame) currentFrame = this._maxFrame; // 只有帧变化时才更新(重要优化) if (currentFrame !== this._lastFrame) { SpriteManager.setFrame(this.spriteId, currentFrame); this._lastFrame = currentFrame; if (this.onFrame) { this.onFrame(currentFrame, this.spriteId); } } }; FrameAnimation.prototype.update = function(currentTime) { if (!this.isPlaying || this.isPaused || this.isComplete) { return; } this.elapsedTime = currentTime - this.startTime; // 无限循环模式,永不结束 if (this.loop && this.loopCount < 0) { this.animate(0); return; } var progress = this.elapsedTime / this.duration; if (progress >= 1) { this.animate(this._easingFunc(1)); this.complete(); } else { this.animate(this._easingFunc(progress)); } }; // ============================================================================ // 动画队列 (优化版) // ============================================================================ /** * 动画队列 * @constructor * @param {string} type - 队列类型: 'serial' 或 'parallel' */ function AnimationQueue(type) { this.id = _nextQueueId++; this.type = type || 'serial'; this.animations = []; this.currentIndex = 0; this.isPlaying = false; this.isPaused = false; this.callback = null; } AnimationQueue.prototype.add = function(animation) { this.animations.push(animation); }; AnimationQueue.prototype.play = function(callback) { this.callback = callback; this.isPlaying = true; this.isPaused = false; this.currentIndex = 0; if (this.type === 'serial') { this._playSerial(); } else { this._playParallel(); } }; AnimationQueue.prototype._playSerial = function() { var self = this; if (self.currentIndex >= self.animations.length) { self._complete(); return; } var animation = self.animations[self.currentIndex]; var originalCallback = animation.callback; animation.callback = function() { if (originalCallback) { originalCallback(); } self.currentIndex++; self._playSerial(); }; _addAnimation(animation); animation.start(); }; AnimationQueue.prototype._playParallel = function() { var self = this; var completedCount = 0; var totalAnimations = self.animations.length; for (var i = 0; i < totalAnimations; i++) { var animation = self.animations[i]; var originalCallback = animation.callback; // 使用闭包捕获originalCallback animation.callback = (function(origCb) { return function() { if (origCb) { origCb(); } completedCount++; if (completedCount >= totalAnimations) { self._complete(); } }; })(originalCallback); _addAnimation(animation); animation.start(); } }; AnimationQueue.prototype._complete = function() { this.isPlaying = false; if (this.callback) { this.callback(); } }; AnimationQueue.prototype.stop = function() { this.isPlaying = false; for (var i = 0; i < this.animations.length; i++) { this.animations[i].stop(); } }; AnimationQueue.prototype.pause = function() { if (!this.isPaused) { this.isPaused = true; for (var i = 0; i < this.animations.length; i++) { this.animations[i].pause(); } } }; AnimationQueue.prototype.resume = function() { if (this.isPaused) { this.isPaused = false; for (var i = 0; i < this.animations.length; i++) { this.animations[i].resume(); } } }; AnimationQueue.prototype.clear = function() { this.stop(); this.animations = []; this.currentIndex = 0; }; // ============================================================================ // 动画循环 (优化版 - 数组遍历) // ============================================================================ /** * 动画循环 * @private */ function _animationLoop() { var currentTime = Date.now(); var i, animation; var toRemove = []; // 使用数组遍历(比 for...in 快) for (i = 0; i < _animationList.length; i++) { animation = _animationList[i]; animation.update(currentTime); // 收集已完成的动画 if (animation.isComplete) { toRemove.push(animation.id); } } // 移除已完成的动画 for (i = 0; i < toRemove.length; i++) { _removeAnimation(toRemove[i]); } // 继续下一帧(使用计数器避免遍历检查) if (_activeCount > 0) { _rafId = requestAnimationFrame(_animationLoop); } else { _rafId = null; } } /** * 启动动画循环 * @private */ function _startAnimationLoop() { if (!_rafId && _activeCount > 0) { _rafId = requestAnimationFrame(_animationLoop); } } // ============================================================================ // 公共API // ============================================================================ /** * 初始化动画管理器 */ AnimationManager.init = function() { if (_initialized) { console.warn('[AnimationManager] Already initialized'); return; } _initialized = true; console.log('[AnimationManager] Initialized (v2.0.0 optimized)'); }; /** * 移动动画 * @param {number} spriteId - 精灵ID * @param {number} fromX - 起始X坐标 * @param {number} fromY - 起始Y坐标 * @param {number} toX - 目标X坐标 * @param {number} toY - 目标Y坐标 * @param {number} duration - 持续时间(ms) * @param {string} easing - 缓动函数 * @param {Function} callback - 完成回调 * @returns {number} 动画ID */ AnimationManager.move = function(spriteId, fromX, fromY, toX, toY, duration, easing, callback) { var animation = new MoveAnimation({ spriteId: spriteId, fromX: fromX, fromY: fromY, toX: toX, toY: toY, duration: duration, easing: easing, callback: callback }); _addAnimation(animation); animation.start(); _startAnimationLoop(); return animation.id; }; /** * 缩放动画 * @param {number} spriteId - 精灵ID * @param {number} fromScale - 起始缩放 * @param {number} toScale - 目标缩放 * @param {number} duration - 持续时间(ms) * @param {string} easing - 缓动函数 * @param {Function} callback - 完成回调 * @returns {number} 动画ID */ AnimationManager.scale = function(spriteId, fromScale, toScale, duration, easing, callback) { var animation = new ScaleAnimation({ spriteId: spriteId, fromScale: fromScale, toScale: toScale, duration: duration, easing: easing, callback: callback }); _addAnimation(animation); animation.start(); _startAnimationLoop(); return animation.id; }; /** * 旋转动画 * @param {number} spriteId - 精灵ID * @param {number} fromAngle - 起始角度 * @param {number} toAngle - 目标角度 * @param {number} duration - 持续时间(ms) * @param {string} easing - 缓动函数 * @param {Function} callback - 完成回调 * @returns {number} 动画ID */ AnimationManager.rotate = function(spriteId, fromAngle, toAngle, duration, easing, callback) { var animation = new RotateAnimation({ spriteId: spriteId, fromAngle: fromAngle, toAngle: toAngle, duration: duration, easing: easing, callback: callback }); _addAnimation(animation); animation.start(); _startAnimationLoop(); return animation.id; }; /** * 淡入淡出动画 * @param {number} spriteId - 精灵ID * @param {number} fromAlpha - 起始透明度(0-1) * @param {number} toAlpha - 目标透明度(0-1) * @param {number} duration - 持续时间(ms) * @param {string} easing - 缓动函数 * @param {Function} callback - 完成回调 * @returns {number} 动画ID */ AnimationManager.fade = function(spriteId, fromAlpha, toAlpha, duration, easing, callback) { var animation = new FadeAnimation({ spriteId: spriteId, fromAlpha: fromAlpha, toAlpha: toAlpha, duration: duration, easing: easing, callback: callback }); _addAnimation(animation); animation.start(); _startAnimationLoop(); return animation.id; }; /** * 帧动画 * @param {number} spriteId - 精灵ID * @param {number} startFrame - 起始帧(从1开始) * @param {number} endFrame - 结束帧 * @param {number} duration - 持续时间(ms) * @param {Object} options - 可选配置 * @param {boolean} [options.loop=false] - 是否循环播放 * @param {number} [options.loopCount=-1] - 循环次数,-1表示无限循环 * @param {boolean} [options.pingPong=false] - 是否往返播放 * @param {string} [options.easing='linear'] - 缓动函数 * @param {Function} [options.onFrame] - 帧变化回调 * @param {Function} [options.callback] - 完成回调 * @returns {number} 动画ID */ AnimationManager.frame = function(spriteId, startFrame, endFrame, duration, options) { options = options || {}; var animation = new FrameAnimation({ spriteId: spriteId, startFrame: startFrame, endFrame: endFrame, duration: duration, loop: options.loop, loopCount: options.loopCount, pingPong: options.pingPong, easing: options.easing, onFrame: options.onFrame, callback: options.callback }); _addAnimation(animation); animation.start(); _startAnimationLoop(); return animation.id; }; /** * 播放循环帧动画(快捷方法) * @param {number} spriteId - 精灵ID * @param {number} startFrame - 起始帧(从1开始) * @param {number} endFrame - 结束帧 * @param {number} frameInterval - 每帧间隔(ms) * @param {Function} [callback] - 完成回调(循环动画通常不会调用) * @returns {number} 动画ID */ AnimationManager.frameLoop = function(spriteId, startFrame, endFrame, frameInterval, callback) { var totalFrames = Math.abs(endFrame - startFrame) + 1; var duration = frameInterval * totalFrames; return AnimationManager.frame(spriteId, startFrame, endFrame, duration, { loop: true, loopCount: -1, callback: callback }); }; /** * 创建动画队列 * @param {string} type - 队列类型: 'serial'(串行) 或 'parallel'(并行) * @returns {number} 队列ID */ AnimationManager.createQueue = function(type) { var queue = new AnimationQueue(type); _queues[queue.id] = queue; return queue.id; }; /** * 添加动画到队列 * @param {number} queueId - 队列ID * @param {Object} animationConfig - 动画配置 * @param {string} animationConfig.type - 动画类型: 'move'/'scale'/'rotate'/'fade'/'frame' */ AnimationManager.addToQueue = function(queueId, animationConfig) { var queue = _queues[queueId]; if (!queue) { console.error('[AnimationManager] Queue not found:', queueId); return; } var animation; switch (animationConfig.type) { case 'move': animation = new MoveAnimation(animationConfig); break; case 'scale': animation = new ScaleAnimation(animationConfig); break; case 'rotate': animation = new RotateAnimation(animationConfig); break; case 'fade': animation = new FadeAnimation(animationConfig); break; case 'frame': animation = new FrameAnimation(animationConfig); break; default: console.error('[AnimationManager] Unknown animation type:', animationConfig.type); return; } queue.add(animation); }; /** * 播放队列 * @param {number} queueId - 队列ID * @param {Function} callback - 完成回调 */ AnimationManager.playQueue = function(queueId, callback) { var queue = _queues[queueId]; if (!queue) { console.error('[AnimationManager] Queue not found:', queueId); return; } queue.play(callback); _startAnimationLoop(); }; /** * 停止队列 * @param {number} queueId - 队列ID */ AnimationManager.stopQueue = function(queueId) { var queue = _queues[queueId]; if (queue) { queue.stop(); } }; /** * 清空队列 * @param {number} queueId - 队列ID */ AnimationManager.clearQueue = function(queueId) { var queue = _queues[queueId]; if (queue) { queue.clear(); delete _queues[queueId]; } }; /** * 停止动画 * @param {number} animationId - 动画ID */ AnimationManager.stopAnimation = function(animationId) { var index = _animationMap[animationId]; if (index !== undefined) { _animationList[index].stop(); _removeAnimation(animationId); } }; /** * 暂停动画 * @param {number} animationId - 动画ID */ AnimationManager.pauseAnimation = function(animationId) { var index = _animationMap[animationId]; if (index !== undefined) { _animationList[index].pause(); } }; /** * 恢复动画 * @param {number} animationId - 动画ID */ AnimationManager.resumeAnimation = function(animationId) { var index = _animationMap[animationId]; if (index !== undefined) { _animationList[index].resume(); _startAnimationLoop(); } }; /** * 获取活动动画数量 * @returns {number} */ AnimationManager.getActiveAnimations = function() { return _activeCount; }; /** * 清空所有动画 */ AnimationManager.clearAllAnimations = function() { // 停止所有动画 for (var i = 0; i < _animationList.length; i++) { _animationList[i].stop(); } // 清空数据结构 _animationList = []; _animationMap = {}; _spriteAnimMap = {}; // 清空精灵动画映射 _activeCount = 0; // 清空队列 for (var qId in _queues) { if (_queues.hasOwnProperty(qId)) { _queues[qId].clear(); } } _queues = {}; // 停止动画循环 if (_rafId) { cancelAnimationFrame(_rafId); _rafId = null; } }; /** * 获取状态 * @returns {Object} */ AnimationManager.getStatus = function() { return { initialized: _initialized, activeAnimations: _activeCount, activeQueues: Object.keys(_queues).length, version: '2.0.0' }; }; /** * 获取可用的缓动函数列表 * @returns {Array} */ AnimationManager.getEasingList = function() { return Object.keys(_easingCache); }; })();