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

作者:代码拾荒时间:2026-10-30 11:55:04浏览:119

本文详细解析JavaScript中计算两条线段交点的三种算法。首先介绍基于直线一般方程求交点并验证范围的基础方法;其次讲解利用法线投影判断端点相对位置的优化策略;最后重点阐述通过向量叉乘计算三角形面积符号来高效判定相交的方法。文章对比了各算法在V8引擎下的性能表现,并提供了完整代码实现与测试结论。

本文内容面向初学者,旨在讲解基础算法实现,欢迎技术专家指正。

引用

算法一:直线方程求交点法

已知线段1由端点a、b定义,线段2由端点c、d定义,目标是求解两线段的交点p。若两线段平行或共线,则视为不相交。

算法一:先计算两条线段所在直线的交点,再验证该交点是否同时位于两条线段内部。

计算直线交点时,可使用直线的一般式方程 ax+by+c=0(其中a、b、c为系数,非端点坐标)。也可使用点斜式或斜截式,此处暂不展开。

得到交点后,需根据交点与线段端点的坐标关系,判断交点是否落在两条线段的范围内。

具体公式如下图所示:

算法一:直线方程求交点法


<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.x和normalLine.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引擎下)。

必须承认,在JavaScript中执行常规计算时,三种算法的时间复杂度基本相当(尤其在V8引擎环境下)。测试用例需进行百万次级别的线段相交测试,才能显现出三种算法间的性能差异。

性能对比与总结

总结

本着精益求精和学习的态度,追求更优算法具有积极意义。以上是利用JavaScript实现线段交点计算的几种算法,内容基础,希望对读者有所帮助。

作者最新文章