手把手教你用js写动画

4,551 阅读3分钟
原文链接: github.com

手把手教你写 js 动画

peek 2018-10-18 02-19

相信大家对这种数字渐变动画效果应该不面生吧。接下来分析一下如何用 js 实现这个动画。

数字渐变动画的实现

仔细想想,这种数字变化不就是在一段时间内从 0 到 5000 吗。换个思维,这不就是 从 0 到 1 的过程(也可以说是 从 0% 到 100%)。动画是需要时间的,在指定的时间内,数字从 0 到 1,是不是可以抽象成时间逐渐流逝?换句话说不就是时间消耗完了?那么数字从 0 到 1 的过程,不就可以转变为时间从开始到结束的过程。接着这个思路,就可以把动画抽象成,当前时间过了百分之多少,再拿这个百分比做相关转换不就可以了。接下来实现这个数字渐变。

<div class="box">0</div>
<script>
  const oBox = document.querySelector('.box')

  const startTime = + new Date
  let timer = null

  function step() {
    const percent = Math.min(1, (+new Date - startTime) / 5000)// 动画时间为 5s

    if (percent < 1) {
      oBox.innerHTML = ~~(5000 * percent)
      timer = requestAnimationFrame(step)
    } else {
      oBox.innerHTML = ~~(5000 * 1)
      cancelAnimationFrame(timer)
    }
  }

  timer = requestAnimationFrame(step)
</script>

可以看到动画效果和预期是一致的,说明通过时间消耗的多少来做动画的思路是对的。

实现 Animator 类

总结上面数字动画的实现方式,我们可以实现这样一个动画类,它可以设置 动画时长,并在 动画过程 中主动调用 onUpdate 函数,在动画完成后,调用 onComplete 函数。

class Animator {
  constructor() {
    this.durationTime = 0
    this.eventHandlers = new Map()
  }

  duration(time) {
    if (typeof time !== 'number') {
      throw new Error('Duration must be a number')
    }

    this.durationTime = time

    return this
  }

  on(type, handler) {
    if (typeof handler !== 'function') {
      throw new Error('Handler must be a function')
    }

    this.eventHandlers.set(type, handler)

    return this
  }

  animate() {
    const duration = this.durationTime
    const update = this.eventHandlers.get('update') || (t => t)
    const complete = this.eventHandlers.get('complete') || (() => {})

    let timer = null
    const startTime = +new Date()

    function step() {
      const percent = Math.min(1, (+new Date() - startTime) / duration)

      if (percent < 1) {
        update(percent)
        timer = requestAnimationFrame(step)
      } else {
        cancelAnimationFrame(timer)
        update(1)
        complete()
      }
    }

    timer = requestAnimationFrame(step)
  }
}

使用该类也可以实现上面数字渐变动画:

const oBox = document.querySelector('.box')

new Animator()
  .duration(3000)
  .on('update', t => (oBox.innerHTML = ~~(t * 5000)))
  .on('complete', () => alert('ok'))
  .animate()

增强 Animator 类

相信很多前端同学对 easingeasing-in-out都不陌生,那么如何用 js 实现类似的动画效果呢?这其中的奥秘就是在于 缓动函数。我们通过将动画转变为时间流逝的百分比来做动画,时间的流逝是线性的,可以想象从 0 到 1 时间的曲线是不变的,但是我们可以用时间流逝百分比 乘以某个函数,只要该函数能保证从 0 到 1 仍旧是从 0 到 1,中间百分比变化我们就可以不管。举个例子: y = x * x,当 0 <= x <=1 的时候,y 的结果便还是 0 到 1 之间,很显然 x => x * x,就是一个缓动函数。

增强 Animator 类:

class Animator {
  constructor() {
    this.durationTime = 0
    this.easingFn = k => k
    this.eventHandlers = new Map()
  }

  easing(fn) {
    if (typeof fn !== 'function') {
      throw new Error('Easing must be a function, such as k => k')
    }

    this.easingFn = fn

    return this
  }

  duration(time) {
    if (typeof time !== 'number') {
      throw new Error('Duration must be a number')
    }

    this.durationTime = time

    return this
  }

  on(type, handler) {
    if (typeof handler !== 'function') {
      throw new Error('Handler must be a function')
    }

    this.eventHandlers.set(type, handler)

    return this
  }

  animate() {
    const duration = this.durationTime
    const easing = this.easingFn
    const update = this.eventHandlers.get('update') || (t => t)
    const complete = this.eventHandlers.get('complete') || (() => {})

    let timer = null
    const startTime = +new Date()

    function step() {
      const percent = Math.min(1, (+new Date() - startTime) / duration)

      if (percent < 1) {
        update(easing(percent))
        timer = requestAnimationFrame(step)
      } else {
        cancelAnimationFrame(timer)
        update(easing(1))
        complete()
      }
    }

    timer = requestAnimationFrame(step)
  }
}

使用 :

const oBox1 = document.querySelector('.box1')
const oBox2 = document.querySelector('.box2')
const oBox3 = document.querySelector('.box3')

new Animator()
  .duration(3000)
  .easing(x => k * k)
  .on('update', t => oBox1.style.left = t * 500 + 'px')
  .on('complete', () => alert('ok'))
  .animate()

new Animator()
  .duration(3000)
  .easing(k => (1 - --k * k * k * k))
  .on('update', t => oBox2.style.left = t * 500 + 'px')
  .on('complete', () => alert('ok'))
  .animate()

new Animator()
  .duration(3000)
  .easing(k => 1 - Math.sqrt(1 - k * k))
  .on('update', t => oBox3.style.left = t * 500 + 'px')
  .on('complete', () => alert('ok'))
  .animate()

peek 2018-10-18 02-01

更多好玩的 easing 函数,推荐看 tween.js 的缓动函数