233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
|
|
import type { Particle } from '../Particle';
|
|||
|
|
import type { IParticleModule } from './IParticleModule';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 动画播放模式
|
|||
|
|
* Animation playback mode
|
|||
|
|
*/
|
|||
|
|
export enum AnimationPlayMode {
|
|||
|
|
/** 单次播放(生命周期内完成一次循环)| Single loop over lifetime */
|
|||
|
|
LifetimeLoop = 'lifetimeLoop',
|
|||
|
|
/** 固定帧率播放 | Fixed frame rate */
|
|||
|
|
FixedFPS = 'fixedFps',
|
|||
|
|
/** 随机选择帧 | Random frame selection */
|
|||
|
|
Random = 'random',
|
|||
|
|
/** 使用速度控制帧 | Speed-based frame */
|
|||
|
|
SpeedBased = 'speedBased',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 动画循环模式
|
|||
|
|
* Animation loop mode
|
|||
|
|
*/
|
|||
|
|
export enum AnimationLoopMode {
|
|||
|
|
/** 不循环(停在最后一帧)| No loop (stop at last frame) */
|
|||
|
|
Once = 'once',
|
|||
|
|
/** 循环播放 | Loop continuously */
|
|||
|
|
Loop = 'loop',
|
|||
|
|
/** 往返循环 | Ping-pong loop */
|
|||
|
|
PingPong = 'pingPong',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 纹理图集动画模块
|
|||
|
|
* Texture sheet animation module
|
|||
|
|
*
|
|||
|
|
* Animates particles through sprite sheet frames.
|
|||
|
|
* 通过精灵图帧动画化粒子。
|
|||
|
|
*/
|
|||
|
|
export class TextureSheetAnimationModule implements IParticleModule {
|
|||
|
|
readonly name = 'TextureSheetAnimation';
|
|||
|
|
enabled = false;
|
|||
|
|
|
|||
|
|
// 图集配置 | Sheet configuration
|
|||
|
|
/** 水平帧数 | Number of columns */
|
|||
|
|
tilesX: number = 1;
|
|||
|
|
|
|||
|
|
/** 垂直帧数 | Number of rows */
|
|||
|
|
tilesY: number = 1;
|
|||
|
|
|
|||
|
|
/** 总帧数(0=自动计算为 tilesX * tilesY)| Total frames (0 = auto-calculate) */
|
|||
|
|
totalFrames: number = 0;
|
|||
|
|
|
|||
|
|
/** 起始帧 | Start frame index */
|
|||
|
|
startFrame: number = 0;
|
|||
|
|
|
|||
|
|
// 播放配置 | Playback configuration
|
|||
|
|
/** 播放模式 | Playback mode */
|
|||
|
|
playMode: AnimationPlayMode = AnimationPlayMode.LifetimeLoop;
|
|||
|
|
|
|||
|
|
/** 循环模式 | Loop mode */
|
|||
|
|
loopMode: AnimationLoopMode = AnimationLoopMode.Loop;
|
|||
|
|
|
|||
|
|
/** 固定帧率(FPS,用于 FixedFPS 模式)| Fixed frame rate (for FixedFPS mode) */
|
|||
|
|
frameRate: number = 30;
|
|||
|
|
|
|||
|
|
/** 播放速度乘数 | Playback speed multiplier */
|
|||
|
|
speedMultiplier: number = 1;
|
|||
|
|
|
|||
|
|
/** 循环次数(0=无限)| Number of loops (0 = infinite) */
|
|||
|
|
cycleCount: number = 0;
|
|||
|
|
|
|||
|
|
// 内部状态 | Internal state
|
|||
|
|
private _cachedTotalFrames: number = 0;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取实际总帧数
|
|||
|
|
* Get actual total frames
|
|||
|
|
*/
|
|||
|
|
get actualTotalFrames(): number {
|
|||
|
|
return this.totalFrames > 0 ? this.totalFrames : this.tilesX * this.tilesY;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新粒子
|
|||
|
|
* Update particle
|
|||
|
|
*
|
|||
|
|
* @param p - 粒子 | Particle
|
|||
|
|
* @param dt - 增量时间 | Delta time
|
|||
|
|
* @param normalizedAge - 归一化年龄 (0-1) | Normalized age
|
|||
|
|
*/
|
|||
|
|
update(p: Particle, dt: number, normalizedAge: number): void {
|
|||
|
|
const frameCount = this.actualTotalFrames;
|
|||
|
|
if (frameCount <= 1) return;
|
|||
|
|
|
|||
|
|
let frameIndex: number;
|
|||
|
|
|
|||
|
|
switch (this.playMode) {
|
|||
|
|
case AnimationPlayMode.LifetimeLoop:
|
|||
|
|
frameIndex = this._getLifetimeFrame(normalizedAge, frameCount);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationPlayMode.FixedFPS:
|
|||
|
|
frameIndex = this._getFixedFPSFrame(p, dt, frameCount);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationPlayMode.Random:
|
|||
|
|
frameIndex = this._getRandomFrame(p, frameCount);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationPlayMode.SpeedBased:
|
|||
|
|
frameIndex = this._getSpeedBasedFrame(p, frameCount);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
frameIndex = this.startFrame;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置粒子的 UV 坐标 | Set particle UV coordinates
|
|||
|
|
this._setParticleUV(p, frameIndex);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生命周期帧计算
|
|||
|
|
* Calculate frame based on lifetime
|
|||
|
|
*/
|
|||
|
|
private _getLifetimeFrame(normalizedAge: number, frameCount: number): number {
|
|||
|
|
const progress = normalizedAge * this.speedMultiplier;
|
|||
|
|
return this._applyLoopMode(progress, frameCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 固定帧率计算
|
|||
|
|
* Calculate frame based on fixed FPS
|
|||
|
|
*/
|
|||
|
|
private _getFixedFPSFrame(p: Particle, dt: number, frameCount: number): number {
|
|||
|
|
// 使用粒子的 age 来计算当前帧 | Use particle age to calculate current frame
|
|||
|
|
const animTime = p.age * this.frameRate * this.speedMultiplier;
|
|||
|
|
const progress = animTime / frameCount;
|
|||
|
|
return this._applyLoopMode(progress, frameCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 随机帧选择(每个粒子使用固定随机帧)
|
|||
|
|
* Random frame selection (each particle uses fixed random frame)
|
|||
|
|
*/
|
|||
|
|
private _getRandomFrame(p: Particle, frameCount: number): number {
|
|||
|
|
// 使用粒子的起始位置作为随机种子(确保一致性)
|
|||
|
|
// Use particle's start position as random seed (for consistency)
|
|||
|
|
const seed = Math.abs(p.startR * 1000 + p.startG * 100 + p.startB * 10) % 1;
|
|||
|
|
return Math.floor(seed * frameCount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 基于速度的帧计算
|
|||
|
|
* Calculate frame based on particle speed
|
|||
|
|
*/
|
|||
|
|
private _getSpeedBasedFrame(p: Particle, frameCount: number): number {
|
|||
|
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
|||
|
|
// 归一化速度(假设最大速度为 500)| Normalize speed (assume max 500)
|
|||
|
|
const normalizedSpeed = Math.min(speed / 500, 1);
|
|||
|
|
const frameIndex = Math.floor(normalizedSpeed * (frameCount - 1));
|
|||
|
|
return this.startFrame + frameIndex;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 应用循环模式
|
|||
|
|
* Apply loop mode to progress
|
|||
|
|
*/
|
|||
|
|
private _applyLoopMode(progress: number, frameCount: number): number {
|
|||
|
|
let loopProgress = progress;
|
|||
|
|
|
|||
|
|
switch (this.loopMode) {
|
|||
|
|
case AnimationLoopMode.Once:
|
|||
|
|
// 停在最后一帧 | Stop at last frame
|
|||
|
|
loopProgress = Math.min(progress, 0.9999);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationLoopMode.Loop:
|
|||
|
|
// 简单循环 | Simple loop
|
|||
|
|
loopProgress = progress % 1;
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationLoopMode.PingPong:
|
|||
|
|
// 往返循环 | Ping-pong
|
|||
|
|
const cycle = Math.floor(progress);
|
|||
|
|
const remainder = progress - cycle;
|
|||
|
|
loopProgress = (cycle % 2 === 0) ? remainder : (1 - remainder);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查循环次数限制 | Check cycle count limit
|
|||
|
|
if (this.cycleCount > 0) {
|
|||
|
|
const currentCycle = Math.floor(progress);
|
|||
|
|
if (currentCycle >= this.cycleCount) {
|
|||
|
|
loopProgress = 0.9999; // 停在最后一帧 | Stop at last frame
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算帧索引 | Calculate frame index
|
|||
|
|
const frameIndex = Math.floor(loopProgress * frameCount);
|
|||
|
|
return this.startFrame + Math.min(frameIndex, frameCount - 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置粒子 UV 坐标
|
|||
|
|
* Set particle UV coordinates
|
|||
|
|
*/
|
|||
|
|
private _setParticleUV(p: Particle, frameIndex: number): void {
|
|||
|
|
// 计算 UV 坐标 | Calculate UV coordinates
|
|||
|
|
const col = frameIndex % this.tilesX;
|
|||
|
|
const row = Math.floor(frameIndex / this.tilesX);
|
|||
|
|
|
|||
|
|
const uWidth = 1 / this.tilesX;
|
|||
|
|
const vHeight = 1 / this.tilesY;
|
|||
|
|
|
|||
|
|
// UV 坐标(左上角为原点)| UV coordinates (top-left origin)
|
|||
|
|
const u0 = col * uWidth;
|
|||
|
|
const v0 = row * vHeight;
|
|||
|
|
const u1 = u0 + uWidth;
|
|||
|
|
const v1 = v0 + vHeight;
|
|||
|
|
|
|||
|
|
// 存储 UV 到粒子的自定义属性
|
|||
|
|
// Store UV in particle's custom properties
|
|||
|
|
// 这里我们使用粒子的 startR/startG/startB 不太合适,需要扩展 Particle
|
|||
|
|
// 暂时通过覆盖 rotation 的高位来存储帧索引(临时方案)
|
|||
|
|
// Temporary: store frame index in a way that can be read by renderer
|
|||
|
|
// The actual UV calculation will be done in the render data provider
|
|||
|
|
(p as any)._animFrame = frameIndex;
|
|||
|
|
(p as any)._animTilesX = this.tilesX;
|
|||
|
|
(p as any)._animTilesY = this.tilesY;
|
|||
|
|
}
|
|||
|
|
}
|