使用贝塞尔曲线绘制多点连接曲线

背景:

       给一系列顶点,如果只是用直线将其中的各个点依次连接起来,最终形成一个折线图,这种很容易实现。但是现实中事物的变化往往具有连续的特性,即使是给定了一系列离散的点,基于以往的生活经验,人们也更愿意接受那种曲线连接的趋势图。可是在程序中绘制直线很容易,要是绘制曲线将各个顶点连接起来,这又要如何实现呢?一种很直观的思路就是将连接各点的直线替换成平滑的曲线,只要各段曲线在顶点处是平滑的过度,那么对应的曲线图就是所需的了。因此问题变成了寻找一种容易实现的曲线来连接各个顶点。

工具--贝塞尔曲线:

       计算机图形学中有一类很常用的曲线,俗称贝塞尔曲线。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);这两个点是原有的点集没有的,根据需要可以适当设置,会影响到第一段和最后一段曲线的转向。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容