贝塞尔曲线的绘制原理与应用

图来自pexels

说来话长,这一切都得从PhotoShop中的钢笔工具开始说起...

声明:本文不含复杂数学公式,学渣放心阅读吧😂(我仿佛看到了学渣们留下了激动的泪水)

背景

贝塞尔曲线(Bézier curve)是应用于二维图形应用程序的数学曲线,贝塞尔曲线基于多个点构成。它的应用非常广泛,比如说PS中的钢笔工具所绘画的曲线就是贝塞尔曲线,绘制动画的运动轨迹等等,而最近一次想用到贝塞尔曲线是想做一个 路径动画

简介

在iOS开发中一般通过UIBezierPath来实现贝塞尔曲线的绘制,平时一般使用绘制二阶和三阶贝塞尔曲线的方法。而我们要做的远超二三阶的贝塞尔曲线,本文 iOS Demo在原理上实现了N阶贝塞尔曲线的绘制,未使用任何相关API,纯手动绘制贝塞尔曲线,并且可以拖动滑块浏览贝塞尔曲线的绘制过程。

本文 iOS Demo 实现以下功能:

实现功能 描述
绘制贝塞尔曲线 1、点击空白处设置贝塞尔曲线的点
2、可以设置贝塞尔曲线阶数
3、播放贝塞尔曲线绘制过程
4、拖动滑块,自由查看绘制过程每一个瞬间
简易曲线图表 每两个点之间都是用3阶贝塞尔曲线连接(细节待完善)
过山车 1、在空白处绘制贝塞尔曲线
2、过山车沿着绘制的贝塞尔曲线行驶
3、支持多个连接的贝塞尔曲线路径

Demo示例图 8阶贝塞尔曲线绘制过程

贝塞尔曲线的绘制原理

说到绘制原理,如果贴👇这张图,我只能说:什么鬼!!!我看不懂,听不见,你说什么... 路人甲:简单点...说话的方式简单点~

失败案例

首先提供一个可以动态绘制贝塞尔曲线的网站帮助你更好地理解贝塞尔曲线的绘制。

1. 点

贝塞尔曲线点的数量决定了曲线的阶数,一般N个点构成的N-1阶贝塞尔曲线,即3个点为二阶,至少由3个点组成,为什么两个点不行,两个点组成的是直线。按顺序,第一个点为 起点 ,最后一个点为 终点 ,其余点都为 控制点

A起点、B控制点 、C终点以及绘制的贝塞尔曲线

2. 点生线

这里说的线不是贝塞尔曲线,而是各个点按顺序连接起来,形成的直线,如上图ABBC两条线。在这里我们要将整个曲线的绘制量化为从0~1的过程,用progress为当前过程的进度,progress的区间即0~1。每一条线都需要根据progress生成一个点,如下图,一个点从P0移动到P1,这是这条线从0~1的过程。 根据进度点从起点向终点移动

下面是绘制一个二阶贝塞尔曲线过程,先给口诀: 点生线,线生点 😂。由ABC这3个点组成2条线ABBC,2条线根据progress分别生成2个移动的点DE,而DE又连成一条线,始终保持AD:DB=BE:EC

progress为0.3 的连线 移动的线

DE,DE再根据progress生成点F,只剩一个点,无法构成线,即为最终构成贝塞尔曲线的点。红色点为progress0~1过程中点F的移动过程,保持AD:DB=BE:EC=DF:FE

progress为0.3 最终的点

点F的移动过程

3. 绘制贝塞尔曲线

经过上面 点生线,线生点 的过程 ,我们拿到了点F在移动中所有点的,将这些点集合连接起来,即形成了贝塞尔曲线。progress自增越慢,点集合的点越多,曲线就越细致。

绘制二阶贝塞尔曲线过程

4. N阶贝塞尔曲线

稍微了解算法的同学就能发现,其实 点生线,线生点 是一个递归的过程,通过底层的点,一步步推算出最高阶的点。整个推导过程像一个金字塔,底部点的数量最多,每高一阶点的数量就减1,直至最高阶只有1个点。

**下面是递归代码: **

// 贝塞尔曲线每高一阶  需要递归次数+1
+ (NSArray *)recursionGetsubLevelPointsWithSuperPoints:(NSArray *)points progress:(CGFloat)progress{
    // 得到最终的点 正确结束递归 
    if (points.count == 1) return points;
    
    NSMutableArray *tempArr = [[NSMutableArray alloc] init];
    for (int i = 0; i < points.count-1; i++) {
        // 第一个点 
        NSValue *preValue = [points objectAtIndex:i];
        CGPoint prePoint = preValue.CGPointValue;
        // 第二个点
        NSValue *lastValue = [points objectAtIndex:i+1];
        CGPoint lastPoint = lastValue.CGPointValue;

        // 两点坐标差
        CGFloat diffX = lastPoint.x-prePoint.x;
        CGFloat diffY = lastPoint.y-prePoint.y;

        // 根据当前progress得出高一阶的点
        CGPoint currentPoint = CGPointMake(prePoint.x+diffX*progress, prePoint.y+diffY*progress);
        [tempArr addObject:[NSValue valueWithCGPoint:currentPoint]];
    }
    // 继续下一次递归过程
    return [self recursionGetsubLevelPointsWithSuperPoints:tempArr progress:progress];
}

8阶贝塞尔曲线绘制过程: 8阶贝塞尔曲线绘制过程

贝塞尔曲线的应用

光讲原理脱离实践这不是程序员的风格,简单地写了2个贝塞尔曲线的应用,都在**本文 iOS Demo** 里面,欢迎运行体验。

1. 过山车

通过点击屏幕收集点,将点集合生成贝塞尔曲线,可生成多个相连的贝塞尔曲线。小车按照生成的贝塞尔曲线路径前进。

a. 画路径 通过计算贝塞尔曲线的长度,根据曲线长度分配点的数量,达到点的相对均匀分布,使过山车 匀速前进画路径

b. 发车 每个点都与前面一个点连线,通过计算得出两点的连线与水平形成的夹角,将角度赋予过山车实现 转向功能发车

2. 简易曲线图表

a. 直线图表 即最简单的两点连成直线。

直线图表

b. 曲线图表 曲线图表的曲线全部由3阶贝塞尔曲线构成,整个曲线图不含任何棱角。 曲线图表

拓展

PaintCode 推荐一个iOS画路径神器PaintCode,画好图形直接生成代码,用钢笔工具画贝塞尔曲线也十分方便。下图为用钢笔工具画一个圆球(貌似不够圆😆):

生成代码

总结

为了准备这一篇文章差不多理解了贝塞尔曲线的绘制原理,但是在细节处,比如说真正意义上贝塞尔曲线点的均匀分布还有待完善,求曲线公式也没有去研究,贝塞尔曲线在复杂的动画方向地应用也是大有作为。

参考

贝塞尔曲线开发的艺术 Android:贝塞尔曲线原理分析

个人水平有限,欢迎提出建议。