詳解js實現線段交點的三種演算法,詳解js線段交點

來源:互聯網
上載者:User

詳解js實現線段交點的三種演算法,詳解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有所協助。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.