使用 cubic-bezier() 实现高级 CSS 动画

Avatar of Temani Afif
Temani Afif 发表于

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

在处理复杂的 CSS 动画时,往往会创建包含大量声明的扩展 @keyframes。不过,我想谈谈几个技巧,它们可能有助于简化操作,同时保持在原生 CSS 范围内。

  1. 多个动画
  2. 定时函数

第一个技巧使用更广泛,也更为熟悉,但第二个技巧则不太常见。这可能是有原因的——使用逗号链接动画比理解我们可用的各种定时函数及其功能要容易得多。有一个特别巧妙的定时函数,它使我们可以完全控制创建自定义定时函数。那就是 cubic-bezier(),在这篇文章中,我将向您展示它的强大功能以及如何用它来创建花哨的动画,而不会过于复杂。

让我们从一个基本的示例开始,展示如何以有趣的方向(如无限(∞)形状)移动球体。

如您所见,代码并不复杂——只有两个关键帧和一个 cubic-bezier() 函数。然而,我们得到了一个看起来相当复杂的最终无限形状动画。

很酷,对吧?让我们深入了解一下吧!

cubic-bezier() 函数

让我们从 官方定义 开始

三次贝塞尔缓动函数是一种 缓动函数,由四个实数定义,这些实数指定三次贝塞尔曲线的两个控制点 P1 和 P2,其端点 P0 和 P3 分别固定在 (0, 0) 和 (1, 1)。P1 和 P2 的 x 坐标限制在 [0, 1] 范围内。

上述曲线定义了输出(y 轴)如何根据时间(x 轴)变化。每个轴的范围都是 [0, 1](或 [0% 100%])。如果我们有一个持续两秒(2s)的动画,那么

0 (0%) = 0s 
1 (100%) = 2s

如果我们想将 left5px 动画到 20px,那么

0 (0%) = 5px 
1 (100%) = 20px

X(时间)始终限制在 [0 1];但是,Y(输出)可以超出 [0 1]

我的目标是调整 P1 和 P2 以创建以下曲线

抛物线
正弦曲线

您可能会认为这不可能实现,因为如定义中所述,P0 和 P3 固定在 (0,0)(1,1),这意味着它们不能在同一轴上。这是真的,我们将使用一些数学技巧来“近似”它们。

抛物线

让我们从以下定义开始:cubic-bezier(0,1.5,1,1.5)。这给了我们以下曲线

cubic-bezier(0,1.5,1,1.5)

我们的目标是移动 (1,1) 并将其置于 (0,1),这在技术上是不可能的。所以我们将尝试伪造它。

我们之前说过我们的范围是 [0 1](或 [0% 100%]),所以让我们想象一下 0% 非常接近 100% 的情况。例如,如果我们想将 top20px (0%) 动画到 20.1px (100%),那么我们可以说初始状态和最终状态是相等的。

嗯,但是我们的元素根本不会移动,对吧?

好吧,它会稍微移动一下,因为 Y 值超过了 20.1px100%)。但这不足以让我们感知到运动

让我们更新曲线,改用 cubic-bezier(0,4,1,4)。注意我们的曲线比之前高得多

cubic-bezier(0,4,1,4)

但仍然没有移动——即使 top 值超过了 3(或 300%)。让我们尝试 cubic-bezier(0,20,1,20)

cubic-bezier(0,20,1,20)

是的!它开始稍微移动了。您是否注意到每次我们增加值时曲线的演变?当我们缩小以查看完整曲线时,它使我们的点 (1,1)“视觉上”更接近 (0,1),这就是技巧所在。

通过使用 cubic-bezier(0,V,1,V)(其中 V 是某个非常大的值,初始状态和最终状态非常接近(或几乎相等)),我们可以模拟抛物线。

示例胜过千言万语

我在其中将“魔法”cubic-bezier 函数应用于 top 动画,以及应用于 left 的线性动画。这给了我们想要的曲线。

深入数学

对于那些精通数学的朋友,我们可以进一步分解这个解释。三次贝塞尔曲线可以使用以下公式定义

P = (1−t)³P0 + 3(1−t)²tP1 + 3(1−t)t²P2 + t³P3

每个点定义如下:P0 = (0,0)P1 = (0,V)P2 = (1,V)P3 = (1,1)

这给了我们 x 和 y 坐标的两个函数

  • X(t) = 3(1−t)t² + t³ = 3t² - 2t³
  • Y(t) = 3(1−t)²tV +3(1−t)t²V + t³ = t³ - 3Vt² + 3Vt

V 是我们的大值,t[0 1] 范围内。如果我们考虑之前的示例,Y(t) 将给出 top 的值,而 X(t) 是时间进度。然后,点 (X(t),Y(t)) 将定义我们的曲线。

让我们找到 Y(t) 的最大值。为此,我们需要找到使 Y'(t) = 0(当导数等于 0 时)的 t 值。

Y'(t) = 3t² - 6Vt + 3V

Y'(t) = 0 是一个二次方程。我将跳过无聊的部分,并给出结果,即 t = V - sqrt(V² - V)

V 是一个大值时,t 将等于 0.5。因此,Y(0.5) = Max,并且 X(0.5) 将等于 0.5。这意味着我们在动画的中途达到最大值,这符合我们想要的抛物线。

此外,Y(0.5) 将给出 (1 + 6V)/8,这将允许我们根据 V 找到最大值。并且由于我们始终将使用 V 的大值,因此我们可以简化为 6V/8 = 0.75V

在最后一个示例中,我们使用了 V = 500,因此那里的最大值将为 375(或 37500%),我们得到以下结果

  • 初始状态 (0):top: 200px
  • 最终状态 (1):top: 199.5px

0 和 1 之间存在 -0.5px 的差异。我们称之为增量。对于 375(或 37500%),我们有 375*-0.5px = -187.5px 的等式。我们的动画元素达到 top: 12.5px200px - 187.5px),并给我们以下动画

top: 200px (at 0% of the time ) → top: 12.5px (at 50% of the time) → top: 199.5px (at 100% of the time) 

或者,换句话说

top: 200px (at 0%) → top: 12.5px (at 50%) → top: 200px (at 100%)

让我们做相反的逻辑。我们应该使用什么 V 值才能使我们的元素达到 top: 0px?动画将是 200px → 0px → 199.5px,因此我们需要 -200px 来达到 0px。我们的增量始终等于 -0.5px。最大值将等于 200/0.5 = 400,因此 0.75V = 400,这意味着 V = 533.33

我们的元素正在触及顶部!

下图总结了我们刚才做的数学运算

使用 cubic-bezier(0,V,1,V) 的抛物线

正弦曲线

我们将使用几乎完全相同的技巧来创建正弦曲线,但使用不同的公式。这次我们将使用 cubic-bezier(0.5,V,0.5,-V)

就像我们之前做的那样,让我们看看当我们增加值时曲线将如何演变

Three graphs from left to right, showing how the sinusoidal curve gets narrower as the V value increases.

我想你现在可能已经明白了。对 V 使用大值可以使我们接近正弦曲线。

这是另一个具有连续动画的示例——真正的正弦动画!

数学

让我们来了解一下这个的数学原理!遵循与之前相同的公式,我们将得到以下函数

  • X(t) = 3/2(1−t)²t + 3/2(1−t)t² + t³ = (3/2)t - (3/2)t² + t³
  • Y(t) = 3(1−t)²tV - 3(1−t)t²V + t³ = (6V + 1)t³ - 9Vt² + 3Vt

这次我们需要找到 Y(t) 的最小值和最大值。Y'(t) = 0 将给我们两个解。求解后

Y'(t) = 3(6V + 1)t² - 18Vt + 3V = 0

…我们得到

  • t' = (3V + sqrt(3V² - V))/(6V + 1)
  • t''= (3V - sqrt(3V² - V))/(6V + 1)

对于 V 的大值,我们有 t'=0.211t"=0.789。这意味着 Y(0.211) = MaxY(0.789) = Min。这也意味着 X(0.211)= 0.26X(0.789) = 0.74。换句话说,我们在时间的 26% 处达到最大值,在时间的 74% 处达到最小值。

Y(0.211) 等于 0.289VY(0.789) 等于 -0.289V。考虑到 V 非常大,我们通过一些四舍五入得到了这些值。

我们的正弦曲线也应该在时间的一半(或 X(t) = 0.5)处穿过 x 轴(或 Y(t) = 0)。为了证明这一点,我们使用 Y(t) 的二阶导数——它应该等于 0——所以 Y''(t) = 0

Y''(t) = 6(6V + 1)t - 18V = 0

解是 3V/(6V + 1),对于 V 的大值,解是 0.5。这给了我们 Y(0.5) = 0X(0.5) = 0.5,这证实了我们的曲线穿过 (0.5,0) 点。

现在让我们考虑之前的示例,并尝试找到使我们回到 top: 0%V 值。我们有

  • 初始状态 (0):top: 50%
  • 最终状态 (1):top: 49.9%

  • 增量:-0.1%

我们需要 -50% 来达到 top: 0%,所以 0.289V*-0.1% = -50%,这给了我们 V = 1730.10

正如你所看到的,我们的元素正在接触顶部并在底部消失,因为我们有以下动画

top: 50% → top: 0% → top: 50% → top: 100% → top: 50% → and so on ... 

一个总结计算结果的图表

使用 cubic-bezier(0.5,V,0.5,-V) 的正弦曲线

以及一个说明所有曲线在一起的示例

是的,你看到了四条曲线!如果你仔细观察,你会注意到我使用了两种不同的动画,一个到 49.9%(增量为 -0.01%),另一个到 50.1%(增量为 +0.01%)。通过改变增量的符号,我们控制曲线的走向。我们还可以控制三次贝塞尔曲线的其他参数(不是应该保持较大值的 V 参数),以从相同的曲线创建更多变化。

下面是一个交互式演示

回到我们的示例

让我们回到我们最初的球体以无限符号形状移动的示例。我简单地组合了两个正弦动画使其工作。

如果我们将之前所做的与多个动画的概念结合起来,我们可以获得惊人的效果。这是最初的示例,这次是作为交互式演示。更改值并查看神奇的效果

让我们更进一步,在其中添加一点 CSS Houdini。我们可以通过 @property 来为复杂的转换声明设置动画(但 CSS Houdini 目前仅限于 Chrome 和 Edge 支持)。

你能用它画出什么样的图形?这里有一些我能够制作的图形

这是一个螺旋图形动画

以及一个没有 CSS Houdini 的版本

从这些示例中可以得出一些结论

  • 每个关键帧仅使用一个包含增量的声明来定义。
  • 元素的位置和动画是独立的。我们可以轻松地将元素放置在任何位置,而无需调整动画。
  • 我们没有进行任何计算。没有大量的角度或像素值。我们只需要关键帧中的一个微小值和 cubic-bezier() 函数中的一个大值。
  • 整个动画可以通过调整持续时间值来控制。

过渡呢?

相同的技术也可以与 CSS transition 属性一起使用,因为它在涉及计时函数时遵循相同的逻辑。这很棒,因为我们在创建一些复杂的悬停效果时能够避免关键帧。

这是我在没有关键帧的情况下制作的

马里奥由于抛物线曲线而跳跃。我们根本不需要关键帧就能在悬停时创建那个抖动动画。正弦曲线完全能够完成所有工作。

这是马里奥的另一个版本,这次使用了 CSS Houdini。而且,是的,他仍然由于抛物线曲线而跳跃

为了确保万无一失,这里还有更多没有关键帧的花哨悬停效果(同样,仅限 Chrome 和 Edge)

就是这样!

现在你有一些神奇的 cubic-bezier() 曲线及其背后的数学原理。当然,好处是像这样的自定义计时函数让我们能够进行花哨的动画,而无需我们通常使用的复杂关键帧。

我理解并非每个人都擅长数学,这没关系。有一些工具可以提供帮助,例如 Matthew Lein 的 Ceaser,它允许你拖动曲线点以获得所需的效果。并且,如果你还没有将其添加为书签,cubic-bezier.com 是另一个工具。如果你想在 CSS 世界之外玩转 cubic-bezier,我推荐 desmos,在那里你可以看到一些数学公式。

无论你如何获取 cubic-bezier() 值,希望现在你都能了解其功能以及它们如何在过程中帮助编写更简洁的代码。