前言

16年年底入坑前端, 原因就是被网页中各种炫酷的动画震撼到了, 想着我要是也能自己写出那些炫酷的动画就好了,
当然动画看起来只是简单的平移旋转, 放大缩小, 其实知识点还是不少, 今天闲来无事, 就来回顾一下我在前端世界中接触到的那些动画. 本人水平有限, 本文参考了很多大神的文章, 结合了自己一些见解, 可能会有一些错误的表述,
欢迎指正!

浏览器架构

对于前端动画, 浏览器基本是我们的唯一舞台, 自然需要了解一下相关基础知识:

首先浏览器是一个多线程多进程的架构,

线程:

  • JS引擎线程: 调用V8处理js相关脚本
  • GUI线程: 负责解析网页, 处理 HTML, CSS, 计算dom元素的样式和位置, 最后渲染出来

进程:

  • Browser process: 负责协调其他线程, 可以当做 ReactRoot 组件,

2020-08-12-21-27-13-2020812212713

主要负责地址栏, 书签栏, 前进后退按钮, 网络请求等通用部分的工作

  • Renderer Process : 一个tab内的网页渲染, 打开一个tab 就新开一个子进程, 超过一定的数量就会合并, 从这里可以看出, 为啥开很多个tab页, 会很卡, 因为占了很多的内存, 当然有 类似 One Tab 的这种插件帮你优化
  • GPU Process : 负责3D绘制, 对, 我们常说的 transform GPU 加速 就是它的工作

我们可以点击浏览器右上角 三个点 -> 更多工具 -> 任务管理器 可以看到浏览器的一个进程情况

2020-08-12-21-39-33-2020812213933

这里只做一个引入, 详细的知识可以看这篇文章, 写的非常好. 对于本文, 我们需要了解

GUI线程GPU进程 这两个

网页渲染

一个网页, 由三剑客 (html + css + js) 组成

1
2
3
4
5
6
7
8
9
10
<head>
<style>
div {
color: red;
}
</style>
</head>
<body>
<div>skr</div>
</body>

浏览器在拿到 DOM 结构后, 会分成若干个图层, 根据编写的css:

  • 绘制颜色,边框, 颜色, 大小等, 这个叫 重绘(repaint)
  • 计算每个图层节点的样式, 生成对应的位置, 这个叫 重排(reflow)
  • 所以图层完事之后, 浏览器会交给 GPU进程 合并所有图层, 最终显示在页面上

重绘和重排会涉及到重新计算, 相对来说很耗费浏览器性能, 在手机上更明显, 这里就引出了第一个前端动画需要注意的点, 动画无非就是对 DOM 进行 缩放, 平移, 放大 等操作,
这些理论上会触发重绘和重排, 会造成卡顿, 所以想让动画很流畅, 我们要使用 不影响文档流, 不造成重绘重排的属性, 也就是 transform3dwill-change 属性, 使用了这两个属性, 可以让当前 dom 生成一个独立的图层, 从而不影响其他图层, 同时触发 GPU加速.

过渡效果

一个状态到另一个状态, 比如将元素位置向右移动20px, 如果没有过渡效果, 立即生效, 这样是没有动画效果的, iOS 系统之所有流畅, 不 “卡”, 除了强大的处理器之外,
还有一个很重要的原因就是动画, 动画自然且舒服, 给人一种很流畅的感觉, 所以一个合理的过渡效果很重要, 在 CSS3, 我们通过 transition 属性 指定元素的过渡效果,

CSS3 内置了几种过渡效果:

2020-08-14-15-12-59-202081415130

那么如果不想用内置的, 想自定义一下, 比如想模拟物体撞击反弹, 这时候就需要用到 贝赛尔曲线,

贝塞尔曲线有四个任意点构成一条曲线, CSS3 的表示如下:

1
transition: all .3s cubic-bezier(0,.66,.31,1.18)

对应的效果如下

gif

当然, 可以看到我这里的自定义贝塞尔曲线有点不自然, 我们使用过渡效果应该考虑现实世界中真实物体的运动规律, 不然就会很鬼畜.

动画的不同种类

讲到这, 好像才点题了, 没办法, 说一下相应的知识点便于我们更好的理解, 这里我结合实际例子, 结合我看到的网上各路大神的文章, 讲解下不同的动画种类的应用, 不讲解太详细的代码, 讲一下大致的思路

平移动画 (轮播图)

平移大概有三种方式

1
margin-left: 10px;
1
2
position: absolute;
left: 10px;
1
transform: translateY(10px)

上面三种代码都是往右平移10px, 但是前两种会触发重排, 性能没有第三种好, 所以第三种, 也是主流的一种平移方式, 当然, 想让元素有动画的效果, 还需要加上 transition,
告诉元素的过渡时间, 这个基本接触过前端的都知道

1
2
transform: translateY(10px);
transition: transform .3s;

我们还可以改写成这样, 触发 GPU 3d加速

1
2
transform: translate3d(10px, 0, 0);
transition: transform .3s;

而轮播图的原理就是将所有元素并排在一起, 依次向左或向右平移

2020-08-12-22-38-07-202081222387

gif

缩放动画 (封面图)

缩放大概有两种方式

1
2
width: 150%;
height: 150%;
1
transform: scale(1.5)

同理, 第一种也会触发重排, 所以我们通常使用第二种 transform 的方式

gif

gif

旋转动画 (音乐播放器专辑封面)

旋转我们可以指定按照水平还是X,Y轴旋转, 比较简单, 没啥好说的

1
2
3
transform: rotate(20deg);
transform: rotateX(20deg);
transform: rotateY(20deg);

旋转动画比较常见的是在Web端的音乐播放器, 有一个一直旋转的封面图,
另一个场景也比较常见, 就是 loading 加载中, 原理都一样, rotate 转就完事了

1
2
3
4
5
6
7
8
9
10
11
12
@keyframes rotate {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}

.cover {
animation: rotate 30s linear infinite;
}

gif

自定义组合动画

自定义动画我们使用 CSS3animation 或者 jsanimate api, 这里我们以 CSS3animation 为例, 文字从屏幕右边飞过来, 并且放大,

首先使用 @keyframes 声明动画的执行规则, 然后 使用 animation: [animateName] 执行动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@keyframes test {
0% {
transform: translateX(2000px);
}

50% {
transform: translateX(0);
}

100% {
transform: scale(1.2);
}
}

div {
width: 100px;
height: 100px;
animation: test 2s forwards;
}

gif

渐变动画 (按钮点击水波效果)

CSS3 中常见的渐变(Gradients) 有两种

  • 线性渐变(Linear Gradients)- 向下/向上/向左/向右/对角方向
  • 径向渐变(Radial Gradients)- 由它们的中心定义

线性渐变比较常见的应用之一就是进度条

1
background: repeating-linear-gradient(-45deg, #FAFAFA 25%, #31c27c 0, #31c27c 50%, #FAFAFA 0, #FAFAFA 75%, #31c27c 0);

2020-08-12-23-14-36-2020812231436

径向渐变一个比较炫酷的例子, 在很多组件库会见到, 鼠标点击按钮时, 会有水波一样的效果, 核心代码如下

1
2
3
4
5
6
7
position: absolute;
width: 100%;
height: 100%;
left: var(--x, 0);
top: var(--y, 0);
background: radial-gradient(circle, #fff, 10%, transparent 10.1%) no-repeat 50%;
transform: translate(-50%, -50%) scale(10);

gif

具体细节可以查看张鑫旭大神的文章:

滤镜动画 (霓虹灯)

现在的相机都提供了很多花哨的滤镜, CSS3 也不例外, 我们通过 filter: [value] 指定滤镜, 场景的滤镜有:

https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter

1
2
3
4
5
6
7
8
9
10
filter: blur(5px);  // 高斯模糊
filter: brightness(0.4); // 亮度
filter: contrast(200%); // 对比度
filter: drop-shadow(16px 16px 20px blue); // 投影
filter: grayscale(50%); // 灰度
filter: hue-rotate(90deg); // 色调
filter: invert(75%); // 反相
filter: opacity(25%); // 透明度
filter: saturate(30%); // 饱和度
filter: sepia(60%); // 褐色

如果只想改变元素的背景区域, 可以使用 backdrop-filter, 属性值一样

比较常见的是 blurgrayscale, 前者可以用来做 IOS毛玻璃 效果, 当然, backdrop-filter 也是可以的

1
2
3
.mask {
backdrop-filter: blur(5px);
}

2020-08-13-11-35-59-202081311360

grayscale 最典型的例子就是最近的疫情, 国家哀悼日, 很多网站你会发现都变灰了, 其实用的就是这个属性

1
2
3
4
html {
filter: grayscale(100%);
transition: 1s;
}

gif

可以看到, 结合 transition, 我们可以在动态改变 filter 滤镜值的时候, 让它有过渡效果, 从而实现动画.

另外再张鑫旭大神的博客中还看到个好玩的动画, 结合 hue-rotate 滤镜 和 linear-gradient 线性渐变 实现了炫酷的霓虹灯动画

一段普通的文本

1
<h1>李金珂666</h1>

加上一段普通的样式

1
2
3
4
h1 {
font-size: 100px;
background-image: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia)
}

一个杀马特风格的文本就出来了

2020-08-13-11-47-57-2020813114758

接着配合

  • hue-rotate 改变文本的色相
  • -webkit-background-clip: text 裁减掉背景, 就像 PS 的魔棒一样, 保留文字
  • -webkit-text-fill-color: transparent 让文本变镂空, 这样就能看到文字背后的彩色背景

一个酷炫的霓虹灯动画就完成了

1
2
3
4
5
6
7
8
9
10
11
12
@keyframes hue {
from { filter: hue-rotate(0deg); }
to { filter: hue-rotate(360deg); }
}

h1 {
font-size: 100px;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia);
animation: hue 3s linear infinite;
}

gif

不同的滤镜组合还能实现各种酷炫的效果, 更多例子可以点击 这里

SVG动画 (文字描边)

其实 SVG 是很复杂且强大的一个东西, 我自己也只掌握到了一点皮毛, 相信大家接触 SVG 还是因为字体图标, iconfont, 写页面的时候, 会用到, 要么是设计师给你切好了 SVG 的图片, 要么是去网上找, 当做字体库引进来,

下面这段代码

1
2
3
4
5
6
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="37" viewBox="0 0 39 37">
<g fill="none" fill-rule="evenodd">
<path d="M0 0L44 0 44 44 0 44z" transform="translate(-4 -3)"/>
<path fill="#E1E2E3" d="M39.226 27.5v10.665C39.225 39.179 38.355 40 37.283 40H5.943c-.519-.003-1.015-.202-1.38-.551-.364-.35-.567-.821-.563-1.312V15.95h11.742c1.08 0 1.957-.828 1.957-1.85V3h19.574c1.078 0 1.953.842 1.953 1.835V11.8L21.615 28.45l-.012 7.84 8.31.012 9.313-8.802zm1.065-11.741L43 18.31 28.1 32.345l-2.713-.004.004-2.548 14.9-14.034zM4 11.93L14.065 3v8.931H4z" transform="translate(-4 -3)"/>
</g>
</svg>

对应的图标长这样

2020-08-14-10-45-42-2020814104542

当然, 这是设计师用工具生成的 SVG 代码, 我发现身边很多开发也不懂这些代码具体的意思, SVG 是可缩放的矢量图形, 可以很灵活的改变大小, 形状, 还可以添加滤镜, 内置的图形有:

https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Basic_Shapes

  • rect 矩形
  • circle 圆形
  • ellipse 椭圆
  • line 线
  • polyline 多边形
  • path 路径

其实 path 是最有东西的, 可以绘制任意的形状, 所以字体图标生成之类的工具, 就是对 path 的 编辑, 从而生成自定义的形状,

其中 d 属性, 用来描述具体的路径, fill 就相当于 background-color , stroke 表示绘制颜色, stroke-width 表示 绘制粗细, 想象一下就好比你用一支铅笔, 在绘制你自己的图形,

然后我们看到上面示例中有很多神秘代码:

1
d="M39.226 27.5v10.665C39.225 39.179 38.355...

其中大写字母代表 绝对定位, 小写字母代表 相对定位, 可以理解成一个指令, 每个指令具体的含义如下

1
2
3
4
5
6
7
8
9
10
M = moveto 移动到
L = lineto 连接一根线到
H = horizontal lineto 水平连线
V = vertical lineto 垂直连线
C = curveto
S = smooth curveto
Q = quadratic Belzier curve
T = smooth quadratic Belzier curveto
A = elliptical Arc 椭圆的线 贝塞尔曲线
Z = closepath 结束当前路径

每个指令具体的应用可以查看 这篇文章

现在我们就使用 path 写三个字, 做一个描边的动画

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
44
45
46
47
48
49
50
51
52
53
54
55
  <svg>
<path d="
M50 50
L120 50
A15 2 0 1 0 110 30
L105 100
v -50
L 70 85
L141 70
L104 120
v 100
A 1 2 0 0 1 69 204
L31 167
L196 127"
/>
<path d="
M500 50
L 410 136
M457 89
L 610 136
M 445 155
h 70
M 445 175
h 70
M 475 155
v 90
M 457 232
L 428 213
M 520 232
L 538 213
M 405 245
h 140"
/>
<path d="
M780 50
h 90
h -45
v 70
h -50
h 100
h -50
v 80
L 740 233
L 932 157
A 4 3 0 0 1 1036 96
L 980 130
v 30
h 30
v -25
h -30
h 30
v 150
L 945 222"
/>
</svg>

2020-08-14-11-48-23-2020814114824

三个炫酷的大字映入眼帘, 这就是 SVG 的强大, 现在我们让他动起来, 实现一个描边动画,
要实现描边动画, 需要用到两个关键属性

1
2
stroke-dasharray:定义描边的虚线长度,如果提供奇数个,则会自动复制该值成偶数
stroke-dashoffset:定义虚线描边的偏移量(在路径开始的前面,看不到)

将这两个 值 设置为一样的值,且相对大一点, 这时你会发现刚才辛辛苦苦写的字看不见了

1
2
stroke-dashoffset: 2000;
stroke-dasharray: 2000;

最后我们让 他的 dashoffset2000 回到 0, 这样看起来就是一个描边的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
path {
stroke-width: 3px;
stroke-linecap: round;
fill: none;
stroke-dashoffset: 2000;
stroke-dasharray: 2000;
animation: path 10s linear forwards;
}
@keyframes path {
to {
stroke-dashoffset: 0;
}
}

gif

Canvas动画 (帧动画)

canvas 不用多说, 不会还有前端不知道 canvas 吧 ? 不会吧不会吧!

canvas就是一个画布, 我们在上面绘制各种图形和动画, 默认支持 GPU 的加速, 性能比较好, 有 2d context3d context (WebGL), 作为一个带专生, WebGL 需要一些数学功底, 我没怎么接触过
所以就不展开说明了, 这里主要说明下 2d 的 canvas.

众所周知, 动画片的原理就是有一组连续播放的静态图片组成, 每一张图片, 我们叫做一帧, 把每一帧快速播放, 人眼会产生视觉暂留, 从而形成 “动画”

2020-08-14-12-10-41-2020814121042

所以在 canvas 里绘制动画, 需要用到 js 相关的 timer api, 让图片快速 “动起来”

  • setInterval 计时器
  • requestAnimationFrame 根据浏览器的刷新频率自动调整动画的时间间隔

当然, 都 2020 年了, 现在做动画基本都是用 requestAnimationFrame, 性能更好, 动画表现更流畅, 这里用一个简单的帧动画伪代码来演示一下:

前面说了, 我们需要将图片一张一张的绘制在 canvas 上, 需要用到 ctx.drawImage API,

每次绘制将上一次的图片清除, 需要用到 ctx.clearRect

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
<body>
<canvas></canvas>
</body>

<script>
let i = 0;
let rafId;
const canvas = document.querySelector("canvas");
const imageList = [帧动画原始素材]
canvas.width = 400;
canvas.height = 400;
const ctx = canvas.getContext("2d");
drawImage();

function drawImage() {
if (i === 20) {
cancelAnimationFrame(rafId);
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imageList[i], 0, 0);
i++;
rafId = requestAnimationFrame(drawImage);
}
</script>

3D动画 (翻书)

话不多说, 先来一张图感受下, 炫酷就完事了

gif

首先声明结构

1
2
3
<div class="container">
<div class="page">如何与sb相处</div>
</div>
  • container 声明3d舞台
  • page 表示一页书
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
.container {
transform-style: preserve-3d;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0) rotateX(30deg);
background: #dcdcdc;
}

.page {
color: #fff;
background: #396;
font-size: 30px;
animation: rotate 5s forwards;
transform-origin: 0 50%;
width: 300px;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}

@keyframes rotate {
from {
transform: rotateY(0deg);
}

to {
transform: rotateY(-150deg);
}
}

transform-style: preserve-3d 表示当前以 3D 的方式进行过渡, 现在以 rotateY(-150deg) Y轴旋转的时候就有3d的效果了

页面滚动动画

页面滚动动画比较常见, 他不会在网页加载的时候立即执行, 而是等用户滚到一定的位置时触发, 视觉和交互的体验更棒,

Ant Design Landing 网站为例, 滚动到指定区域, 会触发对应的滚动动画, 看起来非常酷炫

gif

原理也比较简单, 我们可以编写想执行动画区域的 css, 写好动画, 监听滚动条高度, 匹配对应的区域的 offsetTop , 如果快到达时, 给当前区域添加 class 即可,

另外一种方案, 使用浏览器的API intersectionObserver 监听目标元素是否在可视范围内

https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

1
2
3
4
5
6
7
8
9
10
var intersectionObserver = new IntersectionObserver(function(entries) {
// If intersectionRatio is 0, the target is out of view
// and we do not need to do anything.
if (entries[0].intersectionRatio <= 0) return;

loadItems(10);
console.log('Loaded new items');
});
// start observing
intersectionObserver.observe(document.querySelector('.scrollerFooter'));

一些优秀的第三方库

  • Animate.css 写好了各种类型的动画css, 粘贴复制即可
  • Ant Motion 一个 React 的 动画库

结语

以上是我个人平时学习或者看到的一些动画的总结, 在微软都放弃IE的今天, 我们再也不用考虑那恶心的浏览器兼容, 和hack, 如果今天还有人问你各种IE的问题, 纯属脑瘫, 没有了IE的束缚, 我们可以
放心大胆的使用各种新特性, 实现各种酷炫的动画, 一个恰到好处的动画, 能给网页增色不少, 当然我认为动画的实现手段都是次要的, 最主要的是想象力和创造力, 你说是吗?

参考资料