前言

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 0.3s cubic-bezier(0, 0.66, 0.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 0.3s;

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

1
2
transform: translate3d(10px, 0, 0);
transition: transform 0.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
2
3
4
5
6
7
8
9
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
5
6
7
8
9
10
11
12
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
13
14
15
16
17
18
19
20
21
22
23
24
@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
7
8
9
10
11
12
13
14
15
<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
56
57
58
<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 的束缚,我们可以放心大胆的使用各种新特性,实现各种酷炫的动画,一个恰到好处的动画,能给网页增色不少,当然我认为动画的实现手段都是次要的,最主要的是想象力和创造力,你说是吗?

参考资料