使用四段三次 Bézier 曲线拟合圆
效果预览

使用四段三次贝塞尔曲线拟合圆
认识贝塞尔曲线
在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是计算机图形学中相当重要的参数曲线(贝塞尔曲线通常用于生成平滑曲线,因为它们的计算成本较低并且可以产生高质量的结果),为计算机矢量图形学奠定了基础。
贝塞尔曲线于 1962 年,由法国工程师皮埃尔·贝兹(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由保尔·德·卡斯特里奥于 1959 年运用德卡斯特里奥算法开发,以稳定数值的方法求出贝塞尔曲线。
三次贝塞尔曲线
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次贝塞尔曲线。曲线起始于P0 走向 P1,并从 P2 的方向来到 P3。一般不会经过 P1 或 P2;这两个点只是在那里提供方向信息。P0 和 P1之间的间距,决定了曲线在转而趋进 P2 之前,走向 P1 方向的“长度有多长”。 曲线的参数形式为:

附件未导入:671f651757c2e7efb48da5801e835d3cafe4eeba
对于三次曲线,可由线性贝塞尔曲线描述的中介点 Q0、Q1、Q2,和由二次曲线描述的点 R0、R1 所建构:
三次贝塞尔曲线的结构三次贝塞尔曲线演示动画,t 在 [0,1] 区间
附件未导入:fig.3 三次贝塞尔曲线计算 附件未导入:fig.4 三次贝塞尔曲线演示
MatheMatica 演示三次贝塞尔曲线的计算
p0 = {1, 4};p1 = {3, 5};p2 = {4, 3};p3 = {6, 4}; PA1[t_] := p0 + (p1 - p0)*t;PA2[t_] := p1 + (p2 - p1)*t;PA3[t_] := p2 + (p3 - p2)*t; PB1[t_] := (1 - t)*PA1[t] + t*PA2[t];PB2[t_] := (1 - t)*PA2[t] + t*PA3[t]; PC1[t_] := (1 - t)*PB1[t] + t*PB2[t]; Animate[Show[ ParametricPlot[PA1[t*a], {t, 0, 1}, PlotStyle -> {Thick, Blue}, PlotRange -> {{-1, 8}, {-1, 8}}, GridLines -> Automatic, GridLinesStyle -> LightGray, AxesLabel -> {"X", "Y"}], ParametricPlot[PA2[t*a], {t, 0, 1}, PlotStyle -> {Thick, Blue}], ParametricPlot[PA3[t*a], {t, 0, 1}, PlotStyle -> {Thick, Blue}], Graphics[{Brown, Thick, Line[{PA1[a], PA2[a]}] (*直接使用 a 作为参数*)}], Graphics[{Brown, Thick, Line[{PA2[a], PA3[a]}] (*直接使用 a 作为参数*)}], Graphics[{Green, Thick, Line[{PB1[a], PB2[a]}] (*直接使用 a 作为参数*)}], ParametricPlot[PC1[t*a], {t, 0, 1}, PlotStyle -> {Thick, Red}], Graphics[{PointSize[0.01],(*点的大小*)Black,(*点颜色*) Point[{p0, p1, p2, p3}],(*绘制所有点*)Black, Text[Style["p0", FontSize -> 12], p0, {0, 1.5}],(*{0, 1.5} 表示标签在点正上方*)Text[Style["p1", FontSize -> 12], p1, {0, 1.5}], Text[Style["p2", FontSize -> 12], p2, {0, 1.5}], Text[Style["p3", FontSize -> 12], p3, {0, 1.5}], GrayLevel[0.5],(*直线颜色*)Line[{p0, p1}],(*连接 p0 和 p1*) Line[{p1, p2}], Line[{p2, p3}],}], ImageSize -> Large], {a, 0, 1, 0.01}, AnimationRunning -> False, RefreshRate -> 60]
使用贝塞尔曲线拟合圆
思路:近似圆的标准方法是将其分成四个相等的部分,并用三次贝塞尔曲线替换每个直角圆弧。
在 Android 中,可以使用绘图库 Canvas 来绘制贝塞尔曲线,Canvas 提供 cubicTo 函数来构建一段三次 Bezier 曲线。
/** * 从最后一个点开始添加三次贝塞尔曲线,接近控制点 (x1,y1) 和 (x2,y2),并在 (x3,y3) 处结束。如果没有对此轮廓进行 moveTo() 调用,则第一个点将自动设置为 (0,0) */public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3) { nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);}- 通过一段三次贝塞尔曲线,拟合 1/4 圆
private val C = 0.551915024494f // 常量,绘制圆形贝塞尔曲线控制点的位置,拟合圆的最佳系数 /** * 计算锚点和控制点位置 */ private fun computePoints() { anchorPoint1.apply { x = radius y = 0f } anchorPoint2.apply { x = viewWidth y = radius } contPoint1.apply { x = radius + radius * C y = 0f } contPoint2.apply { x = viewWidth y = radius - radius * C } } @SuppressLint("DrawAllocation") override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val path = Path().apply { moveTo(anchorPoint1.x, anchorPoint1.y) cubicTo(contPoint1.x, contPoint1.y, contPoint2.x, contPoint2.y, anchorPoint2.x, anchorPoint2.y) } canvas.drawPath(path, paint) }- 使用四段三次贝塞尔曲线,拟合圆
class CircleBezierView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : View(context, attrs) { private val C = 0.551915024494f private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) private var centerX = 0 private var centerY = 0 private val radius = 200f // 圆的半径 private val fittingValue = radius * C // 控制点距离锚点的距离 private val anchors = FloatArray(8) // 顺时针记录绘制圆形的四个数据点 private val conts = FloatArray(16) // 顺时针记录绘制圆形的八个控制点 // 模拟从圆形变成心形 private var isAnimActivate = false private val duration = 1000f // 变化总时长 private var current = 0f // 当前已进行时长 private val count = 100f // 将时长总共划分多少份 private val piece = duration / count // 每一份的时长 init { paint.color = Color.BLACK paint.strokeWidth = 8f paint.style = Paint.Style.STROKE paint.textSize = 60f // 初始化数据点 anchors[0] = 0f anchors[1] = radius anchors[2] = radius anchors[3] = 0f anchors[4] = 0f anchors[5] = -radius anchors[6] = -radius anchors[7] = 0f // 初始化控制点 conts[0] = anchors[0] + fittingValue conts[1] = anchors[1] conts[2] = anchors[2] conts[3] = anchors[3] + fittingValue conts[4] = anchors[2] conts[5] = anchors[3] - fittingValue conts[6] = anchors[4] + fittingValue conts[7] = anchors[5] conts[8] = anchors[4] - fittingValue conts[9] = anchors[5] conts[10] = anchors[6] conts[11] = anchors[7] - fittingValue conts[12] = anchors[6] conts[13] = anchors[7] + fittingValue conts[14] = anchors[0] - fittingValue conts[15] = anchors[1] } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) centerX = w / 2 centerY = h / 2 } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) drawCoordinateSystem(canvas) // 绘制坐标系 canvas.translate(centerX.toFloat(), centerY.toFloat()) // 将坐标系移动到画布中央 canvas.scale(1f, -1f) // 翻转Y轴 drawAuxiliaryLine(canvas) // 绘制贝塞尔曲线 paint.color = Color.RED paint.style = Paint.Style.STROKE paint.strokeWidth = 8f val path = Path().apply { moveTo(anchors[0], anchors[1]) cubicTo(conts[0], conts[1], conts[2], conts[3], anchors[2], anchors[3]) cubicTo(conts[4], conts[5], conts[6], conts[7], anchors[4], anchors[5]) cubicTo(conts[8], conts[9], conts[10], conts[11], anchors[6], anchors[7]) cubicTo(conts[12], conts[13], conts[14], conts[15], anchors[0], anchors[1]) } canvas.drawPath(path, paint) toHeartAnim() } fun startToHeartAnim() { isAnimActivate = true postInvalidate() } private fun toHeartAnim() { if (!isAnimActivate) { return } current += piece if (current < duration) { anchors[1] -= 120 / count conts[7] += 80 / count conts[9] += 80 / count conts[4] -= 20 / count conts[10] += 20 / count postInvalidateDelayed(piece.toLong()) } }}魔法数值 C
对三次贝塞尔曲线拟合圆弧,一个关键的因素,魔数数值 C 的计算,开始之前,我们先观察不同 C 对应的三次贝塞尔曲线与圆弧的比较,如下图所示,根据贝塞尔曲线的对称性,图中蓝色斜线上的点对应三次贝塞尔曲线参数方程中的 t=0.5。当蓝色斜线、圆弧、贝塞尔曲线上的点三者重叠时的 C 即是拟合圆弧最佳魔法数值。

针对三次贝塞尔曲线拟合圆弧进行通用公式的求解,如下图所示

P1,P4 为锚点,P2,P3 为控制点。通过圆心 O 作一段圆弧 P1P4,P1P2,P4P3 为切线并且 P1P2=P4P3=C,E 点为圆弧 P1P4 的中点,这样,以 P1、P2、P3 和 P4 作为三次贝塞尔曲线的控制点,求得使曲线的中点经过 E 时,对应的 C 值。
根据贝塞尔曲线的知识,我们知道三次贝塞尔曲线的参数方程如下,其中P1、P2、P3、P4为四个控制点坐标,B(t)表示曲线上的每一点。
附件未导入:671f651757c2e7efb48da5801e835d3cafe4eeba
根据贝塞尔曲线的对称性,可以知道 E 点位于 B(0.5)处,代入公式可以得到:
B(0.5)=(1/8)·P1+(3/8)·P2+(3/8)·P3+(1/8)·P4
根据三角函数的性质,带入P1、P2、P3、P4 可以求得:
C=(4/3)·(1−𝑐𝑜𝑠(𝜃/2))/𝑠𝑖𝑛(𝜃/2)
这样就求出了使用三次贝塞尔曲线拟合圆弧的一般性公式。
我们要拟合 1/4 圆弧,也就是说在 θ=π/2 时,可以计算出该 C 值为 0.551915024494