effects.js 3.04 KB
// effects.js — 屏幕特效播放 / 停止(帧序列轮播)。
//
// 特效层浮在世界之上、不进 worldContainer(与 HUD 同理,不受相机 pan/zoom)。
// 加一种特效 = 在调用方提供帧 url 序列即可,无需改接口(render-pixi §6 可扩展性)。

/**
 * @typedef {Object} EffectDef
 * @property {string[]} frames
 * @property {number=} fps
 * @property {boolean=} loop
 */
/**
 * @typedef {Object} EffectEntry
 * @property {*} spr
 * @property {string[]} frames
 * @property {number} fps
 * @property {boolean} loop
 * @property {number} clock
 */

/**
 * 创建特效播放器。
 * @param {Object} [deps]
 * @param {*=} deps.PIXI
 * @param {*=} deps.layer    特效层容器(建议挂在 app.stage 顶层,非 worldContainer)
 * @param {ReturnType<import('./assets.js').createAssetLoader>=} deps.assets
 * @param {((name:string)=>(EffectDef|null))=} deps.resolve
 *        把特效名解析成帧序列定义(由场景 / bundle 注入;缺省返回 null 跳过)
 * @returns {{ play:(name:string,opts?:Partial<EffectDef>)=>void, stop:(name:string)=>void, update:(dtMs:number)=>void, stopAll:()=>void }}
 */
export function createEffects({ PIXI, layer, assets, resolve } = {}) {
  /** @type {Map<string, EffectEntry>} */
  const active = new Map()

  /** @param {string} name  @param {Partial<EffectDef>} [opts] */
  function play(name, opts = {}) {
    if (!name) return
    /** @type {EffectDef|null} */
    const def =
      (resolve ? resolve(name) : null) ||
      (opts.frames ? /** @type {EffectDef} */ (opts) : null)
    if (!def || !def.frames || !def.frames.length) {
      console.warn('[render-pixi] effect has no frames, skip', name)
      return
    }
    stop(name)
    const spr = new PIXI.Sprite()
    if (layer) layer.addChild(spr)
    /** @type {EffectEntry} */
    const entry = {
      spr,
      frames: def.frames,
      fps: def.fps || opts.fps || 12,
      loop: opts.loop != null ? opts.loop : def.loop != null ? def.loop : false,
      clock: 0,
    }
    for (const u of def.frames) if (assets) assets.load(u)
    active.set(name, entry)
    _renderFrame(entry, 0)
  }

  /** @param {EffectEntry} entry  @param {number} idx */
  function _renderFrame(entry, idx) {
    const url = entry.frames[idx]
    const tex = url && assets ? assets.get(url) : null
    if (tex) entry.spr.texture = tex
  }

  /** @param {number} dtMs */
  function update(dtMs) {
    for (const [name, e] of active) {
      e.clock += dtMs
      const total = e.frames.length
      const raw = Math.floor(e.clock / (1000 / e.fps))
      let idx = raw
      if (raw >= total) {
        if (e.loop) idx = raw % total
        else {
          stop(name)
          continue
        }
      }
      _renderFrame(e, idx)
    }
  }

  /** @param {string} name */
  function stop(name) {
    const e = active.get(name)
    if (!e) return
    try {
      e.spr.destroy()
    } catch (err) {
      void err
    }
    active.delete(name)
  }

  function stopAll() {
    for (const name of [...active.keys()]) stop(name)
  }

  return { play, stop, update, stopAll }
}