代码

export interface SilkyScrollOpts {
  target?: HTMLElement // 滚动目标元素,默认为 document.documentElement
  orientation?: 'vertical' | 'horizontal' // 滚动方向,默认为 vertical
  multiplier?: number // 滚轮事件的乘数,默认为 1
  lerp?: number // 线性插值强度,[0, 1],默认为 0.1。如果定义了 duration 则无用
  fps?: number // 画面刷新率(以帧为单位),默认为 60。定义了 lerp 则使用
  duration?: number // 滚动动画的持续时间(以秒为单位)
  easing?: (t: number) => number // 缓动函数,默认为 easeInOut。定义了 duration 则使用
  onUpdate?: (value: number) => void // 滚动回调,默认为滚动 target
}
 
type RequiredSilkyScrollOpts = Required<SilkyScrollOpts>
 
/**
 * 平滑滚动
 */
export default class SilkyScroll {
  private _target: RequiredSilkyScrollOpts['target'] // 滚动目标元素
  private _orientation: RequiredSilkyScrollOpts['orientation'] // 滚动方向
  private _multiplier: RequiredSilkyScrollOpts['multiplier'] // 滚轮事件的乘数
  private _lerp: RequiredSilkyScrollOpts['lerp'] // 线性插值强度,[0, 1]
  private _fps: RequiredSilkyScrollOpts['fps'] // 画面刷新率(以帧为单位)。
  private _duration?: RequiredSilkyScrollOpts['duration'] // 滚动动画的持续时间(以秒为单位)。
  private _easing: RequiredSilkyScrollOpts['easing'] // 缓动函数
  private _onUpdateOriginal: RequiredSilkyScrollOpts['onUpdate'] // 滚动回调
 
  private _isRunning = false // 是否正在滚动
  private _rafTimeRecord: number = 0 // RAF 时间记录
  private _rafHandle: number = 0 // RAF handle
  private _curScroll = 0 // 当前滚动距离
  private _toScroll = 0 // 目标滚动距离
  private _fromScroll = 0 // 上一次滚动距离
  private _curTime = 0 // 当前时间
  private _onUpdate?: RequiredSilkyScrollOpts['onUpdate'] // 内部滚动回调
 
  constructor(opts?: SilkyScrollOpts) {
    const {
      target = document.documentElement,
      orientation = 'vertical',
      multiplier = 1,
      lerp = 0.1,
      duration,
      fps = 60,
      easing = easeInOut,
      onUpdate,
    } = opts || {}
    let onUpdateOriginal = onUpdate
    if (!onUpdateOriginal) {
      onUpdateOriginal = v => {
        switch (orientation) {
          case 'vertical':
            target.scrollTop = v
            break
          case 'horizontal':
            target.scrollLeft = v
            break
        }
      }
    }
 
    // 初始化
    this._target = target
    this._orientation = orientation
    this._multiplier = multiplier
    this._lerp = lerp
    this._fps = fps
    this._duration = duration
    this._easing = easing
    this._onUpdateOriginal = onUpdateOriginal
 
    // RAF 更新
    this._target.addEventListener('wheel', this._onWheel as any, { passive: false })
    const rafFunc = (time: DOMHighResTimeStamp) => {
      this._raf(time)
      this._rafHandle = requestAnimationFrame(rafFunc)
    }
    this._rafHandle = requestAnimationFrame(rafFunc)
  }
 
  /**
   * @return {boolean} SilkyScroll 实例当前是否正在运行
   */
  get isRunning(): boolean {
    return this._isRunning
  }
 
  /**
   * 停止动画
   */
  stop() {
    this._rafTimeRecord = 0
    this._isRunning = false
    this._curScroll = 0
    this._toScroll = 0
    this._fromScroll = 0
    this._curTime = 0
  }
 
  /**
   * 销毁 SilkyScroll 实例
   */
  destroy() {
    this._target.removeEventListener('wheel', this._onWheel as any)
    this._rafHandle && cancelAnimationFrame(this._rafHandle)
  }
 
  /**
   * 根据当前时间更新动画帧
   * @param {DOMHighResTimeStamp} time 高精度格式的当前时间
   */
  private _raf(time: DOMHighResTimeStamp) {
    if (!this._isRunning) return
    const deltaTime = time - (this._rafTimeRecord || time)
    this._rafTimeRecord = time
    this._advance(deltaTime * 0.001) // 单位转化为秒
  }
 
  /**
   * 按给定的增量时间推进动画
   * @param {number} deltaTime 自上一帧以来经过的时间,单位为秒
   */
  private _advance(deltaTime: number) {
    let completed = false
    let value = 0
    if (this._duration === undefined) {
      // 阻尼效果线性插值
      value = damp(this._curScroll, this._toScroll, this._lerp * this._fps, deltaTime)
      if (Math.round(value) === Math.round(this._toScroll)) {
        completed = true
      }
    } else {
      // 动画持续时间 + 缓动函数
      this._curTime += deltaTime
      const linearProgress = clamp(0, this._curTime / this._duration, 1)
      completed = linearProgress >= 1
      const easedProgress = completed ? 1 : this._easing(linearProgress)
      value = this._fromScroll + (this._toScroll - this._fromScroll) * easedProgress
    }
    this._onUpdate?.(value)
    if (completed) {
      this._isRunning = false
    }
  }
 
  /**
   * wheel 事件回调函数
   * @param {WheelEvent} e
   */
  private _onWheel = (e: WheelEvent) => {
    e.preventDefault() // 阻止默认事件,停止滚动
    let delta = 0
    switch (this._orientation) {
      case 'vertical':
        delta = (e as any).wheelDeltaY
        break
      case 'horizontal':
        delta = (e as any).wheelDeltaX
        break
    }
    this._onVirtualScroll(this._curScroll - delta * this._multiplier)
  }
 
  /**
   * 处理虚拟滚动事件并更新滚动位置
   * @param {number} targetScroll - 目标滚动位置
   */
  private _onVirtualScroll(targetScroll: number) {
    this._isRunning = true
    this._toScroll = targetScroll
    this._fromScroll = this._curScroll
    this._curTime = 0
    this._onUpdate = (value: number) => {
      this._onUpdateOriginal(value)
      this._curScroll = value // 记录滚动后的距离
    }
  }
}
 
/**
 * 缓动函数:缓入缓出,慢快慢
 * @param {number} t
 * @return {number}
 */
export const easeInOut = (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t))
 
/**
 * 缓动函数:指数反向缓动,先快后慢
 * @param {number} t
 * @return {number}
 */
export const easeOut = (t: number) => 1 - Math.pow(1 - t, 2)
 
/**
 * 对两个值进行线性插值
 * @param {number} start 起始值
 * @param {number} end 结束值
 * @param {number} amt 线性插值强度,[0, 1]
 * @return {number} 插值
 */
export const lerp = (start: number, end: number, amt: number): number => (1 - amt) * start + amt * end
 
/**
 * 对两个值进行阻尼效果线性插值
 * @param {number} start 起始值
 * @param {number} end 结束值
 * @param {number} lambda 阻尼系数
 * @param {number} deltaTime 间隔时间
 * @return {number} 插值
 */
export const damp = (start: number, end: number, lambda: number, deltaTime: number): number => lerp(start, end, 1 - Math.exp(-lambda * deltaTime))
 
/**
 * 如果输入值处于 [min, max] 之间,返回其值,否则返回 min 或 max
 * @param {number} min 最小边界
 * @param {number} input 输入值
 * @param {number} max 最大边界
 * @return
 */
export const clamp = (min: number, input: number, max: number) => Math.max(min, Math.min(input, max))

参考

应用