用三阶贝塞尔曲线拟合圆

前言

由于贝塞尔曲线控制简便且具有极强的描述能力,它常被用来生成复杂的平滑曲线;圆形是一种很常用的普通图形,在计算机图形学中也有很多画圆的算法,本文想探究一下如何用三阶贝塞尔曲线拟合圆形。
在研究这个问题时,我从Stackoverflow上搜到了(4/3)tan(π/(2n))这个公式,她的几何意义如下图所示。该公式的值表达的具体意义可以描述为:由n段三阶贝塞尔曲线拟合圆形时,曲线端点到该端点最近的控制点的最佳距离是(4/3)tan(π/(2n))。


一开始看到这个值觉得很奇怪,想知道她是如何被推导出来的,于是花了一点功夫去调查,并自己求解证明,原来是一堆中学生就会的平面几何运算题。下面给出我的求解过程。

求解魔法数值

大家把这个用三阶贝塞尔曲线拟合圆形的数值叫做魔法数,可能是因为她的不同取值会影响到拟合的圆形的效果,这个值决定了贝塞尔曲线拟合圆形的误差。我通过在上面的几何图中添加辅助线、运用平面几何性质来求解该魔法数值。

辅助线及点位置说明

如上图命名各点,点O是圆心,P0、P3分别是圆弧(也是贝塞尔曲线)上的起点和终点,P1、P2是贝塞尔曲线的两个控制点, 点M2是线段P1P2的中点,C1、C2分别是线段P0P1和P2P3的中点,连接OM2并延长与P0P1的延长线交于点F1,过点P1作线段P0P3的垂线交于点F0。
点M1是线段C1M2和C2M2的中点的连线的中点,根据贝塞尔曲线上点的性质可知点M1是圆弧上的点,且是圆弧P0P3的中点,线段P0P3与OF1交于点M0.

证明及推算

一个隐含的条件(或者根据贝塞尔曲线的数学性质可证明的)是|P2P3|=|P0P1|,我们的目标就是要求出这个长度值,记为l(小写L)。
易知圆弧P0P3被射线OF1对称平分,且OF1与线段P0P3、C1C2和P1P2均垂直相交,又因为点C1、C2和M2是相关线段的中点,容易证明|M1M2| = |M0M2|/4,|P1F0| = |M0M2|,所以|M0M1| = (3/4)|M0M2| = (3/4)|P1F0|。
记|P1F0| = d,圆弧半径为r,∠P0-O-P3为θ,则∠P0-O-M0 = θ/2,|M0M1| = (3/4)d,|OM0| = |OM1| - |M0M1| = r - (3/4)d = r·cos(θ/2),整理等式为
(3/4)d = r - r·cos(θ/2) ····················· ①
因为线段P0P1与圆弧相切于点P0,OP0是圆弧的半径,容易证明∆P0-F0-P1与∆O-M0-P0相似,∠P1-P0-F0 = θ/2,则
d = l·sin(θ/2) ································ ②
由方程①②联立解得 l = (4/3)·r·(1-cos(θ/2))/sin(θ/2)
若圆弧半径为1,再由下面的三角函数二倍角公式推导
sin2α = 2·sinα·cosα
cos2α = (cosα)^2 - (sinα)^2 = 1 - 2·(sinα)^2
得到 l = (4/3)tan(θ/4),即是上面图中的值(4/3)tan(π/(2n))

用贝塞尔方程求解魔法数

上面用几何运算的方式求解了魔法数,还可以直接根据贝塞尔曲线方程代入特殊点坐标计算该数值。
用三阶贝塞尔曲线拟合圆形的问题可以简化为考虑拟合1/4圆弧,如下图圆弧P0P3即是端点为P0、P3,控制点为P1、P2的贝塞尔曲线,它们的坐标分别为P0 = (0,1), P1 = (h,1), P2 = (1,h), P3 = (1,0)


我们知道三阶贝塞尔曲线的一般方程如下


把上面的点坐标分别代入曲线方程,取t=0.5计算得到点坐标


另外根据贝塞尔曲线的数学性质可知曲线方程中t=0.5时的点一定在圆弧上,根据圆形方程定义,可得到下面的等式


这样,容易解出h的值为 h=(4/3)(sqrt(2)-1) ≈ 0.552284749831

用贝塞尔曲线画圆关键代码

……
private Paint mPaint;
private Path path = new Path();
private float[] mData = new float[8];               // 顺时针记录绘制圆形的四个数据点
private float[] mCtrl = new float[16];              // 顺时针记录绘制圆形的八个控制点
private static final float C = 0.552284749831f;     // 用来计算绘制圆形贝塞尔曲线控制点的位置的常数

private void initData() {
    mPaint = new Paint();
    mPaint.setColor(Color.BLACK);
    mPaint.setStrokeWidth(8);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setTextSize(60);
    // 初始化数据点
    mData[0] = 0;
    float mCircleRadius = 200;  //圆半径
    mData[1] = mCircleRadius;

    mData[2] = mCircleRadius;
    mData[3] = 0;

    mData[4] = 0;
    mData[5] = -mCircleRadius;

    mData[6] = -mCircleRadius;
    mData[7] = 0;

    // 初始化控制点
    float mDifference = mCircleRadius * C;  //圆形的控制点与数据点的差值
    mCtrl[0]  = mData[0]+ mDifference;
    mCtrl[1]  = mData[1];

    mCtrl[2]  = mData[2];
    mCtrl[3]  = mData[3]+ mDifference;

    mCtrl[4]  = mData[2];
    mCtrl[5]  = mData[3]- mDifference;

    mCtrl[6]  = mData[4]+ mDifference;
    mCtrl[7]  = mData[5];

    mCtrl[8]  = mData[4]- mDifference;
    mCtrl[9]  = mData[5];

    mCtrl[10] = mData[6];
    mCtrl[11] = mData[7]- mDifference;

    mCtrl[12] = mData[6];
    mCtrl[13] = mData[7]+ mDifference;

    mCtrl[14] = mData[0]- mDifference;
    mCtrl[15] = mData[1];
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(mCenterX, mCenterY);   // 将坐标系移动到画布中央
    canvas.scale(1,-1);                       // 翻转Y轴

    drawAuxiliaryLine(canvas);

    // 绘制贝塞尔曲线
    mPaint.setColor(Color.RED);
    mPaint.setStrokeWidth(6);
    path.moveTo(mData[0],mData[1]);

    path.cubicTo(mCtrl[0],  mCtrl[1],  mCtrl[2],  mCtrl[3],     mData[2], mData[3]);
    path.cubicTo(mCtrl[4],  mCtrl[5],  mCtrl[6],  mCtrl[7],     mData[4], mData[5]);
    path.cubicTo(mCtrl[8],  mCtrl[9],  mCtrl[10], mCtrl[11],    mData[6], mData[7]);
    path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15],    mData[0], mData[1]);

    canvas.drawPath(path, mPaint);
}

// 绘制辅助线
private void drawAuxiliaryLine(Canvas canvas) {
    // 绘制数据点和控制点
    mPaint.setColor(Color.GRAY);
    mPaint.setStrokeWidth(10);

    for (int i=0; i<8; i+=2){
        canvas.drawPoint(mData[i],mData[i+1], mPaint);
    }
    for (int i=0; i<16; i+=2){
        canvas.drawPoint(mCtrl[i], mCtrl[i+1], mPaint);
    }

    // 绘制辅助线
    mPaint.setStrokeWidth(4);
    for (int i=2, j=2; i<8; i+=2, j+=4){
        canvas.drawLine(mData[i],mData[i+1],mCtrl[j],mCtrl[j+1],mPaint);
        canvas.drawLine(mData[i],mData[i+1],mCtrl[j+2],mCtrl[j+3],mPaint);
    }
    canvas.drawLine(mData[0],mData[1],mCtrl[0],mCtrl[1],mPaint);
    canvas.drawLine(mData[0],mData[1],mCtrl[14],mCtrl[15],mPaint);
}
效果图

总结

这篇博文是在前一篇《贝塞尔曲线学习笔记》的基础上做的一个关于贝塞尔曲线应用的深入探索,是笔者在工作之余的一点学习收获,内容比较浅陋,主要的收获在于唤起了我的学习兴趣。关于前文主要求解的魔法数值,还应该深入讨论贝塞尔曲线拟合圆形的误差,Approximate a circle with cubic Bézier curves这篇文章中作了误差分析,并给出了一个更精确的魔法数值0.551915024494

Thanks To

How to create circle with Bézier curves?
Approximate a circle with cubic Bézier curves
Drawing a circle with Bézier Curves
用三次贝塞尔曲线拟合圆弧

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

推荐阅读更多精彩内容

  • 根据贝塞尔曲线的知识,我们知道三阶贝塞尔曲线的参数方程如下,其中A、B、C、D为四个控制点坐标,P(t)表示曲线上...
    月隐西边雨阅读 3,821评论 2 4
  • 谈谈贝塞尔曲线 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移… 然而虽...
    雨润听潮阅读 5,986评论 1 16
  • APK下载地址 1.贝塞尔曲线 以下公式中:B(t)为t时间下 点的坐标;P0为起点,Pn为终点,Pi为控制点 一...
    小鱼爱记录阅读 4,009评论 2 27
  • 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移... 然而虽然动画简单,...
    IAMDAEMON阅读 4,274评论 12 69
  • 本文主要内容为贝塞尔曲线原理解析并用 SurfaceView 实现其展示动画 关于SurfaceView 的使用,...
    涤生_Woo阅读 13,401评论 5 94