Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端动画技术 #33

Open
WarpPrism opened this issue May 30, 2018 · 0 comments
Open

前端动画技术 #33

WarpPrism opened this issue May 30, 2018 · 0 comments

Comments

@WarpPrism
Copy link
Owner

WarpPrism commented May 30, 2018

动画相关概念

  • 帧:动画过程中每一个静止的状态,每一张静止的图片
  • 帧率:刷新频率,每秒钟播放的帧数,FPS(frame per second),单位是Hz
  • 帧时长:每一帧停留的时间,如60FPS的动画帧时长约为16.7ms,意味着浏览器必须在16.7ms内绘制完这一帧
  • 硬件加速:硬件有三个处理器:CPU、GPU和APU(声音处理器)。他们通过PCI/AGP/PCIE总线交换数据。GPU在浮点运算、并行计算等部分计算方面,明显高于CPU的性能。硬件加速即利用GPU进行动画的计算
  • 缓动:最普通的动画就是匀速的动画,每次增加固定的值。缓动就是用来修改每次增加的值,让其按照不规律的方式增加,实现动画的变化。
  • 浏览器的刷新率:通常为60Hz

前端动画分类

从控制角度分,前端动画分为两种:

  1. JavaScript控制的动画
  2. CSS控制的动画

从实现方式上分:

  1. 补间动画 (定义关键帧,中间状态自动补全,如css的animation)
  2. 逐帧动画 (逐帧绘制,如requestAnimationFrame)

JS动画

JS动画的原理是通过setTimeout setIntervalrequestAnimationFrame 方法绘制动画帧(render),从而动态地改变网页中图形的显示属性(如DOM样式,canvas位图数据,SVG对象属性等),进而达到动画的目的。

多数情况下,应 首先选用 requestAnimationFrame方法(RAF),因为RAF的原理是会在浏览器下一次重绘之前更新动画,即它的刷新频率和浏览器自身的刷新频率保持一致(一般为60Hz),从而确保了性能。另外RAF在浏览器切入后台时会暂停执行,也可以提升性能和电池寿命。(来自MDN

// requestAnimationFrame Demo
let i = 0
let render = () {
  if (i >= frame.length) i = 0
  let currentFrame = frame[i]
  drawFrame(currentFrame)
  i++
  requestAnimationFrame(render)
}
requestAnimationFrame(render)

下面代码是一个用js + canvas 实现帧动画的一个例子,可以帮你更好的理解js动画原理:

/**
 * 基于canvas的帧动画实现
 * 最近修改日期:2018-06-22
 */

import { IsArray } from 'Utils'

class FrameAnim {
  constructor ({ frames, canvas, fps, useRAF }) {
    this._init({ frames, canvas, fps, useRAF })
  }
  /**
   * 实例初始化
   * @param options ->
   * @param {Array} frames image对象数组
   * @param {Object} canvas canvas dom 对象
   * @param {Number} fps 帧率
   * @param {Boolean} useRAF 是否使用requestAnimationFrame方法
   */
  _init ({ frames, canvas, fps, useRAF }) {
    this.frames = []
    if (IsArray(frames)) {
      this.frames = frames
    }
    this.canvas = canvas
    // 如果使用RAF,则不需要传入FPS,否则可用FPS控制帧率
    this.fps = fps || 60
    this.useRAF = useRAF || false

    this.ctx = this.canvas.getContext('2d') // 绘图上下文
    this.cwidth = this.canvas.width
    this.cheight = this.canvas.height
    this.animTimer = null // 动画定时器
    this.currentIndex = 0 // 当前帧
    this.stopLoop = false // 停止循环播放
  }
  _play (frameSections, fromIndex = 0) {
    return new Promise((resolve, reject) => {
      this.currentIndex = fromIndex || 0
      if (this.useRAF) {
        let render = () => {
          this.ctx.clearRect(0, 0, this.cwidth, this.cheight)
          let currentFrame = frameSections[this.currentIndex]
          this.ctx.drawImage(currentFrame, 0, 0, currentFrame.width, currentFrame.height)
          this.currentIndex++
          if (this.currentIndex <= frameSections.length - 1) {
            requestAnimationFrame(render)
          } else {
            this._stopPlay()
            resolve({finish: true})
          }
        }
        this.animTimer = requestAnimationFrame(render)
      } else {
        this.animTimer = setInterval(() => {
          if (this.currentIndex > frameSections.length - 1) {
            this._stopPlay()
            resolve({finish: true})
            return
          }
          this.ctx.clearRect(0, 0, this.cwidth, this.cheight)
          let currentFrame = frameSections[this.currentIndex]
          this.ctx.drawImage(currentFrame, 0, 0, currentFrame.width, currentFrame.height)
          this.currentIndex++
        }, 1000 / this.fps)
      }
    })
  }
  _stopPlay () {
    if (this.useRAF) {
      cancelAnimationFrame(this.animTimer)
      this.animTimer = null
    } else {
      clearInterval(this.animTimer)
      this.animTimer = null
    }
  }
  stopAllFrameAnimation () {
    this.stopLoop = true
    this._stopPlay()
  }
  /**
   * 顺序播放
   * @param {Array} frameSections 动画帧片段
   */
  linearPlay (frameSections = this.frames) {
    return this._play(frameSections, this.currentIndex)
  }
  /**
   * 顺序循环播放
   * @param {Array} frameSections 动画帧片段
   */
  loopPlay (frameSections = this.frames) {
    this._play(frameSections, this.currentIndex).then((res) => {
      if (!this.stopLoop) {
        this.currentIndex = 0
        this.loopPlay(frameSections, this.currentIndex)
      }
    })
  }
  // 倒序播放
  reversePlay (frameSections = this.frames) {
    frameSections.reverse()
    return this.linearPlay(frameSections)
  }
  // 倒序循环播放
  reverseLoopPlay (frameSections = this.frames) {
    frameSections.reverse()
    this.loopPlay(frameSections)
  }
  // 秋千式(单摆式)循环播放:即从第一帧播放到最后一帧,再由最后一帧播放到第一帧,如此循环
  swingLoopPlay (frameSections = this.frames) {
    this._play(frameSections, this.currentIndex).then((res) => {
      if (!this.stopLoop) {
        this.currentIndex = 0
        frameSections.reverse()
        this.swingLoopPlay(frameSections)
      }
    })
  }
  /**
   * 销毁资源,需谨慎使用
   */
  disposeResource () {
    this.stopAllFrameAnimation()
    for (let i = 0; i < this.frames.length; i++) {
      this.frames[i] = null
    }
    this.frames = null
    this.canvas = this.ctx = null
  }
}

export default FrameAnim

CSS3 动画

css动画的原理是通过transition属性或@keyframes/animation定义元素在动画中的关键帧,以实现渐变式的过渡。

css动画有以下特点:

优点

  • CSS动画实现比较简单
  • CSS动画执行与JS主线程无关,例如在Chromium里,css动画运行在compositor thread线程中,即使你js线程卡住,css动画照常执行
  • 强制使用硬件加速,能有效利用GPU

缺点

  • 只能操作DOM或XML对象的部分属性
  • 动画控制能力薄弱,不能逐帧定义动画状态
  • 支持的缓动函数有限(CSS3动画的贝塞尔曲线是一个标准3次方曲线)
  • 滥用硬件加速也会导致性能问题

前端动画卡顿的原因

丢帧:浏览器绘制某一帧的时长超过了平均时长(帧超时),为了完成整个动画不得不丢弃后面的动画帧,造成丢帧现象。画面就出现了所谓的闪烁,卡顿。

导致帧超时的原因有很多,最主要的原因是layout、paint带来的性能开销:

无论是JS动画,还是CSS动画,在操作元素的某些样式(如height,width,margin,padding),会触发layout和paint,这样每一帧就会产生巨大的性能开销,相反,使用transform属性则不会,具体哪些属性能触发可以参考CSS Trigers,总之,我们应尽可能使用影响小的属性(transform,opacity)来做动画。

如果采用的是基于图片切换的帧动画技术,请确保所有图片预加载完毕,且用cacheImgs数组缓存所有图片资源到内存中,否则也会出现卡顿现象。

layout: 浏览器会对这些元素进行定位和布局,这一步也叫做reflow或者layout。

paint: 浏览器绘制这些元素的样式,颜色,背景,大小及边框等,这一步也叫做repaint。

composite: 然后浏览器会将各层的信息发送给GPU,GPU会将各层合成;显示在屏幕上。

如何选择最适合的动画技术

随着现代web技术的发展,无论是CSS动画还是JS动画,性能瓶颈越来越小,我们只要选择适合业务需要的技术,一样能创作出丝滑般顺畅的web动画。如果实在无法选择,看下图(仅供参考):

AnimationSelect

一般来说,动画性能优劣如下所示:

JS+Canvas > CSS + DOM > JS + DOM

这里是一个动画技术比较的Codepen Demo

动画缓动函数

  • Linear:无缓动效果
  • Quadratic:二次方的缓动(t^2)
  • Cubic:三次方的缓动(t^3)
  • Quartic:四次方的缓动(t^4)
  • Quintic:五次方的缓动(t^5)
  • Sinusoidal:正弦曲线的缓动(sin(t))
  • Exponential:指数曲线的缓动(2^t)
  • Circular:圆形曲线的缓动(sqrt(1-t^2))
  • Elastic:指数衰减的正弦曲线缓动
  • 超过范围的三次方缓动((s+1)t^3 – st^2)
  • 指数衰减的反弹缓动

缓动函数的实现可参考Tween.js

前端绘图技术 VS 前端动画技术

前端绘图技术通常指以HTML5为代表的(canvas,svg,webgl等)2D、3D图形绘制技术。它和前端动画之间没有包含与被包含的关系,更不能将它们 混为一谈,只有两者的有机结合才能创建出炫酷的UI界面。

参考链接

深入浏览器理解CSS animations 和 transitions的性能问题

前端性能优化之更平滑的动画

CSS vs JS动画:谁更快?

一篇文章说清浏览器解析和CSS(GPU)动画优化

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant