深刻浅出贝塞尔曲线及应用示例

前言

贝塞尔曲线是计算机图形学(Computer Graphics)中至关重要的参数曲线,在前端领域中,尤为是可视化项目中扮演者举足轻重的做用,好比:css

  1. CSS动画中使用cubic-bezier缓动函数
  2. 使用贝塞尔曲线拟合折线图中的点位,使折线图看起来更圆滑、美观。
  3. 钢笔工具

今天咱们从贝塞尔曲线的定义出发,再到贝塞尔曲线的应用场景中,为你们深刻的介绍贝塞尔曲线是怎样一回事。html

贝塞尔曲线的定义

递归定义

一次贝塞尔曲线

首先咱们先来看一下一次贝塞尔曲线前端

1-bezier.gif

图中,这个黄色的小球的运动轨迹即为贝塞尔曲线。算法

从上图中咱们能够看到:一次贝塞尔曲线有2个控制点,设黄色小球的位置由一参数 t [ 0 , 1 ] t\in[0,1] 肯定。canvas

image.png

因此:markdown

P = ( 1 t ) P 1 + t P 2 P = (1-t)P_1 + tP_2

app

P = P 1 + t ( P 2 P 1 ) P = P_1 + t(P_2 - P_1)

咱们能够看出,这就是一个线性插值的公式。ide

二次贝塞尔曲线

那么咱们再来看一下二次贝塞尔曲线svg

2-bezier.gif

从上图中咱们能够看到,二次贝塞尔曲线拥有3个控制点。图中绿色小圆点的运动轨迹即为二次贝塞尔曲线。产生该运动轨迹的步骤以下:函数

  1. 依次连线3个控制点,由此产生两条直线(上图中灰色的两条直线)
  2. 根据当前参数t,肯定上述两条直线中,经过线性插值获得的新的两个点的位置(上图中橙色的两个点)
  3. 将上述两个点链接起来,再次根据参数t,经过线性插值获得最后的一个点。
  4. 最后这个一个点的运动轨迹即为贝塞尔曲线

到这里,相信读者已经对贝塞尔曲线有了一个大概的认识,那咱们再看一下三次贝塞尔曲线

三次贝塞尔曲线

3-bezier.gif

与二次贝塞尔曲线相似,咱们也能够经过递归的方式,不断的根据当前参数 t [ 0 , 1 ] t\in[0,1] 进行线性插值来获得新的点,最后获得的一个点运动轨迹则为贝塞尔曲线。

贝塞尔曲线的数学表达式

在上面,咱们已经给出了一次贝塞尔曲线(线性插值)的数学表达式: P = ( 1 t ) P 1 + t P 2 P = (1-t)P_1 + tP_2

那么,对于二次贝塞尔曲线、三次贝塞尔曲线甚至N次贝塞尔曲线的数学表达式又如何呢?

二次贝塞尔曲线的数学表达式推导

image.png

由上图,P点的位置是咱们最后要求的点位。点 P P 是由 P 1 , P 2 P_1', P_2' 和参数 t [ 0 , 1 ] t\in[0,1] 共同决定的。因此:

P = ( 1 t ) P 1 + t P 2 (a) P = (1-t)P_1' + tP_2' \tag{a}

P 1 , P 2 P_1', P_2' 又是由 P 1 , P 2 , P 3 P_1, P_2, P_3 决定,因此又有:

P 1 = ( 1 t ) P 1 + t P 2 (b) P_1' = (1-t)P_1 + tP_2 \tag{b}
P 2 = ( 1 t ) P 2 + t P 3 (c) P_2' = (1-t)P_2 + tP_3 \tag{c}

将b,c 两式子代入到a中。能够获得:

P = ( 1 t ) 2 P 1 + 2 ( 1 t ) t P 2 + t 2 P 3 (d) P = (1-t)^2P_1 + 2(1-t)tP_2 + t^2P_3 \tag{d}

d式即为二次贝塞尔曲线的数学表达式。

任意次数贝塞尔曲线的数学表达式

与上述的二次贝塞尔曲线的推导过程相似,咱们同理能够推导出三次贝塞尔曲线、四次贝塞尔曲线的数学表达式,咱们将一次贝塞尔曲线到四次贝塞尔曲线的表达式一块儿书写出来,咱们来观察他们的系数有什么联系?

一次贝塞尔曲线:

P = ( 1 t ) P 1 + t P 2 P = (1-t)P_1 + tP_2

二次贝塞尔曲线:

P = ( 1 t ) 2 P 1 + 2 ( 1 t ) t P 2 + t 2 P 3 P = (1-t)^2P_1 + 2(1-t)tP_2 + t^2P_3

三次贝塞尔曲线:

P = ( 1 t ) 3 P 1 + 3 ( 1 t ) 2 t P 2 + 3 ( 1 t ) t 2 P 3 + t 3 P 4 P = (1-t)^3P_1 +3(1-t)^2tP_2 + 3(1-t)t^2P_3 + t^3P_4

四次贝塞尔曲线:

P = ( 1 t ) 4 P 1 + 4 ( 1 t ) 3 t P 2 + 6 ( 1 t ) 2 t 2 P 3 + 4 ( 1 t ) t 3 P 4 + t 4 P 5 P = (1-t)^4P_1 +4(1-t)^3tP_2 + 6(1-t)^2t^2P_3 + 4(1-t)t^3P_4 + t^4P_5

tips: 将他们的系数按行排列起来再观察, 再思考一会吧

细心的读者应该也发现规律了,咱们将他们的系数依次写出来:

image.png

这个三角形很熟悉有没有?这就是著名的杨辉三角!

杨辉三角,是二项式系数在三角形中的一种几何排列,中国南宋数学家杨辉1261年所著的《详解九章算法》一书中出现。在欧洲,帕斯卡(1623----1662)在1654年发现这一规律,因此这个表又叫作帕斯卡三角形。帕斯卡的发现比杨辉要迟393年,比贾宪迟600年

杨辉三角有一个很重要的性质: 第n行的第m个数能够表示为:

C n 1 m 1 = ( n 1 ) ! ( m 1 ) ! C_{n-1}^{m-1} = \frac{(n-1)!}{(m-1)!}

即为从n-1个不一样元素中取m-1个元素的组合数。

那么,咱们如今获得了他们的系数的分布规律,咱们能够轻易的写出N介贝塞尔曲线的数学表达式:

P = i = 0 n C n i ( 1 t ) n i t i P i P = \sum_{i=0}^{n}C_n^i(1-t)^{n-i}t^iP_i

上述就是关于贝塞尔曲线的定义的介绍了,接下来咱们从一些实际的应用问题出发,但愿读者对贝塞尔曲线有一个更深刻的理解。

贝塞尔曲线的应用

1、CSS动画中的缓动函数

咱们在网页中制做一些动画或者是过渡效果时一般会用到贝塞尔曲线做缓动函数,例如:

.transition {
    transition: left 2s cubic-bezier(0.5, 0.15, 0.5, 0.9) ;
}
复制代码

这里就是一个经典的将贝塞尔曲线用做缓动函数。缓动函数的做用为将一个线性变化的参数t,经过一系列的运算映射为另外一个值t',好比0.5经过上述的缓动函数映射后的值是0.51875

咱们能够在cubic-bezier.com/ 这个网站上查看上述参数造成的贝塞尔曲线。

image.png

咱们在 cubic-bezier 中填入的4个数字,即为上图中P二、P3点的x,y坐标。即P2 = (0.5, 0.15) P3 = (0.5, 0.9)。

咱们就以一个元素使用上述缓动函数,在2s内从left=0的位置移动到left=200的位置这样的一个动画来进行说明。具体的计算流程以下:

image.png

对于上图中缓动函数求值这一步,咱们经过已逝去时间/总时间算得 progress,这里progress表明的是贝塞尔曲线中x的值。

因为咱们没有创建x与y之间的映射关系,那么咱们如何根据x的值,计算贝塞尔曲线的y值呢?

值得庆幸的是,咱们拥有贝塞尔曲线的参数方程,咱们有函数 P ( t ) = x P(t) = x ,若是咱们可以经过x反计算出t的值,再使用t的值代入参数方程中便可获得y的值了。

因此,如今问题转化为了:如何求解方程 P ( t ) = x P(t) = x

首先咱们思考这样的一道题:
LeetCode-367.有效的彻底平方数

给定一个 正整数 num ,编写一个函数,若是 num 是一个彻底平方数,则返回 true ,不然返回 false 。

简单的讲就是说在不使用Math.sqrt的状况下,如何对一个数字进行开平方?

这里给你们介绍一种很实用的经过迭代的方式来解方程的方法: 牛顿法 (Newton's method)

牛顿法

牛顿法是一种用逼近的思想来求解方程根的方法。此处假设咱们须要求解 x 2 = 4 x^2 = 4 ,其实就是求解方程 x 2 4 = 0 x^2 - 4 = 0 的根,即求函数图像与x轴的交点。

image.png

求解的流程大体以下:

  1. 选取一个初始值 x n x_n
  2. 在函数图像 ( x n , f ( x n ) ) (x_n, f(x_n)) 处做函数的切线,该切线与x轴相交与 x n + 1 x_{n+1}
  3. 判断 f ( x n + 1 ) f(x_{n+1}) f ( x n ) f(x_n) 的值之间的差是否小于某一精度(精度值自行设定),若是达到精度则结束迭代,反之则重复步骤2.

f ( x n + 1 ) f(x_{n+1}) 的求解方法以下:

x n + 1 = x n f ( x n ) f ( x n ) x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}

以上,就是关于牛顿法的内容。

有了牛顿法这样的一个求解方程的利器,咱们如今能够解决上述的问题了: 求解方程 P ( t ) = x P(t) = x ,该问题等价于求解方程: P ( t ) x = 0 P(t) - x = 0

咱们能够写出一个函数来实现这个方法:

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值。

2、贝塞尔曲线重参数化

接着,咱们进入第二个应用场景。思考这样一个问题,我使用贝塞尔曲线定义一条路径L,如今我想让某一个物体沿着贝塞尔曲线的路径匀速运动。我该如何实现?

可能有的读者已经想到一个方法了:

我能够将参数 t [ 0 , 1 ] t\in[0,1] 平均的分红若干份,再将其代入贝塞尔曲线方程中以获得一系列的坐标,我再给这个物体设置刚刚计算出的坐标就行了。

这样作真的能够吗? 答案是否认的。咱们看这样一条贝塞尔曲线,我将参数 t [ 0 , 1 ] t\in[0,1] 平均的分红25分,则曲线上的25个点为以下: image.png

咱们能够看到,上图中,在曲线弯曲的厉害的地方,点位明显比较密集,反之在曲线弯曲的不厉害的地方地位则比较稀疏。这样势必会致使一个问题:在曲线弯曲的厉害的地方物体运动的慢,在弯曲的不厉害的地方物体运动的快。这并非咱们想要获得的匀速运动的效果。

若是咱们要获得匀速运动的效果该怎么作呢? 可能细心的读者已经发现了,咱们要作的不是将参数t进行均分,而是应该将贝塞尔曲线的总长度进行均分才对。对曲线长度均分后的效果以下: image.png

而后,咱们还但愿可以经过任意的曲线长度,找到所对应的参数t,再根据反算出的参数t,代入贝塞尔曲线方程中获得最终的坐标。

小结一下大体的步骤:

  1. 求解曲线总长度L
  2. 对曲线总长度L进行均分
  3. 求解任意曲线长度l所对应的参数t
  4. 根据参数t代入贝塞尔曲线方程获得最终坐标

如今解决第一个问题:如何求解曲线的总长度L。

最容易让人想到的一个办法就是将贝塞尔曲线分割成若干条直线,而后将这些直线的长度相加便可。可是随着分割的精度提升,运算量也是很大的。

可是这是一个很好的思想,微积分正是如此。因此,咱们是否可使用微积分的方法来求解曲线的长度呢?答案是确定的。 image.png

咱们用一小段的直线近似表示曲线,这一小段直线咱们用 d s ds 表示。那么整段曲线的长度则是在整个曲线上进行积分。以下:

S = L d s S = \int_{L}ds

对于 d s ds 能够用 d x 2 + d y 2 \sqrt{dx^2 + dy^2} 来表示。因为咱们是使用参数方程,由

d x d t = ϕ ( t )     d x = ϕ ( t ) d t \frac{dx}{dt} = \phi'(t) \\\ \\\ dx = \phi'(t)dt
d y d t = ψ ( t )     d y = ψ ( t ) d t \frac{dy}{dt} = \psi'(t) \\\ \\\ dy = \psi'(t)dt

能够获得

d s = d x 2 + d y 2 = ϕ 2 ( t ) + ψ 2 ( t ) d t ds = \sqrt{dx^2 + dy^2} = \sqrt{\phi'^2(t) + \psi'^2(t)}dt

因为 ϕ ( t ) \phi(t) ψ ( t ) \psi(t) 的函数表达式相同,而且上述式子表示的就是两点之间的距离。因此

ϕ 2 ( t ) + ψ 2 ( t ) d t = P ( t ) \sqrt{\phi'^2(t) + \psi'^2(t)}dt = ||P'(t)||

曲线总长度则为

S = 0 1 P ( t ) d t S = \int_0^1||P'(t)||dt

对于任意参数 t [ 0 , 1 ] t\in[0,1] 对应的曲线长度为:

L ( t ) = 0 t P ( x ) d x L(t) = \int_0^t||P'(x)||dx

如今另外一个问题呼之欲出了,如今咱们有了如何计算曲线长度的积分表达式,可是咱们如何计算积分呢?

这里介绍一种快速计算一重积分的方法:高斯-勒让德(Gauss-Lengendre)求积法

高斯-勒让德(Gauss-Lengendre)求积法

其中具体的原理就不过多介绍了,简单的来说,就是查表。只能感叹一句高斯是神。

公式以下:

a b f ( x ) d x = b a 2 i = 1 n w i f ( b a 2 x i + b + a 2 ) \int _ { a } ^ { b } f ( x ) dx = \frac{b - a}{2} \sum_{i=1}^{n}w_i f(\frac{b-a}{2}x_i + \frac{b + a}{2})

image.png

至此,咱们已经能够根据贝塞尔曲线的路径积分和高斯-勒让德求积法求的贝塞尔曲线的总长度和任意t时的长度了。接下来的问题来到了,如何根据任意长度L,求所对应的t?

这个问题是否是让人感受不多熟悉?没错,就是使用牛顿法进行求解。

t = a L ( a ) L ( a ) t = a - \frac{L(a)}{L'(a)}

其中

L ( a ) = P ( a ) L'(a) = ||P'(a)||

可是牛顿法也并非万能的,高次方程的解并非惟一的,例如这里的3次方程,可能有1个解,2个解,3个解。咱们使用牛顿法求解的根可能并非咱们想要的那一个根。因此按上述方法对贝塞尔曲线曲线进行重参数化可能会出现下面的这种状况:

image.png

如上图所示,图中的蓝色点位是对曲线长度进行均分后,使用牛顿法求得对应的t值,所对应的点位。咱们能够看出来这明显是有必定问题的,缘由就是因为方程有多个根,因为初始值选择的问题,没有求得咱们想要的那一个根。以下图所示:

image.png

如上图所示,由于初始点选择的问题,致使在迭代过程当中一些中间状态的点发生了“跳跃”的现象,从而找到了更远处的根,但这不是咱们想要的结果。那么如何避免出现这一情况呢?这里咱们退而求其次,使用二分法求根。

二分法求根

二分法始终都会找到离初始值最近的根,因此不会出现相似于牛顿法的“跳跃”现象。可是二分法所以也要付出执行效率不如牛顿法的代价。二分法做为一种经典算法,在此就不过多的赘述了。

咱们改成使用二分法进行方程求解后,能够获得正确的效果,以下图所示:

image.png

如今咱们已经完成了贝塞尔曲线的重参数化。

接着,让咱们进入下一个应用场景:

3、贝塞尔曲线分割

使用过Photoshop中钢笔工具的朋友们应该知道:在一条已有的路径中,咱们能够随意的往其中插入控制点。这样的过程就是分割贝塞尔曲线的过程,咱们将一条贝塞尔曲线一分为二,而且还要保证分割后的两条曲线的链接部分是平滑连续的。那么分割后的两条贝塞尔曲线的控制点应该是处于什么位置的呢?

image.png 如上图所示,有一条点A、B、C、D为控制点造成的贝塞尔曲线,咱们如今要在E点处对该条曲线进行分割,那么分割后的两条曲线的控制点分别应该是哪几个点呢?

咱们经过观察上图,直觉告诉我:左边的贝塞尔曲线的控制点看起来像是:A、F、I、E,右边的贝塞尔曲线的控制点像是:E、J、H、D。那么,其实是这样的嘛??

假设A、F、I、E就是分割后的左边贝塞尔曲线的控制点。

根据贝塞尔曲线的定义,由A、B、C、D组成的贝塞尔曲线的公式能够写为:

E = ( 1 t ) 3 A + 3 ( 1 t ) 2 t B + 3 ( 1 t ) t 2 C + t 3 D t [ 0 , 1 ] E = (1-t)^3A + 3(1-t)^2tB + 3(1-t)t^2C + t^3D\quad t\in[0,1]

那么左边的贝塞尔曲线A、F、I、E,能够表示为:

E l = ( 1 e ) 3 A + 3 ( 1 e ) 2 t F + 3 ( 1 e ) e 2 I + e 3 E e [ 0 , 1 ] (a) E_l = (1-e)^3A + 3(1-e)^2tF + 3(1-e)e^2I + e^3E\quad e\in[0,1] \tag{a}

上式中,F、I能够写为:

F = ( 1 t ) A + t B (b) F = (1 - t)A+ tB \tag{b}
I = ( 1 t ) F + t G = ( 1 t ) 2 A + 2 ( 1 t ) t B + t 2 C (c) I= (1 - t)F+ tG = (1-t)^2A+2(1-t)tB+t^2C \tag{c}

将式(b), (c)代入式子(a)中,化简可得:

E l = ( 1 e t ) 3 A + 3 ( 1 e t ) 2 t B + 3 ( 1 e t ) ( e t ) 2 C + ( e t ) 3 D e [ 0 , 1 ] , e t [ 0 , t ] E_l = (1-et)^3A + 3(1-et)^2tB + 3(1-et)(et)^2C + (et)^3D\quad e\in[0,1], et\in[0,t]

u = e t u = et ,则 u [ 0 , t ] u\in[0,t]

E l = ( 1 u ) 3 A + 3 ( 1 u ) 2 t B + 3 ( 1 u ) u 2 C + u 3 D u [ 0 , t ] (d) E_l = (1-u)^3A + 3(1-u)^2tB + 3(1-u)u^2C + u^3D\quad u\in[0,t] \tag{d}

咱们能够看出式(a)与式(d)的函数方程彻底相同,而且定义域也彻底相同。即A、F、I、E就是分割后左边贝塞尔曲线的控制点。同理的,对于右侧的贝塞尔曲线,咱们也可以证得E、J、H、D即为右侧贝塞尔曲线新的控制点。

接下来,咱们进入最后一个应用场景:使用贝塞尔曲线拟合一系列的点。

4、贝塞尔曲线拟合折线

绘制折线图表是一个很是常见的需求,咱们可使用canvas或者SVG进行折线图的绘制。咱们能够简单的将全部点简单的首尾相连便可,可是这样的折线图,未免也太生硬了叭!以下图:

image.png

若是你用过相似于Echarts的图表库,那么你会发现其中的折线图是这样的:

image.png

这样看起来的就柔和多了。这背后使用到的技术正是贝塞尔曲线。它将一条直线用一条贝塞尔曲线进行替换,以达到在转角处平滑过渡的效果。

image.png

如上图所示,咱们须要拟合A、B、I、J这一段折线。这里有一个简单的公式:

P i l = P i ( P i + 1 P i 1 ) e   P i r = P i + ( P i 1 P i + 1 ) e P_{il} = P_i - (P_{i+1} - P_{i-1})e \\\ P_{ir} = P_i + (P_{i-1} - P_{i+1})e

其中,e是用于控制曲线圆滑程度的参数, e [ 0 , 1 ] e\in[0,1]

对于起始点有: P i + 1 = P i P_{i+1} = P_{i} ,终止点有: P i 1 = P i P_{i-1} = P_{i}

根据上述公式,咱们实现的效果以下:

image.png

能够看出,整体效果仍是很是不错的。

以上就是本文中关于贝塞尔曲线的全部应用示例了。

总结

大体的总结一下,本文主要讲述了如下几个方面:

  1. 介绍了贝塞尔曲线的定义(递归定义及数学定义)
  2. 在数学定义中,揭示了贝塞尔曲线的的解析式中各项系数的分布规律(杨辉三角、二项式分布、组合数)
  3. 介绍了CSS中贝塞尔曲线用做缓动函数其背后的计算逻辑
  4. 介绍了贝塞尔曲线如何进行重参数化
  5. 介绍了牛顿法解方程以及它的一些问题(用二分法来规避,但运算速度会降低)
  6. 介绍了如何使用贝塞尔曲线使折线图变得更圆滑

但愿读者经过阅读本文对贝塞尔曲线有更深入的认识,各位能够经过编码的方式自我实现一下本文中提到了一些算法,对自个人编码能力和数学能力都有不小的提升。

若是你以为本文对你有用,还请点个赞👍哦~

相关文章
相关标签/搜索