iOS 判断点在绘制曲线上的思路

写在前面

最近项目中需要实现画板功能,除了基本的绘制各种图形和曲线的功能,还需要在手指触摸屏幕的时候,判断手指是否在绘制的图形上,在的话就拖动该图形,否则就绘制新的图形,绘制的原理是UIBezierPath + CAShapeLayer,所以判断点在图形上也就是判断点在图形对应的贝塞尔曲线上,对于闭合的贝塞尔曲线,我们更多的倾向于判断点在贝塞尔绘制的曲线内部,比如椭圆和矩形等,UIBezierPath也提供了containsPoint:API可以直接进行判断,很easy,但是对于手指画出的轨迹以及贝塞尔曲线等不闭合且不规则的曲线,判断点在其上就没那么简单了,我思考了不少时间才有了一个我认为比较好的思路,先看看图片效果:

1、点在手指绘制曲线上

点在手指绘制曲线上.gif

2、点在二阶贝塞尔曲线上

点在二阶贝塞尔曲线上.gif

方案

对于不闭合的曲线我觉得总结起来就只有两种:
1、通过若干个点连接起来组成的曲线,这只是视觉上的曲线,其实质是多条细小线段的组合,我们绘制手指轨迹也就是这样做的;
2、根据各种曲线公式绘制的曲线,比如二阶贝塞尔曲线以及N阶贝塞尔曲线,正弦函数等等
这两种情况基本上就概括了所有的情况了。

先来考虑第一种情况:我们的需求是判断点在这条曲线上,其实也就是判断点是否在构成曲线的任意一条小的线段上即可,其实也是判断点到线段的最小距离是否小于你所允许的一个值而已(这个值越大说明判断越松),既然要求最小距离,我们就需要用到点到直线的距离公式:


点到直线的距离公式

该公式表示了点(x0,y0)到直线方程Ax+By+C = 0 的距离。

具体步骤如下:
1、遍历构成曲线的所有点,并从第二个点开始和上一个点构成一条直线,已知直线两点,我们可以求出直线的一般式方程,进而求出ABC的值(也就是直线方程的两点式到一般式的转换)。

2、计算出ABC后即可将手指所在的点带入方程求出点到直线的距离,如果该距离大于你允许的一个值,则认为该点不在该线段上,否则进行进一步判断。

3、如果算出的距离小于你所你允许的值是不是一定就表示这个点在该线段上呢?答案也是不一定的,因为这个值只是点到直线的距离而并非最小距离,此时我们需要考虑该点的投影点是否在线段上,如果在线段上该距离就是最短距离,如果不在线段上,这个距离则并非最短距离,最短距离应该由该点和靠近该点的线段的端点构成,所以我们需要做这个判断才对,这样我们就成功的判断好了,一旦我们检测到了点在某条线段上就可以跳出循环遍历,肯定点在这条曲线上咯!代码如下:

/**
*判断点point是否在p0 和 p1两点构成的线段上
*/
- (BOOL)_xw_point:(CGPoint)point isInLineByTwoPoint:(CGPoint)p0 p1:(CGPoint)p1{
    //先设置一个所允许的最大值,点到线段的最短距离小于该值说明点在线段上
    CGFloat maxAllowOffsetLength = 15;
    //通过直线方程的两点式计算出一般式的ABC参数,具体可以自己拿起笔换算一下,很容易
    CGFloat A = p1.y - p0.y;
    CGFloat B = p0.x - p1.x;
    CGFloat C = p1.x * p0.y - p0.x * p1.y;
    //带入点到直线的距离公式求出点到直线的距离dis
    CGFloat dis = fabs((A * point.x + B * point.y + C) / sqrt(pow(A, 2) + pow(B, 2)));
    //如果该距离大于允许值说明则不在线段上
    if (dis > maxAllowOffsetLength || isnan(dis)) {
        return NO;
    }else{
    //否则我们要进一步判断,投影点是否在线段上,根据公式求出投影点的X坐标jiaoX
        CGFloat D = (A * point.y - B * point.x);
        CGFloat jiaoX = -(A * C + B *D) / (pow(B, 2) + pow(A, 2));
        //判断jiaoX是否在线段上,t如果在0~1之间说明在线段上,大于1则说明不在线段且靠近端点p1,小于0则不在线段上且靠近端点p0,这里用了插值的思想
        CGFloat t = (jiaoX - p0.x) / (p1.x - p0.x);
        if (t > 1  || isnan(t)) {
        //最小距离为到p1点的距离
            dis = XWLengthOfTwoPoint(p1, point);
        }else if (t < 0){
        //最小距离为到p2点的距离
            dis = XWLengthOfTwoPoint(p0, point);
        }
        //再次判断真正的最小距离是否小于允许值,小于则该点在直线上,反之则不在
        if (dis <= maxAllowOffsetLength) {
            return YES;
        }else{
            return NO;
        }
    }
}

//这里是求两点距离公式
static inline CGFloat XWLengthOfTwoPoint(CGPoint point1, CGPoint point2){
    return sqrt(pow(point1.x - point2.x, 2) + pow(point1.y - point2.y, 2));
}

可以看到代码的实质就是一步一步根据公式计算出结果而已,如果能够搞清楚公式,代码也就简单了。

再来看看第二种情况:这里我们并不知道构成曲线的所有点,但是我们知道曲线的公式,刚开始我是想通过直接计算曲线方程的方法来求解,但发现这些高阶的曲线方程的求解对我来说完全是不可能的,而且各种曲线的方程不同,求解也各异,所以我在想要能用第一种情况的方法去解决该问题就好了,那我们就要取得构成曲线的点,我们可以使用插值思想,通过一个循环来取点,取多少个点就看需求了,下面以二阶贝塞尔曲线举例子,二阶贝塞尔的公式如下:

二阶贝塞尔曲线
//我们首先提供一个函数,将上述公式转换成代码
static inline CGPoint XWPointOnPowerCurveLine(CGPoint p0, CGPoint p1, CGPoint p2, CGFloat t){
    CGFloat x = (pow(1 - t, 2) * p0.x + 2 * t * (1 - t) * p1.x + pow(t, 2) * p2.x);
    CGFloat y = (pow(1 - t, 2) * p0.y + 2 * t * (1 - t) * p1.y + pow(t, 2) * p2.y);
    return CGPointMake(x, y);
}


/**
判断点在二阶贝塞尔曲线上
*/
- (BOOL)_xw_containsPointForCurveLineType:(CGPoint)point{
    CGPoint p0 = _startPoint;//我是贝塞尔曲线的起始点
    CGPoint p1 = _allPoints.firstObject.CGPointValue;//我是贝塞尔曲线终点
    CGPoint p2 = _allPoints.lastObject.CGPointValue;//控制点
    CGPoint tempPoint1 = p0;记录采样的每条线段起点,第一次起点就是p0
    CGPoint tempPoint2 = CGPointZero;记录采样线段终点
    //这里我取了100个点,基本上满足要求了
    for (int i = 1; i < 101; i ++) {
    //计算出终点
        tempPoint2 = XWPointOnPowerCurveLine(p0, p1, p2, i / 100.0f);
        //调用我们解决第一种情况的方法,判断点是否在这两点构成的直线上
        if ([self _xw_point:point isInLineByTwoPoint:tempPoint1 p1:tempPoint2]) {
        //如果在可以认为点在这条贝塞尔曲线上,直接跳出循环返回即可
            return YES;
        }
        //如果不在则赋值准备下一次循环
        tempPoint1 = tempPoint2;
    }
    return NO;
}

采用这样的插值取点的思路,对于任何的曲线都能够很轻松的转换成第一种方式求解了,而且你并不需要理解这条曲线公式,只需要对其插值求出一系列的点即可,比如对于一个圆X² + Y² = 100,你只需要在对X在0~10之间进行插值就能取出构成原的点,然后就可以按照同样的方式判断点是否在圆上而不是圆内了!

写在最后

由于本篇文章的主要是围绕这一系列数学公式展开的,所以不熟悉公式可能会有点懵,不过只要明白思路就差不多了,其实这些公式都是我们在初高中烂熟于心的数学公式咯,只不过很多人和我一样丢的差不多了吧,对于我们程序猿来说,多去思考和学习一些数学的思路和想法还是很有帮助的,下一步准备把这个画图控件封装好总结总结,不知道又是啥时候咯o(╯□╰)o!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容