前言
贝塞尔曲线是计算机图形学(Computer Graphics)中相当重要的参数曲线,在前端领域中,尤其是可视化项目中扮演者举足轻重的作用,比如:
- CSS动画中使用cubic-bezier缓动函数
- 使用贝塞尔曲线拟合折线图中的点位,使折线图看起来更圆滑、美观。
- 钢笔工具
今天我们从贝塞尔曲线的定义出发,再到贝塞尔曲线的应用场景中,为大家深入的介绍贝塞尔曲线是怎样一回事。
贝塞尔曲线的定义
递归定义
一次贝塞尔曲线
首先我们先来看一下一次贝塞尔曲线:
图中,这个黄色的小球的运动轨迹即为贝塞尔曲线。
从上图中我们可以看到:一次贝塞尔曲线有2个控制点,设黄色小球的位置由一参数确定。
所以:
或
我们可以看出,这就是一个线性插值的公式。
二次贝塞尔曲线
那么我们再来看一下二次贝塞尔曲线:
从上图中我们可以看到,二次贝塞尔曲线拥有3个控制点。图中绿色小圆点的运动轨迹即为二次贝塞尔曲线。产生该运动轨迹的步骤如下:
- 依次连线3个控制点,由此产生两条直线(上图中灰色的两条直线)
- 根据当前参数t,确定上述两条直线中,通过线性插值得到的新的两个点的位置(上图中橙色的两个点)
- 将上述两个点连接起来,再次根据参数t,通过线性插值得到最后的一个点。
- 最后这个一个点的运动轨迹即为贝塞尔曲线
到这里,相信读者已经对贝塞尔曲线有了一个大概的认识,那我们再看一下三次贝塞尔曲线
三次贝塞尔曲线
与二次贝塞尔曲线类似,我们也可以通过递归的方式,不断的根据当前参数进行线性插值来得到新的点,最后得到的一个点的运动轨迹则为贝塞尔曲线。
贝塞尔曲线的数学表达式
在上面,我们已经给出了一次贝塞尔曲线(线性插值)的数学表达式: 。
那么,对于二次贝塞尔曲线、三次贝塞尔曲线甚至N次贝塞尔曲线的数学表达式又如何呢?
二次贝塞尔曲线的数学表达式推导
由上图,P点的位置是我们最后要求的点位。点是由和参数共同决定的。所以:
又是由决定,所以又有:
将b,c 两式子代入到a中。可以得到:
d式即为二次贝塞尔曲线的数学表达式。
任意次数贝塞尔曲线的数学表达式
与上述的二次贝塞尔曲线的推导过程类似,我们同理可以推导出三次贝塞尔曲线、四次贝塞尔曲线的数学表达式,我们将一次贝塞尔曲线到四次贝塞尔曲线的表达式一起书写出来,我们来观察他们的系数有什么联系?
一次贝塞尔曲线:
二次贝塞尔曲线:
三次贝塞尔曲线:
四次贝塞尔曲线:
tips: 将他们的系数按行排列起来再观察, 再思考一会吧
细心的读者应该也发现规律了,我们将他们的系数依次写出来:
这个三角形很熟悉有没有?这就是著名的杨辉三角!
杨辉三角,是二项式系数在三角形中的一种几何排列,中国南宋数学家杨辉1261年所著的《详解九章算法》一书中出现。在欧洲,帕斯卡(1623----1662)在1654年发现这一规律,所以这个表又叫做帕斯卡三角形。帕斯卡的发现比杨辉要迟393年,比贾宪迟600年
杨辉三角有一个很重要的性质: 第n行的第m个数可以表示为:
即为从n-1个不同元素中取m-1个元素的组合数。
那么,我们现在得到了他们的系数的分布规律,我们可以轻易的写出N介贝塞尔曲线的数学表达式:
上述就是关于贝塞尔曲线的定义的介绍了,接下来我们从一些实际的应用问题出发,希望读者对贝塞尔曲线有一个更深入的理解。
贝塞尔曲线的应用
一、CSS动画中的缓动函数
我们在网页中制作一些动画或者是过渡效果时通常会用到贝塞尔曲线作缓动函数,例如:
.transition {
transition: left 2s cubic-bezier(0.5, 0.15, 0.5, 0.9) ;
}
这里就是一个经典的将贝塞尔曲线用作缓动函数。缓动函数的作用为将一个线性变化的参数t,通过一系列的运算映射为另一个值t',比如0.5通过上述的缓动函数映射后的值是0.51875
我们可以在cubic-bezier.com/ 这个网站上查看上述参数形成的贝塞尔曲线。
我们在 cubic-bezier
中填入的4个数字,即为上图中P2、P3点的x,y坐标。即P2 = (0.5, 0.15) P3 = (0.5, 0.9)。
我们就以一个元素使用上述缓动函数,在2s内从left=0的位置移动到left=200的位置这样的一个动画来进行说明。具体的计算流程如下:
对于上图中缓动函数求值这一步,我们通过已逝去时间/总时间算得 progress
,这里progress
代表的是贝塞尔曲线中x
的值。
由于我们没有建立x与y之间的映射关系,那么我们如何根据x的值,计算贝塞尔曲线的y值呢?
值得庆幸的是,我们拥有贝塞尔曲线的参数方程,我们有函数,如果我们能够通过x
反计算出t
的值,再使用t的值代入参数方程中即可得到y的值了。
所以,现在问题转化为了:如何求解方程?
首先我们思考这样的一道题:
LeetCode-367.有效的完全平方数
给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
简单的讲就是说在不使用Math.sqrt
的情况下,如何对一个数字进行开平方?
这里给大家介绍一种很实用的通过迭代的方式来解方程的方法: 牛顿法 (Newton's method)
牛顿法
牛顿法是一种用逼近的思想来求解方程根的方法。此处假设我们需要求解,其实就是求解方程的根,即求函数图像与x轴的交点。
求解的流程大致如下:
- 选取一个初始值
- 在函数图像处作函数的切线,该切线与x轴相交与点
- 判断与的值之间的差是否小于某一精度(精度值自行设定),如果达到精度则结束迭代,反之则重复步骤2.
的求解方法如下:
以上,就是关于牛顿法的内容。
有了牛顿法这样的一个求解方程的利器,我们现在可以解决上述的问题了: 求解方程,该问题等价于求解方程:
我们可以写出一个函数来实现这个方法:
function solveCurveX(x: number) {
var t2 = x;
var derivative;
var x2;
// 此处的8次是牛顿迭代的最大次数,可以自行设定,防止函数不收敛陷入死循环
for (let i = 0; i < 8; i++) {
x2 = fn(t2) - x;
if (Math.abs(x2) < ZERO_LIMIT) {
return t2;
}
derivative = fnDerivativeX(t2);
if (Math.abs(derivative) < ZERO_LIMIT) {
break;
}
t2 -= x2 / derivative;
}
return t2;
这里需要注意上述代码的for循环部分,设定了循环次数为8.这是为了设定最大的迭代次数,因为牛顿法并不是对于所有的方程都适用,首先要确保方程有根,并且在迭代区间内是单调的而且收敛的。如果不满足上面的条件,使用牛顿法会使程序陷入死循环中。
我们现在通过solveCurveX
这个函数得到了x对应的t值,再将t值代入贝塞尔曲线的方程中,即可得到最终的y值。
二、贝塞尔曲线重参数化
接着,我们进入第二个应用场景。思考这样一个问题,我使用贝塞尔曲线定义一条路径L,现在我想让某一个物体沿着贝塞尔曲线的路径匀速运动。我该如何实现?
可能有的读者已经想到一个方法了:
我可以将参数 平均的分成若干份,再将其代入贝塞尔曲线方程中以得到一系列的坐标,我再给这个物体设置刚刚计算出的坐标就好了。
这样做真的可以吗? 答案是否定的。我们看这样一条贝塞尔曲线,我将参数平均的分成25分,则曲线上的25个点为如下:
我们可以看到,上图中,在曲线弯曲的厉害的地方,点位明显比较密集,反之在曲线弯曲的不厉害的地方地位则比较稀疏。这样势必会导致一个问题:在曲线弯曲的厉害的地方物体运动的慢,在弯曲的不厉害的地方物体运动的快。这并不是我们想要得到的匀速运动的效果。
如果我们要得到匀速运动的效果该怎么做呢? 可能细心的读者已经发现了,我们要做的不是将参数t进行均分,而是应该将贝塞尔曲线的总长度进行均分才对。对曲线长度均分后的效果如下:
然后,我们还希望能够通过任意的曲线长度,找到所对应的参数t,再根据反算出的参数t,代入贝塞尔曲线方程中得到最终的坐标。
小结一下大致的步骤:
- 求解曲线总长度L
- 对曲线总长度L进行均分
- 求解任意曲线长度l所对应的参数t
- 根据参数t代入贝塞尔曲线方程得到最终坐标
现在解决第一个问题:如何求解曲线的总长度L。
最容易让人想到的一个办法就是将贝塞尔曲线分割成若干条直线,然后将这些直线的长度相加即可。但是随着分割的精度提高,运算量也是很大的。
但是这是一个很好的思想,微积分正是如此。所以,我们是否可以使用微积分的方法来求解曲线的长度呢?答案是肯定的。
我们用一小段的直线近似表示曲线,这一小段直线我们用表示。那么整段曲线的长度则是在整个曲线上进行积分。如下:
对于可以用来表示。由于我们是使用参数方程,由
可以得到
由于 与 的函数表达式相同,并且上述式子表示的就是两点之间的距离。所以
曲线总长度则为
对于任意参数对应的曲线长度为:
现在另一个问题呼之欲出了,现在我们有了如何计算曲线长度的积分表达式,但是我们如何计算积分呢?
这里介绍一种快速计算一重积分的方法:高斯-勒让德(Gauss-Lengendre)求积法
高斯-勒让德(Gauss-Lengendre)求积法
其中具体的原理就不过多介绍了,简单的来讲,就是查表。只能感叹一句高斯是神。
公式如下:
至此,我们已经可以根据贝塞尔曲线的路径积分和高斯-勒让德求积法求的贝塞尔曲线的总长度和任意t时的长度了。接下来的问题来到了,如何根据任意长度L,求所对应的t?
这个问题是不是让人感觉很少熟悉?没错,就是使用牛顿法进行求解。
其中
但是牛顿法也并不是万能的,高次方程的解并不是唯一的,例如这里的3次方程,可能有1个解,2个解,3个解。我们使用牛顿法求解的根可能并不是我们想要的那一个根。所以按上述方法对贝塞尔曲线曲线进行重参数化可能会出现下面的这种情况:
如上图所示,图中的蓝色点位是对曲线长度进行均分后,使用牛顿法求得对应的t值,所对应的点位。我们可以看出来这明显是有一定问题的,原因就是由于方程有多个根,由于初始值选择的问题,没有求得我们想要的那一个根。如下图所示:
如上图所示,因为初始点选择的问题,导致在迭代过程中一些中间状态的点发生了“跳跃”的现象,从而找到了更远处的根,但这不是我们想要的结果。那么如何避免出现这一状况呢?这里我们退而求其次,使用二分法求根。
二分法求根
二分法始终都会找到离初始值最近的根,所以不会出现类似于牛顿法的“跳跃”现象。但是二分法因此也要付出执行效率不如牛顿法的代价。二分法作为一种经典算法,在此就不过多的赘述了。
我们改为使用二分法进行方程求解后,可以得到正确的效果,如下图所示:
现在我们已经完成了贝塞尔曲线的重参数化。
接着,让我们进入下一个应用场景:
三、贝塞尔曲线分割
使用过Photoshop中钢笔工具的朋友们应该知道:在一条已有的路径中,我们可以随意的往其中插入控制点。这样的过程就是分割贝塞尔曲线的过程,我们将一条贝塞尔曲线一分为二,并且还要保证分割后的两条曲线的连接部分是平滑连续的。那么分割后的两条贝塞尔曲线的控制点应该是处于什么位置的呢?
如上图所示,有一条点A、B、C、D为控制点形成的贝塞尔曲线,我们现在要在E点处对该条曲线进行分割,那么分割后的两条曲线的控制点分别应该是哪几个点呢?
我们通过观察上图,直觉告诉我:左边的贝塞尔曲线的控制点看起来像是:A、F、I、E,右边的贝塞尔曲线的控制点像是:E、J、H、D。那么,实际上是这样的嘛??
假设A、F、I、E就是分割后的左边贝塞尔曲线的控制点。
根据贝塞尔曲线的定义,由A、B、C、D组成的贝塞尔曲线的公式可以写为:
那么左边的贝塞尔曲线A、F、I、E,可以表示为:
上式中,F、I可以写为:
将式(b), (c)代入式子(a)中,化简可得:
令,则
我们可以看出式(a)与式(d)的函数方程完全相同,并且定义域也完全相同。即A、F、I、E就是分割后左边贝塞尔曲线的控制点。同理的,对于右侧的贝塞尔曲线,我们也能够证得E、J、H、D即为右侧贝塞尔曲线新的控制点。
接下来,我们进入最后一个应用场景:使用贝塞尔曲线拟合一系列的点。
四、贝塞尔曲线拟合折线
绘制折线图表是一个非常常见的需求,我们可以使用canvas或者SVG进行折线图的绘制。我们可以简单的将所有点简单的首尾相连即可,但是这样的折线图,未免也太生硬了叭!如下图:
如果你用过类似于Echarts的图表库,那么你会发现其中的折线图是这样的:
这样看起来的就柔和多了。这背后使用到的技术正是贝塞尔曲线。它将一条直线用一条贝塞尔曲线进行替换,以达到在转角处平滑过渡的效果。
如上图所示,我们需要拟合A、B、I、J这一段折线。这里有一个简单的公式:
其中,e是用于控制曲线圆滑程度的参数,
对于起始点有:,终止点有:
根据上述公式,我们实现的效果如下:
可以看出,总体效果还是非常不错的。
以上就是本文中关于贝塞尔曲线的所有应用示例了。
总结
大致的总结一下,本文主要讲述了以下几个方面:
- 介绍了贝塞尔曲线的定义(递归定义及数学定义)
- 在数学定义中,揭示了贝塞尔曲线的的解析式中各项系数的分布规律(杨辉三角、二项式分布、组合数)
- 介绍了CSS中贝塞尔曲线用作缓动函数其背后的计算逻辑
- 介绍了贝塞尔曲线如何进行重参数化
- 介绍了牛顿法解方程以及它的一些问题(用二分法来规避,但运算速度会下降)
- 介绍了如何使用贝塞尔曲线使折线图变得更圆滑
希望读者通过阅读本文对贝塞尔曲线有更深刻的认识,各位可以通过编码的方式自我实现一下本文中提到了一些算法,对自我的编码能力和数学能力都有不小的提高。
如果你觉得本文对你有用,还请点个赞👍哦~