You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何统一实现逐帧调整与固定时长触发的Lerp位置插值逻辑

统一双场景Lerp移动方案

你当前两套实现的核心差异本质是插值策略的不同,完全可以通过一层抽象封装成统一接口,不需要在业务层写两套逻辑。

现有实现的问题

  • 第一种每帧动态目标的实现属于指数平滑跟随,直接用固定0.9作为lerp系数没有做帧率适配,帧率波动时跟随速度会明显变化,高帧率下跟手偏慢、低帧率下容易冲过目标
  • 第二种固定时长补间的实现属于定长过渡动画,没有内置目标变更时的打断、续跑逻辑,手动维护raf循环容易在组件卸载、目标快速变更时出现内存泄漏、动画错乱

统一封装思路

把两类场景的公共逻辑抽成独立的移动控制器,对外只暴露一个更新目标坐标的方法,内部自动处理插值策略选择、帧循环启停、帧率适配、清理逻辑:

  • 内置两种插值模式:平滑跟随(对应小幅度高频目标变更)、固定时长补间(对应大幅度单次目标变更),支持手动指定模式,也支持根据目标位移幅度自动切换模式
  • 统一帧循环逻辑,不管哪种模式都复用同一个raf tick流程,自动在到达目标时停止循环,避免空跑性能损耗
  • 内置帧率修正,保证不同刷新率设备上的移动速度表现一致
  • 内置清理方法,适配类SolidJS框架的组件生命周期(从你代码里的createEffect/onCleanup判断你用的是Solid)

具体实现代码

// 基础插值工具函数
const lerp = (a, b, t) => a + (b - a) * t

class UnifiedLerpMover {
  /**
   * @param {Object} positionRef 要移动的位置对象,即你代码里的_
   * @param {Object} options 配置项
   */
  constructor(positionRef, options = {}) {
    this.pos = positionRef
    // 当前实际坐标
    this.curX = positionRef.x
    this.curY = positionRef.y
    // 目标坐标
    this.targetX = null
    this.targetY = null
    // 帧循环相关
    this.rafId = null
    this.lastFrameTime = 0
    this.tweenStart = 0
    // 上一帧的目标值,用来判断目标是否在高频变动
    this.lastRecordTargetX = null
    this.lastRecordTargetY = null

    // 合并默认配置
    this.config = Object.assign({
      smoothFactor: 0.9, // 平滑跟随的基准系数(以60帧为基准)
      tweenDuration: 1000, // 固定补间时长,单位ms
      tweenEasing: t => t * 0.8, // 补间缓动曲线
      autoModeDistanceThreshold: 5, // 自动模式下,位移超过该值就用固定补间,否则用平滑跟随
      mode: 'auto' // 可选值: auto / smooth / tween
    }, options)
    this.activeMode = null
  }

  /**
   * 对外唯一调用入口:更新目标坐标
   * @param {number} x 目标x
   * @param {number} y 目标y
   */
  setTarget(x, y) {
    this.targetX = x
    this.targetY = y
    // 没在跑循环就启动
    if (!this.rafId) {
      this.lastFrameTime = performance.now()
      // 决定本次用什么模式
      if (this.config.mode === 'tween' || (this.config.mode === 'auto' && this.#getTargetDistance() > this.config.autoModeDistanceThreshold)) {
        this.activeMode = 'tween'
        this.tweenStart = this.lastFrameTime
      } else {
        this.activeMode = 'smooth'
      }
      this.#tick()
    }
  }

  // 内部帧更新逻辑
  #tick = () => {
    const now = performance.now()
    const dt = now - this.lastFrameTime
    this.lastFrameTime = now

    if (this.activeMode === 'smooth') {
      // 帧率修正:以60帧每帧16.67ms为基准,保证不同帧率下跟随速度一致
      const adjustedFactor = 1 - Math.pow(1 - this.config.smoothFactor, dt / 16.67)
      this.curX = lerp(this.curX, this.targetX, adjustedFactor)
      this.curY = lerp(this.curY, this.targetY, adjustedFactor)

      // 已经足够接近目标、且目标停止变动时,终止循环
      const isTargetStable = Math.abs(this.targetX - this.lastRecordTargetX) < 0.01 && Math.abs(this.targetY - this.lastRecordTargetY) < 0.01
      if (this.#getTargetDistance() < 0.01 && isTargetStable) {
        this.#stop()
        return
      }
      this.lastRecordTargetX = this.targetX
      this.lastRecordTargetY = this.targetY
    } else {
      // 固定时长补间逻辑
      const elapsed = now - this.tweenStart
      const progress = Math.min(elapsed / this.config.tweenDuration, 1)
      const easeVal = this.config.tweenEasing(progress)
      this.curX = lerp(this.curX, this.targetX, easeVal)
      this.curY = lerp(this.curY, this.targetY, easeVal)

      if (progress >= 1) {
        // 补间走完了,如果目标还没到(中途变更过)就重启补间,否则停止
        if (this.#getTargetDistance() < 0.01) {
          this.#stop()
          return
        }
        this.tweenStart = now
      }
    }

    // 同步坐标到目标位置对象
    this.pos.x = this.curX
    this.pos.y = this.curY
    this.rafId = requestAnimationFrame(this.#tick)
  }

  // 计算当前位置到目标的直线距离
  #getTargetDistance() {
    const dx = this.targetX - this.curX
    const dy = this.targetY - this.curY
    return Math.hypot(dx, dy)
  }

  // 停止帧循环
  #stop() {
    if (this.rafId) {
      cancelAnimationFrame(this.rafId)
      this.rafId = null
    }
  }

  // 组件卸载时调用,清理资源
  destroy() {
    this.#stop()
  }
}

业务层使用方式

不需要再写两套不同的effect逻辑,整个组件生命周期内只需要初始化一次控制器,所有场景都调用同一个setTarget方法即可:

// 初始化控制器,传入你的位置对象_
const mover = new UnifiedLerpMover(_, {
  // 这里可以传自定义配置覆盖默认值
})
// 组件卸载时自动清理
onCleanup(() => mover.destroy())

// 不管是每帧小幅更新目标,还是单次跳变更新目标,都只需要这一段逻辑
createEffect(() => {
  mover.setTarget(x, y)
})

扩展说明

  • 如果需要更丰富的缓动效果,只需要修改配置里的tweenEasing传入对应缓动函数即可,不需要改动核心逻辑
  • 如果需要手动强制使用某一种模式,只需要初始化时把mode配置改成对应值即可,比如做拖拽跟手场景就强制设为smooth,做页面跳转的位移动画就强制设为tween
  • 这套逻辑也可以直接扩展到旋转、缩放等其他需要插值的属性,只需要把x/y的逻辑改成对应属性即可

内容的提问来源于stack exchange,提问作者eguneys

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 17:33:44