IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 贝塞尔曲线及实践案例 -> 正文阅读

[数据结构与算法]贝塞尔曲线及实践案例

1. 前言

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。

2. 介绍

2.1 一阶贝济埃曲线

一阶贝济埃曲线的公式如下:

 B(t)=(1-t)P_0+tP_1,t属于0-1

P0为起始点,P1为终点,t 表示当前时间,B(t)表示公式的结果值。其实也就是一条从P0到P1的直线上,匀速运动的点值。

2.2 二阶贝塞尔曲线

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x1sAavqD-1647325585493)(images/screenshot_1647310332528.png)]

在这里插入图片描述

这条曲线的构成也就是每个t时刻,Q0和Q1的所属的直线的的t时刻的距离的点,这里也就是B。不妨将上面这个图简单标注下:
在这里插入图片描述

也就是在从P0到P1,进行匀速运动,在t=0.25的时刻走到Q0,类似的,从P1到P2经过匀速运动,在t=0.25的时刻走到Q1,对于Q0到Q1,经过匀速运动,在t=0.25的时刻走到B。而B也就是二阶贝塞尔曲线上的点。

2.3 三阶贝塞尔曲线

在这里插入图片描述

也就是说此时有两个控制点,对应着也就是三根连着的线段,类似的我们可以得到最终的t点:
在这里插入图片描述

那么,根据上面的规则,我们可以自己来实现一下贝赛尔曲线的计算方式,并将曲线绘制出来。

3. 一、二、三阶贝塞尔曲线实现

定义为:

class Point(var x: Float, var y: Float){
}

/**
 * 得到贝赛尔曲线上的点集
 * @param points 起始、控制和终止点坐标
 * @param number 需要计算的贝赛尔曲线上的点的个数
 * @return 返回路径
 */
private fun getBezierPointsPath(points: Array<Point>, number: Int): Path{
    val path = Path()
    for (time in 0 until number){
        val t = time * 1f / number
        val point = calcPoint(points, t)
        if(time == 0){
            path.moveTo(point.x, point.y)
        } else {
            path.lineTo(point.x, point.y)
        }
        Log.e("TAG", "getBezierPointsPath: ${point.x} , ${point.y}", )
    }
    return path
}


/**
 * 计算在t时刻上,位于贝赛尔曲线上的点的坐标
 * @param points 点的集合
 * @param t 时刻,属于0-1
 * @return 点坐标 Point
 */
private fun calcPoint(points: Array<Point>, t: Float): Point{
    // 分别求任意两个点之间的在t时刻运动的距离
    // 任意两点,按照顺序分别为始和终
    var index = 0
    var len = points.size - 1
    while (index < len){
        points[index].x = getValueByTime(points[index].x, points[index + 1].x, t)
        points[index].y = getValueByTime(points[index].y, points[index + 1].y, t)
        index++
        if(index == len){
            index = 0
            len--
        }
    }
    return points[0]
}

/**
 * 定义匀速运动的计算坐标
 * @param start 开始的位置
 * @param end 结束的位置
 * @param time 运动的时间,范围0-1
 * @return time时刻的运动位置
 */
private fun getValueByTime(start: Float, end: Float, time: Float): Float{
    return start + (end - start) * time
}

然后使用:

// 绘图方法
override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.apply {
        val points = arrayOf(Point(200f, 400f), Point(100f, 20f), Point(500f, 20f), Point(800f, 400f))
        val numberOfPoint = 100
        mPath = getBezierPointsPath(points, numberOfPoint)
        drawPath(mPath, mPaint)
    }
}

在这里插入图片描述

很明显,这里细粒度不够。可以把numberOfPoint 设置的更大些。当设置为1000的时候:
在这里插入图片描述

当然这里可以使用arrayOf的时候添加更多的点,以做到更加高阶的贝塞尔曲线,比如简单修改一下:

val points = arrayOf(Point(200f, 400f),
    Point(100f, 20f),
    Point(500f, 20f),
    Point(800f, 400f),
    Point(1000f, 20f)
)

也就是对应三个控制点,对应四阶本塞尔曲线,对应效果:
在这里插入图片描述
当然,在系统中其实也提供了一、二、三阶的贝赛尔曲线的API,所以通常直接调用即可。对应的如下:

  • mPath.lineTo:进行直线绘制 ;
  • mPath.quadTo(x1, y1, x2, y2) :生成二次贝塞尔曲线,(x1,y1) 为控制点,(x2,y2)为结束点 ;
  • mPath.cubicTo(x1, y1, x2, y2, x3, y3):生成三次贝塞尔曲线, (x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3) 为结束点;

4. 案例

/**
 * 学习波浪效果,其实也就是移动类似于正弦的连续图像,带来的视觉效果
 * @author 梦否
 * 2022年3月15日
 */
class WaterRippleView : View {
    constructor(context: Context?) : super(context) {
        init()
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        init()
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        init()
    }

    private lateinit var mPath: Path
    private lateinit var mPaint: Paint
    private lateinit var points1: Array<MyPoint>
    private lateinit var points2: Array<MyPoint>

    class MyPoint(var x: Float, var y: Float)

    /**
     * 初始化方法
     */
    private fun init() {
        mPath = Path()
        mPaint = Paint()
        mPaint.isDither = true
        mPaint.isAntiAlias = true
        mPaint.strokeWidth = 5f
        mPaint.color = Color.GRAY
        mPaint.style = Paint.Style.FILL

        val viewWidth = resources.displayMetrics.widthPixels
        points1 = arrayOf(
            MyPoint(0f * viewWidth, 200f),
            MyPoint(.33f * viewWidth, 20f),
            MyPoint(.66f * viewWidth, 360f),
            MyPoint(1f * viewWidth, 200f)
        )
        points2 = arrayOf(
            MyPoint(-1f * viewWidth, 200f),
            MyPoint(-.66f * viewWidth, 20f),
            MyPoint(-.33f * viewWidth, 360f),
            MyPoint(0f * viewWidth, 200f),
        )

        // 三阶贝塞尔曲线,传入0,也就是初始时刻
        updatePathByDistance(0f)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        canvas?.apply {
            drawPath(mPath, mPaint)
        }

    }

    /**
     * 根据距离来进行更新在贝赛尔曲线中的点的坐标值
     * @param distance 传入的距离
     */
    private fun updatePathByDistance(distance: Float) {
        // 重置
        mPath.reset()
        // 设置
        mPath.moveTo(points2[0].x, points2[0].y)
        mPath.cubicTo(
            points2[1].x + distance,
            points2[1].y,
            points2[2].x + distance,
            points2[2].y,
            points2[3].x + distance,
            points2[3].y
        )

        mPath.cubicTo(
            points1[1].x + distance,
            points1[1].y,
            points1[2].x + distance,
            points1[2].y,
            points1[3].x + distance,
            points1[3].y
        )

        val y = resources.displayMetrics.heightPixels
        mPath.lineTo(points1[3].x, y.toFloat())
        mPath.lineTo(points2[0].x + distance,  y.toFloat())
        mPath.lineTo(points2[0].x + distance, points2[0].y)
    }

    /**
     * 一直移动绘制的两个类似于正弦函数的路径
     */
    var startedMove = false
    private fun startMove() {
        startedMove = true
        val animator = ValueAnimator.ofFloat(0f, resources.displayMetrics.widthPixels.toFloat())
        animator.duration = 800
        // 线性插值器,使之匀速运动
        animator.interpolator = LinearInterpolator()
        // 循环
        animator.repeatCount = ValueAnimator.INFINITE
        animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator?) {
                val value = animator.getAnimatedValue()
                updatePathByDistance(value as Float)
                // 重绘
                invalidate()
            }
        })
        animator.start()
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        super.onTouchEvent(event)
        var flag = false
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                flag = true
                if(!startedMove) startMove()
            }
            MotionEvent.ACTION_MOVE,
            MotionEvent.ACTION_UP -> {
                flag = false
            }
        }
        return flag
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val minHeight = dp2px(300)
        val minWidth = dp2px(500)
        val widthSize = getMeasureSize(widthMeasureSpec, minWidth.toInt())
        val heightSize = getMeasureSize(heightMeasureSpec, minHeight.toInt())
        setMeasuredDimension(widthSize, heightSize)
    }

    /**
     * 计算高度和宽度
     */
    private fun getMeasureSize(Spec: Int, minValue: Int): Int {
        var result = 0
        // 获取模式
        val mode = MeasureSpec.getMode(Spec)
        val size = MeasureSpec.getSize(Spec)

        // 判断一下
        when (mode) {
            MeasureSpec.AT_MOST -> {
                result = Math.min(size, minValue)
            }
            MeasureSpec.UNSPECIFIED -> {
                result = minValue
            }
            MeasureSpec.EXACTLY -> {
                result = size
            }
        }
        return result
    }

    /**
     * dp转换为px
     */
    private fun dp2px(size: Int): Float {
        return resources.displayMetrics.density * size
    }
}

5. 后记

当然关于贝赛尔曲线的应用远不止如此。比如:Android开发之贝塞尔曲线进阶篇(仿直播送礼物,饿了么购物车动画),感兴趣的可以查阅原文。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-03-16 22:43:30  更:2022-03-16 22:50:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 11:52:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码