前端動畫技術的研究和比較

第一次在segmentfault上發文章 :),歡迎評論指正。原文最初發表在 https://github.com/WarpPrism/...javascript

動畫相關概念

  • 幀:動畫過程當中每個靜止的狀態,每一張靜止的圖片
  • 幀率:刷新頻率,每秒鐘播放的幀數,FPS(frame per second),單位是Hz
  • 幀時長:每一幀停留的時間,如60FPS的動畫幀時長約爲16.7ms,意味着瀏覽器必須在16.7ms內繪製完這一幀
  • 硬件加速:硬件有三個處理器:CPU、GPU和APU(聲音處理器)。他們經過PCI/AGP/PCIE總線交換數據。GPU在浮點運算、並行計算等部分計算方面,明顯高於CPU的性能。硬件加速即利用GPU進行動畫的計算
  • 緩動:最普通的動畫就是勻速的動畫,每次增長固定的值。緩動就是用來修改每次增長的值,讓其按照不規律的方式增長,實現動畫的變化。
  • 瀏覽器的刷新率:一般爲60Hz

前端動畫分類

從控制角度分,前端動畫分爲兩種:css

  1. JavaScript控制的動畫
  2. CSS控制的動畫

JS動畫

JS動畫的原理是經過setTimeout setIntervalrequestAnimationFrame 方法繪製動畫幀(render),從而動態地改變網頁中圖形的顯示屬性(如DOM樣式,canvas位圖數據,SVG對象屬性等),進而達到動畫的目的。前端

多數狀況下,應 首先選用 requestAnimationFrame方法(RAF),由於RAF的原理是會在瀏覽器下一次重繪以前更新動畫,即它的刷新頻率和瀏覽器自身的刷新頻率保持一致(通常爲60Hz),從而確保了性能。另外RAF在瀏覽器切入後臺時會暫停執行,也能夠提高性能和電池壽命。(來自MDNjava

// 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動畫原理:git

/**
 * 基於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
    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定義元素在動畫中的關鍵幀,以實現漸變式的過渡。github

css動畫有如下特色:web

優勢canvas

  • CSS動畫實現比較簡單
  • CSS動畫執行與JS主線程無關,例如在Chromium裏,css動畫運行在compositor thread線程中,即便你js線程卡住,css動畫照常執行
  • 強制使用硬件加速,能有效利用GPU

缺點segmentfault

  • 只能操做DOM或XML對象的部分屬性
  • 動畫控制能力薄弱,不能逐幀定義動畫狀態
  • 支持的緩動函數有限(CSS3動畫的貝塞爾曲線是一個標準3次方曲線)
  • 濫用硬件加速也會致使性能問題

<!-- 能夠經過以下代碼強制開啓硬件加速:數組

.anim-cube {
  z-index: 9999; // 指定z-index,儘量高
}
.anim-cube {
  transform: translateZ(0);
  /* 或 */
  transform: translate3d(0, 0, 0);
}

前端動畫卡頓的緣由

丟幀:瀏覽器繪製某一幀的時長超過了平均時長(幀超時),爲了完成整個動畫不得不丟棄後面的動畫幀,形成丟幀現象。畫面就出現了所謂的閃爍,卡頓。

致使幀超時的緣由有不少,最主要的緣由是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)動畫優化

相關文章
相關標籤/搜索