sampling.js 2.76 KB
// sampling.js —— 关键帧采样(纯函数)。移植自原型 evaluator.js。
//
// clip 的关键帧值是**相对 setup pose 的增量**(drot/dx/dy/dscale),未给的属性
// 回落默认(drot/dx/dy=0、dscale=1)。本文件只把「一个 clip + 已播时长」算成
// 「每根 bone 的当前增量」,不碰轨道叠加、不碰世界矩阵——那些在 anim-state / evaluator。

import { lerp, PROP_DEFAULT } from './matrix.js'

/**
 * 在一组关键帧里采样某个属性(线性插值,区间外夹到首 / 末帧)。
 * @param {import('./types.js').Keyframe[]} keys
 * @param {number} t      归一化时间 0..1
 * @param {'drot'|'dx'|'dy'|'dscale'} prop
 * @returns {number}
 */
export function sampleProp(keys, t, prop) {
  const def = PROP_DEFAULT[prop]
  if (keys.length === 0) return def
  if (t <= keys[0].t) return keys[0][prop] ?? def
  const last = keys[keys.length - 1]
  if (t >= last.t) return last[prop] ?? def
  for (let i = 0; i < keys.length - 1; i++) {
    const k0 = keys[i]
    const k1 = keys[i + 1]
    if (t >= k0.t && t <= k1.t) {
      const span = k1.t - k0.t || 1
      const f = (t - k0.t) / span
      return lerp(k0[prop] ?? def, k1[prop] ?? def, f)
    }
  }
  return def
}

/**
 * 归一化播放进度。pose 恒为 0;loop 取模循环;oneshot 停在末帧(夹到 1)。
 * @param {import('./types.js').AnimationClip} clip
 * @param {number} elapsed  已播时长 ms
 * @returns {number} 0..1
 */
export function clipPhase(clip, elapsed) {
  if (clip.kind === 'pose' || clip.duration <= 0) return 0
  if (clip.kind === 'loop') return (elapsed % clip.duration) / clip.duration
  // oneshot:停在末帧
  return Math.min(elapsed / clip.duration, 1)
}

/**
 * 采样一个 clip 的当前帧,返回 { boneName: BoneOffset }。
 * @param {import('./types.js').AnimationClip} clip
 * @param {number} elapsed
 * @returns {Record<string, import('./types.js').BoneOffset>}
 */
export function sampleClip(clip, elapsed) {
  const t = clipPhase(clip, elapsed)
  /** @type {Record<string, import('./types.js').BoneOffset>} */
  const out = {}
  for (const [boneName, keys] of Object.entries(clip.bones)) {
    out[boneName] = {
      drot: sampleProp(keys, t, 'drot'),
      dx: sampleProp(keys, t, 'dx'),
      dy: sampleProp(keys, t, 'dy'),
      dscale: sampleProp(keys, t, 'dscale'),
    }
  }
  return out
}

/**
 * 按权重 w 混合两个增量(交叉淡入用)。w=0 取 a,w=1 取 b。
 * @param {import('./types.js').BoneOffset} a
 * @param {import('./types.js').BoneOffset} b
 * @param {number} w
 * @returns {import('./types.js').BoneOffset}
 */
export function blendOffset(a, b, w) {
  return {
    drot: lerp(a.drot, b.drot, w),
    dx: lerp(a.dx, b.dx, w),
    dy: lerp(a.dy, b.dy, w),
    dscale: lerp(a.dscale, b.dscale, w),
  }
}