你是否像我一样喜欢 贝塞尔曲线?
除了优雅之外,贝塞尔曲线由于其 定义 和 构造 具有良好的数学特性。 难怪它们在如此多的领域得到广泛使用
- 作为绘图/设计工具:它们在矢量绘图软件中通常被称为“路径”。
- 作为表示曲线的格式:它们用于 SVG、字体和许多其他矢量图形格式。
- 作为数学函数:常用于控制动画时序。
现在,如何使用贝塞尔曲线作为 CSS 的运动路径呢?
快速回顾
根据上下文,当提到“贝塞尔曲线”时,我们通常假设的是二维三次贝塞尔曲线。
这样的曲线由四个点定义

注意:在本文中,我们通常将 P0
和 P3
称为端点, P1
和 P2
称为控制点。
“三次”一词表示曲线的底层函数是三次多项式。 还有“二次”贝塞尔曲线,它们类似,但控制点少一个。
问题
假设给定一个任意二维三次贝塞尔曲线,如何使用纯 CSS 动画来动画化一个元素,使其沿着曲线 精确 移动?
例如,如何重现此动画?
在本文中,我们将探讨三种不同风格的方法。 对于每个解决方案,我们将提供一个交互式演示,然后解释其工作原理。 幕后有很多数学计算和证明,但不用担心,我们不会深入探讨。
让我们开始吧!
方法 1:时间扭曲
这是基本思路
- 设置
@keyframes
以将元素从曲线的某个端点移动到另一个端点。 - 使用
animation-timing-function
为每个坐标单独扭曲时间。
注意:在 Temani Afif 的文章(2021 年)中有很多示例和解释。
使用带有正确参数的 cubic-bezier()
函数,我们可以创建任何三次贝塞尔曲线的运动路径
此演示显示了一个纯 CSS 动画。 但是使用了 canvas 和 JavaScript,它们有两个用途
- 可视化底层的贝塞尔曲线(红色曲线)。
- 允许使用典型的“路径”UI 调整曲线。
您可以拖动两个端点(黑点)和两个控制点(黑方块)。 JavaScript 代码将通过更新一些 CSS 变量来相应地更新动画。
注意:这是一个供参考的 纯 CSS 版本。
工作原理
假设所需的立方贝塞尔曲线由四个点定义:p0
、p1
、p2
和 p3
。 我们设置如下 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-x
和 move-y
确定元素的起始和结束位置。 在 animation-timing-function
中,我们有两个神奇的 cubic-bezier()
函数,参数的计算方式使得 top
和 left
在任何时候都始终具有正确的值。
我将跳过数学部分,但我在这里起草了一个简短的证明 这里,供您好奇的数学头脑参考。
讨论
此方法应该适用于大多数情况。 您甚至可以制作一个 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
动画化。
在下面的演示中,move
2 的起始位置可视化为移动的蓝点
二次贝塞尔曲线
上面的演示类似于二次贝塞尔曲线的构造

但它们看起来不同。 构造具有三个线性移动的点(两个绿色,一个黑色),但我们的演示只有两个(蓝点和目标元素)。
实际上,演示中的运动路径 是 二次贝塞尔曲线,我们只需要仔细调整关键帧即可。 我将跳过 数学部分,只揭示魔法
假设二次贝塞尔曲线由点 p0
、p1
和 p2
定义。 为了沿着曲线移动元素,我们执行以下操作
/* 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 动画(蓝色部件)
两者都可以使用复选框切换。
三次贝塞尔曲线
此方法也适用于三次贝塞尔曲线。 如果曲线由点 p0
、p1
、p2
和 p3
定义。 动画应定义如下
/* 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:标准贝塞尔曲线构造
贝塞尔曲线的数学构造已经给了我们很好的提示。

逐步地,我们可以确定所有移动点的坐标。首先,我们确定在p0
和p1
之间移动的绿色点的坐标
@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 动画一样,我们可以在此处使用任何受支持的时序函数,但我们需要同时对所有动画(move1
、move2
和move3
)应用相同的函数。
这是一个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/3
和u2=2/3
。事实上,我们可以通过更改这两个参数来调整时序。同样,所有动画(例如,move-x
和move-y
)必须具有相同的u1
和u2
值。
以下是u1=1
和u2=0
时的效果
使用**方法 2**,我们可以通过将animation-timing-function
设置为cubic-bezier(1, 0.333, 0, 0.667)
来实现完全相同的效果。
事实上,它以更通用的方式工作
假设我们得到了一条三次贝塞尔曲线,并且我们分别使用**方法 1**和**方法 2**为该曲线创建了两个动画。对于u1
和u2
的任何有效值,以下两种设置都具有相同的动画时序
- **方法 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
,并非所有现代浏览器都支持。
就是这样!希望您觉得本文有趣。请告诉我您的想法!
出色的作品和解释!