Files
2026-08-19 08:14:48 +08:00

1169 lines
40 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.
/**
* 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<string>}
*/
AnimationManager.getEasingList = function() {
return Object.keys(_easingCache);
};
})();