CSS秘密花园: 灵活的过渡

CSS Secrets》是@Lea Verou最新著作,这本书讲解了有关于CSS中一些小秘密。是一本CSSer值得一读的一本书,经过一段时间的阅读,大漠@南北@彦子一起将在W3cplus发布一系列相关的读后感,与大家一起分享。

CSS Secrets

问题

灵活的过渡和动画效果(如bounce的过渡效果)一直是一个流行的效果,给人有一个更好的感觉——在现实生活中,物体从A位置移动到B位置,很少是不灵活的移动。

从技术的角度来看,一个弹跳(bounce)效果是过弹跳效果,一点一点弹跳,然后再到达最终值,在这个过程中会缩小一次或多次,直到它到达终点。例如下图的效果,我们假设是一个球体降落效果,它执行的就是translateY值从无过渡到translateY(350px)

bounce效果

为什么使用transform而不使用像top或者margin-top这样的CSS属性呢?那是因为使用transform动画是平滑的,而其他CSS属性通常是用来对象边界的。

当然,bounce效果不仅是改变位置的运动。几乎可以在过渡中改变transform任何类型的属性,主要包括:

  • 大小过渡:在元素的悬浮状态:hover,可以把一个元素变得更大,常常通过transform:scale(0)慢慢变大,比如在一个条形图中给其添加一个球体变大的动画效果
  • 角度的旋转: 比如在一个饼图中,扇形从0旋转过渡到360deg的一个动画

有很多JavaScript库提供了bounce动画功能。然而,这些天在思考,不使用脚本来做这个动画。那么使用CSS实现bounce动画效果,最好的实现方法是什么呢?

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-funcitonanimation-timing-function),它会得到的是一个默认值,而不是类似下图所示的一个线性函数。(注意:如下图所示,粉红色点表示一半时间是如何运行,过渡到80%后沿什么路线运行):

时间函数

注意:transitionanimation中默认的时间函数值为ease

默认的时间函数也可以显式的设置为指定的关键词,在transitionanimation简写中或者transition-timing-functionanimation-timing-function默认的时候函数值都是ease。但是,由于ease是默认的计时功能,其作用并不是很有用。其实在CSS中提供了其他四种预设的典线函数用来改变动画的运动方式,如下图所示:

时间函数

对于预定的时间函数可以使用这些关键字。

正如你所看到的,ease-outease-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,你会注意到.calloutheight: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信息收缩到到消失时,会发生如下图的效果:

灵活过渡

这里发生了什么呢?这里看起来有点怪怪的,但这效果实际上就是预期会发生的:当我们在文本域中输入字段,transformscale(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简写。例如,如果你想对widthheight两个属性做过渡,你可以这样写transition: .5s height, .8s .5s width;,其中width的延迟时间和height持续时间相同。

原文链接:https://www.w3cplus.com/css3/css-secrets/elastic-transitions.html

发表评论

登录后才能评论