使用自定义属性和 cubic-bezier() 构建复杂的 CSS 过渡

Avatar of Temani Afif
Temani Afif

DigitalOcean 为您的旅程各个阶段提供云产品。立即开始使用 $200 免费积分!

我最近演示了如何 使用 cubic-bezier() 实现复杂的 CSS 动画,以及如何在 CSS 过渡中实现相同的动画效果。我能够创建复杂的悬停效果,而无需使用关键帧。在本文中,我将向您展示如何创建更复杂的 CSS 过渡。

这次,让我们使用 @property 特性。目前,它只在基于 Chrome 的浏览器中受支持,但我们仍然可以玩玩它,并演示如何使用它来构建复杂的动画。

我强烈建议您阅读我的 上一篇文章,因为我将引用我在那里详细解释的一些概念。此外,请注意,本文中的演示最好在基于 Chromium 的浏览器中查看,因为 @property 支持仍然有限。

让我们从一个演示开始

点击按钮(多次),您会看到我们得到的“神奇”曲线。乍一看,它可能看起来很琐碎,因为我们可以使用一些复杂的关键帧来实现这种效果。但诀窍是那里没有关键帧!该动画仅使用过渡完成。

很棒吧?而且这仅仅是开始,所以让我们深入研究吧!

主要思路

前面示例中的技巧依赖于此代码

@property --d1 {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
@property --d2 {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

.box {
  top: calc((var(--d1) + var(--d2)) * 1%);
  transition:
    --d1 1s cubic-bezier(0.7, 1200, 0.3, -1200),
    --d2 1s cubic-bezier(0.5, 1200, 0.5, -1200);
}
.box:hover {
  --d1: 0.2;
  --d1: -0.2;
}

我们定义了两个自定义属性,--d1--d2。然后,我们使用这两个属性的总和在 .box 元素上声明 top 属性。目前还没有什么特别复杂的——只是将 calc() 应用于两个变量。

这两个属性被定义为 <number>,我将这些值乘以 1% 以将其转换为百分比。我们可以直接将这些定义为 <percentage> 以避免乘法。但我选择使用数字而不是百分比,以便在以后进行更复杂的运算时具有更大的灵活性。

请注意,我们对每个变量应用了不同的过渡——更准确地说,是不同的 timing-function,但持续时间相同。实际上,这两个变量都有不同的正弦曲线,这一点我在我的 上一篇文章 中做了深入介绍。

从那里开始,当 .box 被悬停时,属性值会发生变化,从而触发动画。但是为什么我们会得到在演示中看到的那个结果呢?

这都与数学有关。我们正在将两个函数加在一起以创建一个第三个函数。对于 --d1,我们有一个函数(我们称之为 F1);对于 --d2,我们有另一个函数(我们称之为 F2)。这意味着 top 的值为 F1 + F2。

一个更清楚地说明的示例

前两个过渡分别说明了每个变量。第三个是它们的总和。假设在动画的每个步骤中,我们获取两个变量的值并将它们加在一起以获得最终曲线上每个点。

让我们尝试另一个示例

这次,我们将两个抛物线曲线组合在一起,得到……嗯,我不知道它的名字,但它是另一个复杂的曲线!

此技巧不仅限于抛物线和正弦曲线。它可以与任何类型的定时函数一起使用,即使结果并不总是复杂的曲线。

这次

  • --d10 变为 30,并使用 ease-in 定时函数
  • --d20 变为 -20,并使用 ease-out 定时函数

结果如何?top 值从 0 变为 1030-20),并使用自定义定时函数(ease-inease-out 的总和)。

在这种情况下,我们不会得到复杂的过渡——它更多地是为了说明这是一个通用的想法,不仅仅局限于 cubic-bezier()

我认为是时候进行交互式演示了。

您只需调整一些变量就可以构建自己的复杂过渡。我知道 cubic-bezier() 可能很棘手,所以请考虑 使用这个在线曲线生成器,并参考我的 上一篇文章

以下是我创建的一些示例

如您所见,我们可以将两个不同的定时函数(使用 cubic-bezier() 创建)组合在一起以创建一个第三个函数,该函数足够复杂以实现花哨的过渡。组合(和可能性)是无限的!

在最后一个示例中,我想演示如何将两个相反的函数加在一起,会导致逻辑结果为常数函数(无过渡)。因此,是一条平直的线。

让我们添加更多变量!

您认为我们会止步于只有两个变量吗?当然不是!我们可以将逻辑扩展到 N 个变量。没有限制——我们用定时函数定义每个变量,并将它们加在一起。

使用三个变量的示例

在大多数情况下,两个变量足以创建一个花哨的曲线,但知道这个技巧可以扩展到更多变量很有趣。

我们可以减去、乘以和除以变量吗?

当然可以!我们还可以将相同的想法扩展到更多操作。我们可以加、减、乘、除——甚至可以在变量之间执行复杂的公式。

在这里,我们正在将值相乘

我们还可以使用一个变量并将其自乘以获得二次函数!

让我们通过引入 min()/max() 来模拟 abs() 函数,以添加更多乐趣

请注意,在第二个框中,我们将永远不会高于 y 轴上的中心点,因为 top 始终为正值。(我添加了一个 margin-top 以使框的中心成为 0 的参考点。)

我不会深入所有数学细节,但您可以想象我们创建任何类型的定时函数的可能性。我们所要做的就是找到合适的公式,无论是使用一个变量还是组合多个变量。

我们的初始代码可以泛化

@property --d1 { /* we do the same for d2 .. dn */
  syntax: '<number>';
  inherits: false;
  initial-value: i1; /* the initial value can be different for each variable */
}

.box {
  --duration: 1s; /* the same duration for all */
  property: calc(f(var(--d1),var(--d2), .. ,var(--dn))*[1UNIT]);
  transition:
    --d1 var(--duration) cubic-bezier( ... ),
    --d2 var(--duration) cubic-bezier( ... ),
    /* .. */
    --dn var(--duration) cubic-bezier( ... );
}
.box:hover {
  --d1:f1;
  --d2:f2;
  /* .. */
  --dn:f3;
}

这是为了说明逻辑的伪代码

  1. 我们使用 @property 定义数字自定义属性,每个属性都有一个初始值。
  2. 每个变量都有自己的定时函数,但持续时间相同。
  3. 我们定义了一个 f 函数,它是变量之间使用的公式。该函数提供一个数字,我们用它来乘以相关的单位。所有这些都在应用于该属性的 calc() 中运行。
  4. 我们在悬停(或切换,或任何操作)时更新每个变量的值。

鉴于此,该属性会使用自定义定时函数从 f(i1,i2,…,in) 过渡到 f(f1,f2,..,fn)

链接定时函数

我们已经达到了能够通过组合基本定时函数来创建复杂定时函数的程度。让我们尝试另一个想法,它使我们能够拥有更复杂的定时函数:将定时函数链接在一起

诀窍是使用 transition-delay 属性按顺序运行过渡。让我们回顾一下交互式演示,并将延迟应用于其中一个变量

我们正在链接定时函数而不是将它们加在一起,这又是一种创建更复杂定时函数的方法!从数学上讲,它仍然是求和,但由于过渡没有同时运行,因此我们将对函数进行求和,并加上一个常数,从而模拟链接。

现在想象一下我们逐步延迟的 N 个变量的情况。我们不仅可以由此创建复杂的过渡,而且我们拥有足够的灵活性来构建复杂的时间线

以下是我使用该技术构建的有趣悬停效果

您不会在那里找到关键帧。一个小型动作场景完全使用一个元素和一个 CSS 过渡完成。

以下是一个使用相同想法的逼真钟摆动画

或者,一个自然反弹的球呢?

或者也许是一个沿着曲线滚动的球

看到了吗?我们刚刚在没有任何关键帧的情况下创建了复杂的动画!

总结

我希望您从本文和 上一篇文章 中领会了三个关键要点

  1. 我们可以使用 cubic-bezier() 获得抛物线和正弦曲线,这些曲线使我们能够在没有关键帧的情况下创建复杂的过渡。
  2. 我们可以使用自定义属性和 calc() 通过组合不同的定时函数来创建更多曲线。
  3. 我们可以使用 transition-delay 链接曲线以构建复杂的时间线。

由于这三个特性,我们在创建复杂的动画方面没有限制。