看到中间的上下两个曲线了吧,它们用到了,当然这是一个二阶贝塞尔【啥叫二阶贝塞尔呢?下面会学习到】,也是最最基础的,了解了基础的画法,对于它更加复杂的画法你也比较容易上手了,好,这是一个整体的目标。
首先要明白,对于贝塞尔曲线,它是分阶的,不同的阶数,其绘制的曲线效果也不一样,所以这里先来了解不同阶数的贝塞尔曲线的绘制情况。
先看一下效果:
一条直线。。不是贝塞尔是用来绘制曲线的么?是的,因为对于一阶贝塞尔实际没啥用,但是对于你了解它的概念是一个基础,等于它的核心目的其实就只是由两点控制的一条直线,绘制就是从一点绘制到另一点的整个轨迹,记住这个绘制过程,因为在理解二阶贝塞尔曲线的过程中,需要使用到它。
对于二阶贝塞尔曲线来说,应该是应用最最广泛的,也是核心中的核心,对于贝塞尔曲线不是还有更加高阶复杂的曲线么?而要理解更加复杂的贝塞尔曲线,理解二阶就成为一个必须要掌握的了,只有掌握了二阶的画法,你才能更加从容的面对更加复杂的贝塞尔曲线,所以这块务必要理解透。
先来直观的感受一下它的绘制过程:
嗯,此图来自于csdn,看完此效果之后是不是觉得挺酷炫的?而在上面QQ汽泡效果中的那个曲线,是不是跟这个二阶曲线能联系起来?这也就是为啥在之后的贝塞尔曲线的案例应用时QQ汽泡效果使用的是二阶贝塞尔的原因了,但是你能看懂它的绘制规则么?反正我光看这动图还是一脸懵逼的,所以接下来会了解其原理,这样才能真正理解它。
先明显对于二阶贝塞尔曲线是由三个点来进行构成的,如下:
其中AC这两点本来是应该绘制一条直线的对吧:
但是该直线需要受B点往下进行拖动,这个B其实就是一个控制点,正因为有这个控制点才会让本应该绘直线的最终变成了绘制曲线了,其实在我们电脑的绘制软件中也能直观的感受一下这个B点的控制点的魅力,比如我mac上用Paintbrush这个软件有一个曲线的绘制:
我要绘制一条曲线,也是先绘制一条直线,然后再通过对直线的拖拽才完成曲线的绘制的,如下:
所以,此时应该对于B点它的出现的意义有了直观的了解了吧,接下来重点是就来看一下这个曲线的绘制规则了,这里先把两点到控制点连接两条直线,如下:
然后曲线的整体绘制是由AB这条一阶贝塞尔曲线来控制:
那AC的这根曲线很显然也是由一大堆的点的连续构成的对吧,让曲线上的绘制点又是如何来确定的呢?现在整个曲线的控制是由AB这个贝塞尔曲线来控制,它是一个运动的轨迹【这一点必须要理解到位,它是不断在走的,不明白的可以看一下一阶贝塞尔曲线的那个动图】,也就是AB上走了多少个点,就会对应的生成曲线上的各个点,这里以AB上的某一个静止点进行分析,把它如何确定最终曲线上的绘制点的搞清楚了,你也就明白了整个曲线的绘制原理了,比如AB上的贝塞尔曲线走到D这个点了:
其中这条直线上的AD跟DB就有一个比例关系了对吧,这时在BC上也取一个点E,其比例跟D点比例一样【注意关键词,比例要一样】,如下:
好,此时将DE进行一个连线:
接下来的绘制点就是在DE上了,那。DE上对应的绘制点是如何确定的呢?也是同样的套路,在这条直线上取一点F,要保证比例跟AD的一样,如下:
此时F点就是最终要绘制在曲线上的一个点【注意它只是曲线上的一个点哟】,如下:
曲线上的某个点我们已经知道怎么算出来了,我们由A-C开始启动绘制,则会算出曲线上的N个点,用PATH记录这个点进行绘制,从而y就得到了一条曲线,这条曲线就是所谓的贝塞尔曲线。这里再来回忆一下整个二阶贝塞尔曲线完整的绘制过程:
其中t表示整个曲线的点的次数,盯着那根绿色的那根线看,是不是能感受到比例正好跟P0到P1的一阶贝塞尔走的位置比例一样,截一个静态图说明一下:
但是我看不太懂,其实有一个比较容易理解的计算公式,还是以这个图为例:
想要找到绘制点的话只需要遵守DF:DE= AD:AB= BE:BC,那么此时F点就是绘制点,因为比例是一样的。
其原理跟二阶的一样,看一下图:
了解了一、二、三阶贝塞尔曲线之后,接着再来感受一下更多阶的效果,让你瞬间晕眩:
1、四阶贝塞尔曲线:
2、五阶贝塞尔曲线:
这对于学习贝塞尔曲线来说是一个不错的辅助工具,有兴趣的可以下下来跑一跑。
而我在Android7.0的源码中貌似对于三创的API是这个方法名:
应该是有版本差异,这里就不过多纠结了,反正系统支持最多是3阶。
这里以绘制二阶为例,来体验一下画法。
这里还是基于原来学习UI时搭建的工程新建一个module:
接下来新建一个自定义View,为曲线的绘制搭好环境:
也就是我手指移动的位置就是一个控制点。
对于三阶的贝塞尔曲线,除了控制点之外,还需要有一个开始点和结束点,这里定死一下:
整个代码如下:
这里运行看一下:
既然是四阶,肯定是需要五个点的,一个开始点,一个结束点,还有三个控制点,所以在正式绘制曲线之前,先将准备工作做到位,这块比较简单,直接给出代码了:
代码如下:
运行看一下效果:
当然这个点是随机的,每次运行是不一样的。
好,接下来的核心就是怎么来绘制贝塞尔曲线的问题了,这里则需要使用到一个比较“高深”的算法了,如小标题所示:德卡斯特里奥算法,百度一下它:
在正式了解该算法之前,我们先从贝塞尔曲线绘制特点来分析一下,比如这个三阶贝塞尔曲线:
是不是最终其实就是降阶来进行处理的,如下:
而对于“德卡斯特里奥算法”而言,它的目的就是可以达到降阶的效果,其实也就是通过这个算法,最终能够算出降为一阶的绘制点:
有了绘制点,那么将所有绘制点连接起来,不就成了一个曲线了么,这就是该算法的目的之所在,另外该算法能实现N阶贝塞尔曲线,是一个通用的算法,所以首先先明白该算法的一个目的很重要
关于它的算法逻辑,说实话是有一点难理解的,反正我是在网上找了一圈,貌似说得都有点生涩,如果不理解透,你在编写代码时肯定会懵,不信,我先贴出来之后利用此算法实现贝塞尔曲线的一个代码:
是不是在你不了解此算法的公式之前,这代码是完全理解不了的,仅仅只能把这个方法当一个工具方法来用,但是别人问你怎么实现时,可以丢一句:“代码如此,自己看吧”。
1、先理解如何在一个线段中获得具体比例的点:【这块有点小绕,但是必须理解透】
因为,有一个式子,跟我提前贴的代码中的式子很像呀:
其实两篇思想一样,只是这篇语言组织上详细通俗一点,咱们就根据两位大佬的博客,带着自己的理解来挼一挼:
其中提到了“向量”这个词, 不知各位都对它还记得么,对我来说印象比较深刻,因为之前学了“线性代数”:
那:
那是不是:
所以很显然C就把向量AB分成了u:1-u了。接下来则需要来解释上面标红的一句话了:“整个AB看成是1”,这里回忆一下最开始对于一阶贝塞尔曲线的执行过程:
看到其中的t了么?最大就是1,因为曲线的绘制就是根据算出来的绘制点连线而成,而多少个绘制点则你可以自己来根据这个1进行拆分,可能这边说得有点绕,这里先把之后咱们实现时需要用到的一个代码先提前亮出来吧,这样就比较好理解了:
理解一下,“A到B的向量是B - A”,这个应该容易理解吧,就是指AB之间的距离嘛,然后C点占整个1的百分比是u,那么C点的位置很明显就是u * (B -A)嘛,这样就把C给算出来了呀,但是“考虑到A点的位置”,由于A点的位置不一定是原点,比如发生偏移之类的,那么整个C点的位置还得基于A的位置来算,所以整个式子就变成了“A + u(B - A)”,再根据结合率之类的,最终就可以变化成这个式子了:“(1 - u)*A + u*B”,其中是不是可以发现,只要我将u这个比例值知道了,整个C点的绘制位置就知道了?
2、理解递归的逻辑:
对于一个N阶贝塞尔曲线而言,你最终要绘制的只是最里面1阶贝塞尔曲线,所以光知道了,比如这样的一个五阶贝塞尔曲线的演变过程:
也就是在计算过程中会涉及到N多个点对吧,但是绘制而言其实只需要它就可以了:
很明显这里需要一个递归的过程最终再算出指定t下的贝塞尔曲线所在的那个绘制点对吧, 那递归的思路是啥呢?下面基于图中的这个场景来简单挼一下:
其中的u为0.4,也就是该点是整个贝塞尔曲线40%的位置,说实话,这个递归过程不是那么好描述,其实就是降级的过程,最终降到一阶,其绘制点就给算出来了,这块具体的过程,我打算先把代码实现贴出来之后,再结合程序来进行理解,这样可能更加容易理解一点,所以这里的递归过程先忽略。
接下来直接来实现多阶贝塞尔曲线了,代码其实还是比较亲切的,如下:
a、先将整个绘制分成1000等份,然后一等份一等份的算出绘制点:
而具体算法,目前还未实现:
b、完成绘制点的计算:
整体的思路就是如果当前是1阶了,则直接根据德卡斯特里奥算法来算出绘制点,如果大于1阶的,则递归降级处理,具体的细节这里先不解释,因为下面会根据运行结果再来分析整个程序的执行过程的,这样你就明白了其递归的一个逻辑了。
接下来运行看一下,目前看的是4阶的效果,由于点是随机产生的,多运行几次,看曲线画得完不完美:
嗯,挺完美的~~
为了验证这种方式的通用性,用两个极端的阶数来测试一下,一个是2阶,一个是7阶,先来看一个2阶的吧,咱们把程序写一个数字既可,其它完全不需要动:
运行:
接下来再看一下7阶的,同样改个数字既可:
运行:
完美,可见通过这种算法来实现的贝塞尔曲线可以满足所有阶次。
好!!最后这里还遗留一个问题,那就是整个递归的思路是咋样的呢?对应的代码是:
这里以三阶贝塞尔曲线绘制为例,来debug一下咱们的程序,为了方便分析,这里将随机的点改为定死的点,如下:
先看一下运行的样子:
注意,起始点和结束点的位置,因为关系到待会的逻辑分析涉及到的控制点的控制:
而为了方便分析流程,这里将t定死一个,只分析一个绘制点既可,如下:
目前长这样了:
而为了更加精简,只分析一下坐标点的x的值计算过程既可,因为y值的计算过程是一模一样的,也就是它:
另外,为了让分析代码变得清晰,这里将deCasteljauX调整一下:
是不是这样跟咱们理论所分析的公式就一模一样了:
这样分析递归也好分析一些,不然一行代码有两个递归看着有点晕,一切就绪,接下来进入代码分析阶段。
1、i=3,j=0:
其中i代表是阶数,j代表的是控制点,也就是首先是来计算三阶的这
由于阶数不是1,所以此时会进入递归环节。
2、递归计算A点:float A = deCasteljauX(i - 1, j, t);
【注意】:这里我制造效果时有点顺序问题,应该p0在p3的位置的,由于在写逻辑时已经基于这样的图进行了,这里就说明一下,p3是第一个点,p0是最后一个点【正常p0是第一个点,p3是最后一个点嘛,犯了个细节错误,将错就错了,不过不影响整体的逻辑理解】,这个注意一下!!!
而这个A点的算出,很明显是需要根据降阶操作之后的这俩A,B点来算出,如下:
所以,为了降阶,这里将阶数减一再次递归了:
然后此时进入递归环节:
由于阶数目前还是不等于1,又会执行到这:
算二阶的A:
而此时由于阶数已经降为2了,再递归降一级,就变为1阶了,此时结果就直接可以算出返回了:
此时的A为619.23193,也就是这个点坐标的x值:
算二阶的B:
此时则开始算降阶的B点了:
同样由于阶数已经还是2,再递归降一级,就变为1阶了,此时结果就直接可以算出返回了:
此时的B为361.1466,也就是这个点坐标的x值:
结果得出:
此时再根据德卡斯特里奥算法,此时这个二阶上的点结果x值就出来了:
也就是这个点算出来为482.70563:
这里好好理会一下整个A值的计算过程,很明显的一个降级,也就是不管多少阶,最终都会递归降到1阶,然后再算出其中的A,B点的值,再算出最终的曲线绘制点,当然这个递归不是很好理解的,需要自己边画边挼一下才行。
3、计算B点:float B = deCasteljauX(i - 1, j + 1, t);
理清了A点的计算递归逻辑,这个B点的计算逻辑是一模一样的,同样在分析前,需要明确目标,这句代码的意思是算出它:
而这个B点的算出,很明显是需要根据降阶操作之后的这俩A,B点来算出,如下:
所以,为了降阶,这里将阶数减一再次递归了:
然后注意,此时的j控制点+1了:
这是因为此时B点的算出得根据第二个控制点来:
是不是这两个点得基于p2起步来算出?所以这里的控制点数+1了,这个细节需要好好体会。
然后此时进入递归环节:
由于阶数目前还是不等于1,又会执行到这:
算二阶的A:
而此时由于阶数已经降为2了,再递归降一级,就变为1阶了,此时结果就直接可以算出返回了:
此时的A为361.1466,也就是这个点坐标的x值:
算二阶的B:
此时则开始算降阶的B点了:
同样由于阶数已经还是2,再递归降一级,就变为1阶了,此时结果就直接可以算出返回了:
此时的B为213.16849,也就是这个点坐标的x值:
结果得出:
此时再根据德卡斯特里奥算法,此时这个二阶上的点结果x值就出来了:
也就是这个点算出来为282.86667:
4、根据德卡斯特里奥算法来算出结果:
目前A,B两点已经算出来了,你说要绘制的这个点怎么得出应该就很简单了吧?
这样,绘制点就成功算出:
经过这么完整的一个debug分析,是不是比直接分析代码要容易理解一些?对于递归的程序,如果当你光看代码不好理解的话,建议用debug的方式完整的梳理一遍,这样你就会比较容易理解,不过!!!整体还是比较晕的,这里最后再整理一下,其实递归的目的就是降级,降到1阶,递归的目的就是为了算出1阶的开始点和结束点:
有了这俩点,最终绘制点就可以根据德卡斯特里奥算法来算出了。
最后,咱们将debug时的一些代码给还原,最后再来看个东东,就是关于它的作用:
之前不是说这个数值越大其绘制出来的曲线就越细腻么,为了体现,咱们改为很小,反着看一下,是不是越小,越不细腻,这里先把1000的图贴出来:
然后咱们将其改为10,再对比看一下:
是不是对比非常明显,等份小了,很明显点与点之间的距离大了,然后连线当然就不圆润了啦,通过这个对比应该就明白这个等份的意义了吧?
至此,我们已经学会了N阶贝塞尔曲线的画法了,但是!!!在之后咱们实现的QQ汽泡的效果时只会使用到Android Path API来绘制二阶贝塞尔曲线的,那这么复杂的算法有何意义呢?可以让你对贝塞尔曲线的概念理解得更加深刻呀,如果你只学Android API和二三阶贝塞尔曲线的用法,人家都把实现细节给你封装好了,能知道贝塞尔曲线的精髓么?这篇耗了我业余大概一周多时间梳理吧,收获反正是挺大的,有种茅塞顿开的感觉,如果你有心,建议自己写一篇博客从头至尾的按自己的思路来梳理分享出来,哪怕是网上参考的【但是一定得要带着自己的理解去解读,而不是直接copy】,这样你就能体会到虽说时间成本比较大,但是你的收获也很大,这也是为啥我一直坚持的原因,这样可以让自己摒弃浮躁,脚踏实地的一步步朝前走。
本来计划还要实现一个qq汽泡的效果滴,不过在这个算法上花得篇幅较长,放下次吧。