效果预览
使用四段三次贝塞尔曲线拟合圆
认识贝塞尔曲线
在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是计算机图形学中相当重要的参数曲线 (贝塞尔曲线通常用于生成平滑曲线,因为它们的计算成本较低并且可以产生高质量的结果) 。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝兹(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由保尔·德·卡斯特里奥于1959年运用德卡斯特里奥算法开发,以稳定数值的方法求出贝塞尔曲线。
三次贝塞尔曲线
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向信息。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。
曲线的参数形式为:
对于三次曲线,可由线性贝塞尔曲线描述的中介点 Q0、Q1、Q2,和由二次曲线描述的点 R0、R1所建构:
三次贝塞尔曲线的结构三次贝塞尔曲线演示动画,t 在[0,1]区间
使用贝塞尔曲线拟合圆
思路:近似圆的标准方法是将其分成四个相等的部分,并用三次贝塞尔曲线替换每个直角圆弧。
在 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)
}
2. 使用四段三次贝塞尔曲线,拟合圆
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)表示曲线上的每一点。
根据贝塞尔曲线的对称性,可以知道 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