美文网首页
贝塞尔曲线

贝塞尔曲线

作者: echoSuny | 来源:发表于2020-06-30 17:45 被阅读0次

    在Path的系列函数中,除了一些基本的设置和绘图方法外,还有一个比较强大的功能就是贝塞尔曲线。它能够将moveTo和lineTo连接的生硬路径变得平滑,能够实现比较多炫酷的效果。

    贝塞尔曲线公式

    一阶贝塞尔曲线

    一阶贝塞尔曲线公式
    ps: 属于符号∈是数学中用于表示某个数值在某个区间或某个集合当中
    P0为起始点,P1为终点,t表示当前时间,B(t)表示公式的结果值,最后的t∈ [0, 1]表示t<=0<=1。
    一阶贝塞尔曲线动画演示
    黑色的点表示在不同的时间t下,B(t)的值,红线则表示随着时间的移动,B(t)所形成的轨迹。
    据图可知一阶贝塞尔曲线就是一条在起点和终点形成的直线上匀速移动的点
    二阶贝塞尔曲线
    二阶贝塞尔曲线公式
    二阶贝塞尔曲线动画演示
    P0是起点,P2是终点,P1是控制点。首先P0和P1形成了一条一阶贝塞尔曲线,上面说了一阶贝塞尔曲线就是在直线上作匀速运动,所以称在P0-P1这条直线上匀速运动的点为O1。同理在P1-P2上也有一个匀速运动的点为O2。最后O1和O2又形成了一条一阶贝塞尔曲线,在这条贝塞尔曲线上移动的点就是上图中的黑点,称之为O3。O3最终的轨迹就是上图中的红线。
    三阶贝塞尔曲线
    三阶贝塞尔曲线公式
    三阶贝塞尔曲线动画显示
    P0是起点,P3是终点,P1是第一个控制点,P2是第二个控制点。首先有三条一阶贝塞尔曲线P0-P1,P1-P2,P2-P3,那么就会分别对应有三个在这三条直线上迅速运动的点O1,O2,O3。这三个点会再次形成两个一阶贝塞尔曲线O1-O2和O2-O3。同理在这两条一阶贝塞尔曲线上又会有两个匀速的点M1和M2,则这两个点又会形成一条一阶贝塞尔曲线,这条线上的点就是上图中的黑点,最终随着时间的推移,就会形成红色的轨迹。
    另外还有四阶贝塞尔曲线和五阶贝塞尔曲线,Android只支持到三阶贝塞尔曲线,但原理都是一样的。
    二阶贝塞尔曲线
    • public void quadTo(float x1 , float y1, float x2 , float y2)
      参数(x1,y1)是控制点的坐标,(x2,y2)是终点的坐标。起点的话则是Path中的moveTo()函数指定的。如果没有调用moveTo()函数,则以控件的左上角(0,0)为起始点。另外如果连续调用quadTo()函数的话,前一个quadTo()函数的终点就是下一个quadTo()函数的起点。



      先看P1和P2这一段路线,P1是起点,P2是终点,O1是控制点。假设P1的坐标是(100,400),P2的坐标是(300,400),虽然O1的坐标不能确定,但是O1的X坐标一定满足大于P1的X坐标且小于P2的X坐标,Y坐标一定是小于P1和P2的Y坐标。那么就可以给O1的坐标为一个符合条件的值,例如(150,150)。同样的来分析O2的坐标,可以给O2的坐标一个满足条件的值(450,550)。最终代码如下:

            val path = Path()
            path.moveTo(100f,400f)
            path.quadTo(150f,150f,300f,400f)
            path.quadTo(450f,550f,600f,400f)
            canvas.drawPath(path,paint)
    
    效果图

    下面实现一个根据手势绘制曲线的简单例子:

    override fun onTouchEvent(event: MotionEvent): Boolean {
    
            if (event.action == MotionEvent.ACTION_MOVE) {
            }
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    downX = event.x
                    downY = event.y
                    path.moveTo(downX, downY)
                    return true
                }
                MotionEvent.ACTION_MOVE -> {
                    val endX = (downX + event.x) / 2
                    val endY = (downY + event.y) / 2
                    path.quadTo(downX, downY, endX, endY)
                    downX = event.x
                    downY = event.y
                    invalidate()
                }
                else -> {
                    // do noting
                }
            }
            return super.onTouchEvent(event)
        }
    
    • public void rQuadTo(float dx1, float dy1 , float dx2, float dy2)
      这个函数的四个参数的意义和quadTo()的四个参数是一样的,都是前两个表示控制点的位置,后两个表示终点的位置。区别在于rQuadTo()的四个参数是相对于上一个终点的一个相对值,而quadTo()是绝对值,参考点是屏幕原点(0,0)。
            path.moveTo(100f,400f)
            path.rQuadTo(50f,-250f,200f,0f)
            path.rQuadTo(150f,150f,300f,0f)
            canvas.drawPath(path, paint)
    

    可以看到和上面是用quadTo()函数实现的效果是一样的。前面说了rQuatTo()是相对于上一个终点计算的。显然在这段代码中上一个点是(100,400)。那控制点的绝对坐标就应该是(100+50,400-250)也就是(150,150),终点的坐标就应该是(100+200,400+0),即(300,400)。这与上面使用quatTo()时传入的坐标path.quadTo(150f,150f,300f,400f)是对应的。此时终点就成了(300,400),下面就可以计算path.rQuadTo(150f,150f,300f,0f)的绝对坐标了。那么控制点就为(300+150,400+150) = (450,550),终点则为(300+300,400+0) = (600,400),与quatTo()中 path.quadTo(450f,550f,600f,400f)一致,故最后画出的曲线是一致的。

        fun startWave() {
            val animator = ValueAnimator.ofFloat(0f, mWaveLength)
            animator.duration = 3000
            animator.repeatCount = ValueAnimator.INFINITE
            animator.interpolator = LinearInterpolator()
            animator.addUpdateListener {
                dx = it.animatedValue as Float
                postInvalidate()
            }
            animator.start()
        }
    
        override fun onDraw(canvas: Canvas) {
                super.onDraw(canvas)
            path.reset() 
            val originY = 300f 
            val halfWaveLength = mWaveLength / 2 
            path.moveTo(-mWaveLength + dx, originY) 
            val total = width + mWaveLength
            for (i in (-mWaveLength).toInt()..total.toInt() step mWaveLength.toInt()) {
                path.rQuadTo(halfWaveLength / 2, -100f, halfWaveLength, 0f)
                path.rQuadTo(halfWaveLength / 2, 100f, halfWaveLength, 0f)
            }
            path.lineTo(width.toFloat(), height.toFloat())
            path.lineTo(0f, height.toFloat())
            path.close()
            canvas.drawPath(path, paint)
        }
    

    根据上面这幅简图来解释一下上面的代码:
    path.moveTo(-mWaveLength + dx, originY) 就是把起点设在最左边红色箭头指的坐标
    val total = width + mWaveLength 相当于在屏幕外的右侧画来一个和左边一样的波行
    for循环就是把所有的路径都连起来,重点是for 循环里的内容
    path.rQuadTo(halfWaveLength / 2, -100f, halfWaveLength, 0f)这句代码的前两个参数就是上图中的第一个黑点的坐标,是贝塞尔曲线的控制点。由于起始点的Y坐标为300,而rQuadTo()是相对于上一个点的,则求出来的Y坐标值则是300+(-100) = 200。后两个参数则是下面右边蓝色箭头所指的位置,是贝塞尔曲线的终点。
    path.rQuadTo(halfWaveLength / 2, 100f, halfWaveLength, 0f) 这句代码和上一句的逻辑是一样的,只不过是求的后一半的波长中的控制点和终点。
    最后的调用Path函数的三行代码只是把整个Path闭合了,如下图,所以在屏幕上是看不见的:



    而startWave()就只是启动了一个无限循环的属性动画,并监听动画的值,然后不断的改变moveTo()的坐标来达到让整个曲线动起来的效果

    三阶贝塞尔曲线
    • public void cubicTo (float x1 , float y1, float x2 , float y2 , float x3 , float y3)
      (x1,y1)表示第一个控制点的坐标,(x2,y2)表示第二个控制点的坐标,(x3,y3)则是路径终点的坐标。在使用二阶贝塞尔曲线函数的时候,我们得调用两次quadTo()函数才能实现一个波浪形的曲线,下面看一下使用三阶贝塞尔曲线函数来实现类似的效果:
            path.moveTo(100f,400f)
            path.cubicTo(150f,150f,350f,550f,600f,400f)
            canvas.drawPath(path, paint)
    
    • public void rCubicTo(float x1 , float y1, float x2 , float y2 , float x3 , float y3 )
      参数和上面的cubicTo()的意义是一样的,区别就是相对坐标。可以参考rQuadTo()。

    相关文章

      网友评论

          本文标题:贝塞尔曲线

          本文链接:https://www.haomeiwen.com/subject/aonzfktx.html