【译】如丝般顺滑-使用css3实现60帧的动画

原文: Smooth as Butter: Achieving 60 FPS Animations with CSS3

在移动端上实现动画很简单。

如果采取我们的建议的话,在移动端正确的实现动画也会比较容易。

虽然现在很多人在手机上运用CSS3动画,但许多人用的都不够恰当。很多应加以考虑的最佳实践常常被忽略,因为仍然有人不明白这些最佳实践的真正意义。

如今有这么多的设备规范,如果还不有针对性地优化你的代码,糟糕的用户体验将让你死无葬身之地。

记住:虽然市场上始终有一些高端的旗舰机在挑战性能极限,但你面对的仍将是和这些性能怪兽相比只是玩具一样的低端设备。

我们想帮助你正确地驾驭 CSS3。首先先要了解几件事。

理解时间轴

当渲染和处理HTML元素时,浏览器做了什么?这个非常简单的时间轴我们称之为 关键渲染路径

想要达到流畅的动画效果,我们需要关注修改属性会对 composite (合成)阶段造成怎样的影响。而不是去关注前面的其他阶段。

1. Styles

浏览器开始计算样式以应用在元素上——重新计算样式

2. Layout

接下来,浏览器会开始为每个元素生成用于布局的形状和位置信息。在该步骤浏览器会设置的页面属性包括 widthheight,还有margin,以及left/top/right/bottom 等。

3. Paint

在该步骤,浏览器开始用像素渲染填充每个元素,此时用到的属性有 box-shadown, border-radius,color, background-color 等。

4. Composite

这步就是你施展拳脚的地方了,浏览器开始在屏幕上渲染所有的元素。

现代浏览器可以使用transformopacity属性很好的实现四种动画。

  • 位置 —— transform: transformX(n) transformY(n) translateZ(n);
  • 缩放 —— transform: scale(n);
  • 旋转 —— rotate(ndeg);
  • 透明 —— opacity:n;

如何达到每秒60帧

想法有了,可以撸起袖子干活了。

首先从HTML开始,创建一个简单的结构,把类名为app-menu的元素放在一个类名为layout的元素中。

<div class="layout">
    <div class="app-menu"></div>
    <div class="header">
        <div class="menu-icon"></div>
    </div>
</div>

用错误的方法来实现这个效果

1
2
3
4
5
6
7
8
9
.app-menu {
left: -300px;
transition: left 300ms linear;
}

.app-menu-open .app-menu {
left: 0px;
transition: left 300ms linear;
}

看见我们这些我们修改的属性了吗?我们应该避免使用left/top/right/bottom这些属性作为动画。这些属性不能实现流畅的动画,因为他们会让浏览器每次都创建布局,而这会影响他们的子元素。

这样做的结果大概是这样的:

这个动画一点都不流畅。我们使用开发者工具的时间轴来看看发生了什么,结果如下:

这清楚地显示了FPS是不平整的并且表现也很慢。

绿色的条代表FPS,高的条表示动画渲染的帧数达到60FPS。低的条表示小于60帧。所以理想情况下,你想让绿色的长条贯穿整个时间轴。红色的条代表着糟糕的性能,所以另一方面评估程序的方法是消除这些红色的条。

使用 Transform

1
2
3
4
5
6
7
8
9
10
.app-menu {
-webkit-transform: translateX(-100%);
transform: translateX(-100%);
transition: transform 300ms linear;
}
.app-menu-open .app-menu {
-webkit-transform: none;
transform: none;
transition: transform 300ms linear;
}

transform属性影响Composite(合成)属性。这里我们告诉浏览器布局将被渲染并且已经准备好,当动画开始时。所以让渲染动画时卡顿更少。

时间轴的显示如下:

结果变的好点了,FPS更加规律了,因此动画更顺畅了。

使用GPU运行动画

让我们来上升一个等级,准备好让动画变得如丝般顺滑,我们开始使用GPU来渲染动画。

1
2
3
4
5
6
.app-menu {
-webkit-transform: translateX(-100%);
transform: translateX(-100%);
transition: transform 300ms linear;
will-change: transform;
}

虽然一些浏览器仍然需要 translateZ()translate3d() 作为备选方案,但will-change 被广泛支持已经是势不可挡了。它的功能是把元素提升到另一个层中,这样浏览器就不必关心布局渲染或者绘制了。

看见有多顺畅了吗? 时间轴目前是这样的:

动画的FPS更稳定了,渲染也更快了。但是有一帧仍然渲染得很久。在开始处还有一点点瓶颈。

还记得开始时创建的HTML结构吗?让我们来通过JavaScript来控制类名为app-menu的元素。

1
2
3
4
5
6
7
8
9
10
function toggleClassMenu() {
var layout = document.querySelector(".layout");
if (!layout.classList.contains("app-menu-open")) {
layout.classList.add("app-menu-open");
} else {
layout.classList.remove("app-menu-open");
}
}
var oppMenu = document.querySelector(".menu-icon");
oppMenu.addEventListener("click", toggleClassMenu, false);

这儿的问题是给布局中的div元素增加了类名,这样使得浏览器多了一次重新计算样式的步骤,因此影响了渲染表现。

如丝般顺滑的60帧动画解决方案

如果我们在视窗外的区域创建menu元素呢?在脱离主区域的地方这么做,可以确保影响仅限于你想赋予动画的元素。

因此,我们打算使用下面的HTML结构:

1
2
3
4
5
6
7
8
<div class="menu">
<div class="app-menu"></div>
</div>
<div class="layout">
<div class="header">
<div class="menu-icon"></div>
</div>
</div>

现在我们可以以略微不同的方式来控制菜单的状态。我们可以通过使用JavaScript的transitionend方法,在动画结束时移除类名来控制动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function toggleClassMenu() {
myMenu.classList.add("menu--animatable");
if(!myMenu.classList.contains("menu--visible")) {
myMenu.classList.add("menu--visible");
} else {
myMenu.classList.remove('menu--visible');
}
}

function OnTransitionEnd() {
myMenu.classList.remove("menu--animatable");
}

var myMenu = document.querySelector(".menu");
var oppMenu = document.querySelector(".menu-icon");
myMenu.addEventListener("transitionend", OnTransitionEnd, false);
oppMenu.addEventListener("click", toggleClassMenu, false);
myMenu.addEventListener("click", toggleClassMenu, false);

我们来整理一下代码然后检查下结果。

下面是完整的CSS3代码示例:

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
59
60
61
62
63
64
.menu {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 150;
}

.menu--visible {
pointer-events: auto;
}

.app-menu {
background-color: #fff;
color: #fff;
position: relative;
max-width: 400px;
width: 90%;
height: 100%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
-webkit-transform: translateX(-103%);
transform: translateX(-103%);
display: flex;
flex-direction: column;
will-change: transform;
z-index: 160;
pointer-events: auto;
}

.menu--visible .app-menu {
-webkit-transform: none;
transform: none;
}

.menu--animatable .app-menu {
transition: all 130ms ease-in;
}

.menu--visible.menu--animatable .app-menu {
transition: all 330ms ease-out;
}

.menu:after {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
will-change: opacity;
pointer-events: none;
transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1);
}

.menu--visible.menu:after {
opacity: 1;
pointer-events: auto;
}

那么时间轴表现如何呢?

是不是如丝般顺滑呢?

翻译总结:

目前在给手机页面通过CSS3实现简单动画时,一般很少考虑流畅问题。因为在开发阶段使用的PC机性能都极高,很难主要到这一点。如我目前使用的Mac Pro(i7,16g)。即便使用文中提到的错误示例(第一种方案,通过left控制动画),通过审查工具分析时,其帧数依旧如诗般顺滑...,然而应该当考虑到实际的使用场景是移动端,且就目前而言,确实仍有一大批人在只用较为低端的安卓机。所以采用最佳实践很有必要,并且实际使用的动画会比菜单滑动要复杂。所以是否采取最佳实践方案在实际的设备中运行会出现较大差异。

background滤镜效果及高斯模糊

background滤镜效果

场景及实现:父级div嵌套子div,父div有背景图片,子div中有文字,需要实现背景图加黑色半透明遮罩,不影响子div中文字的显示效果。

CSS的filter滤镜效果的Brightness属性可以实现遮罩效果,但是类似于opacity属性,这种效果会影响子div的显示效果:

解决方法:

1
2
3
4
<div id="section1">
<div id="content">
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#section1 {
background: url('../images/bg-img.jpg') center center no-repeat;
background-size: cover;
position: relative;
}
#content {
position: absolute;
}
#section1::before {
content: '';
display: block;
position: absolute;
background-color: #000;
opacity: 0.5;
width: 100%;
height: 100%;
}

效果:

模拟iOS实现高斯模糊效果

使用filter的blur属性实现背景高斯模糊:

1
2
3
4
div {
-webkit-filter: blur(5px); /* Chrome, Safari, Opera */
filter: blur(5px);
}