详解js实现线段交点的三种算法

本文讲的内容都很初级, 主要是面向和我一样的初学者, 所以请各位算法帝们轻拍啊

引用

已知线段1(a,b) 和线段2(c,d) ,其中a b c d为端点, 求线段交点p .(平行或共线视作不相交)

算法一: 求两条线段所在直线的交点, 再判断交点是否在两条线段上.

求直线交点时 我们可通过直线的一般方程 ax+by+c=0 求得(方程中的abc为系数,不是前面提到的端点,另外也可用点斜式方程和斜截式方程,此处暂且不论).

然后根据交点的与线段端点的位置关系来判断交点是否在线段上.

公式如下图:

<code class="hljs avrasm">function segmentsIntr(a, b, c, d){ 

/** 1 解线性方程组, 求线段交点. **/
// 如果分母为0 则平行或共线, 不相交
 var denominator = (b.y - a.y)*(d.x - c.x) - (a.x - b.x)*(c.y - d.y);
 if (denominator==0) {
 return false;
 } 

// 线段所在直线的交点坐标 (x , y)
 var x = ( (b.x - a.x) * (d.x - c.x) * (c.y - a.y)
  + (b.y - a.y) * (d.x - c.x) * a.x
  - (d.y - c.y) * (b.x - a.x) * c.x ) / denominator ;
 var y = -( (b.y - a.y) * (d.y - c.y) * (c.x - a.x)
  + (b.x - a.x) * (d.y - c.y) * a.y
  - (d.x - c.x) * (b.y - a.y) * c.y ) / denominator; 

/** 2 判断交点是否在两条线段上 **/
 if (
 // 交点在线段1上
 (x - a.x) * (x - b.x) <= 0 && (y - a.y) * (y - b.y) <= 0
 // 且交点也在线段2上
  && (x - c.x) * (x - d.x) <= 0 && (y - c.y) * (y - d.y) <= 0
 ){ 

 // 返回交点p
 return {
  x : x,
  y : y
  }
 }
 //否则不相交
 return false 

} </code>

算法一思路比较清晰易懂, 但是性能并不高. 因为它在不确定交点是否有效(在线段上)之前, 就先去计算了交点, 耗费了较多的时间.

如果最后发现交点无效, 那么之前的计算就白折腾了. 而且整个计算的过程也很复杂.

那么有没有一种思路,可以让我们先判断是否存在有效交点,然后再去计算它呢?

显然答案是肯定的. 于是就有了后面的一些算法.

算法二: 判断每一条线段的两个端点是否都在另一条线段的两侧, 是则求出两条线段所在直线的交点, 否则不相交.

第一步判断两个点是否在某条线段的两侧, 通常可采用投影法:

求出线段的法线向量, 然后把点投影到法线上, 最后根据投影的位置来判断点和线段的关系.

见下图

点a和点b在线段cd法线上的投影如图所示, 这时候我们还要做一次线段cd在自己法线上的投影(选择点c或点d中的一个即可).

主要用来做参考.

图中点a投影和点b投影在点c投影的两侧, 说明线段ab的端点在线段cd的两侧.

同理, 再判断一次cd是否在线段ab两侧即可.

求法线 , 求投影 什么的听起来很复杂的样子, 实际上对于我来说也确实挺复杂,在几个月前我也不会(念书那会儿的几何知识都忘光了 :'( )'

不过好在学习和实现起来还不算复杂, 皆有公式可循

求线段ab的法线:

var nx=b.y - a.y,
 ny=a.x - b.x;
var normalLine = { x: nx, y: ny };

注意: 其中 normalLine.xnormalLine.y的几何意义表示法线的方向, 而不是坐标.

求点c在法线上的投影位置:

var dist= normalLine.x*c.x + normalLine.y*c.y; 

注意: 这里的"投影位置"是一个标量, 表示的是到法线原点的距离, 而不是投影点的坐标.

通常知道这个距离就足够了.

当我们把图中 点a投影(distA),点b投影(distB),点c投影(distC) 都求出来之后, 就可以很容易的根据各自的大小判断出相对位置.

distA==distB==distC 时, 两条线段共线

distA==distB!=distC 时, 两条线段平行

distA 和 distB 在distC 同侧时, 两条线段不相交.

distA 和 distB 在distC 异侧时, 两条线段是否相交需要再判断点c点d与线段ab的关系.

前面的那些步骤, 只是实现了"判断线段是否相交", 当结果为true时, 我们还需要进一步求交点.

求交点的过程后面再说, 先看一下该算法的完整实现 :

function segmentsIntr(a, b, c, d){ 

 //线段ab的法线N1
 var nx1 = (b.y - a.y), ny1 = (a.x - b.x); 

 //线段cd的法线N2
 var nx2 = (d.y - c.y), ny2 = (c.x - d.x); 

 //两条法线做叉乘, 如果结果为0, 说明线段ab和线段cd平行或共线,不相交
 var denominator = nx1*ny2 - ny1*nx2;
 if (denominator==0) {
 return false;
 } 

 //在法线N2上的投影
 var distC_N2=nx2 * c.x + ny2 * c.y;
 var distA_N2=nx2 * a.x + ny2 * a.y-distC_N2;
 var distB_N2=nx2 * b.x + ny2 * b.y-distC_N2; 

 // 点a投影和点b投影在点c投影同侧 (对点在线段上的情况,本例当作不相交处理);
 if ( distA_N2*distB_N2>=0 ) {
 return false;
 } 

 //
 //判断点c点d 和线段ab的关系, 原理同上
 //
 //在法线N1上的投影
 var distA_N1=nx1 * a.x + ny1 * a.y;
 var distC_N1=nx1 * c.x + ny1 * c.y-distA_N1;
 var distD_N1=nx1 * d.x + ny1 * d.y-distA_N1;
 if ( distC_N1*distD_N1>=0 ) {
 return false;
 } 

 //计算交点坐标
 var fraction= distA_N2 / denominator;
 var dx= fraction * ny1,
 dy= -fraction * nx1;
 return { x: a.x + dx , y: a.y + dy };
}

最后 求交点坐标的部分 所用的方法看起来有点奇怪, 有种摸不着头脑的感觉.

其实它和算法一 里面的算法是类似的,只是里面的很多计算项已经被提前计算好了.

换句话说, 算法二里求交点坐标的部分 其实也是用的直线的线性方程组来做的.

现在来简单粗略 很不科学的对比一下算法一和算法二:

1、最好情况下, 两种算法的复杂度相同

2、最坏情况, 算法一和算法二的计算量差不多

3、但是算法二提供了 更多的”提前结束条件”,所以平均情况下,应该算法二更优.

实际测试下来, 实际情况也确实如此.

前面的两种算法基本上是比较常见的可以应付绝大多数情况. 但是事实上还有一种更好的算法.
这也是我最近才新学会的(我现学现卖了,大家不要介意啊…)

算法三: 判断每一条线段的两个端点是否都在另一条线段的两侧, 是则求出两条线段所在直线的交点, 否则不相交.

(咦? 怎么感觉和算法二一样啊? 不要怀疑 确实一样 … 囧)

所谓算法三, 其实只是对算法二的一个改良, 改良的地方主要就是 :

不通过法线投影来判断点和线段的位置关系, 而是通过点和线段构成的三角形面积来判断.

先来复习下三角形面积公式: 已知三角形三点a(x,y) b(x,y) c(x,y), 三角形面积为:

<code class="hljs avrasm">var triArea=( (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x) ) /2 ; </code>

因为 两向量叉乘==两向量构成的平行四边形(以两向量为邻边)的面积 , 所以上面的公式也不难理解.

而且由于向量是有方向的, 所以面积也是有方向的, 通常我们以逆时针为正, 顺时针为负数.

改良算法关键点就是:

如果”线段ab和点c构成的三角形面积”与”线段ab和点d构成的三角形面积” 构成的三角形面积的正负符号相异,

那么点c和点d位于线段ab两侧.

如下图所示:

图中虚线所示的三角形, 缠绕方向(三边的定义顺序)不同, 所以面积的正负符号不同.

下面还是先看代码:

由于我们只要判断符号即可, 所以前面的三角形面积公式我们就不需要后面的 除以2 了.

function segmentsIntr(a, b, c, d){ 

 // 三角形abc 面积的2倍
 var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x); 

 // 三角形abd 面积的2倍
 var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x); 

 // 面积符号相同则两点在线段同侧,不相交 (对点在线段上的情况,本例当作不相交处理);
 if ( area_abc*area_abd>=0 ) {
 return false;
 } 

 // 三角形cda 面积的2倍
 var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x);
 // 三角形cdb 面积的2倍
 // 注意: 这里有一个小优化.不需要再用公式计算面积,而是通过已知的三个面积加减得出.
 var area_cdb = area_cda + area_abc - area_abd ;
 if ( area_cda * area_cdb >= 0 ) {
 return false;
 } 

 //计算交点坐标
 var t = area_cda / ( area_abd- area_abc );
 var dx= t*(b.x - a.x),
 dy= t*(b.y - a.y);
 return { x: a.x + dx , y: a.y + dy }; 

}

最后 计算交点坐标的部分 和算法二同理.

算法三在算法二的基础上, 大大简化了计算步骤, 代码也更精简. 可以说,是三种算法里, 最好的.实际测试结果也是如此.

当然必须坦诚的来说, 在Javascript里, 对于普通的计算, 三种算法的时间复杂度其实是差不多的(尤其是V8引擎下).
我的测试用例里也是进行变态的百万次级别的线段相交测试 才能拉开三种算法之间的差距.

总结

不过本着精益求精 以及学习的态度而言, 追求一个更好的算法, 总是有其积极意义的。以上就是利用js实现线段交点的几种算法,内容不是很深奥,希望对大家学习js有所帮助。

(0)

相关推荐

  • js交换排序 冒泡排序算法(Javascript版)

    比较相邻的元素.如果第一个比第二个大,就交换他们两个. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对.在这一点,最后的元素应该会是最大的数. 针对所有的元素重复以上的步骤,除了最后一个. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较. function sort(elements){ for(var i=0;i<elements.length-1;i++){ for(var j=0;j<elements.length-i-1;j++){ if(elemen

  • javascript &&和||运算法的另类使用技巧

    &&和||在JQuery源代码内尤为使用广泛,由于本人没有系统的学习js,所以只能粗略的自我理解出来,希望大家指点下. 粗略理解如下: a() && b() :如果执行a()后返回true,则执行b()并返回b的值:如果执行a()后返回false,则整个表达式返回a()的值,b()不执行: a() || b() :如果执行a()后返回true,则整个表达式返回a()的值,b()不执行:如果执行a()后返回false,则执行b()并返回b()的值: && 优先

  • js算法中的排序、数组去重详细概述

    其实在js中实现数组排序,采用数组中sort方法实现还是比较简单的: 一.排序 简单实现数组排序 复制代码 代码如下: var arr = [];  for(var i=0;i<20;i++){      arr.push(Math.floor(Math.random()*100))  }  arr.sort(function(a,b){      return a>b?1:-1;  })  alert(arr) 不能简单使用sort方法,默认情况下 sort方法是按ascii字母顺序排序的,

  • JavaScript生成GUID的多种算法小结

    全局唯一标识符(GUID,Globally Unique Identifier)也称作 UUID(Universally Unique IDentifier) . GUID是一种由算法生成的二进制长度为128位的数字标识符.GUID 的格式为"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",其中的 x 是 0-9 或 a-f 范围内的一个32位十六进制数.在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID. GUID 的总数达到了2^128(3

  • 一个简单的JavaScript 日期计算算法

    复制代码 代码如下: <script type="text/javascript"> var today=new Date(); //定义当天日期对象 var year = today.getYear(); //获取年份 var month = today.getMonth(); //获取月份 var date = today.getDate(); //获取日期值 try{ //定义下个日期对象,日期值加上30天 var nextDay = new Date(year,mo

  • JavaScript blog式日历控件新算法

    使用说明: 程序比较简单,代码中都有说明,这里说说怎么使用. 首先是实例化一个Calendar,并设置参数. 参数说明: Year:要显示的年份 Month:要显示的月份 SelectDay:选择日期 onSelectDay:在选择日期触发 onToday:在当天日期触发 onFinish:日历画完后触发 一般SelectDay设置成选择了的日期,并在onSelectDay中设置一个函数用来设置这个日期的样式, 例如实例里SelectDay设置成今个月10号并在那天样式设为onSelect: 复

  • 很好用的js日历算法详细代码

    复制代码 代码如下: <script type="text/javascript">         var lunarInfo = new Array( 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2,

  • Javascript中的常见排序算法

    具体代码及比较如下所示: 复制代码 代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  <html xmlns="http://www.w3.org/1999/xhtml" lang="gb2312">

  • js版本A*寻路算法

    说到做游戏,必不可少的需要用到寻路算法,一般游戏里的寻路算法大多数都以A*算法为主,这里也就实现了js里采用a*寻路的程序,在51js和蓝色都开了帖. 程序是以前写的,后来也没有修正或者精简,有冗余之处大家还见谅一下. 当然,这个寻路算法也不是最优化的,像幻宇开发的"交点寻径法"也是个中精品,两者可谓各有千秋,只是如果地图很大的情况下,我们会惊讶于"交点寻径法"的迅速. use A* to find path... /* written by 百晓生 email:j

  • 详解js实现线段交点的三种算法

    本文讲的内容都很初级, 主要是面向和我一样的初学者, 所以请各位算法帝们轻拍啊 引用 已知线段1(a,b) 和线段2(c,d) ,其中a b c d为端点, 求线段交点p .(平行或共线视作不相交) 算法一: 求两条线段所在直线的交点, 再判断交点是否在两条线段上. 求直线交点时 我们可通过直线的一般方程 ax+by+c=0 求得(方程中的abc为系数,不是前面提到的端点,另外也可用点斜式方程和斜截式方程,此处暂且不论). 然后根据交点的与线段端点的位置关系来判断交点是否在线段上. 公式如下图:

  • 详解JS异步加载的三种方式

    一:同步加载 我们平时使用的最多的一种方式. <script src="http://yourdomain.com/script.js"></script> <script src="http://yourdomain.com/script.js"></script> 同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止后续的解析,只有当当前加载完成,才能进行下一步操作.所以默认同步执行才是安全的.但这样如果js中有输

  • 详解Vue 单文件组件的三种写法

    JS构造选项写法 export defaul { data, methods, ...} JS class写法 @Component export default class Cpn extends Vue{ counter = 0 //data add(){ //methods this.counter += 1 } } TS class写法 @Component export default class Cpn extends Vue{ @Prop(Number) sum : number

  • 详解JavaScript中分解数字的三种方法

    本文基于免费代码营基本算法脚本"分解数字" 在数学中,非负整数n的阶乘可能是一个棘手的算法.在本文中,我将解释这种方法,首先使用递归函数,第二种使用而循环,第三种使用以循环. 算法挑战 返回提供的整体的阶乘. 如果整体用字母n表示,则阶乘是所有小于或等于n的正整数的乘积. 阶乘经常用简写符号n!表示! 例如:5!= 1 * 2 * 3 * 4 * 5 = 120 function factorialize(num) { return num; } factorialize(5); 提供

  • 详解Nginx 虚拟主机配置的三种方式(基于IP)

    Nginx配置虚拟主机支持3种方式:基于IP的虚拟主机配置,基于端口的虚拟主机配置,基于域名的虚拟主机配置. 详解Nginx 虚拟主机配置的三种方式(基于端口) https://www.jb51.net/article/14977.htm 详解Nginx 虚拟主机配置的三种方式(基于域名) https://www.jb51.net/article/14978.htm 1.基于IP的虚拟主机配置 如果同一台服务器有多个IP,可以使用基于IP的虚机主机配置,将不同的服务绑定在不同的IP上. 1.1

  • 详解Nginx 虚拟主机配置的三种方式(基于端口)

    Nginx配置虚拟主机支持3种方式:基于IP的虚拟主机配置,基于端口的虚拟主机配置,基于域名的虚拟主机配置. 详解Nginx 虚拟主机配置的三种方式(基于IP) https://www.jb51.net/article/14974.htm 详解Nginx 虚拟主机配置的三种方式(基于域名) https://www.jb51.net/article/14978.htm 2.Nginx基于端口的虚拟主机配置 如一台服务器只有一个IP或需要通过不同的端口访问不同的虚拟主机,可以使用基于端口的虚拟主机配

  • 详解Golang开启http服务的三种方式

    前言 都说go标准库实用,Api设计简洁.这次就用go 标准库中的net/http包实现一个简洁的http web服务器,包括三种版本. v1最简单版 直接使用http.HandleFunc(partern,function(http.ResponseWriter,*http.Request){}) HandleFunc接受两个参数,第一个为路由地址,第二个为处理方法. //v1 func main() { http.HandleFunc("/", func(w http.Respon

  • 详解git merge命令应用的三种情景

    一.git merge 命令应用的三种情景 1.1 "快进"(无冲突) master分支 假设现在只有一个默认的 master 分支,并提交了3次,B0.B1和B2都是提交对象. 首先要清楚,每次产生的提交对象会包含一个指向上次提交对象(父对象)的指针,所以图中B0.B1和B2之间的箭头是指针指向父对象的意思,真正的提交顺序还是B0到B1再到B2.同时 master 指针指向最新的提交B2. 另外Git中还有一个名为 HEAD 的特殊指针,它是一个指针,指向当前所在的本地分支(可以将

  • 详解使用scrapy进行模拟登陆三种方式

    scrapy有三种方法模拟登陆方式: - 直接携带cookies - 找url地址,发送post请求存储cookie - 找到对应的form表单,自动解析input标签,自动解析post请求的url地址,自动带上数据,自动发送请求 1.携带cookies登陆github import scrapy import re class Login1Spider(scrapy.Spider): name = 'login1' allowed_domains = ['github.com'] start_

  • 详解Redis实现限流的三种方式

    面对越来越多的高并发场景,限流显示的尤为重要. 当然,限流有许多种实现的方式,Redis具有很强大的功能,我用Redis实践了三种的实现方式,可以较为简单的实现其方式.Redis不仅仅是可以做限流,还可以做数据统计,附近的人等功能,这些可能会后续写到. 第一种:基于Redis的setnx的操作 我们在使用Redis的分布式锁的时候,大家都知道是依靠了setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期实践(expire),我们在限流的主要目的就

随机推荐