本文講的內容都很初級, 主要是面向和我一樣的初學者, 所以請各位算法帝們輕拍啊
引用
已知線段1(a,b) 和線段2(c,d) ,其中abcd為端點, 求線段交點p .(平行或共線視作不相交)
算法一: 求兩條線段所在直線的交點, 再判斷交點是否在兩條線段上.
求直線交點時我們可通過直線的一般方程ax+by+c=0求得(方程中的abc為係數,不是前面提到的端點,另外也可用點斜式方程和斜截式方程,此處暫且不論).
然後根據交點的與線段端點的位置關係來判斷交點是否在線段上.
公式如下圖:
<code>function segmentsIntr(a, b, c, d){ /** 1 解線性方程組, 求線段交點. **/ // 如果分母為0 則平行或共線, 不相交var denominator = (by - ay)*(dx - cx) - (ax - bx)*(cy - dy); if (denominator==0) { return false; } // 線段所在直線的交點坐標(x , y) var x = ( (bx - ax) * (dx - cx) * (cy - ay) + (by - ay) * (dx - cx) * ax - (dy - cy) * (bx - ax) * cx ) / denominator ; var y = -( (by - ay) * (dy - cy) * (cx - ax) + (bx - ax) * (dy - cy) * ay - (dx - cx) * (by - ay) * cy ) / denominator; /** 2 判斷交點是否在兩條線段上**/ if ( // 交點在線段1上(x - ax) * (x - bx) <= 0 && (y - ay) * (y - by) <= 0 // 且交點也在線段2上&& (x - cx) * (x - dx) <= 0 && (y - cy) * (y - dy) <= 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=by - ay, ny=ax - bx; var normalLine = { x: nx, y: ny };注意:其中normalLine.x和normalLine.y的幾何意義表示法線的方向, 而不是坐標.
求點c在法線上的投影位置:
var dist= normalLine.x*cx + normalLine.y*cy;
注意:這裡的"投影位置"是一個標量, 表示的是到法線原點的距離, 而不是投影點的坐標.
通常知道這個距離就足夠了.
當我們把圖中點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 = (by - ay), ny1 = (ax - bx); //線段cd的法線N2 var nx2 = (dy - cy), ny2 = (cx - dx); //兩條法線做叉乘, 如果結果為0, 說明線段ab和線段cd平行或共線,不相交var denominator = nx1*ny2 - ny1*nx2; if (denominator==0) { return false; } //在法線N2上的投影var distC_N2=nx2 * cx + ny2 * cy; var distA_N2=nx2 * ax + ny2 * ay-distC_N2; var distB_N2=nx2 * bx + ny2 * by-distC_N2; // 點a投影和點b投影在點c投影同側(對點在線段上的情況,本例當作不相交處理); if ( distA_N2*distB_N2>=0 ) { return false; } // //判斷點c點d 和線段ab的關係, 原理同上// //在法線N1上的投影var distA_N1=nx1 * ax + ny1 * ay; var distC_N1=nx1 * cx + ny1 * cy-distA_N1; var distD_N1=nx1 * dx + ny1 * dy-distA_N1; if ( distC_N1*distD_N1>=0 ) { return false; } //計算交點坐標var fraction= distA_N2 / denominator; var dx= fraction * ny1, dy= -fraction * nx1; return { x: ax + dx , y: ay + dy }; }最後求交點坐標的部分所用的方法看起來有點奇怪, 有種摸不著頭腦的感覺.
其實它和算法一里面的算法是類似的,只是裡面的很多計算項已經被提前計算好了.
換句話說, 算法二里求交點坐標的部分其實也是用的直線的線性方程組來做的.
現在來簡單粗略很不科學的對比一下算法一和算法二:
1、最好情況下, 兩種算法的複雜度相同
2、最壞情況, 算法一和算法二的計算量差不多
3、但是算法二提供了更多的”提前結束條件”,所以平均情況下,應該算法二更優.
實際測試下來, 實際情況也確實如此.
前面的兩種算法基本上是比較常見的可以應付絕大多數情況. 但是事實上還有一種更好的算法.
這也是我最近才新學會的(我現學現賣了,大家不要介意啊…)
算法三: 判斷每一條線段的兩個端點是否都在另一條線段的兩側, 是則求出兩條線段所在直線的交點, 否則不相交.
(咦? 怎麼感覺和算法二一樣啊? 不要懷疑確實一樣… )
所謂算法三, 其實只是對算法二的一個改良, 改良的地方主要就是:
不通過法線投影來判斷點和線段的位置關係, 而是通過點和線段構成的三角形面積來判斷.
先來複習下三角形面積公式: 已知三角形三點a(x,y) b(x,y) c(x,y), 三角形面積為:
<code>var triArea=( (ax - cx) * (by - cy) - (ay - cy) * (bx - cx) ) /2 ; </code>
因為兩向量叉乘==兩向量構成的平行四邊形(以兩向量為鄰邊)的面積, 所以上面的公式也不難理解.
而且由於向量是有方向的, 所以面積也是有方向的, 通常我們以逆時針為正, 順時針為負數.
改良算法關鍵點就是:
如果”線段ab和點c構成的三角形面積”與”線段ab和點d構成的三角形面積” 構成的三角形面積的正負符號相異,
那麼點c和點d位於線段ab兩側.
如下圖所示:
圖中虛線所示的三角形, 纏繞方向(三邊的定義順序)不同, 所以面積的正負符號不同.
下面還是先看代碼:
由於我們只要判斷符號即可, 所以前面的三角形面積公式我們就不需要後面的除以2 了.
function segmentsIntr(a, b, c, d){ // 三角形abc 面積的2倍var area_abc = (ax - cx) * (by - cy) - (ay - cy) * (bx - cx); // 三角形abd 面積的2倍var area_abd = (ax - dx) * (by - dy) - (ay - dy) * (bx - dx); // 面積符號相同則兩點在線段同側,不相交(對點在線段上的情況,本例當作不相交處理); if ( area_abc*area_abd>=0 ) { return false; } // 三角形cda 面積的2倍var area_cda = (cx - ax) * (dy - ay) - (cy - ay) * (dx - ax); // 三角形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*(bx - ax), dy= t*(by - ay); return { x: ax + dx , y: ay + dy }; }最後計算交點坐標的部分和算法二同理.
算法三在算法二的基礎上, 大大簡化了計算步驟, 代碼也更精簡. 可以說,是三種算法裡, 最好的.實際測試結果也是如此.
當然必須坦誠的來說, 在Javascript裡, 對於普通的計算, 三種算法的時間複雜度其實是差不多的(尤其是V8引擎下).
我的測試用例裡也是進行變態的百萬次級別的線段相交測試才能拉開三種算法之間的差距.
總結
不過本著精益求精以及學習的態度而言, 追求一個更好的算法, 總是有其積極意義的。以上就是利用js實現線段交點的幾種算法,內容不是很深奧,希望對大家學習js有所幫助。