1169 lines
40 KiB
JavaScript
1169 lines
40 KiB
JavaScript
/**
|
||
* 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);
|
||
};
|
||
|
||
})();
|