在我研究生的时候,我上了一门OpenGL的课程。我非常喜欢OpenGL,因为它让我写的程序从此不再只局限于黑白的终端界面,而变得艳丽多彩。从那么课上,我也第一次认识了“贝塞尔曲线Bézier Curve”,它是那么的神奇,仅仅用几个点通过一个公式就能表现出一条优美的曲线。
这让一直热爱数学的我无法自拔,仿佛又回到了高中,坐在课堂里听着彪哥(我的高中数学老师)给我们讲述着各种曲线方程:抛物线、椭圆、双曲线……可惜怎么就从来没有提到过贝塞尔曲线呢。好吧,不在高考考纲之中。
贝塞尔方程的优点在于可以利用较少的存储几个点就能描绘出光滑的曲线或者曲面。而方程的计算也不会消耗太多的时间,真可谓是图形学中的一把利器。虽然后来并没有继续学习和使用OpenGL,但是也在其它很多方面又再次接触到了贝塞尔曲线,比如CSS中的一些渐变方法(Timing Function),iOS动画的时间方程。
前阵子在学习SVG(Scalable Vector Graphics)的一些基础知识,希望能在以后文章中添加一些演示图片和动画。对于优秀图形呈现,必然会有对贝塞尔曲线的支持,SVG也不例外,其中的path标签就可以通过简单地指定端点和中间的控制点(Control Point)来描绘出一条光滑的曲线。
比如下边例子就是SVG中Path的 Q指令和C指令做出的贝塞尔曲线,其中红色的曲线是二次贝塞尔曲线(Quadratic Bézier Curves)“画”的抛物线的一段,蓝色的则是三次贝塞尔曲线(Cubic Bézier curves)作出的水滴状的封闭曲线。
-
version="1.0"?>
-
-
-
d="M40 20 Q 100 200, 160 20" stroke="red" fill="none" /> -
-
d="M100 100 C 0 0, 200 0, 100 100" stroke="blue" fill="none" /> -
-
>
对于SVG的贝塞尔曲线,最让我感觉惊喜和惊奇的不是Q和C指令,而是其相对应的T指令和S指令。利用这两个指令可以继续不断地画出连续而又平滑的曲线,而所需要的指定的点控制点或目标点却要少于Q和C指令。
-
version="1.0"?>
-
-
-
d="M20 70 Q 55 120, 90 70 T 160 70" stroke="red" fill="none" /> -
-
d="M20 100 C45 50 75 50 100 100 S155 150 180 100" stroke="blue" fill="none" /> -
-
>
因为T和S指令会根据之前的Q和C指令的数据推算出所缺失的控制点的信息,以达到曲线能在交点处能够变得平滑。在本文,将通过数学方面的知识来了解一下它们之间的等式关系,今后使用时也能更了解隐含的控制点的位置,以便达到亟需的目标和避免不必要的错误。
二次贝塞尔曲线
假如P代表曲线的终端结点,C代表曲线的控制结点,那么对于二次根据二次贝塞尔曲线的定义,我们可以得到如下Q指令下的曲线方程:
B0(t) = (1-t)2P0 + 2(1-t)tC1 + t2P1 (0 ≤ t ≤ 1)
对于T指令的曲线,根据定义也可以得到如下方程:
B1(t) = (1-t)2P1 + 2(1-t)tC2 + t2P2 (0 ≤ t ≤ 1)
其中P1
是之前指令的终点作为起点,而P2
在已知的新给定点,我们所想要知道的则是C2
的值或者C2
与P1,2,3
之间的关系。
根据高等数学的知识知道,所谓在某一点连续是指在该点方程有相同的值。因为B0(1) = B1(0) = P1
所以表明曲线是连续性,这点其实也是因为前后两个曲线以P1
为交点所决定的。而要曲线在该点是光滑的,首先除了是连续的外,还需要在该点的导数是一致的。所以就需要先来求二次贝塞尔曲线的关于t的导数:
B0'(t) = -2(1-t)P0 + (-2tC1) + 2(1-t)C1 + 2tP1
= 2(1-t)(C1-P0) + 2t(P1-C1)
同样类推得到:
B1'(t) = 2(1-t)(C2-P1) + 2t(P2-C2)
为了使得曲线是光滑的,必须要满足B0'(1) = B1'(0)
,于是就有
2*(1-1)*(C1-P0) + 2*1*(P1-C1) = 2*(1-0)(C2-P1) + 2*0*(P2-C2)
2(P1-C1) = 2(C2-P1)
C2 = 2P1 - C1
也就是说为了连续,前一条曲线的终点必将成为后一条曲线的起点;而为了光滑前一条曲线的终点和控制点的位置已经决定了后一条曲线的控制点的位置,所以只需要再给出一个终点的位置即可。
三次贝塞尔曲线
对于三次贝塞尔曲线也是一样的,我们先通过曲线方程得到方程的导数,进而通过导数值相同以保证曲线是连续的
B0(t) = (1-t)3P0 + 3(1-t)2tC0 + 3(1-t)t2C1 + t3P1
B1(t) = (1-t)3P1 + 3(1-t)2tC2 + 3(1-t)t2C3 + t3P2
曲线方程的导数方程为:
B0'(t) = -3(1-t)2P0 + (-6(1-t)tC0) + 3(1-t)2C0 + (-3t2C1) + 3(1-t)tC1 + 3t2P1
= 3(1-t)2(C0-P0) + 6(1-t)t(C1-C0) + 3t2(P1-C1)
B1'(t) = 3(1-t)2(C2-P1) + 6(1-t)t(C3-C2) + 3t2(P2-C3)
类似于之前的演算过程,通过B0'(1) = B1'(0)
我们就可以得到控制点和起止点之间的关系了:
3*(1-1)2*(C0-P0) + 6*(1-1)*1*(C1-C0) + 3*12*(P1-C1) = 3*(1-0)2*(C2-P1) + 6*(1-0)*0*(C3-C2) + 3*02*(P2-C3)
3(P1-C1)=3(C2-P1)
C2 = 2P1 – C1
从最后的结果可以看出,前一条曲线的最后一个控制点和终点的位置已经决定了下一条曲线起点和第一个控制点的位置,因此对于SVG的S指令只需要再给出第二个控制点和终点的位置即可。另外还可以二次贝塞尔曲线和三次贝塞尔曲线在该已经点和隐含点之间的关系保持高度一致性。我们也可以根据这简单的关系,找到各种端点和控制点的准确位置,进而方便观察SVG中使用Q、T、C、S所作曲线的变换。
-
version="1.0"?>
-
-
-
d="M20 70 Q 55 120, 90 70 T 160 70" stroke="red" fill="none" /> -
cx="20" cy="70" r="2" fill="brown"/> -
cx="55" cy="120" r="2" fill="brown"/> -
cx="90" cy="70" r="2" fill="brown"/> -
cx="125" cy="20" r="2" fill="none" stroke="brown"/> -
cx="160" cy="70" r="2" fill="brown"/> -
-
points="20 70 55 120" stroke="pink"/> -
points="55 120 90 70" stroke="pink"/> -
-
points="90 70 125 20" stroke="pink" stroke-dasharray="5,5"/> -
points="125 20 160 70" stroke="pink" stroke-dasharray="5,5"/> -
-
d="M20 100 C45 50 75 50 100 100 S155 150 180 100" stroke="blue" fill="none" /> -
-
cx="20" cy="100" r="2" fill="darkblue"/> -
cx="45" cy="50" r="2" fill="darkblue"/> -
cx="75" cy="50" r="2" fill="darkblue"/> -
cx="100" cy="100" r="2" fill="darkblue" /> -
cx="125" cy="150" r="2" fill="none" stroke="darkblue"/> -
cx="155" cy="150" r="2" fill="darkblue"/> -
cx="180" cy="100" r="2" fill="darkblue"/> -
-
points="20 100 45 50" stroke="cyan"/> -
points="75 50 100 100" stroke="cyan"/> -
-
points="100 100 125 150" stroke="cyan" stroke-dasharray="5,5"/> -
points="155 150 180 100" stroke="cyan"/> -
>
- Bézier curve – Wikipedia, the free encyclopedia
- Paths – SVG 1.1 (Second Edition)
- Paths – SVG | MDN
- Easing Functions Cheat Sheet
- Animated Bézier Curves – Jason Davies
- JavaScript port of Webkit CSS cubic-bezier – mckamey