《 CSS Secrets 》是 @Lea Verou 最新著作,这本书讲解了有关于CSS中一些小秘密。是一本CSSer值得一读的一本书,经过一段时间的阅读,我、@南北和@彦子一起将在W3cplus发布一系列相关的读后感,与大家一起分享。
灵活的过渡和动画效果(如 bounce 的过渡效果)一直是一个流行的效果,给人有一个更好的感觉——在现实生活中,物体从A位置移动到B位置,很少是不灵活的移动。
从技术的角度来看,一个弹跳( bounce )效果是过弹跳效果,一点一点弹跳,然后再到达最终值,在这个过程中会缩小一次或多次,直到它到达终点。例如下图的效果,我们假设是一个球体降落效果,它执行的就是 translateY 值从无过渡到 translateY(350px) 。
为什么使用 transform 而不使用像 top 或者 margin-top 这样的CSS属性呢?那是因为使用 transform 动画是平滑的,而其他CSS属性通常是用来对象边界的。
当然, bounce 效果不仅是改变位置的运动。几乎可以在过渡中改变 transform 任何类型的属性,主要包括:
有很多JavaScript库提供了 bounce 动画功能。然而,这些天在思考,不使用脚本来做这个动画。那么使用CSS实现 bounce 动画效果,最好的实现方法是什么呢?
一直觉得,制作 bounce 这样的弹跳动画效果,要使用到CSS的 @keyframes ,如下所示:
@keyframes bounce { 60%, 80%, to { transform: translateY(350px); } 70% { transform: translateY(250px); } 90% { transform: translateY(300px); }}.ball { /* 省略球体其他样式。 */ animation: bounce 3s;}
上面代码中指定的关键帧相同的步骤类似上图所示。但是你运行这个动画,你会很快发现,这个动画效果看起来很假(根本不像球体弹跳效果)。主要原因之一,每次球体都改变了方向,继续加速,造成看起来非常不自然。主要是因为这些关键帧都采用了相同的时间。
“时间.....这是什么”?你可能会问。其实每一个过渡和动画都与曲线有关, 指定如何随着时间的推移发展 (从其他的文章介绍中,都知道有一个 easing )。如果你不为过渡或动画显式的指定一个时间函数( transition-timing-funciton 或 animation-timing-function ),它会得到的是一个默认值,而不是类似下图所示的一个线性函数。(注意:如下图所示,粉红色点表示一半时间是如何运行,过渡到80%后沿什么路线运行):
默认的时间函数也可以显式的设置为指定的关键词,在 transition 、 animation 简写中或者 transition-timing-function 、 animation-timing-function 默认的时候函数值都是 ease 。但是,由于 ease 是默认的计时功能,其作用并不是很有用。其实在CSS中提供了其他四种预设的典线函数用来改变动画的运动方式,如下图所示:
对于预定的时间函数可以使用这些关键字。
正如你所看到的, ease-out 和 ease-in 刚好相反。这正是我们所要的 bounce 效果:我们想要每次改变方向时只需要扭转时间函数即可。因此,我们可以给关键动画指定一个主要时间函数来覆盖默认的时间函数。还有如果我们想给主要方向加速时,可以给时间函数设置 ease-out ,给另外一个方向设置降速函数 ease-in 。
@keyframes bounce { 60%, 80%, to { transform: translateY(400px); animation-timing-function: ease-out; } 70% { transform: translateY(300px); } 90% { transform: translateY(360px); }}.ball { /* 这里省略其他样式 */ animation: bounce 3s ease-in;}
如果你运行这个代码,你将看到,即使这里只做了一个简单的变化,其结果更像一个真实的弹跳动画效果。但是只用这五个时间函数对我们限制太大了。如果可以选择任意时间函数,我们的动画效果是不是更为真实。例如,如果 bounce 动画是一个下降的物体,在下降过程有一个更快的加速度(比如提供是 ease )将创建一个更为真实的物体下降的真实动画。但是,在没在关键词时,我们如何创建一个与 ease 相反的时间函数呢?
其实这五个关键词时间函数都是三次贝塞尔曲线(Bézier曲线)。Bézier曲线和你平时制作图工具中的曲线路径工具类似(比如:Adobe Illustrator)。他们身上有很多路径段,每段有两个控制曲线率的把手(通常把这个把手称为控制点)组成。赛格等复杂曲线包括曲线段和控制点,如下图所示:
而CSS的时间函数的Bézier曲线只有一段,因此他们有两个控制点。拿 ease 时间函数对应的Bézier曲线图来做示例:
除了五个预定的时间函数之外,接下来我们将一起讨论另一个时间函数: cubic-bezier() ,它允许我们指定一个自定义的时间函数。它接受四个参数,就是两个控制点的坐标参数,创建Bézier曲线可以根据这样的形式来创建: cubic-bezier(x1, y1, x2, y2) ,其中 (x1,y1) 是指典线的第一个控制点坐标, (x2,yx) 是第二个控制点坐标。曲线的起点是固定在 (0,0) 位置,表示计时开始(即零时间,零进展),结束点位置在 (1,1) (表示 100% 的运行时间, 100% 的进展)。
注意,限制一段曲线的端点并不仅仅是固定的一个。两个控制点的 x 值限制在 [0,1] 区间(即,我们不能把控制点移动多图的水平之外)。这种限制并不是任意的。我们不能穿越时间,不能指定过度的触发时间。这里唯一限制的是节点的数量:限制了节点的结果就限制曲线的结果,这也使用 cubic-bezier() 函数的使用变得更为简单。尽管受到这些限制, cubic-bezier() 允许我们创建一个非常多样化的时间函数。
从逻辑上而言,我们可以通过把时间函数的水平和垂直坐标的控制点对外而获取反转的时间函数。我们使用的时间函数关键字,也可以使用 cubic-bezier() 对应值。例如, ease 函数相当于 cubic-bezier(.25,.1,.25,1) ,其反转的时间函数为 cubic-bezier(.1,.25,1,.25) ,如下图所示:
这种方式,使我们的 bounce 动画效果实现更简单,效果看起来更加逼真:
@keyframes bounce { 60%, 80%, to { transform: translateY(400px); animation-timing-function: ease; } 70% { transform: translateY(300px); } 90% { transform: translateY(360px); }}.ball { /* 省略其他样式 */ animation: bounce 3s cubic-bezier(.1,.25,1,.25);}
使用在线的图形工具,比如 cubic-bezier.com ,我们可以对 cubic-bezier() 函数做进一步的试验,使 bounce 动画效果更完善。
Cubic Bézier 曲线在没有可视化之下,是出了名的难以指定和理解,特别是当它们作为 transition-timing-function 时。幸运的是,有很多在线工具可以帮助我们,比如上面提到的 cubic-bezier.com 。
友情提示:在 Dan Eden 写的 Animation.css 动画库中,时间函数用的就是 cubic-bezier(.215,.61,.355,1) 和 cubic-bezier(.755,.05,.855,.06) ,使动画效果更真实。
假设我们想要给聚焦后的文本展示一个提示信息。模板结构如下所示:
<label> Your username: <input id="username" /> <span class="callout">Only letters, numbers, underscores (_) and hyphens (-) allowed!</span></label>
提示:如果你给提示信息 .callout 使用的是 heihgt 而不是 transform ,你会注意到 .callout 从 height:0 (或其他值)过渡到 height:auto ,并不能正常工作。那是因为在动画中对关键词是不能识别的。在这种情况下,使用 max-height 来替换 height 。
使用CSS来切换显示、隐藏的效果,代码如下(我们省略了一切相关的样式或布局):
input:not(:focus) + .callout { transform: scale(0);}.callout { transition: .5s transform; transform-origin: 1.4em -.4em;}
目前,当用户获取文本焦点时,有 .5s 的过渡时间,来显示提示信息 .callout ,如下图所示:
如果显示信息超过最后一点(例如,提示信息放大到 110% ,然后在回到 100% ),这样的效果看起来更为自然。我们可以把 transition 效果换成 animation 属性,并且把前面所学到的知识运用到这里:
@keyframes elastic-grow { from { transform: scale(0); } 70% { transform: scale(1.1); /* Reverse ease */ animation-timing-function:cubic-bezier(.1,.25,1,.25); }}input:not(:focus) + .callout { transform: scale(0);}input:focus + .callout { animation: elastic-grow .5s;}.callout { transform-origin: 1.4em -.4em;}
如果我们尝试后就知道他是确实能正确的工作。可以的看看下图的效果,看看它与之前的过渡效果对比:
然而,我们需要的是一个过渡效果,但基本上使用了一个动画效果。动画的功能是非常强大,但在这种情况下,需要给过渡添加一些灵活度而使用动画,感觉有点过会,就像是杀鸡焉用牛刀。那么有没有办法改变这一切呢?
解决方案还是使用 cubic-bezier() 时间函数。到目前为止,我们只讨论了曲线的 0-1 之间的控制点。正如前面所提到的,我们不能超出这个水平范围,但是在垂直范围,我们可以超过 0-1 的范围,让我们的过渡进展范围可以小于 0% ,也可以大于 100% 。你可能猜出这是什么意思。他的意思是,如果 transform:scale(0) 过渡到 transform:scale(1) ,我们可以把最终值变得更大,比如 scale(1.1) ,或者更大的值。同时还要依赖于过渡的时间函数。
在这个示例中,我们只需要很少的弹性效果,所以我们希望在时间函数上来实现,其中进展到 110% 对应的是 scale(1.1) ,然后进展到 100% ,对应的就是 scale(1) 。开始点使用 cubic-bezier(.25,.1,.25,1) 时间函数,移动到第二个控制点,直到我们达到的时间函数 cubic- bezier(.25,.1,.3,1.5) 。
正如上图你所看到的:过渡总持续时间到达 50% 这个点时,过渡进展到 100% 。但过渡不能就到这就停止,它需要继续移动到最终值,总持续时间到达 70% 时,过渡进展到 110% ,然后剩下的 30% 的可用时间,让过渡进展到我们需要的最终值,这样就实现了使用 transition 达到 animation 制作的动画效果,而整个实现过程只需要一行代码就可以实现,现在比较一下我们实现用例效果的代码:
input:not(:focus) + .callout { transform: scale(0); }.callout { transform-origin: 1.4em -.4em; transition: .5s cubic-bezier(.25,.1,.3,1.5);}
然而,尽管我们的过渡效果看起来达到了预期的那样,但我们文本失去焦点时, .callout 信息收缩到到消失时,会发生如下图的效果:
这里发生了什么呢?这里看起来有点怪怪的,但这效果实际上就是预期会发生的:当我们在文本域中输入字段, transform 从 scale(0) 过渡到 scale(1.1) ,也就是他的最终值。因此,因为他们使用了相同的时间函数,过渡在 350ms 进展到 110% 。但这一次, 110% 处理的不是 scale(1.1) ,而是 (-0.1) 。
不要放弃,因为解决这个问题只需要增加一行代码。如果我们只想在 .callout 收缩时设置一个普能的时间函数,我们可以通过CSS的规则来覆盖当前时间函数:
input:not(:focus) + .callout { transform: scale(0); transition-timing-function: ease;}.callout { transform-origin: 1.4em -.4em; transition: .5s cubic-bezier(.25,.1,.3,1.5);}
如果你再试一次,你会发现以完全相同的方式关闭弹出的信息框。就像前面定制的 cubic-bezier() 时间函数,但他打开是有一个很好的弹性动画效果。
大家需要特别注意的是:关闭 .callout 感觉非常缓慢。那是为什么呢?思考一下,当它不断增长时,动画进展到 50% 是,尺寸达到 100% (也就是 250ms 后)。然而,当它收缩时,从 0% 到 100% 占有了所有的时间 ,我们指定的过渡时间为 500ms ,所以感觉速度慢了一半。
解决最后一个问题,我们可以覆盖时间,通过 transition-duration 或使用 transition 来覆盖。如果我们使用后者,我们不需要显式指定,它为它是初始值:
input:not(:focus) + .callout { transform: scale(0); transition: .25s;}.callout { transform-origin: 1.4em -.4em; transition: .5s cubic-bezier(.25,.1,.3,1.5);}
虽然弹性过渡可以运用在任何类型的过渡上,但这也是一个可怕的想法。典型的情况,你不希望弹性的对颜色做过渡。尽管弹性的给颜色做过渡效果很有意思,如下图所示,但这通常是不可取的UI方案。
使用 cubic-bezier(.25,.1,.2,3) 过渡函数,把颜色 rgb(100%, 0%, 40%) 弹性过渡到 gray (rgb(50%, 50%, 50%)) 。整个过程会篡改 rgb 颜色,所以我们看到一些奇怪的颜色,比如 RGB(0%、100%、60%) 。
为了防范意外的将颜色做了弹性过渡,可以尝试指定特定的属性,而不是像以前一样不指定任何属性。当我们使用简写的 transition 属性, transition-property 其默认值为 all 。这意味着任何能转过渡的属性都将过渡。因此,如果我们在 .callout 上添加一个背景颜色,那么在过渡效果中也会运用到这个属性。那么最后的代码如下所示:
input:not(:focus) + .callout { transform: scale(0); transition: .25s transform;}.callout { transform-origin: 1.4em -.4em; transition: .5s cubic-bezier(.25,.1,.3,1.5) transform;}
提示:说到限制对指定属性过渡,你甚至可以通过 transition-delay 对多个属性进行过渡,也可以使用 transition 简写。例如,如果你想对 width 和 height 两个属性做过渡,你可以这样写 transition: .5s height, .8s .5s width; ,其中 width 的延迟时间和 height 持续时间相同。