纯 CSS 贝塞尔曲线运动路径

Avatar of Lu Wang
王陆

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

你是否像我一样喜欢 贝塞尔曲线

除了优雅之外,贝塞尔曲线由于其 定义构造 具有良好的数学特性。 难怪它们在如此多的领域得到广泛使用

  • 作为绘图/设计工具:它们在矢量绘图软件中通常被称为“路径”。
  • 作为表示曲线的格式:它们用于 SVG、字体和许多其他矢量图形格式。
  • 作为数学函数:常用于控制动画时序。

现在,如何使用贝塞尔曲线作为 CSS 的运动路径呢?

快速回顾

根据上下文,当提到“贝塞尔曲线”时,我们通常假设的是二维三次贝塞尔曲线。

这样的曲线由四个点定义

Bezier curve
MarianSigler,公共领域,经由 Wikimedia Commons

注意:在本文中,我们通常将 P0 和 P3 称为端点, P1 和 P2 称为控制点。

“三次”一词表示曲线的底层函数是三次多项式。 还有“二次”贝塞尔曲线,它们类似,但控制点少一个。

问题

假设给定一个任意二维三次贝塞尔曲线,如何使用纯 CSS 动画来动画化一个元素,使其沿着曲线 精确 移动?

例如,如何重现此动画?

在本文中,我们将探讨三种不同风格的方法。 对于每个解决方案,我们将提供一个交互式演示,然后解释其工作原理。 幕后有很多数学计算和证明,但不用担心,我们不会深入探讨。

让我们开始吧!

方法 1:时间扭曲

这是基本思路

  • 设置 @keyframes 以将元素从曲线的某个端点移动到另一个端点。
  • 使用 animation-timing-function 为每个坐标单独扭曲时间。

注意:在 Temani Afif 的文章(2021 年)中有很多示例和解释。

使用带有正确参数的 cubic-bezier() 函数,我们可以创建任何三次贝塞尔曲线的运动路径

此演示显示了一个纯 CSS 动画。 但是使用了 canvas 和 JavaScript,它们有两个用途

  • 可视化底层的贝塞尔曲线(红色曲线)。
  • 允许使用典型的“路径”UI 调整曲线。

您可以拖动两个端点(黑点)和两个控制点(黑方块)。 JavaScript 代码将通过更新一些 CSS 变量来相应地更新动画。

注意:这是一个供参考的 纯 CSS 版本

工作原理

假设所需的立方贝塞尔曲线由四个点定义:p0p1p2p3。 我们设置如下 CSS 规则

/* pseudo CSS code */
div {
  animation-name: move-x, move-y;
  /*
    Define:
    f(x, a, b) = (x - a) / (b - a)
    qx1 = f(p1.x, p0.x, p3.x)
    qx2 = f(p2.x, p0.x, p3.x)
    qy1 = f(p1.y, p0.y, p3.y)
    qy2 = f(p2.y, p0.y, p3.y)
  */
  animation-timing-function: 
    cubic-bezier(1/3, qx1, 2/3, qx1),
    cubic-bezier(1/3, qy1, 2/3, qy2);
}

@keyframes move-x {
  from {
    left: p0.x;
  }
  to {
    left: p3.x;
  }
}

@keyframes move-y {
  from {
    top: p0.y;
  }
  to {
    top: p3.y;
  }
}

@keyframes 规则 move-xmove-y 确定元素的起始和结束位置。 在 animation-timing-function 中,我们有两个神奇的 cubic-bezier() 函数,参数的计算方式使得 topleft 在任何时候都始终具有正确的值。

我将跳过数学部分,但我在这里起草了一个简短的证明 这里,供您好奇的数学头脑参考。

讨论

此方法应该适用于大多数情况。 您甚至可以制作一个 3D 立方贝塞尔曲线,方法是为 z 值引入另一个动画。

但是有一些小的注意事项

  • 由于除以零的错误,当两个端点都位于水平线或垂直线上时,此方法不起作用。

注意:在实践中,您可以添加一个微小的偏移量作为解决方法。

  • 它不支持阶数高于 3 的贝塞尔曲线。
  • 动画时序选项有限。
    • 我们使用 1/3 和 2/3 来实现线性时序。
    • 您可以调整这两个值以调整时序,但与其他方法相比,它受到限制。 稍后将详细介绍。

方法 2:竞争动画

作为热身,想象一个具有两个动画的元素

div {
  animation-name: move1, move2;
}

如果动画定义如下,元素的运动路径是什么?

@keyframes move1 {
  to {
    left: 256px;
  }
}

@keyframes move2 {
  to {
    top: 256px;
  }
}

正如您可能猜到的那样,它会沿对角线移动

现在,如果动画改为这样定义呢

@keyframes move1 {
  to {
    transform: translateX(256px);
  }
}

@keyframes move2 {
  to {
    transform: translateY(256px);
  }
}

“啊哈,你骗不了我!”你可能会说,因为你注意到这两个动画都在更改相同的属性,“move2 必须像这样覆盖 move1:”

好吧,之前我也这么认为。 但实际上我们得到了这个

诀窍在于 move2 没有 from 帧,这意味着起始位置由 move1 动画化。

在下面的演示中,move2 的起始位置可视化为移动的蓝点

二次贝塞尔曲线

上面的演示类似于二次贝塞尔曲线的构造

Bézier 2 big
Phil Tregoning,公共领域,经由 Wikimedia Commons

但它们看起来不同。 构造具有三个线性移动的点(两个绿色,一个黑色),但我们的演示只有两个(蓝点和目标元素)。

实际上,演示中的运动路径  二次贝塞尔曲线,我们只需要仔细调整关键帧即可。 我将跳过 数学部分,只揭示魔法

假设二次贝塞尔曲线由点 p0p1p2 定义。 为了沿着曲线移动元素,我们执行以下操作

/* pseudo-CSS code */
div {
  animation-name: move1, move2;
}

@keyframes move1 {
  from {
    transform: translate3d(p0.x, p0.y, p0.z);
  }
  /* define q1 = (2 * p1 - p2) */
  to {
    transform: translate3d(q1.x, q1.y, q1.z);
  }
}

@keyframes move2 {
  to {
    transform: translate3d(p2.x, p2.y, p2.z);
  }
}

类似于 方法 1 的演示,您可以查看或调整曲线。 此外,演示还显示了另外两条信息

  • 数学构造(灰色移动部件)
  • CSS 动画(蓝色部件)

两者都可以使用复选框切换。

三次贝塞尔曲线

此方法也适用于三次贝塞尔曲线。 如果曲线由点 p0p1p2p3 定义。 动画应定义如下

/* pseudo-CSS code */
div {
  animation-name: move1, move2, move3;
}

@keyframes move1 {
  from {
    transform: translate3d(p0.x, p0.y, p0.z);
  }
  /* define q1 = (3 * p1 - 3 * p2 + p3) */
  to {
    transform: translate3d(q1.x, q1.y, q1.z);
  }
}

@keyframes move2 {
  /* define q2 = (3 * p2 - 2 * p3) */
  to {
    transform: translate3d(q2.x, q2.y, q2.z);
  }
}

@keyframes move3 {
  to {
    transform: translate3d(p3.x, p3.y, p3.z);
  }
}

扩展

3D 贝塞尔曲线怎么样? 实际上,事实是,所有以前的示例 都是 3D 曲线,我们只是从未关心过 z 值。

更高阶的贝塞尔曲线呢? 我 90% 确定该方法可以自然地扩展到更高阶。 如果您已经计算出四阶贝塞尔曲线的公式,或者更好的是,N 阶贝塞尔曲线的通用公式,请告诉我。

方法 3:标准贝塞尔曲线构造

贝塞尔曲线的数学构造已经给了我们很好的提示。

Bézier 3 big
Phil Tregoning,公共领域,经由 Wikimedia Commons

逐步地,我们可以确定所有移动点的坐标。首先,我们确定在p0p1之间移动的绿色点的坐标

@keyframes green0 {
  from {
    --green0x: var(--p0x);
    --green0y: var(--p0y);
  }
  to {
    --green0x: var(--p1x);
    --green0y: var(--p1y);
  }
}

其他绿色点可以用类似的方式构造。

接下来,我们可以像这样确定蓝色点的坐标

@keyframes blue0 {
  from {
    --blue0x: var(--green0x);
    --blue0y: var(--green0y);
  }
  to {
    --blue0x: var(--green1x);
    --blue0y: var(--green1y);
  }
}

重复此操作,最终我们将得到所需的曲线。

与**方法 2**类似,使用此方法我们可以轻松构建 3D 贝塞尔曲线。扩展此方法以用于更高阶的贝塞尔曲线也很直观。

唯一的缺点是使用了@property,这并非所有浏览器都支持。

动画时序

到目前为止,所有示例都使用了“线性”时序,那么缓动或其他时序函数呢?

注意:“线性”指的是曲线中的变量t从 0 线性变化到 1。换句话说,t与动画进度相同。

在**方法 2**和**方法 3**中从未使用过animation-timing-function。与其他 CSS 动画一样,我们可以在此处使用任何受支持的时序函数,但我们需要同时对所有动画(move1move2move3)应用相同的函数。

这是一个animation-timing-function: cubic-bezier(1, 0.1, 0, 0.9)的示例

以下是使用animation-timing-function: steps(18, end)的效果

另一方面,**方法 1**比较棘手,因为它已经使用了cubic-bezier(u1, v1, u2, v2)时序函数。在上面的示例中,我们有u1=1/3u2=2/3。事实上,我们可以通过更改这两个参数来调整时序。同样,所有动画(例如,move-xmove-y)必须具有相同的u1u2值。

以下是u1=1u2=0时的效果

使用**方法 2**,我们可以通过将animation-timing-function设置为cubic-bezier(1, 0.333, 0, 0.667)来实现完全相同的效果。

事实上,它以更通用的方式工作

假设我们得到了一条三次贝塞尔曲线,并且我们分别使用**方法 1**和**方法 2**为该曲线创建了两个动画。对于u1u2的任何有效值,以下两种设置都具有相同的动画时序

  • **方法 1**使用animation-timing-function: cubic-bezier(u1, *, u2, *)
  • **方法 2**使用animation-timing-function: cubic-bezier(u1, 1/3, u2, 2/3)

现在我们明白了为什么**方法 1**是“受限的”:使用**方法 1**,我们只能使用两个参数进行cubic-bezier(),但使用**方法 2**和**方法 3**,我们可以使用任何 CSS animation-timing-function

结论

在本文中,我们讨论了三种仅使用 CSS 动画即可沿贝塞尔曲线精确移动元素的不同方法。

虽然所有三种方法或多或少都实用,但它们都有自己的优缺点

  • **方法 1**对于熟悉时序函数技巧的人来说可能更直观。但它在动画时序方面不太灵活。
  • **方法 2**具有非常简单的 CSS 规则。任何 CSS 时序函数都可以直接应用。但是,公式可能难以记住。
  • **方法 3**对于熟悉贝塞尔曲线数学构造的人来说更有意义。动画时序也很灵活。另一方面,由于使用了@property,并非所有现代浏览器都支持。

就是这样!希望您觉得本文有趣。请告诉我您的想法!