背景:
给一系列顶点,如果只是用直线将其中的各个点依次连接起来,最终形成一个折线图,这种很容易实现。但是现实中事物的变化往往具有连续的特性,即使是给定了一系列离散的点,基于以往的生活经验,人们也更愿意接受那种曲线连接的趋势图。可是在程序中绘制直线很容易,要是绘制曲线将各个顶点连接起来,这又要如何实现呢?一种很直观的思路就是将连接各点的直线替换成平滑的曲线,只要各段曲线在顶点处是平滑的过度,那么对应的曲线图就是所需的了。因此问题变成了寻找一种容易实现的曲线来连接各个顶点。
工具--贝塞尔曲线:
计算机图形学中有一类很常用的曲线,俗称贝塞尔曲线。1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名是为贝塞尔曲线。很多程序语言都有实现贝塞尔曲线的API,而该曲线本身也拥有强大的近似其它曲线的能力,即使一条不能够胜任,那么分段的多条贝塞尔曲线也足够用来近似我们想绘制的曲线。
贝塞尔曲线数学表示:
一阶贝塞尔曲线:给定点P0、P1,一阶贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:
其中P0和P1为两个端点,P对应于贝塞尔曲线上的点,随着t在[0,1]中变化,P点的集合构成一条连接P0与P1的线段。
二阶贝塞尔曲线:当引入一个控制点P1的时候,就可以生成二阶贝塞尔曲线,它是一个由二次函数描述的曲线,最多有一个顶点。
如下图所示,P点的集合构成一个抛物线
这里解释下上图的绿线是如何产生的:
首先,我们已知端点P0、P2以及控制点P1,那么如何确定确定当t取某个固定值时位于贝塞尔曲线上的点P?一种简单的方式可以通过贝塞尔曲线的公式,算出P的x和y坐标。但如何通过几何画法来计算出来呢?
根据贝塞尔曲线的定义,首先P0A/P0P1 = t,P1B/P1P2 = t,这样我们可以分别确定点A与点B。然后连接AB,取AP/AB = t,那么P点就是贝塞尔曲线上的点了。
三阶贝塞尔曲线:
三阶贝塞尔曲线可以用一个三次函数描述,最多拥有两个拐点。用来做两点之间的曲线连接已经够用了。我们来看下它的直观形式:
一般参数公式:
给定点P0、P1、…、Pn,其贝塞尔曲线即:
公式说明
1.开始于P0并结束于Pn的曲线,即所谓的端点插值法属性。
2.曲线是直线的充分必要条件是所有的控制点都位在曲线上。同样的,贝塞尔曲线是直线的充分必要条件是控制点共线。
3.曲线的起始点(结束点)相切于贝塞尔多边形的第一节(最后一节)。
4.一条曲线可在任意点切割成两条或任意多条子曲线,每一条子曲线仍是贝塞尔曲线。
5.一些看似简单的曲线(如圆)无法以贝塞尔曲线精确的描述,或分段成贝塞尔曲线(虽然当每个内部控制点对单位圆上的外部控制点水平或垂直的的距离为时,分成四段的贝兹曲线,可以小于千分之一的最大半径误差近似于圆)。
6.位于固定偏移量的曲线(来自给定的贝塞尔曲线),又称作偏移曲线(假平行于原来的曲线,如两条铁轨之间的偏移)无法以贝兹曲线精确的形成(某些琐屑实例除外)。无论如何,现存的启发法通常可为实际用途中给出近似值
已知P0、P1...PN如何确定贝塞尔曲线上的点呢?
如上图所示,存在顶点00,05;控制点01,02,03,04;实际上这是一条5阶贝塞尔曲线。
首先我们将点00到05连接起来,这样它会有5条边,这些边用棕色表示。
针对边00-01,我们取一个点10,使得该点将边00-01分成比例为t和1-t的两部分。针对每条边我们都取一个这样的点。然后将这一系列点再次连接起来,这次会有4条边,在上图用墨绿色表示。注意到我们这样操作之后,边会比前面少一条。
重复上面的操作,直到只有一条边。在上图中用绿色表示,我们取到点50,该点将这条边分成t与(1-t)的两部分。点50就是最终在贝塞尔曲线上的点。上述操作可以用如下公式表示:
可以查看原文更详细的解释
回到我们最初的问题
我们已经了解关于贝塞尔曲线的公式以及几何画法,但是要如何来解决我们用曲线来连接各个顶点的问题呢?
前面已经提到,对于两个点之间我们可以使用三阶贝塞尔曲线来连接,这样通过多段贝塞尔曲线相连,就可以得到我们想要的曲线。而三阶贝塞尔曲线需要两个控制点来确定,很显然贝塞尔曲线不一定通过控制点,但是肯定通过端点。所以给定的顶点只能做端点,那问题就变成了如何计算所需要的控制点?
首先要保证曲线在顶点处连续,就要求左边曲线在顶点处的切线和右边曲线在顶点处的切线一致。即函数的左导数等于右导数。
根据前面的公式说明3 曲线的起始点(结束点)相切于贝塞尔多边形的第一节(最后一节),我们知道,保持连续的必要条件是顶点和它前后的控制点在同一条直线上,而该直线就是曲线在该顶点的切线。
这里有一种思路:穿过已知点画平滑曲线;英文原文地址,这里也有一篇文章说的是用lua语言来实现的:开放的多点贝塞尔曲线实现
总结一下如下图所示:
如上图所示:如果需要绘制一条通过点A、B、C的曲线,我们需要计算各条用于连接的贝塞尔曲线的控制点。
以顶点B为例:
1、取AB和BC的中点E、F,并连接E、F
2、在EF上取点D,使得FD/DE = BC/AB
3、将直线EF按照矢量DB平移到通过B点,并且使得平移后的D和B点重合
4、得到E'与F'点用作贝塞尔曲线的控制点。
将上述算法应用于多边形的各个顶点,可以计算出2*n个控制点(每个顶点对应两个控制点)
下面是一种利用OC代码的实现(实现还比较粗糙在获,取控制点之后直接绘制了曲线,实际应用中应该先缓存起来等到绘制的时候再使用控制点。)
-(void) getControlPointx0:(CGFloat)x0 andy0:(CGFloat)y0
x1:(CGFloat)x1 andy1:(CGFloat)y1
x2:(CGFloat)x2 andy2:(CGFloat)y2
x3:(CGFloat)x3 andy3:(CGFloat)y3
path:(UIBezierPath*) path
{
CGFloat smooth_value =0.6;
CGFloat ctrl1_x;
CGFloat ctrl1_y;
CGFloat ctrl2_x;
CGFloat ctrl2_y;
CGFloat xc1 = (x0 + x1) /2.0;
CGFloat yc1 = (y0 + y1) /2.0;
CGFloat xc2 = (x1 + x2) /2.0;
CGFloat yc2 = (y1 + y2) /2.0;
CGFloat xc3 = (x2 + x3) /2.0;
CGFloat yc3 = (y2 + y3) /2.0;
CGFloat len1 = sqrt((x1-x0) * (x1-x0) + (y1-y0) * (y1-y0));
CGFloat len2 = sqrt((x2-x1) * (x2-x1) + (y2-y1) * (y2-y1));
CGFloat len3 = sqrt((x3-x2) * (x3-x2) + (y3-y2) * (y3-y2));
CGFloat k1 = len1 / (len1 + len2);
CGFloat k2 = len2 / (len2 + len3);
CGFloat xm1 = xc1 + (xc2 - xc1) * k1;
CGFloat ym1 = yc1 + (yc2 - yc1) * k1;
CGFloat xm2 = xc2 + (xc3 - xc2) * k2;
CGFloat ym2 = yc2 + (yc3 - yc2) * k2;
ctrl1_x = xm1 + (xc2 - xm1) * smooth_value + x1 - xm1;
ctrl1_y = ym1 + (yc2 - ym1) * smooth_value + y1 - ym1;
ctrl2_x = xm2 + (xc2 - xm2) * smooth_value + x2 - xm2;
ctrl2_y = ym2 + (yc2 - ym2) * smooth_value + y2 - ym2;
[path addCurveToPoint:CGPointMake(x2, y2) controlPoint1:CGPointMake(ctrl1_x, ctrl1_y)controlPoint2:CGPointMake(ctrl2_x, ctrl2_y)];
}
代码中的smooth_value是一个缩放值,取值范围为[0,1]。通过调整这个值可以控制曲线的锐度。
给定一组测试顶点如下:
CGFloat dx =20;
CGFloat x0 =0+ dx;
CGFloat y0 =0+ dx;
CGFloat x1 =80+ dx;
CGFloat y1 =120+ dx;
CGFloat x2 =150+ dx;
CGFloat y2 =200+ dx;
CGFloat x3 =200+ dx;
CGFloat y3 =50+ dx;
调用的代码如下:
UIBezierPath* path = [[UIBezierPathalloc]init];
[pathmoveToPoint:CGPointMake(x1, y1)];
[selfgetControlPointx0:x0andy0:y0x1:x1andy1:y1x2:x2andy2:y2x3:x3andy3:y3path:path];
[selfgetControlPointx0:x1andy0:y1x1:x2andy1:y2x2:x3andy2:y3x3:x0andy3:y0path:path];
[selfgetControlPointx0:x2andy0:y2x1:x3andy1:y3x2:x0andy2:y0x3:x1andy3:y1path:path];
[selfgetControlPointx0:x3andy0:y3x1:x0andy1:y0x2:x1andy2:y1x3:x2andy3:y2path:path];
[pathstroke];
效果:
可是虽然实现了用曲线包围多边形,但是依然没有实现我们的需求,用曲线连接各个顶点...
其实走到这一步已经离我们的目标很近了!只需要修改一下我们生成贝塞尔曲线的调用方式
[pathmoveToPoint:CGPointMake(x0, y0)];
[selfgetControlPointx0:0andy0:0x1:x0andy1:y0x2:x1andy2:y1x3:x2andy3:y2path:path];
[selfgetControlPointx0:x0andy0:y0x1:x1andy1:y1x2:x2andy2:y2x3:x3andy3:y3path:path];
[selfgetControlPointx0:x1andy0:y1x1:x2andy1:y2x2:x3andy2:y3x3:250andy3:0path:path];
[pathstroke];
效果如下:
这里需要注意的是要处理一下起始点和结束点。上面设置的为(0,0)和(250,0);这两个点是原有的点集没有的,根据需要可以适当设置,会影响到第一段和最后一段曲线的转向。