• 前端动画技术的研究和比较


    第一次在segmentfault上发文章 :),欢迎评论指正。原文最初发表在 https://github.com/WarpPrism/...

    动画相关概念

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

    前端动画分类

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

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

    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
        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次方曲线)
    • 滥用硬件加速也会导致性能问题

    <!-- 可以通过如下代码强制开启硬件加速:

    .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)动画优化

  • 相关阅读:
    JavaScript—飞机大战
    JavaScript—瀑布流
    JavaScript—原生轮播和无缝滚动
    JavaScript—封装animte动画函数
    JavaScript—offset、client、scroll
    JavaScript—对象创建方式
    JavaScript—var lef const区别
    P1352 没有上司的舞会 题解
    P1829 [国家集训队]Crash的数字表格 / JZPTAB 题解
    P2522 [HAOI2011]Problem b 题解
  • 原文地址:https://www.cnblogs.com/10manongit/p/12789151.html
Copyright © 2020-2023  润新知