CD's blog CD's blog
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

CD_wOw

内卷的行情,到不了的梦
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基础原理及工具方法

  • Babel

  • WebComponent

  • 专项知识汇总

    • 图片类型、优化、处理知识点汇总
    • 如何统一 npm 包管理
    • JS如何做请求并发限制
    • JS如何做实现动画序列播放
      • CSS动画
        • transition
        • animation
        • js实现
        • 小结
      • 动画序列库实现
        • 具体实现
      • 相关资料
    • dotenv如何实现env环境变量注入
  • JavaScript笔记
  • 专项知识汇总
CD
2022-10-30
目录

JS如何做实现动画序列播放

提示

前言:我们知道,一般的浏览器的刷新率为60Hz,也就是说,1秒钟就会刷新60次,相当于大概每过16.6ms浏览器会渲染一帧画面。我们想看到流畅连贯的动画,我们一般希望动画间隔时间为16.6ms。 当我们想要实现下面这个动画,可以有以下几种方法 实现模块滑动 (opens new window)

# CSS动画

# transition

transition的使用相当的简单,但其也有着极大的限制性,即其本身没法在网页加载时自动发生,需要事件触发,其次它是一次性除非再次触发不然不能重复发生,再者它不能定义中间状态,而且一条语句也只能只能定义一个属性的变化,例:

.trans {
  transition: left 5s linear 0s;
}
1
2
3

# animation

animation相比transtion强大了不少,其可以通过js控制动画的开始、暂停,起始、结束位置,也可以定义中间状态

.ani{
  animation: left 5s linear;
}

@keyframes left {
  from {transform: translateX(0px)}
  to {transform: translateX(300px)}
}
1
2
3
4
5
6
7
8

# js实现

通常我们js实现滑动等动画,都会采用循环或是计时器的方式来进行。例如:

const box = document.querySelector('.box');
let boxLeft = 0;
const animation = () => {
  boxLeft+=1;
  box.style.left = boxLeft + 'px';
  if(boxLeft<=300) {
    setTimeout(animation,16.6);
  }
}
1
2
3
4
5
6
7
8
9

# 小结

JS相比CSS而言,CSS动画我们是定义不同时间的状态,然后它进行补间动画,而JS实现为了保证其过程流畅,是帧动画,CSS可以实现简单的动画效果,但对于一些复杂的运动,则需要通过JS进行呈现。对于动画的卡顿,浏览器为了提升动画的性能,为了在动画的每一帧的过程中不必每次都重新绘制整个页面。在特定方式下可以触发生成一个合成层,合成层拥有单独的 GraphicsLayer。需要进行动画的元素包含在这个合成层之下,这样动画的每一帧只需要去重新绘制这个 Graphics Layer 即可,从而达到提升动画性能的目的。

那么一个元素什么时候会触发创建一个 Graphics Layer 层?从目前来说,满足以下任意情况便会创建层:

  • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
  • 硬件加速的插件,比如 flash 等等
  • 使用加速视频解码的
  • 3D 或者 硬件加速的 2D Canvas 元素
  • 3D 或透视变换(perspective、transform) 的 CSS 属性
  • 对自己的 opacity 做 CSS 动画或使用一个动画变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素

通常来说,我们希望我们的动画得到 GPU 硬件加速,所以我们会利用类似 transform: translate3d() 这样的方式生成一个 Graphics Layer 层。Graphics Layer 虽好,但不是越多越好,每一帧的渲染内核都会去遍历计算当前所有的 Graphics Layer ,并计算他们下一帧的重绘区域,所以过量的 Graphics Layer 计算也会给渲染造成性能影响。

# 动画序列库实现

在我们日常需求库的过程中,我们难免会遇到一些需求,它要求A元素在n秒后向上移动后再向左滑行等进行多段动画的播放,这类相对复杂的运动通常我们会想到用动画序列的方法来处理。所谓的动画序列,也就是说可以在上一段动画播放结束之后进行下一段动画的播放,这样可以方便用多段动画实现各种不同的复杂效果。而我们不难想到,要实现这个目的,将动画接口实现成 Promise 是一个非常好的方案 例:

var a1 = new Animator(1000,  function(p){
  var tx = 500 * p;
  box.style.transform = 'translateX('
    + tx + 'px)';
});

var a2 = new Animator(1000,  function(p){
  var ty = 400 * p;
  box.style.transform = 'translate(100px,'
    + ty + 'px)';
});

var a3 = new Animator(1000,  function(p){
  var tx = 300 * (1-p);
  box.style.transform = 'translate('
    + tx + 'px, 100px)';
});

var a4 = new Animator(1000,  function(p){
  var ty = 200 * (1-p);
  box.style.transform = 'translateY('
    + ty + 'px)';
});


box.addEventListener('click', async function(){
  await a1.animate();
  await a2.animate();
  await a3.animate();
  await a4.animate();
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

我们可以看到,一个物体的运动通过async await的加持下,其序列运动就变得非常简单。下面我们通过了解animator库,来实现这样的promise动画的实现。

# 具体实现

在这里,我们用到了requestAnimationFrame(请求动画帧),它将告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。window.requestAnimationFrame(fn)我们只需要传入回调函数fn,它则会返回一个一个整数请求 ID 用于取消动画的执行,当下次重绘时去执行传入的fn函数

首先,我们需要对其使用的方法进行polyfill,当然自带babel插件的话可以让其帮忙做这件事。

function nowtime(){
  if(typeof performance !== 'undefined' && performance.now){
    return performance.now();
  }
  return Date.now ? Date.now() : (new Date()).getTime();
}

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }
 
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
 
    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  1. 这里兼容了requestAnimationFrame,当浏览器不支持时使用定时器来代替其功能。
  2. 时间的获取方面,对于requestAnimationFrame,它回调的参数 timestamp 是一个 DOMHighResTimeStamp 对象,它比 Date 的计时要更精确(可以精确到纳秒)。因此获取时间我们优先使用 performance.now(),如果浏览器不支持 performance.now(),我们再降级使用 Date.now()。

接下来我们看看具体实现:

class Animator {
  constructor(duration, update, easing) {
    this.duration = duration
    this.update = update
    this.easing = easing
  }
  animate(startTime) {
    startTime = startTime || 0

    const duration = this.duration,
      update = this.update,
      easing = this.easing,
      self = this

    return new Promise(((resolve, reject) => {
      let qId = 0
      function step(timestamp) {
        startTime = startTime || timestamp
        const p = Math.min(1.0, (timestamp - startTime) / duration)

        update.call(self, easing ? easing(p) : p, p)

        if(p < 1.0) {
          qId = requestAnimationFrame(step)
        } else {
          resolve(startTime + duration)
        }
      }

      self.cancel = function () {
        cancelAnimationFrame(qId)
        update.call(self, 0, 0)
        resolve(startTime + duration)
      }
      qId = requestAnimationFrame(step)
    }))
  }
  ease(easing) {
    return new Animator(this.duration, this.update, easing)
  }
}

module.exports = Animator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

Animator 构造的时候可以传三个参数:

  1. 第一个是动画的总时长(duration)
  2. 第二个是动画每一帧的 update 事件,在这里可以改变元素的属性,从而实现动画
  3. 第三个参数是 easing, 也就是动画运动的贝塞尔曲线。

其中第二个参数 update 事件回调提供两个参数,一是 ep,是经过 easing 之后的动画进程,二是 p,是不经过 easing 的动画进程,ep 和 p 的值都是从 0 开始,到 1 结束。我们从而可以通过时间的进程去改变运动的轨迹。

Animator 有一个 animate 的对象方法,它返回一个 promise,当动画播放完成时,它的 promise 被 resolve,使用者还可以在 promise resolve 前调用 cancel 方法,这样它的 promise 会被 reject。

Animator搭配上 async/await 代码,时序动画的实现简洁且优雅,扩展性强,是后续实现复杂动画的优选。

# 相关资料

  • requestAnimationFrame 规范 (opens new window)
  • easing (opens new window)
  • animator (opens new window)
编辑 (opens new window)
#JavaScript
上次更新: 2022/12/11, 20:19:48
JS如何做请求并发限制
dotenv如何实现env环境变量注入

← JS如何做请求并发限制 dotenv如何实现env环境变量注入→

最近更新
01
gsap动画库学习笔记 - 持续~
06-05
02
远程组件加载方案笔记
05-03
03
小程序使用笔记
03-29
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CD | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式