认识贝塞尔曲线
前言
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。"贝赛尔曲线"是由法国数学家Pierre Bézier所发明,由此为计算机矢量图形学奠定了基础。
前面的章节我们介绍了弧线的绘制,这节我们将在 Canvas 中绘制常见的两种贝塞尔曲线,二次贝塞尔曲线 和三次贝塞尔曲线。如果后续有时间的话,可以抽时间来详细讲解一下贝塞尔曲线,下面我们还是简单的介绍一下贝塞尔曲线吧,以便于我们更好的理解与应用。
贝塞尔曲线
1. 线性贝塞尔曲线
线性贝塞尔曲线如上图中所示,也就是 点D' 从线段 D点 运动到 F点 的轨迹。
此处我们假设 AB 距离为 t(t ∈ [0, 1]), AC 距离为1;那么:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A B ‾ A C ‾ = t ( 1 − 1 ) \frac{\overline{AB}}{\overline{AC}} = t \qquad(1 - 1) </math>ACAB=t(1−1)
根据相似三角形原理:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A E ‾ A D ‾ = t ( 1 − 2 ) \frac{\overline{AE}}{\overline{AD}} = t \qquad(1- 2) </math>ADAE=t(1−2)
则
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A E ‾ = t A D ‾ ( 1 − 3 ) \overline{AE} = t\overline{AD} \qquad(1-3) </math>AE=tAD(1−3)
则AE的距离就等于:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A E ‾ = x E − x A ( 1 − 4 ) \overline{AE} = x_{E} - x_{A} \qquad(1-4) </math>AE=xE−xA(1−4)
同理AD的距离就等于:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> A D ‾ = x D − x A ( 1 − 5 ) \overline{AD} = x_{D} - x_{A} \qquad(1-5) </math>AD=xD−xA(1−5)
将 (1-4) 与(1-5)带入 (1-3)可得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x E − x A = t ( x D − x A ) ( 1 − 6 ) x_{E} - x_{A} = t(x_{D} - x_{A}) \qquad(1-6) </math>xE−xA=t(xD−xA)(1−6)
如图所示:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x E = x B ; x D = x C ( 1 − 7 ) x_{E} = x_{B};x_{D} = x_{C} \qquad(1-7) </math>xE=xB;xD=xC(1−7)
带入(1-6)得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x B − x A = t ( x c − x A ) ( 1 − 8 ) x_{B} - x_{A} = t(x_{c} - x_{A}) \qquad(1-8) </math>xB−xA=t(xc−xA)(1−8)
最后计算可得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x B = x A + ( x C − x A ) t ( 1 − 9 ) x_{B} = x_{A} + (x_{C} - x_{A})t \qquad(1-9) </math>xB=xA+(xC−xA)t(1−9)
同理可得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> y B = y A + ( y C − y A ) t ( 1 − 10 ) y_{B} = y_{A} + (y_{C} - y_{A})t \qquad(1-10) </math>yB=yA+(yC−yA)t(1−10)
最后可总结出B的坐标为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P B ( x ∣ y ) = P A ( x ∣ y ) + ( P C ( x ∣ y ) − ( P A ( x ∣ y ) ) t ( 1 − 11 ) P_{B}\left(x \mid y\right) = P_{A}\left(x \mid y\right) + (P_{C}\left(x \mid y\right) - (P_{A}\left(x \mid y\right))t \qquad(1-11) </math>PB(x∣y)=PA(x∣y)+(PC(x∣y)−(PA(x∣y))t(1−11)
我们把公式简化一下
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P B ( t ) = P A + ( P C − P A ) t = ( 1 − t ) P A + t P C ( 1 − 12 ) P_{B}(t) = P_{A} + (P_{C} - P_{A})t = (1-t)P_{A} + tP_{C} \qquad(1-12) </math>PB(t)=PA+(PC−PA)t=(1−t)PA+tPC(1−12)
到这我们就把线性贝塞尔曲线的求线上任一点的坐标公式给推导出来了。但是因为在 Canvas 中已经有了绘制直线的方法了,也就不再赘述了。其实利用这种方法来进行分解,可能很多小伙伴也已经回忆起来,当时我们学习向量的时候,三维中的向量,可以分解x,y,z各个方向上的分向量,感兴趣的小伙伴可以去找资料学习一下,后边也会提供一些写得更为详细的一些文章。
2. 二次贝塞尔曲线
我们从上边贝塞尔曲线图中的二次贝塞尔曲线中可以看到,我们在原来两个点的基础上增加到三个点,A点的运动轨迹则为二次贝塞尔曲线:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P B ′ ( t ) = P B + ( P C − P B ) t = ( 1 − t ) P B + t P C ( 2 − 1 ) P_{B'}(t) = P_{B} + (P_{C} - P_{B})t = (1-t)P_{B} + tP_{C} \qquad(2-1) </math>PB′(t)=PB+(PC−PB)t=(1−t)PB+tPC(2−1)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P C ′ ( t ) = P C + ( P E − P C ) t = ( 1 − t ) P C + t P E ( 2 − 2 ) P_{C'}(t) = P_{C} + (P_{E} - P_{C})t = (1-t)P_{C} + tP_{E} \qquad(2-2) </math>PC′(t)=PC+(PE−PC)t=(1−t)PC+tPE(2−2)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P A ( t ) = P B ′ + ( P C ′ − P B ′ ) t = ( 1 − t ) P B ′ + t P C ′ ( 2 − 3 ) P_{A}(t) = P_{B'} + (P_{C'} - P_{B'})t = (1-t)P_{B'} + tP_{C'} \qquad(2-3) </math>PA(t)=PB′+(PC′−PB′)t=(1−t)PB′+tPC′(2−3)
我们将B' 与 C' 带入(2-3)可得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P A ( t ) = ( 1 − t ) 2 P B + 2 t ( 1 − t ) P C + t 2 P E P_{A}(t) = (1-t)^2P_{B} + 2t(1-t)P_{C} + t^2P_{E} </math>PA(t)=(1−t)2PB+2t(1−t)PC+t2PE
通过我们上边的计算,我们是否可以找到点什么规律呢?二次贝塞尔曲线,实际上就是两个线性贝塞尔曲线的叠加,那三次贝塞尔曲线呢?
3. 三次贝塞尔曲线
我们通过上边的动图可以看出,三次贝塞尔曲线,可以看做是两个二次贝塞尔曲线的叠加,图中我也绘制出了三次贝塞尔曲线中的2条二次贝塞尔曲线。感兴趣的 jym 可以去推算一下。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P Q ( t ) = ( 1 − t ) 3 P G + 3 t ( 1 − t ) 2 P H + 3 t 2 ( 1 − t ) P I + t 3 P J P_{Q}(t) = (1-t)^3P_{G} + 3t(1-t)^2P_{H} + 3t^2(1-t)P_{I} + t^3P_{J} </math>PQ(t)=(1−t)3PG+3t(1−t)2PH+3t2(1−t)PI+t3PJ
4. n次贝塞尔曲线
定义:给顶点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 , P 1 , ... , P n P_{0},P_{1},\dots,P_{n} </math>P0,P1,...,Pn,则n次贝塞尔曲线为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P ( t ) = ∑ i = 0 n ( n i ) P i ( 1 − t ) n − i t i = ( n 0 ) P 0 ( 1 − t ) n t 0 + ⋯ + ( n n ) P n ( 1 − t ) 0 t n P(t) = \sum_{i=0}^n \left(\begin{matrix}n\\i\end{matrix}\right)P_{i}(1-t)^{n-i}t^i=\left(\begin{matrix}n\\0\end{matrix}\right)P_{0}(1-t)^nt^0 + \dots + \left(\begin{matrix}n\\n\end{matrix}\right)P_{n}(1-t)^0t^n </math>P(t)=i=0∑n(ni)Pi(1−t)n−iti=(n0)P0(1−t)nt0+⋯+(nn)Pn(1−t)0tn
贝塞尔曲线的特性
相对于曲线的推导公式这些,可能我们对其性质我们更应该有所了解,因为我们再应用的时候,就是利用的它特有的性质做一系列的处理。
特性1:各项系数之和为1
从上边曲线通式中任一系数B可以展开成如下形式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> B i , n ( t ) = C n i t i ( 1 − t ) n − i = n ! i ! ( n − i ) ! t i ( 1 − t ) n − i ( T 1 − 1 ) B_{i,n}(t) = C_{n}^it^i(1-t)^{n-i} = \frac{n!}{i!(n-i)!}t^i(1-t)^{n-i} \qquad(T_{1} -1) </math>Bi,n(t)=Cniti(1−t)n−i=i!(n−i)!n!ti(1−t)n−i(T1−1)
从0到n,对所有系数B的进行求和,不难发现最后的结果可以化简成二项式的形式,即:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ∑ i = 1 n B i , n = ∑ i = 1 n C n i t i ( 1 − t ) n − i = ( t + ( 1 − t ) ) n = 1 n \sum_{i=1}^nB_{i,n} =\sum_{i=1}^nC_{n}^it^i(1-t)^{n-i} = (t + (1-t))^n = 1^n </math>i=1∑nBi,n=i=1∑nCniti(1−t)n−i=(t+(1−t))n=1n
特性2:曲线的起始点和终点对应第一个和最后一个控制点
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 0 x = { 1 , x = 0 0 , x ≠ 0 ( T 2 − 1 ) 0^x=\begin{cases}1,\quad x = 0\\0,\quad x ≠ 0 \end{cases} \qquad(T_{2}-1) </math>0x={1,x=00,x=0(T2−1)
分别计算出
当 t=0 (起始点 i = 0)结合公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( T 1 − 1 ) (T_{1}-1) </math>(T1−1) 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( T 2 − 1 ) (T_{2}-1) </math>(T2−1) 可得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> B 0 , n ( 0 ) = n ! 0 ! ( n ) ! 0 0 ( 1 ) n = 1 B_{0,n}(0) = \frac{n!}{0!(n)!}0^0(1)^{n} = 1 </math>B0,n(0)=0!(n)!n!00(1)n=1
当 t=1 (结束点 i = n)结合公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( T 1 − 1 ) (T_{1}-1) </math>(T1−1) 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( T 2 − 1 ) (T_{2}-1) </math>(T2−1) 可得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> B n , n ( 1 ) = n ! 0 ! ( n ) ! 1 n ( 0 ) 0 = 1 B_{n,n}(1) = \frac{n!}{0!(n)!}1^n(0)^{0} = 1 </math>Bn,n(1)=0!(n)!n!1n(0)0=1
因为各项系数之和为1,这两种情况下其他点的系数都为0,所以得出结论:曲线的起始点和终点对应第一个和最后一个控制点。这个性质可以帮助我们绘制曲线时能进行部分精确控制。
特性3:凸包性
贝塞尔曲线被包含了所有控制点的最小凸多边形所包含, 这里要和控制点依次围成的最小多边形区分开,我们可以从上边的贝塞尔曲线中能看得到效果。这个特性可以帮助我们通过控制点的凸包来限制规划曲线的范围。
特性4:一阶导数
贝塞尔曲线起始点和第一个控制点相切,终止点和最后一个控制点相切 。 贝塞尔曲线 t 是由左到右从0到1增加的,那么贝塞尔曲线在 t=0 时的导数是和直线 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 P 1 P_{0}P_{1} </math>P0P1 的斜率(导数)是相同, t=1 时的导数是和直线 <math xmlns="http://www.w3.org/1998/Math/MathML"> P n − 1 P n P_{n-1}P_{n} </math>Pn−1Pn的斜率(导数)是相同。
特性5:几何不变性
指某些几何特性不随坐标变换而变化的特性,贝塞尔曲线的形状仅与控制多边形各顶点的相对位置有关,而与坐标系的选择无关。
- 将所有的控制点按照同样的向量平移之后,得到的新曲线形状没有改变。
- 对控制点做旋转变换也不会改变曲线的形状。
贝塞尔曲线的应用
根据前面针对贝塞尔曲线生成算法特别是系数B的研究,不难发现一个规律:贝塞尔曲线的次数会随着控制点的增加提高,而高次多项式的计算必然会增加计算的成本。
实际应用中的贝塞尔曲线,是根据实际场景构造若干段三次或者四次的曲线(Canvas 中只提供了二次与三次贝塞尔曲线方法),再将它逐段拼接而形成的。这样就意味着,需要想办法在接合处保持一定的连续条件。
在确定拼接之前,我还是觉得有必要说一下连续性的定义:
- 位置连续:即两条曲线在某处的点坐标相同,又称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> C 0 C_{0} </math>C0 连续
- 斜率连续:在满足1的条件上,第一段曲线的末端切矢量与第二段曲线的首端切矢方向相同,模长相等,又称 <math xmlns="http://www.w3.org/1998/Math/MathML"> C 1 C_{1} </math>C1 连续
- 曲率连续:在满足2的条件上,连接处曲率相等且满足主法线一致。
所以我们要绘制平滑的曲线,在拼接时我们需要满足如下条件
- 首尾控制点相接: 两条曲线的起始点重合。
- 连接处的四个控制点共线: 四个控制点(第一段的最末两个控制点,第二段的最初两个控制点,其中第一段末端和第二段起始控制点重合)共线。
上面通过对贝塞尔曲线的介绍,相信大家也或多或少有了一些了解,下面我们来看看,在 Canvas 中我们怎么来运用贝塞尔曲线来进行图形的绘制。
Canvas 中的二次贝塞尔曲线
语法:
js
ctx.moveTo(startX, startY);
ctx.quadraticCurveTo(controlX, controlY, endX, endY);
二次贝塞尔曲线的用法跟弧线 arcTo 方法很是相似,只是没有 radius 参数,(controlX, controlY) 仍然为控制点坐标,(endX, endY) 为结束点坐标。
从前面的知识点我们已经知道,二次贝塞尔曲线需要 3 个控制点,而 quadraticCurveTo 方法中只提供了一个控制点和一个结束点,则开始点需要我们用 moveTo 或 lineTo 方法来提供。
Canvas 中的三次贝塞尔曲线
语法:
js
ctx.moveTo(startX, startY);
ctx.bezierCurveTo(control1X, control1Y, control1X, control1Y, endX, endY);
同二次贝塞尔曲线一样,三次贝塞尔曲线需要 4 个控制点,起始点也是由 moveTo 或 lineTo 方法提供,bezierCurveTo 方法提供两个控制点和一个结束点。
Canvas中贝塞尔曲线绘制方法还是比较简单,但是我们该怎么用起来呢?我们在绘制的时候,需要应用到比如凸包性来选择绘制曲线范围,曲线的绘制比较注重圆滑平顺,那么我们在绘制时,需要注意曲线 <math xmlns="http://www.w3.org/1998/Math/MathML"> C 1 C_{1} </math>C1连续的条件。
我们来看一下我们在控制贝塞尔曲线叠加时需要注意的点:
js
<template>
<canvas ref="cnv" width="710" height="400" style="border: 1px dashed gray"></canvas>
</template>
<script setup>
import {ref, onMounted} from "vue";
const cnv = ref();
const drawArc = (cnv, ctx) => {
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(160, 0, 160, 120);
ctx.bezierCurveTo(160, 240, 320, 180, 710, 400);
ctx.strokeStyle = "hotpink";
ctx.setLineDash([1, 0]);
ctx.stroke();
}
onMounted(() => {
const ctx = cnv.value.getContext('2d');
drawArc(cnv.value, ctx);
});
</script>
这样我们可以得到一段二次贝塞尔曲线与一段三次贝塞尔曲线的 <math xmlns="http://www.w3.org/1998/Math/MathML"> C 1 C_{1} </math>C1连续的曲线的组合。
从图中我们可以看到,在连接处四点都在 x = 160 直线上,这样我们能绘制出连续的曲线,当我们更改一个控制点的坐标时,也就是让这个点不在 x = 160 这条直线上时,看一下会是什么结果。
此时我们是把三次贝塞尔曲线的第一个控制点 x 坐标改成 240 时,我们能看到图中出现了一个不再顺滑的拐点,此时的曲线就是一条连接但是不连续的。
所以我们在绘制曲线时,一定运用好贝塞尔曲线的一些特性,这样我们在绘制时才能游刃有余,信手拈来。
人生这一旅途,就如曲线之美,可以停顿,可以弯曲,甚至重蹈覆辙,但每当整顿好自己,重新出发时,千万记得看看当时停顿时的样子,缓缓出发,沿着每一个助力点,稳稳前行即可。
对于贝塞尔曲线的一些详细的文档,小伙伴还可以去看看这些文章,对你们的理解或许也是有一些帮助。