3. 时间函数
当我们要求一个元素从一个位置过渡到另一个位置时,浏览器需要计算出每个“中间”帧应该是什么样子的。
例如:假设我们正在将一个元素从左移动到右,持续1秒。流畅的动画应该以60帧每秒
的速度运行,这意味着我们需要在起始和结束之间计算出60个单独的位置。
我们先看一个让每个位置都均匀分布的情况:
每个圆圈代表一个时间点。随着圆圈从左到右移动,这些是向用户显示的帧。
在这个动画中,我们使用的是线性(linear
)时间函数。这意味着元素以恒定的速度移动;我们的圆圈每一帧都移动相同的距离。
在CSS
中有几种可用的内置时间函数。我们可以使用transition-timing-function
属性指定要使用的时间函数:
.btn { transition: transform 250ms; transition-timing-function: linear; }
或者,我们可以直接将其传递给transition
简写属性:
.btn { transition: transform 250ms linear; }
除了linear
我们还有其他的选择:
ease-out
ease-out
就像野牛一样冲来,但最后它精力不济。到最后,它就像一只慢慢爬行的乌龟。
我们用坐标轴来描述元素随时间的位移图,它会看起来像这样:
那什么时候会使用
ease-out
?它最常用于某些东西从屏幕外部进入视图(例如,弹窗出现)的情况。它产生了一种事物从远处急速赶来并停在用户面前的效果。
ease-in
ease-in
是ease-out
的反义词。它开始缓慢然后加速
:
正如我们所看到的,ease-out
适用于从屏幕外部进入视图的情况。自然而然,ease-in
适用于相反情况:将某物移出视口边界。
这个组合在某物进入和退出视口时非常有用,比如一个弹窗的显示和隐藏。
ease-in
几乎只用于元素以屏幕外或不可见结束的动画;否则,突然的停止可能会令人不适。
ease-in-out
接下来是ease-in-out
。它是前两个时间函数的组合:
这个时间函数是对称的。它具有相等数量的加速和减速。
这个曲线对于在循环中发生的任何事情都很有用(例如,元素一遍又一遍地淡入和淡出)。
ease
与ease-in-out
不同,它不是对称的;它具有短暂的加速段和大量的减速。
ease
是默认值 —— 如果我们没有指定时间函数,将使用ease
。老实说,这对大多数情况都感觉正确。如果一个元素移动,而不是进入或退出视口,通常ease
是一个不错的选择。
时间是恒定的
关于上面所有的例子需要有一个说明:动画经历的时间是恒定的。时间函数描述了一个值如何在固定时间间隔内从0到1,而不是动画应该多快完成。一些时间函数可能会感觉更快或更慢,但在这些示例中,它们都需要完全1秒来完成。
自定义曲线
如果提供的内置选项不符合我们的需求,我们可以使用三次贝塞尔(cubic bézier)时间函数来定义自己的自定义缓动曲线!
.btn { transition: transform 250ms cubic-bezier(0.1, 0.2, 0.3, 0.4); }
到目前为止,我们所见到的所有值实际上都只是这个cubic-bezier
函数的预设值。它需要4个数字,表示2个控制点。
与此同时,我们可以使用Lea Verou来开始创建自己的贝塞尔时间函数:
一旦我们找到一个满意的动画曲线,点击顶部的Copy
并将其粘贴到我们的CSS中!
我们还可以从这个扩展的时间函数集合中进行选择。不过要注意:其中一些更奇特的选项在CSS中可能无法正常工作。
当我们刚开始尝试使用自定义贝塞尔曲线时,可能很难找到一个感觉自然的曲线。但通过一些实践,这将成为一个非常有表现力的工具。
用三次贝塞尔来表示内置函数
.btn { /* ease-out */ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); /* ease-in */ transition-timing-function: cubic-bezier(0.75, 0, 1, 1); /* ease-in-out */ transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1); /* ease */ transition-timing-function: cubic-bezier(0.44, 0.21, 0, 1); }
这些自定义的时间函数替代方案可以让我们在动画中使用更具表现力的缓动效果。
4. 动画优化
早些时候,我们提到动画应该以60fps
的速度运行。然而,当我们进行计算时,我们意识到这意味着浏览器只有16.6毫秒来绘制每一帧。
如果我们的动画计算开销过大,它将会看起来不流畅,而且会出现卡顿。帧会被丢弃,因为设备无法跟得上。
我们之前写过像素是怎样练成的有关丢帧的介绍。
动画性能是一个庞杂的领域,不在本文的讨论范围内。但让我们挑选几个比较重要的点来简单说说:
- 一些CSS属性比其他属性更耗时。
- 例如,
height
是一个非常耗时的属性,因为它影响布局。当一个元素的高度缩小时,会引发一连串的反应;所有兄弟元素都需要向上移动以填充空间!
- 其他属性,如
background-color
,在进行动画时成本较高。
- 它们不影响布局,但它们需要在每一帧上进行重绘。
- 两个属性 —
transform
和opacity
— 在进行动画时耗时狠少。
- 如果一个动画当前调整了类似
width
或left
这样的属性,通过将其移动到transform
,可以显著改善动画性能。
硬件加速
让我们来看一个小例子:(根据浏览器和操作系统的不同,效果可能不同)
鼠标悬停在我们的Hello World
按钮上,仔细观察字母,它们在过渡的开头和结尾似乎位置发生了偏移。
这是因为计算机的
CPU
和GPU
之间的数据切换导致的。
当我们使用transform
和opacity
来对元素进行动画时,浏览器有时会尝试优化这个动画。它不会在每一帧上将像素光栅化,而是将一切都作为纹理传输到GPU
上。GPU
非常擅长执行这种基于纹理的变换,因此我们得到了非常流畅、性能非常好的动画效果。这被称为硬件加速。
问题在于:GPU
和CPU
以不同的方式呈现事物。当CPU
将其传递给GPU
,反之亦然,就会出现因为数据变更而导致元素位置和样式变化的情况。
我们可以通过添加以下CSS属性来解决这个问题:
.btn { will-change: transform; }
will-change
是一个属性,允许我们提示浏览器我们将要对所选元素进行动画处理,并且它应该为此情况进行优化。
这意味着浏览器将始终让GPU处理这个元素。不再有CPU
和GPU
之间的切换,也就不再有明显的位置微偏的现象。
will-change
让我们可以有意识地选择哪些元素应该使用硬件加速。
硬件加速还有另一个好处:我们可以利用子像素渲染
。
现在我们有两个元素。一个采用了硬件加速,而另一个没有。它们又一个共同特点就是-当鼠标悬浮在它们上面时,它们会向下移动
<button class="accelerated box">硬件加速</button> <button class="janky box">非硬件加速</button>
.accelerated.box { transition: transform 750ms; will-change: transform; background: slateblue; } .accelerated.box:hover { transform: translateY(10px); } .janky.box { transition: margin-top 750ms; will-change: margin-top; background: deeppink; } .janky.box:hover { margin-top: 10px; }
我们可以线上环境中查看对应的效果。
上面的代码中,效果大相径庭,但是硬件加速框
移动得比非硬件加速框
更加流畅。
像margin-top
这样的属性不能进行子像素渲染,这意味着它们需要四舍五入到最接近的像素,从而创建出一个阶梯状、不流畅的效果。而transform
可以通过GPU的反锯齿技巧在像素之间平滑移动。
生活中没有免费的午餐,硬件加速也不例外。
通过将一个元素的渲染委托给GPU,它将消耗更多的视频内存(
VRAM
),这是一种有限的资源,特别是在低端移动设备上。这也是我们为什么,建议不要把xx 设置为all
的原因。
硬件加速已经存在很长时间了——实际上比will-change
属性还要早!
在很长一段时间内,通过使用3D变换来实现,例如transform: translateZ(0px)
。即使值为0px,浏览器仍会将其交给GPU处理,因为在3D空间中移动显然是GPU的强项。还有backface-visibility: hidden
。
当will-change
出现时,它旨在为开发人员提供一种适当的、语义化的方式来提示浏览器优化元素。
基于动作驱动的动画
开头我们给出了一个Hello CSS
的代码案例。它有一个“对称”的过渡——进入动画
与退出动画
相同:
- 当鼠标悬停在元素上时,它在250毫秒内向上移动10像素。
- 当鼠标移开时,元素在250毫秒内向下移动10像素。
现在,小可爱产品提出了一个优化点,就是在进入
和退出
想要不同的效果。
进入
时快退出
时慢
.btn { will-change: transform; transition: transform 450ms; } .btn:hover { transition: transform 125ms; transform: translateY(-10px); }
我们对核心代码做一下简单解释:
transition: transform 450ms;
:
transition
属性用于定义元素状态变化时的平滑过渡效果。- 这行代码指定了按钮元素在
transform
属性上应用过渡效果,持续时间为450毫秒。这意味着当按钮的transform
属性发生变化时,变化将以平滑的方式在450毫秒内发生。
transition: transform 125ms;
:
- 这行代码重新定义了按钮元素在鼠标悬停时的
transform
属性的过渡效果。 - 它指定了一个更短的过渡时间,为125毫秒。这意味着当鼠标悬停在按钮上时,按钮的
transform
属性将以更快的速度改变。
transform: translateY(-10px);
:
- 这行代码定义了鼠标悬停时按钮的
transform
属性的新值。它将按钮向上平移了10像素(-10px),创建了一个垂直方向的位移效果。 - 当用户悬停在按钮上时,按钮将向上移动10像素,创建了一个视觉反馈,以指示按钮可以被点击。
另一个常见的例子是弹窗(modals
)。对于弹窗,使用ease-out
动画进入,以及更快的ease-in
动画退出,通常会很有用。
6. 过渡延迟
最后,让我们谈谈过渡延迟
。
我相信在项目开发中,或多或少遇到过如下的情况:
作为开发者,我们可能可以理解为什么会发生这种情况:下拉菜单只在鼠标悬停在上面时保持打开!当我们以对角线移动鼠标来选择子菜单时,我们的光标会超出菜单边界,然后菜单关闭。
这个问题可以以一种相当优雅的方式解决,而无需使用JavaScript。我们可以使用transition-delay
!
.dropdown { opacity: 0; transition: opacity 400ms; + transition-delay: 300ms; } .dropdown-wrapper:hover .dropdown { opacity: 1; transition: opacity 100ms; + transition-delay: 0ms; }
transition-delay
允许我们在一段时间内保持事物的现状不变。在这种情况下,当用户将鼠标移出.dropdown-wrapper
时,在300毫秒内不会发生任何事情。如果他们在这300毫秒窗口内重新进入元素,过渡就不会发生。
在经过300毫秒后,过渡会正常启动,下拉菜单会在400毫秒内出现。
到目前为止,我们一直使用transition
简写将所有与过渡相关的值捆绑在一起。transition-delay
也可以与简写一起使用:
.dropdown { opacity: 0; transition: opacity 250ms 300ms; }
我更喜欢使用transition-delay
,因为简写感觉模糊不清;我们传递了两个时间数字,但没有指明哪一个是哪一个!
规范明确规定,当传递多个数字时,第一个是持续时间,第二个是延迟。
元素快闪
当在悬停时将一个元素向上或向下移动时,我们需要非常小心,以确保不会出现快闪现象。
在我们上面的例子中,其实也会出现这种情况。
问题出现在鼠标靠近元素边界时。悬停效果将元素从鼠标下方移开,这会导致它再次落回鼠标下方,从而再次触发悬停效果...每秒多次。
我们如何解决这个问题呢?关键是将触发与效果分开。
<button class="btn"> <span class="background"> Hello CSS </span> </button>
.background { will-change: transform; transition: transform 450ms; } .btn:hover .background { transition: transform 150ms; transform: translateY(-10px); } /* 开启查看实现原理 */ .btn { /* outline: auto; */ }
我们的 <button>
现在有一个新的子元素,.background
。这个 span
元素包含了所有的样式(背景颜色、字体等等)。
当我们悬停在这个普通的按钮上时,它会导致子元素从上方露出。然而,按钮本身是静止的。
线上代码观看真实效果
后记
分享是一种态度。
参考资料
- GSAP:greensock.com/gsap/
- Framer Motion:www.framer.com/motion/intr…
- React Spring:www.react-spring.dev/
- 视频地址:www.youtube.com/watch?v=-P2…
- 线上代码:code.juejin.cn/pen/7281580…
- code 链接:code.juejin.cn/pen/7281565…
- Lea Verou:cubic-bezier.com/
- 这个扩展的时间函数集合:easings.net/
- 线上环境:code.juejin.cn/pen/7282676…
- 反锯齿技巧:www.gpumag.com/anti-aliasi…
- 视频内存:www.techtarget.com/searchstora…
- 线上代码:code.juejin.cn/pen/7282973…
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。