用贝塞尔曲线画画

1. 什么是贝塞尔曲线

一句话解释:它可以将任何平滑曲线转化为精确的数学公式。例如PS中的钢笔工具,它的原理就是二阶贝塞尔曲线。
在这里插入图片描述

1.1. 一阶贝塞尔曲线

在这里插入图片描述



一阶贝塞尔曲线描述的是从p0到p1的连续点,是一条直线。公式如下:

B ( t ) = ( 1 − t ) p 0 + t p 1 , t ϵ ( 0 , 1 ) B(t)=(1-t)p_{0}+tp_{1},t\epsilon (0,1) B(t)=(1t)p0+tp1,tϵ(0,1)

写成下面这种形式,更好理解一点:

B ( t ) = p 0 + ( p 1 − p 0 ) t , t ϵ ( 0 , 1 ) B(t)=p_{0}+(p_{1}-p_{0})t,t\epsilon (0,1) B(t)=p0+(p1p0)t,tϵ(0,1)

其中 p 0 p_{0} p0是起始点坐标, p 1 p_{1} p1是结束点坐标。

1.2. 二阶贝塞尔曲线

二阶贝塞尔曲线相对于一阶多了一个控制点,其公式为:

B ( t ) = ( 1 − t ) 2 p 0 + 2 t ( 1 − t ) p 1 + t 2 p 2 , t ϵ [ 0 , 1 ] B(t)=(1-t)^{2}p_{0}+2t(1-t)p_{1}+t^{2}p_{2},t\epsilon [0,1] B(t)=(1t)2p0+2t(1t)p1+t2p2,tϵ[0,1]

其中 p 0 p_{0} p0为起始点坐标, p 1 p_{1} p1为控制点, p 2 p_{2} p2为结束点。
在这里插入图片描述

数学理论

  • 连接 p 0 p_{0} p0 p 1 p_{1} p1 p 1 p_{1} p1 p 2 p_{2} p2
  • 在线段AB上找到点D,BC上找到点E,使得AD/AB=BE/BC=t, t ϵ [ 0 , 1 ] t\epsilon [0,1] tϵ[0,1]
  • 连接线段DE,在DE上找到点F,使得DF/DE=AD/AB=BE/BC=t, t ϵ [ 0 , 1 ] t\epsilon [0,1] tϵ[0,1]
  • t从0开始,线性缓慢变化到1,使用上述规则找出所有的点F,并将它们连接,最终得到二阶贝塞尔曲线

公式推导

二阶贝塞尔曲线可以看成是分别由前两个顶点( p 0 p_{0} p0, p 1 p_{1} p1)和后两个顶点( p 1 p_{1} p1, p 2 p_{2} p2)决定的一阶贝塞尔曲线的线性组合。
说的有点绕,我们知道贝塞尔函数其是一系列连续的点组成的曲线,我们可以将t值固定,先将 ( p 0 p_{0} p0, p 1 p_{1} p1)代入一阶贝塞尔函数公式得到点D,再将( p 1 p_{1} p1, p 2 p_{2} p2)代入一阶贝塞尔函数公式得到点E,最后将得到的(D,E)再利用一次一阶贝塞尔函数,就得到了点F。接下来的步骤就很熟悉了,让t在[0,1]内线性增大,得到了一些列的F点,连起来就是我们的二阶贝塞尔曲线了。

  • 由一阶贝塞尔曲线公式: B ( t ) = ( 1 − t ) p 0 + t p 1 , t ϵ ( 0 , 1 ) B(t)=(1-t)p_{0}+tp_{1},t\epsilon (0,1) B(t)=(1t)p0+tp1,tϵ(0,1)
  • p 0 p_{0} p0替换为 B ( t ) = ( 1 − t ) p 0 + t p 1 B(t)=(1-t)p_{0}+tp_{1} B(t)=(1t)p0+tp1
  • p 1 p_{1} p1替换为 B ( t ) = ( 1 − t ) p 1 + t p 2 B(t)=(1-t)p_{1}+tp_{2} B(t)=(1t)p1+tp2
  • 可得到: B ( t ) = ( 1 − t ) [ ( 1 − t ) p 0 + t p 1 ] + t [ ( 1 − t ) p 1 + t p 2 ] , t ϵ ( 0 , 1 ) B(t)=(1-t)\left [ (1-t)p_{0}+tp_{1} \right ]+t\left [(1-t)p_{1}+tp_{2}\right ],t\epsilon (0,1) B(t)=(1t)[(1t)p0+tp1]+t[(1t)p1+tp2],tϵ(0,1)
  • 化简后就得到二阶贝塞尔函数: B ( t ) = ( 1 − t ) 2 p 0 + 2 t ( 1 − t ) p 1 + t 2 p 2 , t ϵ [ 0 , 1 ] B(t)=(1-t)^{2}p_{0}+2t(1-t)p_{1}+t^{2}p_{2},t\epsilon [0,1] B(t)=(1t)2p0+2t(1t)p1+t2p2,tϵ[0,1]

在这里插入图片描述

1.3. 三阶贝塞尔曲线及n阶贝塞尔曲线

在这里插入图片描述
三阶贝塞尔曲线有两个控制点,其公式为:

B ( t ) = p 0 ( 1 − t ) 3 + 3 p 1 t ( 1 − t ) 2 + 3 p 2 t 2 ( 1 − t ) + p 3 t 3 , t ϵ [ 0 , 1 ] B(t)=p_{0}(1-t)^{3}+3p_{1}t(1-t)^{2}+3p_{2}t^{2}(1-t)+p_{3}t^{3},t\epsilon [0,1] B(t)=p0(1t)3+3p1t(1t)2+3p2t2(1t)+p3t3,tϵ[0,1]

其中 p 0 p_{0} p0为起始点, p 1 p_{1} p1为控制点1, p 2 p_{2} p2为控制点2, p 3 p_{3} p3为结束点。

公式推导

按照二阶的推导原理,三阶贝塞尔曲线可被定义为分别由( p 0 p_{0} p0, p 1 p_{1} p1, p 2 p_{2} p2)和( p 1 p_{1} p1, p 2 p_{2} p2, p 3 p_{3} p3)确定的二阶贝塞尔曲线的线性组合:

B 3 ( t ) = ( 1 − t ) B ( 0 , 1 , 2 ) 2 ( t ) + t B ( 1 , 2 , 3 ) 2 ( t ) , t ϵ [ 0 , 1 ] B^{3}(t)=(1-t)B_{(0,1,2)}^{2}(t)+tB_{(1,2,3)}^{2}(t),t\epsilon [0,1] B3(t)=(1t)B(0,1,2)2(t)+tB(1,2,3)2(t),tϵ[0,1]

其中 B n ( t ) B^{n}(t) Bn(t)代表n阶贝塞尔函数, B ( 0 , 1 , 2 ) 2 B_{(0,1,2)}^{2} B(0,1,2)2代表使用( p 0 p_{0} p0, p 1 p_{1} p1, p 2 p_{2} p2)点计算的二阶贝塞尔函数。
由此我们还可推出n阶贝塞尔函数公式:

B n ( t ) = { p i , n = 0 ( 1 − t ) B ( 0... n − 1 ) n − 1 ( t ) + t B ( 1... n ) n − 1 ( t ) , n = [ 1 , + ∞ ] B^{n}(t)=\left\{\begin{matrix} p_{i},n=0 & \\ (1-t)B_{(0...n-1)}^{n-1}(t)+tB_{(1...n)}^{n-1}(t),n=[1,+\infty ]& \end{matrix}\right. Bn(t)={ pi,n=0(1t)B(0...n1)n1(t)+tB(1...n)n1(t),n=[1,+]

(很明显可以用递归做)

2. Android中的应用

贝塞尔曲线在Android中的应用主要表现在自定义View和动画上,理论上你可以利用贝塞尔曲线得到任何你想要的路径,除了圆形,但可以通过二阶、三阶贝塞尔曲线来拟合圆,具体算法可参考这篇文章Approximate a circle with cubic Bézier curves

2.1. Android提供的API

Android中的Path类也封装了二阶及三阶贝塞尔曲线函数。可以输入控制点、起始点、结束点来绘制相应的贝塞尔曲线。


    public void quadTo(float x1, float y1, float x2, float y2) {
    
    
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }

   
    public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
    
    
        isSimplePath = false;
        nRQuadTo(mNativePath, dx1, dy1, dx2, dy2);
    }

    public void cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3) {
    
    
        isSimplePath = false;
        nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
    }

    public void rCubicTo(float x1, float y1, float x2, float y2,
                         float x3, float y3) {
    
    
        isSimplePath = false;
        nRCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
    }

quadTo()

二阶贝塞尔曲线,其中(x1,y1)是控制点的坐标,(x2,y2)是结束点的坐标,起始点是通过path.moveTo()来控制的,如果没有调用,则默认控件左上角(0,0)为起始点。然后下个点的起始点是本次的结束点。

rQuadTo()

也是二阶贝塞尔函数,只不过其中的(dx1,dy1)是相对于上次终点坐标值的改变量,比如上次终点坐标(100,200),调用path.rQuadTo(100,-200,100,200),效果就是以(100,200)为起始点,(200,0)为控制点,(200,400)为终点画一条二阶贝塞尔曲线。

cubicTo()和rCubicTo()

cubicTo()是三阶贝塞尔曲线,同样通过path.moveTo()设置起始点,(x1,y1)为1号控制点,(x2,y2)为2号控制点,(x3,y3)为结束点。rCubicTo()同样也是使用相对坐标。

2.2. LodingView

在这里插入图片描述
左右两边的小球运动路径就是自定义的二阶贝塞尔曲线,通过设置一个属性动画得到一个从0到1变化的值,再根据二阶贝塞尔函数公式动态计算出小球坐标,canvas中再不断绘制小球。

 val valueAnimation = ValueAnimator.ofFloat(0f,1f)
 valueAnimation.interpolator = LinearInterpolator()
 valueAnimation.duration = 500L
 valueAnimation.repeatCount = ValueAnimator.INFINITE
 valueAnimation.repeatMode = ValueAnimator.REVERSE
 valueAnimation.addUpdateListener {
    
    
            val t = it.animatedValue as Float
            showPoint1 = secondBessel(t,startPoint1,controlPoint1,endPoint1)
            postInvalidate()
        }
/**
     * @param t 
     * @param startPoint 起始点
     * @param controlPoint 控制点
     * @param endPoint 结束点
     * 二阶贝塞尔函数
     */
    private fun secondBessel(t:Float,startPoint: PointF,controlPoint: PointF,endPoint: PointF):PointF{
    
    
        val pointF = PointF()
        val tem =1-t
        pointF.x = startPoint.x*tem*tem+2*t*tem*controlPoint.x+t*t*endPoint.x
        pointF.y = startPoint.y*tem*tem+2*t*tem*controlPoint.y+t*t*endPoint.y
        return pointF
    }

然后再通过AnimatorSet()调一调左右两个小球动画的播放顺序就行了。
DEMO源码

2.3. 做一个炫酷的入场动画

在这里插入图片描述
这是我毕设项目的Splash界面,原理和LoadingView一样,这里画了五条二阶贝塞尔曲线,并且加了个六边形边长逐渐变大的效果(中间那个button点击效果不是我做的,用的是ShineButton)。

猜你喜欢

转载自blog.csdn.net/qq_38817183/article/details/105770421