'use strict'
// ******************************** 动画播放器类 ********************************
class Animation {
/** 动画可见性
* @type {boolean}
*/ visible
/** 暂停播放
* @type {boolean}
*/ paused
/** 结束播放
* @type {boolean}
*/ ended
/** 动画位置对象
* @type {Object}
*/ position
/** 动画帧当前的位置
* @type {number}
*/ index
/** 当前动作动画帧的数量
* @type {number}
*/ length
/** 动画循环开始位置
* @type {number}
*/ loopStart
/** 动画循环次数
* @type {number}
*/ cycleIndex
/** 动画播放速度
* @type {number}
*/ speed
/** 动画锚点在光照纹理中的位置
* @type {number}
*/ anchorX
/** 动画锚点在光照纹理中的位置
* @type {number}
*/ anchorY
/** 动画的垂直偏移位置
* @type {number}
*/ offsetY
/** 动画是否可旋转
* @type {boolean}
*/ rotatable
/** 动画的旋转角度(弧度)
* @type {number}
*/ rotation
/** 动画的不透明度
* @type {number}
*/ opacity
/** 动画渲染排序的优先级
* @type {priority}
*/ priority
/** 动画当前角度
* @type {number}
*/ angle
/** 动画缩放系数
* @type {number}
*/ scale
/** 动画当前方向(0, 1, 2, ...)
* @type {number}
*/ direction
/** 动画镜像模式
* @type {string}
*/ mirror
/** 动画色调列表
* @type {Array<Array<number>>}
*/ tints
/** 动画文件数据
* @type {AnimFile}
*/ data
/** 动画方向映射表
* @type {Array<Object>}
*/ dirMap
/** 动画方向数据列表
* @type {Array<Object>}
*/ dirCases
/** 动画当前动作图层
* @type {Array<Object>}
*/ layers
/** 动画当前播放的动作名称
* @type {string}
*/ motionName
/** 动画当前播放的动作对象
* @type {Object}
*/ motion
/** 名称->动作数据映射表
* @type {Object}
*/ motions
/** 精灵ID->精灵数据映射表
* @type {Object}
*/ sprites
/** 精灵ID->图像ID映射表
* @type {Object}
*/ images
/** 精灵ID->图像纹理映射表
* @type {Object}
*/ textures
/** 图层动画帧上下文列表
* @type {Array<Object>}
*/ contexts
/** 是不是UI组件
* @type {boolean}
*/ isUIComponent
/** 已激活的粒子发射器列表
* @type {ParticleEmitterList}
*/ emitters
/** 已激活的粒子发射器数量
* @type {number}
*/ emitterCount
/** 存在粒子
* @type {boolean}
*/ existParticle = false
/** 动画播放结束回调函数列表
* @type {Array<Function>|null}
*/ callbacks
/** 动画播放器对自己的引用
* @type {Animation}
*/ animation
/** 父节点对象
* @type {Object}
*/ parent
/**
* 动画播放器
* @param {AnimFile} data 动画文件数据
*/
constructor(data) {
this.visible = true
this.paused = false
this.ended = false
this.position = {x: 0, y: 0}
this.index = 0
this.length = 0
this.loopStart = 0
this.cycleIndex = 0
this.speed = 1
this.anchorX = 0
this.anchorY = 0
this.offsetY = 0
this.rotatable = false
this.rotation = 0
this.opacity = 1
this.priority = 0
this.angle = 0
this.scale = 1
this.direction = -1
this.mirror = false
this.tints = []
this.data = data
this.dirMap = Array.empty
this.dirCases = null
this.layers = null
this.motionName = ''
this.motion = null
this.motions = data.motions
this.sprites = data.sprites
this.images = data.images
this.textures = {}
this.contexts = []
this.isUIComponent = false
this.emitters = null
this.emitterCount = 0
this.callbacks = null
this.animation = this
this.parent = null
}
// 跳转到指定帧
goto(index) {
index = Math.clamp(index, 0, this.length - 1)
// 跳转到前面的动画帧时增加循环计数
if (index < this.index) {
this.cycleIndex++
}
this.index = index
this.ended = false
}
/** 重新开始播放 */
restart() {
this.index = 0
this.ended = false
}
/**
* 设置动画动作
* @param {string} motionName 动作名称
* @returns {boolean} 操作是否成功
*/
setMotion(motionName) {
this.motionName = motionName
const motion = this.motions[motionName]
if (motion) {
// 执行结束回调
this.finish()
this.motion = motion
this.dirCases = motion.dirCases
// 如果方向模式发生变化,重新计算方向
if (this.dirMap !== motion.dirMap) {
this.dirMap = motion.dirMap
this.direction = -1
this.setAngle(this.angle)
} else {
this.loadDirCase()
}
return true
}
return false
}
/** 加载动画方向 */
loadDirCase() {
const params = this.dirMap[this.direction]
if (params) {
const dirCase = this.dirCases[params.index]
this.layers = dirCase.layers
// 销毁上下文中的粒子发射器
// 加载当前动作的上下文
this.destroyContextEmitters()
this.loadContexts(this.contexts)
this.length = dirCase.length
this.loopStart = dirCase.loopStart
this.cycleIndex = 0
this.ended = false
}
}
/**
* 设置动画角度
* @param {number} angle 弧度
* @returns {boolean} 动画是否成功切换了方向
*/
setAngle(angle) {
this.angle = angle
const directions = this.dirMap.length
// 将角度映射为0~方向数量的数值
const proportion = Math.modRadians(angle) / (Math.PI * 2)
const section = (proportion * directions + 0.5) % directions
// 如果角度的位置刚好是两个方向的交界处,则优先使用之前的方向(容错值0.01)
if (Math.abs(section - Math.round(section)) < 0.01 && this.direction >= 0) {
const distance = Math.abs(section - (this.direction + 0.5))
if (distance < 1 || distance > directions - 1) {
this.updateRotation()
return false
}
}
const direction = Math.floor(section)
const dirChanged = this.setDirection(direction)
this.updateRotation()
return dirChanged
}
/**
* 设置动画方向
* @param {number} direction 方向
* @returns {boolean} 动画是否成功切换了方向
*/
setDirection(direction) {
if (this.direction !== direction) {
const params = this.dirMap[direction]
if (!params) return false
this.direction = direction
this.mirror = params.mirror
this.finish()
this.loadDirCase()
return true
}
return false
}
/**
* 获取动画当前播放时间
* @returns {number} 毫秒
*/
getCurrentTime() {
return this.index * Animation.step
}
/**
* 获取动画动作持续时间
* @returns {number} 毫秒
*/
getDuration() {
return this.length * Animation.step
}
/**
* 获取动画方向的角度
* @param {number} direction 动画方向
* @returns {number} 动画方向的角度(弧度)
*/
getDirectionAngle() {
const directions = this.dirMap.length
return this.direction / directions * Math.PI * 2
}
/** 更新旋转角度 */
updateRotation() {
// 如果开启了动画旋转,调整旋转角度
if (this.rotatable) {
this.rotation = this.mirror
? -this.angle - this.getDirectionAngle()
: +this.angle - this.getDirectionAngle()
}
}
/**
* 设置动画在场景中的位置
* @param {{x: number, y: number}} position 具有场景坐标的对象
*/
setPosition(position) {
this.position = position
}
/**
* 设置动画绘制在屏幕中的位置
* @param {number} x 场景X
* @param {number} y 场景Y
*/
setDrawingPosition(x, y) {
const matrix = Animation.matrix.set6f(1, 0, 0, 1, x, y)
// 设置镜像
if (this.mirror) {
matrix.mirrorh()
}
// 设置垂直偏移
if (this.offsetY !== 0) {
matrix.translateY(this.offsetY)
}
// 设置旋转
if (this.rotation !== 0) {
matrix.rotate(this.rotation)
}
// 设置缩放
if (this.scale !== 1) {
matrix.scale(this.scale, this.scale)
}
}
/**
* 设置精灵图像映射表
* @param {Object} images 优先使用的角色精灵图像映射表
*/
setSpriteImages(images) {
// 让指定的图像映射表(更高优先级)继承当前的动画图像映射表
this.images = Object.setPrototypeOf(images, this.images)
}
/** 恢复精灵图像映射表 */
restoreSpriteImages() {
this.images = Object.getPrototypeOf(this.images)
}
/**
* 更新当前播放的动画帧参数
* @param {Object[]} contexts 动画图层上下文列表
* @param {number} index 动画帧当前的位置
*/
updateFrameParameters() {
const {contexts, index} = this
const {count} = contexts
// 遍历所有动画图层
outer: for (let i = 0; i < count; i++) {
const context = contexts[i]
const frames = context.layer.frames
const last = frames.length - 1
for (let i = 0; i <= last; i++) {
const frame = frames[i]
const start = frame.start
const end = frame.end
// 查找index所在的动画关键帧
if (index >= start && index < end) {
const easingId = frame.easingId
// 如果存在过渡,并且不是尾部关键帧
if (easingId && i < last) {
// 在当前帧和下一帧之间进行过渡插值
const next = frames[i + 1]
const time = Easing.get(easingId).map(
(index - start) / (next.start - start)
)
// 更新插值后的上下文
context.update(frame, time, next)
} else {
// 更新当前帧的上下文
context.update(frame)
}
continue outer
}
}
// 找不到关键帧就重置上下文
context.reset()
}
}
/**
* 加载动画图层上下文列表
* @param {Object[]} contexts 动画图层上下文列表
*/
loadContexts(contexts) {
Animation.loadContexts(this, contexts)
}
/**
* 更新动画播放进度
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
deltaTime *= this.speed
if (this.paused === false && this.ended === false) {
this.existParticles = false
if (this.length !== 0) {
// 递增动画帧索引
this.index += deltaTime / Animation.step
// 如果动画播放结束
if (this.index >= this.length) {
if (this.motion.loop) {
// 如果动作是循环的,重新开始
this.index = this.index % this.length + this.loopStart
this.cycleIndex++
} else {
// 否则设为尾帧索引,执行结束回调
this.index = this.length - 1
this.ended = true
this.finish()
// 临时用于播放结束后回调(会修改)
this.end?.()
}
}
}
}
// 更新粒子发射器
if (this.emitterCount !== 0) {
this.emitParticles(deltaTime)
}
}
/**
* 激活动画(当动画可见时)
* @param {number} drawX 动画的场景像素X
* @param {number} drawY 动画的场景像素Y
* @param {number} lightX 动画的光照采样X
* @param {number} lightY 动画的光照采样Y
*/
activate(drawX, drawY, lightX, lightY) {
this.setDrawingPosition(drawX, drawY)
this.updateFrameParameters()
this.anchorX = lightX
this.anchorY = lightY
}
/**
* 获取精灵图纹理
* @param {string} spriteId 精灵图ID
* @returns {ImageTexture|null} 已加载完成的纹理
*/
getTexture(spriteId) {
const textures = this.textures
const texture = textures[spriteId]
// 初次访问需要先创建纹理
if (texture === undefined) {
this.loadTexture(spriteId)
return textures[spriteId]
}
return texture
}
/**
* 加载精灵图纹理
* @param {string} spriteId 精灵图ID
* @returns {ImageTexture|null} 已加载完成或正在加载中的纹理
*/
loadTexture(spriteId) {
const {textures} = this
if (textures[spriteId]) {
return textures[spriteId]
}
textures[spriteId] = null
const sprite = this.sprites[spriteId]
const imageId = this.images[spriteId]
if (sprite !== undefined && imageId) {
const texture = new ImageTexture(imageId)
// 如果纹理已完成加载,设置好参数直接返回
texture.on('load', () => {
if (this.textures === textures) {
// 纹理加载结束后如果动画还存在,设置参数
const {floor, max} = Math
const {base} = texture
const {hframes, vframes} = sprite
const width = floor(max(base.width / hframes, 1))
const height = floor(max(base.height / vframes, 1))
texture.x = null
texture.y = null
texture.width = width
texture.height = height
textures[spriteId] = texture
} else {
// 如果动画已销毁,则销毁纹理
texture.destroy()
}
})
return texture
}
return null
}
/**
* 删除精灵图纹理
* @param {string} spriteId 精灵图ID
*/
deleteTexture(spriteId) {
const {textures} = this
if (spriteId in textures) {
textures[spriteId]?.destroy()
delete textures[spriteId]
}
}
/**
* 发射粒子
* @param {number} deltaTime 增量时间(毫秒)
*/
emitParticles(deltaTime) {
const {contexts} = this
const {count} = contexts
// 遍历所有图层上下文,查找粒子层
for (let i = 0; i < count; i++) {
const context = contexts[i]
const {layer} = context
if (layer.class === 'particle') {
const {frame, emitter} = context
// 如果当前帧有效,且存在粒子发射器
if (frame !== null && emitter !== undefined) {
switch (layer.angle) {
case 'default':
// 发射器角度设为默认
emitter.angle = 0
break
case 'inherit': {
// 发射器角度继承动画帧
const {matrix} = context
const a = matrix[0]
const b = matrix[1]
emitter.angle = Math.atan2(b, a)
break
}
}
emitter.emitParticles(deltaTime)
}
}
}
}
/**
* 绘制动画
* @param {string} [light] 光线采样模式
*/
draw(light) {
const {contexts} = this
const {count} = contexts
// 遍历所有图层上下文,查找精灵层
for (let i = 0; i < count; i++) {
const context = contexts[i]
const {layer} = context
if (layer.class === 'sprite' &&
context.frame !== null) {
const key = layer.sprite
const texture = this.getTexture(key)
// 如果获取到了精灵纹理,则绘制图像
if (texture !== null) {
this.drawSprite(context, texture, light)
}
}
}
}
/**
* 绘制精灵图像
* @param {Object} context 动画图层上下文
* @param {ImageTexture} texture 精灵图纹理
* @param {string} [light] 光线采样模式
*/
drawSprite(context, texture, light = context.layer.light) {
const gl = GL
const vertices = gl.arrays[0].float32
const attributes = gl.arrays[0].uint32
const renderer = gl.batchRenderer
const response = renderer.response
const matrix = context.matrix
const layer = context.layer
const frame = context.frame
const tint = context.tint
const base = texture.base
const tw = base.width
const th = base.height
const sw = texture.width
const sh = texture.height
// 优先使用纹理的x和y值作为采样的左上角位置
// 方便使用脚本修改(设置非网格化的采样区域)
const sx = texture.x ?? frame.spriteX * sw
const sy = texture.y ?? frame.spriteY * sh
const L = -(sw * context.anchorX + context.pivotX)
const T = -(sh * context.anchorY + context.pivotY)
const R = L + sw
const B = T + sh
const a = matrix[0]
const b = matrix[1]
const c = matrix[3]
const d = matrix[4]
const e = matrix[6]
const f = matrix[7]
const x1 = a * L + c * T + e
const y1 = b * L + d * T + f
const x2 = a * L + c * B + e
const y2 = b * L + d * B + f
const x3 = a * R + c * B + e
const y3 = b * R + d * B + f
const x4 = a * R + c * T + e
const y4 = b * R + d * T + f
const sl = sx / tw
const st = sy / th
const sr = (sx + sw) / tw
const sb = (sy + sh) / th
renderer.setBlendMode(layer.blend)
renderer.push(base.index)
const vi = response[0] * 8
const mode = Animation.lightSamplingModes[light]
const alpha = Math.round(context.opacity * 255)
const param = response[1] | alpha << 8 | mode << 16
const redGreen = tint[0] + (tint[1] << 16) + 0x00ff00ff
const blueGray = tint[2] + (tint[3] << 16) + 0x00ff00ff
const anchor = light !== 'anchor' ? 0 : (
Math.round(Math.clamp(this.anchorX, 0, 1) * 0xffff)
| Math.round(Math.clamp(this.anchorY, 0, 1) * 0xffff) << 16
)
// 设置顶点数据:顶点坐标,纹理坐标,纹理索引,不透明度,光照模式,色调,锚点
vertices [vi ] = x1
vertices [vi + 1] = y1
vertices [vi + 2] = sl
vertices [vi + 3] = st
attributes[vi + 4] = param
attributes[vi + 5] = redGreen
attributes[vi + 6] = blueGray
attributes[vi + 7] = anchor
vertices [vi + 8] = x2
vertices [vi + 9] = y2
vertices [vi + 10] = sl
vertices [vi + 11] = sb
attributes[vi + 12] = param
attributes[vi + 13] = redGreen
attributes[vi + 14] = blueGray
attributes[vi + 15] = anchor
vertices [vi + 16] = x3
vertices [vi + 17] = y3
vertices [vi + 18] = sr
vertices [vi + 19] = sb
attributes[vi + 20] = param
attributes[vi + 21] = redGreen
attributes[vi + 22] = blueGray
attributes[vi + 23] = anchor
vertices [vi + 24] = x4
vertices [vi + 25] = y4
vertices [vi + 26] = sr
vertices [vi + 27] = st
attributes[vi + 28] = param
attributes[vi + 29] = redGreen
attributes[vi + 30] = blueGray
attributes[vi + 31] = anchor
}
/** 释放资源 */
release() {
this.destroy()
this.textures = {}
}
/** 销毁动画实例 */
destroy() {
if (!this.textures) return
// 销毁图像纹理
for (const texture of Object.values(this.textures)) {
texture?.destroy()
}
this.textures = null
// 销毁上下文的粒子发射器
this.destroyContextEmitters()
// 销毁已激活的粒子发射器
this.destroyActiveEmitters()
}
/** 销毁图层上下文的粒子发射器 */
destroyContextEmitters() {
const {contexts} = this
const {count} = contexts
for (let i = 0; i < count; i++) {
const context = contexts[i]
const emitter = context.emitter
if (emitter !== undefined) {
// 标记发射器为已禁用
emitter.disabled = true
delete context.emitter
this.emitterCount--
}
}
}
/** 销毁已激活的粒子发射器 */
destroyActiveEmitters() {
if (this.isUIComponent) {
for (const emitter of this.emitters) {
emitter.destroy()
}
}
}
/**
* 设置当前动作播放结束时的回调函数
* @param {function} callback 回调函数
*/
onFinish(callback) {
if (this.complete) {
callback(this)
} else {
if (this.callbacks) {
this.callbacks.push(callback)
} else {
this.callbacks = [callback]
}
}
}
/** 执行当前动作播放结束回调 */
finish() {
const {callbacks} = this
if (callbacks !== null) {
this.callbacks = null
for (const callback of callbacks) {
callback(this)
}
}
}
// 设置为UI动画组件
setAsUIComponent() {
if (!this.isUIComponent) {
this.isUIComponent = true
this.emitters = new ParticleEmitterList()
}
}
// 每一帧动画的持续时间(毫秒)
static step = 0
// 动画公共矩阵
static matrix = new Matrix()
// 光照采样模式映射表
static lightSamplingModes = {raw: 0, global: 1, anchor: 2}
// 各种模式的动画方向映射表
static dirMaps = {
'1-dir': [
{index: 0, mirror: false},
],
'1-dir-mirror': [
{index: 0, mirror: false},
{index: 0, mirror: true},
],
'2-dir': [
{index: 1, mirror: false},
{index: 0, mirror: false},
],
'3-dir-mirror': [
{index: 1, mirror: false},
{index: 0, mirror: false},
{index: 1, mirror: true},
{index: 2, mirror: false},
],
'4-dir': [
{index: 2, mirror: false},
{index: 0, mirror: false},
{index: 1, mirror: false},
{index: 3, mirror: false},
],
'5-dir-mirror': [
{index: 1, mirror: false},
{index: 3, mirror: false},
{index: 0, mirror: false},
{index: 3, mirror: true},
{index: 1, mirror: true},
{index: 4, mirror: true},
{index: 2, mirror: false},
{index: 4, mirror: false},
],
'8-dir': [
{index: 2, mirror: false},
{index: 5, mirror: false},
{index: 0, mirror: false},
{index: 4, mirror: false},
{index: 1, mirror: false},
{index: 6, mirror: false},
{index: 3, mirror: false},
{index: 7, mirror: false},
],
}
/** 初始化动画相关数据 */
static initialize() {
// 计算一帧动画的时长
this.step = 1000 / Data.config.animation.frameRate
}
/**
* 加载动画图层上下文列表
* @param {Animation} animation 动画播放器实例
* @param {Object[]} contexts 动画图层上下文列表
*/
static loadContexts(animation, contexts) {
contexts.count = 0
if (animation.layers !== null) {
// 如果动画已设置动作,加载所有图层上下文
this._loadContext(animation, animation.layers, null, contexts)
}
}
/**
* 加载动画图层上下文
* @param {Animation} animation 动画播放器实例
* @param {Object[]} layers 动画图层列表
* @param {Object} parent 父级动画图层
* @param {Object[]} contexts 动画图层上下文列表
*/
static _loadContext(animation, layers, parent, contexts) {
for (const layer of layers) {
let context = contexts[contexts.count]
if (context === undefined) {
// 新建动画图层上下文
context = contexts[contexts.count] = {
animation: animation,
parent: null,
layer: null,
frame: null,
matrix: new Matrix(),
anchorX: 0,
anchorY: 0,
pivotX: 0,
pivotY: 0,
opacity: 0,
update: null,
reset: Animation.contextReset,
}
}
contexts.count++
context.parent = parent
context.layer = layer
// 为不同类型的图层设置各自的更新方法
switch (layer.class) {
case 'joint':
context.update = Animation.contextUpdate
break
case 'sprite':
context.update = Animation.contextUpdateSprite
break
case 'particle':
context.update = Animation.contextUpdateParticle
break
case 'sound':
context.update = Animation.contextUpdateSound
context.version = -1
break
}
// 如果是关节层,则加载子图层列表
if (layer.class === 'joint') {
this._loadContext(animation, layer.children, context, contexts)
}
}
}
/** 图层上下文方法 - 重置 */
static contextReset() {
const parent = this.parent
const matrix = this.matrix
if (parent !== null) {
// 从父级图层继承属性
matrix.set(parent.matrix)
this.opacity = parent.opacity
} else {
// 没有父级图层的情况
matrix.set(Animation.matrix)
this.opacity = this.animation.opacity
}
this.frame = null
}
/**
* 上下文方法 - 更新(通用)
* @param {Array} frame 动画图层当前帧的数据
* @param {number} time 当前帧到下一帧的过渡时间(比率)
* @param {Array} next 动画图层下一帧的数据
*/
static contextUpdate(frame, time, next) {
const parent = this.parent
const matrix = this.matrix
if (parent !== null) {
// 从父级图层继承属性
matrix.set(parent.matrix)
this.opacity = parent.opacity
} else {
// 没有父级图层的情况
matrix.set(Animation.matrix)
this.opacity = this.animation.opacity
}
// 获取当前动画帧的属性
let positionX = frame.x
let positionY = frame.y
let rotation = frame.rotation
let scaleX = frame.scaleX
let scaleY = frame.scaleY
let opacity = frame.opacity
if (next !== undefined) {
// 与下一帧属性进行插值计算
const reverse = 1 - time
positionX = positionX * reverse + next.x * time
positionY = positionY * reverse + next.y * time
rotation = rotation * reverse + next.rotation * time
scaleX = scaleX * reverse + next.scaleX * time
scaleY = scaleY * reverse + next.scaleY * time
opacity = opacity * reverse + next.opacity * time
}
// 使用动画帧属性进行矩阵变换
matrix
.translate(positionX, positionY)
.rotate(Math.radians(rotation))
.scale(scaleX, scaleY)
this.opacity *= opacity
this.frame = frame
}
/**
* 上下文方法 - 更新精灵层
* @param {Array} frame 动画图层当前帧的数据
* @param {number} time 当前帧到下一帧的过渡时间(比率)
* @param {Array} next 动画图层下一帧的数据
*/
static contextUpdateSprite(frame, time, next) {
Animation.contextUpdate.call(this, frame, time, next)
// 读取当前动画帧的参数
let anchorX = frame.anchorX
let anchorY = frame.anchorY
let pivotX = frame.pivotX
let pivotY = frame.pivotY
let red = frame.tint[0]
let green = frame.tint[1]
let blue = frame.tint[2]
let gray = frame.tint[3]
// 与下一帧参数进行插值计算
if (next !== undefined) {
const reverse = 1 - time
anchorX = anchorX * reverse + next.anchorX * time
anchorY = anchorY * reverse + next.anchorY * time
pivotX = pivotX * reverse + next.pivotX * time
pivotY = pivotY * reverse + next.pivotY * time
red = red * reverse + next.tint[0] * time
green = green * reverse + next.tint[1] * time
blue = blue * reverse + next.tint[2] * time
gray = gray * reverse + next.tint[3] * time
}
// 混合全局色调
for (const tint of this.animation.tints) {
red += tint[0]
green += tint[1]
blue += tint[2]
gray += tint[3]
}
// 获取或创建色调数组
let tint = this.tint
if (tint === undefined) {
tint = this.tint = new Int16Array(4)
}
// 写入参数(限制色调范围)
this.anchorX = anchorX
this.anchorY = anchorY
this.pivotX = pivotX
this.pivotY = pivotY
tint[0] = Math.clamp(red, -255, 255)
tint[1] = Math.clamp(green, -255, 255)
tint[2] = Math.clamp(blue, -255, 255)
tint[3] = Math.clamp(gray, 0, 255)
}
/**
* 上下文方法 - 更新粒子层
* @param {Array} frame 动画图层当前帧的数据
* @param {number} time 当前帧到下一帧的过渡时间(比率)
* @param {Array} next 动画图层下一帧的数据
*/
static contextUpdateParticle(frame, time, next) {
Animation.contextUpdate.call(this, frame, time, next)
// 获取或创建粒子发射器
let emitter = this.emitter
if (emitter === undefined) {
const guid = this.layer.particleId
const data = Data.particles[guid]
if (!data) return
const position = this.animation.position
emitter = new ParticleEmitter(data)
emitter.disabled = false
emitter.matrix = this.matrix
emitter.priority = this.animation.priority
emitter.x = position.x
emitter.y = position.y
this.emitter = emitter
this.animation.emitterCount++
const {alwaysDraw} = emitter
if (this.animation.isUIComponent) {
this.animation.emitters.append(emitter)
} else {
// 委托给场景粒子发射器列表进行更新
Scene.emitters.append(emitter)
}
emitter.update = deltaTime => {
// 偏移已激活粒子
if (this.layer.position === 'relative') {
let ox = position.x - emitter.x
let oy = position.y - emitter.y
if (!this.animation.isUIComponent) {
ox *= Scene.binding.tileWidth
oy *= Scene.binding.tileHeight
}
emitter.translateParticles(ox, oy)
}
// 同步发射器和动画的位置
if (!this.animation.isUIComponent) {
emitter.x = position.x
emitter.y = position.y
}
// 更新已激活粒子
const speed = this.animation.speed
const count = emitter.updateParticles(deltaTime * speed)
this.animation.existParticles = true
if (count === 0 && emitter.disabled) {
Callback.push(() => {
emitter.destroy()
emitter.parent.remove(emitter)
})
}
// 只要存在粒子,总是进行绘制
if (alwaysDraw === false) {
emitter.alwaysDraw = count !== 0
}
}
}
// 获取当前动画帧的属性
let scale = frame.scale * this.animation.scale
let speed = frame.speed
if (next !== undefined) {
// 与下一帧属性进行插值计算
const reverse = 1 - time
scale = scale * reverse + next.scale * time
speed = speed * reverse + next.speed * time
}
emitter.scale = scale
emitter.speed = speed
emitter.opacity = this.opacity
}
/**
* 上下文方法 - 更新音效层
* @param {Array} frame 动画图层当前帧的数据
*/
static contextUpdateSound(frame) {
// 如果当前帧是关键帧
const animation = this.animation
if (animation.paused) return
if (animation.index < frame.start + 1) {
// 如果当前帧未播放过
const version = animation.cycleIndex * animation.length + Math.floor(animation.index)
if (this.version !== version) {
this.version = version
// 在动画的位置播放衰减音效
if (frame.sound && animation.position) {
const speed = this.layer.playbackRate === 'inherit' ? animation.speed : 1
if (animation.isUIComponent) {
AudioManager.se.play(frame.sound, frame.volume, speed)
} else {
AudioManager.se.playAt(frame.sound, animation.position, frame.volume, speed)
}
}
}
}
}
}
// ******************************** 粒子发射器类 ********************************
class ParticleEmitter {
/** 是否总是发射粒子
* @type {boolean}
*/ alwaysEmit
/** 是否总是绘制粒子
* @type {boolean}
*/ alwaysDraw
/** 粒子发射器可见性
* @type {boolean}
*/ visible
/** 粒子发射初始水平位置
* @type {number}
*/ startX
/** 粒子发射初始垂直位置
* @type {number}
*/ startY
/** 粒子发射角度
* @type {number}
*/ angle
/** 粒子缩放系数
* @type {number}
*/ scale
/** 粒子速度
* @type {number}
*/ speed
/** 不透明度
* @type {number}
*/ opacity
/** 粒子发射器优先级
* @type {number}
*/ priority
/** 粒子发射器的矩阵
* @type {number}
*/ matrix
/** 粒子发射器的图层
* @type {Array<ParticleLayer>}
*/ layers
/** 视差图的父级对象
* @type {Object|null}
*/ parent
/**
* 粒子发射器
* @param {ParticleFile} data 粒子文件数据
*/
constructor(data) {
let alwaysEmit = ParticleEmitter.alwaysEmit
let alwaysDraw = ParticleEmitter.alwaysDraw
// 创建粒子图层
const sLayers = data.layers
const sLength = sLayers.length
const dLayers = new Array(sLength)
for (let i = 0; i < sLength; i++) {
const sLayer = sLayers[i]
// 如果有一个粒子层的发射区域是屏幕边缘,设为总是发射和绘制
if (sLayer.area.type === 'edge') {
alwaysEmit = true
alwaysDraw = true
}
dLayers[i] = new ParticleLayer(this, sLayer)
}
this.alwaysEmit = alwaysEmit
this.alwaysDraw = alwaysDraw
this.visible = true
this.startX = 0
this.startY = 0
this.angle = 0
this.scale = 1
this.speed = 1
this.opacity = 1
this.priority = 0
this.matrix = null
this.layers = dLayers
this.parent = null
}
/**
* 更新粒子发射器
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
this.emitParticles(deltaTime)
this.updateParticles(deltaTime)
}
/**
* 发射粒子
* @param {number} deltaTime 增量时间(毫秒)
*/
emitParticles(deltaTime) {
for (const layer of this.layers) {
layer.emitParticles(deltaTime)
}
}
/**
* 更新粒子的运动
* @param {number} deltaTime 增量时间(毫秒)
* @returns {number} 发射器中已激活的粒子数量
*/
updateParticles(deltaTime) {
let count = 0
for (const layer of this.layers) {
count += layer.updateParticles(deltaTime)
}
// 累计场景中的粒子数量
Scene.particleCount += count
// 返回已激活粒子的数量
return count
}
/**
* 平移粒子的位置
* @param {number} x 水平位移
* @param {number} y 垂直位移
*/
translateParticles(x, y) {
for (const layer of this.layers) {
layer.translateParticles(x, y)
}
}
/**
* 绘制所有图层中的粒子元素
* @param {Matrix} [matrix] 投影矩阵
*/
draw(matrix) {
for (const layer of this.layers) {
layer.draw(matrix)
}
}
/**
* 判断发射器是否为空(粒子数量为0)
* @returns {boolean}
*/
isEmpty() {
for (const {elements} of this.layers) {
if (elements.count !== 0) {
return false
}
}
return true
}
/** 销毁所有图层 */
destroy() {
for (const layer of this.layers) {
layer.destroy()
}
}
/** 当发射器不可见时,是否总是发射粒子 */
static alwaysEmit = false
/** 当发射器不可见时,是否总是绘制粒子 */
static alwaysDraw = false
}
// ******************************** 粒子图层类 ********************************
class ParticleLayer {
/** 绑定的粒子发射器对象
* @type {ParticleEmitter}
*/ emitter
/** 粒子图层数据
* @type {Object}
*/ data
/** 粒子精灵纹理
* @type {ImageTexture|null}
*/ texture
/** 粒子精灵纹理的宽度
* @type {number}
*/ textureWidth
/** 粒子精灵纹理的高度
* @type {number}
*/ textureHeight
/** 粒子精灵的单位宽度
* @type {number}
*/ unitWidth
/** 粒子精灵的单位高度
* @type {number}
*/ unitHeight
/** 粒子已经播放的时间
* @type {number}
*/ elapsed
/** 当前图层的粒子容量
* @type {number}
*/ capacity
/** 已发射的粒子数量
* @type {number}
*/ count
/** 可用的粒子库存量
* @type {number}
*/ stocks
/** 已激活的粒子元素数组
* @type {Array<ParticleElement>}
*/ elements
/** 可复用的粒子元素数组
* @type {Array<ParticleElement>}
*/ reserves
/**
* 粒子图层对象
* @param {ParticleEmitter} emitter 绑定的粒子发射器对象
* @param {Object} data 粒子图层数据
*/
constructor(emitter, data) {
this.emitter = emitter
this.data = data
this.texture = null
this.textureWidth = 0
this.textureHeight = 0
this.unitWidth = 0
this.unitHeight = 0
this.elapsed = data.interval - data.delay
this.capacity = 0
this.count = 0
this.stocks = 0
this.elements = []
this.elements.count = 0
this.reserves = []
this.reserves.count = 0
// 更新发射数量
this.updateCount()
// 更新过渡映射表
this.updateEasing()
// 加载纹理
this.loadTexture()
}
/**
* 发射粒子元素
* @param {number} deltaTime 增量时间(毫秒)
*/
emitParticles(deltaTime) {
let stocks = this.stocks
// 如果粒子库存量为0,停止发射
if (stocks === 0) return
this.elapsed += deltaTime * this.emitter.speed
const data = this.data
const interval = data.interval
let count = Math.floor(this.elapsed / interval)
if (count > 0) {
// 0 * Infinity returns NaN
this.elapsed -= interval * count || 0
const elements = this.elements
const maximum = ParticleLayer.maximum
let eCount = elements.count
// 如果激活的粒子达到最大数量,停止发射
if (eCount === maximum) return
const reserves = this.reserves
let rCount = reserves.count
spawn: {
// 重用旧的粒子
while (rCount > 0) {
const element = reserves[--rCount]
elements[eCount++] = element
element.initialize()
count--
stocks--
if (count === 0 || stocks === 0) {
break spawn
}
}
// 创建新的粒子
for (let i = this.capacity; i < maximum; i++) {
elements[eCount++] = new ParticleElement(this)
this.capacity = i + 1
count--
stocks--
if (count === 0 || stocks === 0) {
break spawn
}
}
}
elements.count = eCount
reserves.count = rCount
this.stocks = stocks
}
}
/**
* 更新粒子的运动
* @param {number} deltaTime 增量时间(毫秒)
* @returns {number} 当前图层中已激活粒子的数量
*/
updateParticles(deltaTime) {
const elements = this.elements
let eCount = elements.count
if (eCount === 0) return 0
const reserves = this.reserves
let rCount = reserves.count
let offset = 0
deltaTime *= this.emitter.speed
for (let i = 0; i < eCount; i++) {
const element = elements[i]
switch (element.update(deltaTime)) {
case false:
// 回收未激活粒子
reserves[rCount + offset] = element
offset++
continue
default:
// 重新排序已激活粒子
if (offset !== 0) {
elements[i - offset] = element
}
continue
}
}
// 如果有粒子被回收,更新相关属性
if (offset !== 0) {
elements.count = eCount - offset
reserves.count = rCount + offset
eCount = elements.count
}
// 返回已激活粒子的数量
return eCount
}
/**
* 平移粒子的位置
* @param {number} x 水平位移
* @param {number} y 垂直位移
*/
translateParticles(x, y) {
const {elements} = this
const {count} = elements
for (let i = 0; i < count; i++) {
const element = elements[i]
element.x += x
element.y += y
}
}
/**
* 批量绘制粒子元素
* @param {Matrix} [matrix] 投影矩阵
*/
draw(matrix) {
const gl = GL
const data = this.data
const texture = this.texture
const elements = this.elements
const count = elements.count
let vi = 0
switch (data.sort) {
case 'youngest-in-front':
// 粒子排序:最新的在前面
for (let i = 0; i < count; i++) {
elements[i].draw(vi)
vi += 20
}
break
case 'oldest-in-front':
// 粒子排序:最旧的在前面
for (let i = count - 1; i >= 0; i--) {
elements[i].draw(vi)
vi += 20
}
break
case 'by-scale-factor': {
// 粒子排序:近大远小
const {min, abs, round} = Math
const layers = ParticleLayer.layers
const starts = ParticleLayer.zeros
const ends = ParticleLayer.sharedUint32A
const set = ParticleLayer.sharedUint32B
const times = 0x3ffff / 10
let li = 0
let si = 2
for (let i = 0; i < count; i++) {
const element = elements[i]
// 使用缩放系数来计算排序优先级,作为键
const key = min(0x3ffff, round(
abs(element.scaleFactor) * times
))
if (starts[key] === 0) {
// 如果当前键没有粒子
starts[key] = si
layers[li++] = key
} else {
// 已存在相同优先级的粒子
// 添加到set的下一个位置
set[ends[key] + 1] = si
}
ends[key] = si
set[si++] = i
set[si++] = 0
}
// 借助类型化数组对键(优先级)进行排序
const queue = new Uint32Array(layers.buffer, 0, li).sort()
for (let i = 0; i < li; i++) {
// 通过排序后的键来获取粒子
const key = queue[i]
let si = starts[key]
starts[key] = 0
do {
elements[set[si]].draw(vi)
vi += 20
} while ((si = set[si + 1]) !== 0)
}
break
}
}
// 绘制图像
if (vi !== 0) {
gl.alpha = this.emitter.opacity
gl.blend = data.blend
const program = gl.particleProgram.use()
const vertices = gl.arrays[0].float32
if (matrix === undefined) {
matrix = gl.matrix.project(
gl.flip,
gl.width,
gl.height,
)
.scale(Camera.zoom, Camera.zoom)
.translate(-Camera.scrollLeft, -Camera.scrollTop)
}
gl.bindVertexArray(program.vao)
gl.uniformMatrix3fv(program.u_Matrix, false, matrix)
switch (data.color.mode) {
default:
// 颜色模式:纯色
gl.uniform1i(program.u_Mode, 0)
break
case 'texture': {
// 颜色模式:纹理采样
const tint = data.color.tint
const red = tint[0] / 255
const green = tint[1] / 255
const blue = tint[2] / 255
const gray = tint[3] / 255
gl.uniform1i(program.u_Mode, 1)
gl.uniform4f(program.u_Tint, red, green, blue, gray)
break
}
}
const lightMode = ParticleLayer.lightSamplingModes[data.light]
gl.uniform1i(program.u_LightMode, lightMode)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW, 0, vi)
gl.bindTexture(gl.TEXTURE_2D, texture.base.glTexture)
gl.drawElements(gl.TRIANGLES, vi / 20 * 6, gl.UNSIGNED_INT, 0)
gl.alpha = 1
}
}
/** 加载粒子纹理 */
loadTexture() {
const guid = this.data.image
if (guid) {
const texture = new ImageTexture(guid)
// 如果纹理已完成加载,设置好参数直接返回
if (texture.complete) {
this.texture = texture
this.calculateElementSize()
return
}
this.texture = texture
texture.on('load', () => {
if (this.texture === texture) {
// 纹理加载结束后如果粒子层还存在
// 设置参数,并恢复默认draw函数
this.texture = texture
this.calculateElementSize()
delete this.draw
} else {
// 如果粒子层已销毁,则销毁纹理
texture.destroy()
}
})
}
// 加载完成前禁用draw函数
this.draw = Function.empty
}
/** 计算粒子元素大小 */
calculateElementSize() {
const {data, texture} = this
this.textureWidth = texture.width
this.textureHeight = texture.height
this.unitWidth = Math.floor(texture.width / data.hframes)
this.unitHeight = Math.floor(texture.height / data.vframes)
}
/** 更新可发射的粒子数量 */
updateCount() {
let {count} = this.data
// 如果发射数量为0,表示可以无限发射
if (count === 0) {
count = 1e16
}
this.count = count
this.stocks = count
}
/** 更新过渡映射表 */
updateEasing() {
const {color} = this.data
if (color.mode === 'easing') {
this.easing = Easing.get(color.easingId)
}
}
/** 销毁图层中的粒子纹理 */
destroy() {
if (this.texture instanceof ImageTexture) {
this.texture.destroy()
this.texture = null
}
}
// 静态 - 同时存在的最大粒子数量
static maximum = 1000
// 静态 - 图层数组
static layers = new Uint32Array(0x40000)
// 静态 - 零值数组(用完后要确保所有值归零)
static zeros = new Uint32Array(0x40000)
// 静态 - 共享数组
static sharedUint32A = new Uint32Array(GL.arrays[0].uint32.buffer, 512 * 512 * 88, 512 * 512)
static sharedUint32B = new Uint32Array(GL.arrays[0].uint32.buffer, 512 * 512 * 92, 512 * 512)
// 静态 - 光照采样模式映射表
static lightSamplingModes = {raw: 0, global: 1, ambient: 2}
}
// ******************************** 粒子元素类 ********************************
class ParticleElement {
/** 绑定的粒子发射器对象
* @type {ParticleEmitter}
*/ emitter
/** 绑定的粒子图层对象
* @type {ParticleLayer}
*/ layer
/** 粒子图层数据
* @type {Object}
*/ data
/** 粒子已播放时间
* @type {number}
*/ elapsed
/** 粒子的生存周期
* @type {number}
*/ lifetime
/** 粒子渐出持续时间
* @type {number}
*/ fadeout
/** 粒子渐出的时间点
* @type {number}
*/ fadeoutTime
/** 粒子全局角度
* @type {number}
*/ globalAngle
/** 粒子的水平位置
* @type {number}
*/ x
/** 粒子的垂直位置
* @type {number}
*/ y
/** 粒子的水平锚点位置
* @type {number}
*/ anchorX
/** 粒子的垂直锚点位置
* @type {number}
*/ anchorY
/** 粒子的水平锚点速度
* @type {number}
*/ anchorSpeedX
/** 粒子的垂直锚点速度
* @type {number}
*/ anchorSpeedY
/** 粒子的水平移动速度
* @type {number}
*/ movementSpeedX
/** 粒子的垂直移动速度
* @type {number}
*/ movementSpeedY
/** 粒子的水平移动加速度
* @type {number}
*/ movementAccelX
/** 粒子的垂直移动加速度
* @type {number}
*/ movementAccelY
/** 粒子的旋转角度
* @type {number}
*/ rotationAngle
/** 粒子的旋转速度
* @type {number}
*/ rotationSpeed
/** 粒子的旋转加速度
* @type {number}
*/ rotationAccel
/** 粒子的水平旋转偏移X
* @type {number}
*/ hRotationOffsetX
/** 粒子的水平旋转偏移Y
* @type {number}
*/ hRotationOffsetY
/** 粒子的水平旋转半径
* @type {number}
*/ hRotationRadius
/** 粒子的水平旋转半径扩张速度
* @type {number}
*/ hRotationExpansionSpeed
/** 粒子的水平旋转半径扩张加速度
* @type {number}
*/ hRotationExpansionAccel
/** 粒子的水平旋转角度
* @type {number}
*/ hRotationAngle
/** 粒子的水平旋转角速度
* @type {number}
*/ hRotationAngularSpeed
/** 粒子的水平旋转角加速度
* @type {number}
*/ hRotationAngularAccel
/** 粒子的缩放系数
* @type {number}
*/ scaleFactor
/** 粒子的缩放速度
* @type {number}
*/ scaleSpeed
/** 粒子的缩放加速度
* @type {number}
*/ scaleAccel
/** 粒子精灵的水平索引
* @type {number}
*/ hindex
/** 粒子精灵的垂直索引
* @type {number}
*/ vindex
/** 粒子的不透明度
* @type {number}
*/ opacity
/** 粒子的颜色数组
* @type {Array<number>}
*/ color
/** 粒子设置初始位置函数
* @type {Function}
*/ setStartPosition
/** 粒子后期处理函数
* @type {Function}
*/ postProcessing
/** 粒子设置初始颜色函数
* @type {Function}
*/ setStartColor
/** 粒子更新颜色函数
* @type {Function}
*/ updateColor
/**
* 粒子元素对象
* @param {ParticleLayer} layer 绑定的粒子图层对象
*/
constructor(layer) {
this.emitter = layer.emitter
this.layer = layer
this.data = layer.data
this.elapsed = 0
this.lifetime = 0
this.fadeout = 0
this.fadeoutTime = 0
this.globalAngle = 0
this.x = 0
this.y = 0
this.anchorX = 0
this.anchorY = 0
this.anchorSpeedX = 0
this.anchorSpeedY = 0
this.movementSpeedX = 0
this.movementSpeedY = 0
this.movementAccelX = 0
this.movementAccelY = 0
this.rotationAngle = 0
this.rotationSpeed = 0
this.rotationAccel = 0
this.hRotationOffsetX = 0
this.hRotationOffsetY = 0
this.hRotationRadius = 0
this.hRotationExpansionSpeed = 0
this.hRotationExpansionAccel = 0
this.hRotationAngle = 0
this.hRotationAngularSpeed = 0
this.hRotationAngularAccel = 0
this.scaleFactor = 0
this.scaleSpeed = 0
this.scaleAccel = 0
this.hindex = 0
this.vindex = 0
this.opacity = 0
this.color = new Uint32Array(5)
this.updateMethods()
this.initialize()
}
/** 初始化粒子元素 */
initialize() {
const {emitter} = this
const {lifetime, lifetimeDev, fadeout, anchor, rotation, hRotation, movement, scale, hframes, vframes} = this.data
// 计算初始属性
this.elapsed = 0
this.lifetime = lifetime + lifetimeDev * (Math.random() * 2 - 1)
this.fadeout = fadeout
this.fadeoutTime = this.lifetime - fadeout
this.globalAngle = emitter.angle
this.scaleFactor = ParticleElement.getRandomParameter(scale.factor) * emitter.scale
this.scaleSpeed = ParticleElement.getRandomParameter(scale.speed) / 1e3 * emitter.scale
this.scaleAccel = ParticleElement.getRandomParameter(scale.accel) / 1e6 * emitter.scale
this.anchorX = ParticleElement.getRandomParameter(anchor.x)
this.anchorY = ParticleElement.getRandomParameter(anchor.y)
this.anchorSpeedX = ParticleElement.getRandomParameter(anchor.speedX) / 1e3
this.anchorSpeedY = ParticleElement.getRandomParameter(anchor.speedY) / 1e3
this.rotationAngle = Math.radians(ParticleElement.getRandomParameter(rotation.angle)) + emitter.angle
this.rotationSpeed = Math.radians(ParticleElement.getRandomParameter(rotation.speed)) / 1e3
this.rotationAccel = Math.radians(ParticleElement.getRandomParameter(rotation.accel)) / 1e6
this.hRotationOffsetX = 0
this.hRotationOffsetY = 0
this.hRotationRadius = ParticleElement.getRandomParameter(hRotation.radius) * emitter.scale
this.hRotationExpansionSpeed = ParticleElement.getRandomParameter(hRotation.expansionSpeed) * emitter.scale / 1e3
this.hRotationExpansionAccel = ParticleElement.getRandomParameter(hRotation.expansionAccel) * emitter.scale / 1e6
this.hRotationAngle = Math.radians(ParticleElement.getRandomParameter(hRotation.angle))
this.hRotationAngularSpeed = Math.radians(ParticleElement.getRandomParameter(hRotation.angularSpeed)) / 1e3
this.hRotationAngularAccel = Math.radians(ParticleElement.getRandomParameter(hRotation.angularAccel)) / 1e6
const movementAngle = Math.radians(ParticleElement.getRandomParameter(movement.angle)) + emitter.angle
const movementSpeed = ParticleElement.getRandomParameter(movement.speed) * emitter.scale / 1e3
this.movementSpeedX = movementSpeed * Math.cos(movementAngle)
this.movementSpeedY = movementSpeed * Math.sin(movementAngle)
const movementAccelAngle = Math.radians(ParticleElement.getRandomParameter(movement.accelAngle)) + emitter.angle
const movementAccel = ParticleElement.getRandomParameter(movement.accel) * emitter.scale / 1e6
this.movementAccelX = movementAccel * Math.cos(movementAccelAngle)
this.movementAccelY = movementAccel * Math.sin(movementAccelAngle)
const frame = Math.floor(Math.random() * hframes * vframes)
this.hindex = frame % hframes
this.vindex = Math.floor(frame / hframes)
this.opacity = 1
// 设置初始位置
this.setStartPosition(movementAngle)
// 设置初始颜色
this.setStartColor()
}
/**
* 更新粒子的运动
* @param {number} deltaTime 增量时间(毫秒)
* @returns {boolean} 返回false表示粒子可以被回收
*/
update(deltaTime) {
// 计算当前帧新的粒子位置
this.elapsed += deltaTime
this.scaleSpeed += this.scaleAccel * deltaTime
this.scaleFactor += this.scaleSpeed * deltaTime
this.rotationSpeed += this.rotationAccel * deltaTime
this.rotationAngle += this.rotationSpeed * deltaTime
this.movementSpeedX += this.movementAccelX * deltaTime
this.movementSpeedY += this.movementAccelY * deltaTime
this.anchorX += this.anchorSpeedX * deltaTime
this.anchorY += this.anchorSpeedY * deltaTime
this.x += this.movementSpeedX * deltaTime
this.y += this.movementSpeedY * deltaTime
// 计算水平旋转
this.hRotationExpansionSpeed += this.hRotationExpansionAccel * deltaTime
this.hRotationRadius += this.hRotationExpansionSpeed * deltaTime
this.hRotationAngularSpeed += this.hRotationAngularAccel * deltaTime
this.hRotationAngle += this.hRotationAngularSpeed * deltaTime
const hRotationOffset = this.hRotationRadius * Math.cos(this.hRotationAngle)
const hRotationOffsetX = hRotationOffset * Math.cos(this.globalAngle)
const hRotationOffsetY = hRotationOffset * Math.sin(this.globalAngle)
this.x += hRotationOffsetX - this.hRotationOffsetX
this.y += hRotationOffsetY - this.hRotationOffsetY
this.hRotationOffsetX = hRotationOffsetX
this.hRotationOffsetY = hRotationOffsetY
// 更新颜色
this.updateColor()
// 后期处理
return this.postProcessing()
}
/**
* 绘制粒子的精灵图像
* @param {number} vi 顶点数组的起始索引位置
*/
draw(vi) {
const layer = this.layer
const sw = layer.unitWidth
const sh = layer.unitHeight
const tw = layer.textureWidth
const th = layer.textureHeight
const ax = this.anchorX + 0.5
const ay = this.anchorY + 0.5
const vertices = GL.arrays[0].float32
const colors = GL.arrays[0].uint32
const matrix = GL.matrix.reset()
.translate(this.x, this.y)
.rotate(this.rotationAngle)
.scale(this.scaleFactor, this.scaleFactor)
.translate(-ax * sw, -ay * sh)
const R = sw
const B = sh
const a = matrix[0]
const b = matrix[1]
const c = matrix[3]
const d = matrix[4]
const e = matrix[6]
const f = matrix[7]
const sx = this.hindex * sw
const sy = this.vindex * sh
const color = this.getColorInt()
const sl = sx / tw
const st = sy / th
const sr = (sx + sw) / tw
const sb = (sy + sh) / th
vertices[vi ] = e
vertices[vi + 1] = f
vertices[vi + 2] = sl
vertices[vi + 3] = st
colors [vi + 4] = color
vertices[vi + 5] = c * B + e
vertices[vi + 6] = d * B + f
vertices[vi + 7] = sl
vertices[vi + 8] = sb
colors [vi + 9] = color
vertices[vi + 10] = a * R + c * B + e
vertices[vi + 11] = b * R + d * B + f
vertices[vi + 12] = sr
vertices[vi + 13] = sb
colors [vi + 14] = color
vertices[vi + 15] = a * R + e
vertices[vi + 16] = b * R + f
vertices[vi + 17] = sr
vertices[vi + 18] = st
colors [vi + 19] = color
}
/**
* 获取整数型颜色
* @returns {Uint32Array} 粒子颜色
*/
getColorInt() {
const {color} = this
if (color.changed) {
color.changed = false
const r = color[0]
const g = color[1]
const b = color[2]
const a = Math.round(color[3] * this.opacity)
// 将RGBA生成的整数型颜色代码存放在color[4]中
color[4] = r + (g + (b + a * 256) * 256) * 256
}
return color[4]
}
/** 更新粒子方法(根据粒子的特性来设置) */
updateMethods() {
const {area, color} = this.data
// 给不同的发射区域设置特有的方法
switch (area.type) {
case 'point':
this.setStartPosition = this.setStartPositionPoint
this.postProcessing = this.postProcessingCommon
break
case 'rectangle':
this.setStartPosition = this.setStartPositionRectangle
this.postProcessing = this.postProcessingCommon
break
case 'circle':
this.setStartPosition = this.setStartPositionCircle
this.postProcessing = this.postProcessingCommon
break
case 'edge':
this.setStartPosition = this.setStartPositionEdge
this.postProcessing = this.postProcessingEdge
break
}
// 给不同的颜色模式设置特有的方法
switch (color.mode) {
case 'fixed':
this.setStartColor = this.setStartColorFixed
this.updateColor = Function.empty
break
case 'random':
this.setStartColor = this.setStartColorRandom
this.updateColor = Function.empty
break
case 'easing':
if (this.color.start === undefined) {
this.color.start = new Uint8Array(4)
this.color.end = new Uint8Array(4)
}
this.setStartColor = this.setStartColorEasing
this.updateColor = this.updateColorEasing
break
case 'texture':
this.setStartColor = this.setStartColorTexture
this.updateColor = Function.empty
break
}
}
/** 变换粒子的初始位置 */
transformStartPosition() {
const {matrix} = this.emitter
// 如果发射器中存在矩阵,变换位置
if (matrix !== null) {
const a = matrix[0]
const b = matrix[1]
const c = matrix[3]
const d = matrix[4]
const e = matrix[6]
const f = matrix[7]
const {x, y} = this
this.x = a * x + c * y + e
this.y = b * x + d * y + f
}
}
/** 获取区域位置 */
getAreaPosition() {
const array = ParticleElement.sharedFloat64Array
const emitter = this.emitter
const area = this.data.area
const cos = Math.cos(emitter.angle)
const sin = Math.sin(emitter.angle)
const x = area.x * emitter.scale
const y = area.y * emitter.scale
array[0] = x * cos - y * sin
array[1] = x * sin + y * cos
return array
}
/** 设置初始位置 - 点发射区域 */
setStartPositionPoint() {
const {emitter} = this
const pos = this.getAreaPosition()
this.x = emitter.startX + pos[0]
this.y = emitter.startY + pos[1]
this.transformStartPosition()
}
/** 设置初始位置 - 矩形发射区域 */
setStartPositionRectangle() {
const {emitter} = this
const {area} = this.data
const pos = this.getAreaPosition()
const x = emitter.startX + pos[0]
const y = emitter.startY + pos[1]
const wh = area.width * emitter.scale / 2
const hh = area.height * emitter.scale / 2
this.x = Math.randomBetween(x - wh, x + wh)
this.y = Math.randomBetween(y - hh, y + hh)
this.transformStartPosition()
}
/** 设置初始位置 - 圆形发射区域 */
setStartPositionCircle() {
const {emitter} = this
const {area} = this.data
const pos = this.getAreaPosition()
const x = emitter.startX + pos[0]
const y = emitter.startY + pos[1]
const angle = Math.random() * Math.PI * 2
const distance = Math.random() * area.radius * emitter.scale
this.x = x + distance * Math.cos(angle)
this.y = y + distance * Math.sin(angle)
this.transformStartPosition()
}
/**
* 设置初始位置 - 屏幕边缘发射区域
* @param {number} movementAngle 初始移动角度(弧度)
*/
setStartPositionEdge(movementAngle) {
// 计算屏幕边缘的位置
const scrollLeft = Camera.scrollLeft
const scrollTop = Camera.scrollTop
const scrollRight = Camera.scrollRight
const scrollBottom = Camera.scrollBottom
const width = scrollRight - scrollLeft
const height = scrollBottom - scrollTop
const weightX = Math.abs(Math.sin(movementAngle) * width)
const weightY = Math.abs(Math.sin(movementAngle - Math.PI / 2) * height)
const threshold = weightX / (weightX + weightY)
const random = Math.random()
if (random < threshold) {
// 从屏幕水平位置(上边或下边)生成粒子
const forward = this.movementSpeedY >= 0
this.x = scrollLeft + random / threshold * width
this.y = forward ? scrollTop : scrollBottom
const vertices = this.getBoundingRectangle()
this.x -= (vertices[0] + vertices[2]) / 2 - this.x
this.y -= forward
? vertices[3] - this.y
: vertices[1] - this.y
} else {
// 从屏幕垂直位置(左边或右边)生成粒子
const forward = this.movementSpeedX >= 0
this.y = scrollTop + (random - threshold) / (1 - threshold) * height
this.x = forward ? scrollLeft : scrollRight
const vertices = this.getBoundingRectangle()
this.y -= (vertices[1] + vertices[3]) / 2 - this.y
this.x -= forward
? vertices[2] - this.x
: vertices[0] - this.x
}
}
/**
* 后期处理 - 通用发射区域
* @returns {boolean} 返回false表示粒子可以被回收
*/
postProcessingCommon() {
// 粒子已到达生命周期,返回false(进行回收)
if (this.elapsed >= this.lifetime) {
return false
}
// 粒子已到达淡出时间,计算不透明度
if (this.elapsed > this.fadeoutTime) {
const elapsed = this.elapsed - this.fadeoutTime
const time = elapsed / this.fadeout
this.opacity = Math.max(1 - time, 0)
this.color.changed = true
}
return true
}
/**
* 后期处理 - 屏幕边缘发射区域
* @returns {boolean} 返回false表示粒子可以被回收
*/
postProcessingEdge() {
// 粒子处于屏幕内的情况
const vertices = this.getBoundingRectangle()
if (vertices[0] < Camera.scrollRight &&
vertices[1] < Camera.scrollBottom &&
vertices[2] > Camera.scrollLeft &&
vertices[3] > Camera.scrollTop &&
this.elapsed < this.lifetime) {
// 标记为已经出现的状态
this.appeared = true
// 粒子已到达淡出时间,计算不透明度
if (this.elapsed > this.fadeoutTime) {
const elapsed = this.elapsed - this.fadeoutTime
const time = elapsed / this.fadeout
this.opacity = Math.max(1 - time, 0)
this.color.changed = true
}
return true
}
// 粒子处于屏幕外的情况
// 若是粒子已经出现过(刚生成可能在屏幕外,给它一个出场的机会)
// 或者时间超过500ms或生命周期
// 则返回false(进行回收)
if (this.appeared ||
this.elapsed > 500 ||
this.elapsed >= this.lifetime) {
this.appeared = false
return false
}
return true
}
/**
* 获取粒子元素的外接矩形
* @returns {Float64Array} [minX, minY, maxX, maxY]
*/
getBoundingRectangle() {
const layer = this.layer
const sw = layer.unitWidth
const sh = layer.unitHeight
const ax = this.anchorX + 0.5
const ay = this.anchorY + 0.5
const matrix = GL.matrix.reset()
.translate(this.x, this.y)
.rotate(this.rotationAngle)
.scale(this.scaleFactor, this.scaleFactor)
.translate(-ax * sw, -ay * sh)
const R = sw
const B = sh
const a = matrix[0]
const b = matrix[1]
const c = matrix[3]
const d = matrix[4]
const e = matrix[6]
const f = matrix[7]
const x1 = e
const y1 = f
const x2 = c * B + e
const y2 = d * B + f
const x3 = a * R + c * B + e
const y3 = b * R + d * B + f
const x4 = a * R + e
const y4 = b * R + f
const vertices = ParticleElement.sharedFloat64Array
vertices[0] = Math.min(x1, x2, x3, x4)
vertices[1] = Math.min(y1, y2, y3, y4)
vertices[2] = Math.max(x1, x2, x3, x4)
vertices[3] = Math.max(y1, y2, y3, y4)
return vertices
}
/** 设置初始颜色 - 固定 */
setStartColorFixed() {
const {rgba} = this.data.color
const {color} = this
color.changed = true
color[0] = rgba[0]
color[1] = rgba[1]
color[2] = rgba[2]
color[3] = rgba[3]
}
/** 设置初始颜色 - 随机 */
setStartColorRandom() {
const {min, max} = this.data.color
const {color} = this
color.changed = true
color[0] = Math.randomBetween(min[0], max[0])
color[1] = Math.randomBetween(min[1], max[1])
color[2] = Math.randomBetween(min[2], max[2])
color[3] = Math.randomBetween(min[3], max[3])
}
/** 设置初始颜色 - 过渡 */
setStartColorEasing() {
const {startMin, startMax, endMin, endMax} = this.data.color
const {start, end} = this.color
start[0] = Math.randomBetween(startMin[0], startMax[0])
start[1] = Math.randomBetween(startMin[1], startMax[1])
start[2] = Math.randomBetween(startMin[2], startMax[2])
start[3] = Math.randomBetween(startMin[3], startMax[3])
end[0] = Math.randomBetween(endMin[0], endMax[0])
end[1] = Math.randomBetween(endMin[1], endMax[1])
end[2] = Math.randomBetween(endMin[2], endMax[2])
end[3] = Math.randomBetween(endMin[3], endMax[3])
}
/** 更新颜色 - 过渡 */
updateColorEasing() {
const {easing} = this.layer
const {color} = this
const {start, end} = color
const clamp = ParticleElement.sharedClampedArray
const time = Math.min(easing.map(this.elapsed / this.lifetime), 1)
color.changed = true
// 使用clamped数组限制过渡插值的颜色范围(0-255)
clamp[0] = start[0] * (1 - time) + end[0] * time
clamp[1] = start[1] * (1 - time) + end[1] * time
clamp[2] = start[2] * (1 - time) + end[2] * time
clamp[3] = start[3] * (1 - time) + end[3] * time
color[0] = clamp[0]
color[1] = clamp[1]
color[2] = clamp[2]
color[3] = clamp[3]
}
/** 设置初始颜色 - 纹理采样 */
setStartColorTexture() {
const {color} = this
color.changed = true
color[3] = 255
}
// 静态属性
static count = 0
static sharedFloat64Array = new Float64Array(4)
static sharedClampedArray = new Uint8ClampedArray(4)
// 生成随机参数
static getRandomParameter([standard, deviation]) {
return standard + deviation * (Math.random() * 2 - 1)
}
}
// ******************************** 粒子发射器列表类 ********************************
class ParticleEmitterList extends Array {
/**
* 添加粒子发射器到管理器中
* @param {ParticleEmitter} emitter 粒子发射器
*/
append(emitter) {
if (emitter.parent === null) {
emitter.parent = this
this.push(emitter)
}
}
/**
* 从管理器中移除粒子发射器
* @param {ParticleEmitter} emitter 粒子发射器
*/
remove(emitter) {
if (emitter.parent === this) {
emitter.parent = null
super.remove(emitter)
}
}
/**
* 更新粒子发射器
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
const length = this.length
for (let i = 0; i < length; i++) {
this[i].update(deltaTime)
}
}
/**
* 绘制粒子
* @param {Matrix} [matrix] 投影矩阵
*/
draw(matrix) {
for (const emitter of this) {
emitter.draw(matrix)
}
}
/** 销毁管理器中的粒子发射器 */
destroy() {
const length = this.length
for (let i = 0; i < length; i++) {
this[i].destroy()
}
}
}