1、贝塞尔曲线简介
贝塞尔曲线的本质是通过数学公式的计算绘制出一条平滑的曲线。
从A向C绘制一条贝塞尔曲线,控制点为B,在线段AB中取点D,在线段BC中取点E,在DE线段中取点F,保证AD:AB=BE:BC=DF:DE,则F点移动的轨迹就是A点到C点的贝塞尔曲线,也称作二阶贝塞尔曲线。
三阶贝塞尔曲线:从A点向D点绘制一条贝塞尔曲线,控制点为B和C,在线段AB取点E,线段BC取点F,线段CD取点G,线段EF取点H,线段FG取点I,线段HI取点J,保证AE:AB=BF:BC=CG:CD=EH:EF=FI:FG=HJ:HI,则J点移动的轨迹就是从点A到点D的三阶贝塞尔曲线。
高阶贝塞尔曲线以此类推,由多个控制点来绘制曲线。
2、贝塞尔曲线绘制
2.1 绘制图形
iOS系统提供了一些绘制指定图形的方法:
//在指定区域内绘制矩形
+ (instancetype)bezierPathWithRect:(CGRect)rect;
//在指定区域内绘制圆形(椭圆、正圆)
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
//绘制带圆角的矩形
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
//绘制圆弧
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
也有自定义添加曲线的方法:
//向指定点添加一条直线
- (void)addLineToPoint:(CGPoint)point;
//向指定点添加一条二阶曲线
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
//向指定点添加一条三阶曲线
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
//添加一段圆弧
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise API_AVAILABLE(ios(4.0));
贝塞尔曲线只是一条曲线,如果想让曲线在屏幕上渲染,需要和CAShapeLayer(图层)配合使用,曲线用于制定路径,图层负责绘制曲线。
//创建一个曲线
UIBezierPath *path = [UIBezierPath bezierPath];
//创建一个图层
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
以矩形为例,有两种绘制方式:
方法一:使用由系统方法,在指定区域内绘制一个矩形;
方法二:从A到B、B到C、C到D、D到A分别绘制的四条边,组成一个完整的矩形。
其它图形的绘制方法大同小异,或者使用系统提供的绘制特定图形的方法,或者利用一些特定的数学公式进行计算得到各个点进行绘制。
2.2 绘制画笔
与图形绘制不同,画笔绘制没有固定的公式,是由手指(鼠标)在屏幕上滑动时取到多个点连接而成的轨迹。所以书笔记可以用增量的方式绘制,如下图所示,手指在屏幕上从左向右滑动,当移动到的新的点B时,只绘制了上一点A和B之间的部分曲线,也就是说手指在屏幕上每捕获一个新的点只需要绘制两点间的曲线。所以笔记绘制时的性能消耗是比较小的,教室内多人同时书写也不会卡顿。
2.3 绘制圆弧
2.3.1 圆弧坐标系
//绘制圆弧
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
2.3.2 实现圆环进度指示器
实现如上图所示的圆环进度条,需要绘制一条从-0.5π到1.5π的顺时针圆环,通过CAShapeLayer显示。
//创建一个完整的圆形
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CenterPoint startAngle:-0.5f*M_PI endAngle:1.5f*M_PI clockwise:YES];
//创建一个图层
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer有两个属性strokeStart(起点值)和strokeEnd(终点值),取值范围0-1,strokeStart默认为0,strokeEnd默认为1。设置这两个属性可以让layer展示path指定的一部分。进度条默认将strokeEnd设置为0,接收到进度更新时,将layer的strokeEnd属性设置为下载的百分比,就得到了圆形进度条的效果。
2.3.3 支付宝支付中效果
和绘制圆环进度条相同,首先绘制一个从-0.5π到1.5π的顺时针圆环,需要特定的时机更新layer的strokeEnd和strokeStart,分解效果如下:
一个循环过程可分解为前后两段:
前半段:从0到0.75匀速更新圆环的strokeEnd(即圆环完成-0.5π到π的绘制),此过程中strokeStart保持不变。
后半段:strokeEnd从0.75开始速度变慢,逐渐增加到1。在这个过程中strokeStart以strokeEnd4倍的速度从0变化到1(弧度从-0.5π到1.5π),在角度为1.5π时,strokeStart追上strokeEnd都变化到1,完成一个循环过程。
下个循环开始时将strokeEnd和strokeStart设置为0,重复上面的逻辑。
2.4 曲线拼接
iOS系统提供了曲线拼接的方法,path可以拼接其它的path对象。
//拼接曲线
- (void)appendPath:(UIBezierPath *)bezierPath;
下面以删除笔记为例,讲一下曲线拼接的在魔法课堂中的应用,系统中提供了区域包含点的方法:
CG_EXTERN bool CGPathContainsPoint(CGPathRef cg_nullable path,
const CGAffineTransform * __nullable m, CGPoint point, bool eoFill)
CG_AVAILABLE_STARTING(10.4, 2.0);
笔记删除也是围绕区域和点进行匹配的原则实现的,橡皮擦作为一个点,但是笔记是通过贝塞尔曲线进行绘制的,点无法和线进行匹配,所以给笔记添加了扩展区域来辅助计算,调节橡皮擦大小本质是在调解扩展区域大小,扩展区域有前后两个版本:
老版本:遍历曲线中的每个点,取前一个点生成两个辅助点A、B,后一个点生成两个辅助点C、D,生成一个闭合的平行四边形ABCD,这样笔记的路径上就出现了多个平行四边行,当橡皮擦在屏幕上移动时,通过橡皮擦的点和扩展平行四边行就行匹配,来计算待删除笔记。
不足:如果笔记点中的点比较多,生成小的四边行较多,计算量会比较大。其次笔记末端匹配准确。
新版本:设笔记绘制方向为正向,将正向方向所有左侧和右侧点各自绘制出一条曲线,两个末端绘制两条曲线,将四条曲线拼接成一个完整的扩展区域。这种方式可以保证橡皮擦移动时每条笔记只需计算一个扩展区域,并提高笔记末端计算精度。
3、贝塞尔曲线应用举例
3.1 作为视图的运动路径
正常的View(视图)对象移动可以通过调整frame实现,如果需要查看移动轨迹,可以使用基本动画将view的移动轨迹渲染出来。
[UIView animateWithDuration:0.3 animations:^{
view.frame = frame;
}];
如果移动路径是一条曲线,或者是比较复杂的路径,则需要用贝塞尔曲线绘制出运动的路径,在结合关键帧动画实现:
UIBezierPath *path = [UIBezierPath bezierPath];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.path = path;
[view.layer addAnimation:animation forKey:@"YourKey"];
举例,圆球移动效果:
分析:
UIBezierPath *path = [UIBezierPath bezierPath];
path拼接一段从-π到0的逆时针半圆
path拼接一段从π到0的逆时针半圆
path拼接一段从右向左的直线
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.path = path;
[view.layer addAnimation:animation forKey:@"YourKey"];
3.2 绘制函数曲线
贝塞尔曲线是根据点进行绘制的,如果每个点的y都是x通过函数f进行计算所得:(x,f(x)),就可以绘制出函数f的曲线,以绘制波浪为例:
正弦曲线公式:y=Asin(ωx+φ)+k
A :振幅,曲线最高位和最低位的距离
ω :角速度,用于控制周期大小,单位x中起伏的个数
K :偏距,曲线上下偏移量
φ :初相,曲线左右偏移量
实现步骤如下:
绘制两条正弦函数曲线 | 更新函数初相 | 填充颜色 |
---|---|---|
3.2 处理视图遮罩
系统给CALayer提供了mask属性
@property(nullable, strong) __kindof CALayer *mask;
图层设置遮罩后,将以遮罩区域显示,如下图所示,给视图添加一个圆形遮罩,除圆形遮罩区域后,其它部分将不再显示:
举例1,百度贴吧加载效果:
分析:共由三层组合而成,包括底层蓝色文字,中间白色文字+灰色波浪层,顶部白色文字+蓝色波浪层,其中波浪并没有直接绘制出来,而是作为了图层的mask,使白色文字以波浪的形式动态展示。
举例2,视图间专场效果:
分析:
以按钮点击位置为圆心,半径从0到R,动态绘制圆形区域,以圆形区域作为试图的遮罩,再结合缩放效果,就可以实现上图的转场效果。
3.3 贝塞尔曲线交互
要实现QQ未读消息这样的黏性拖拽效果,主要实现部分是连接固定点和拖动点的曲线部分,由之前总结的经验可得一下几种方案:
绘制两条贝塞尔曲线 | 绘制两个外接圆 | 绘制两条函数曲线 | 其它方案 |
---|---|---|---|
… |
以第一种为例:需要在拖动的过程中,确定两个控制点的位置,并且使两个控制点间距越来越小,形成黏性效果,加入辅助线效果如下:
将UI问题转化为数学问题,理解起来就会比较方便:
已知条件为点O1和O2的坐标,可计算出O1和O2的间距和直线O1O2和竖直方向的夹角θ
根据角θ和AB和CD与O1O2的垂直关系,可计算出点A、B、C、D的坐标
最后计算出点E和点F
利用贝塞尔曲线在点A和点D、点B和点C之间绘制平滑曲线,在点A和点B、点C和点D间绘制直线,拼接成一个闭合的区域:
//计算出两点间距离
CGFloat d = sqrtf((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
//计算出角θ的正弦和余弦值
CGFloat sinθ = (x2 - x1)/d;
CGFloat cosθ = (y2 - y1)/d;
//根据sinθ、cosθ、两点坐标,计算出A、B、C、D的坐标
CGPoint pointA = CGPointMake(x1 - r1*cosθ, y1 + r1*sinθ);
CGPoint pointB = CGPointMake(x1 + r1*cosθ, y1 - r1*sinθ);
CGPoint pointC = CGPointMake(x2 - r2*cosθ, y2 + r2*sinθ);
CGPoint pointD = CGPointMake(x2 + r2*cosθ, y2 - r2*sinθ);
//计算出点A点B计算出点O、P的坐标
CGPoint pointE = CGPointMake(pointA.x + (d/2)*sinθ, pointA.y + (d/2)*cosθ);
CGPoint pointF = CGPointMake(pointB.x + (d/2)*sinθ, pointB.y + (d/2)*cosθ);
//绘制曲线
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:pointA];
[path addLineToPoint:pointB];
[path addQuadCurveToPoint:pointD controlPoint:pointF];
[path addLineToPoint:pointC];
[path addQuadCurveToPoint:pointA controlPoint:pointE];
[path closePath];
//显示曲线
self.shadowLayer.path = path.CGPath;
4、总结
贝塞尔曲线除了绘制还可以做一些辅助性的工作,在遇这类问题时,可以先转化为数学问题,解决起来就会相对简单一点。