射線法是一種很簡單直觀的判斷平面內點是否在多邊形內的方法。除了射線法還有很多其他的方法,今天就再介紹一種通過迴轉數來判斷的方法。
平面中的閉合曲線關於一個點的迴轉數(又叫卷繞數),代表了曲線繞過該點的總次數。下面這張圖動態演示了迴轉數的概念:圖中紅色曲線關於點(人所在位置)的迴轉數爲 2。
迴轉數是拓撲學中的一個基本概念,具有很重要的性質和用途。本文並不打算在這一點上展開論述,這需要具備相當的數學知識,否則會非常乏味和難以理解。我們暫時只需要記住迴轉數的一個特性就行了:
當迴轉數爲 0 時,點在閉合曲線外部。
對於給定的點和多邊形,迴轉數應該怎麼計算呢?
用線段分別連接點和多邊形的全部頂點。
計算所有點與相鄰頂點連線的夾角。
計算所有夾角和。注意每個夾角都是有方向的,所以有可能是負值。
最後根據角度累加值計算迴轉數。看過本文開頭的介紹,很容易理解 360°(2π)相當於一次迴轉。
思路介紹完了,下面兩點是實現中需要留意的問題。
JavaScript 的數只有 64 位雙精度浮點這一種。對於三角函數產生的無理數,浮點數計算不可避免會造成一些誤差,因此在最後計算迴轉數需要做取整操作。
通常情況下,平面直角座標系內一個角的取值範圍是 -π 到 π 這個區間,這也是 JavaScript 三角函數 Math.atan2() 返回值的範圍。但 JavaScript 並不能直接計算任意兩條線的夾角,我們只能先計算兩條線與 X 正軸夾角,再取兩者差值。這個差值的結果就有可能超出 -π 到 π 這個區間,因此我們還需要處理差值超出取值區間的情況。
老規矩,上代碼。
/**
* @description 迴轉數法判斷點是否在多邊形內部
* @param {Object} p 待判斷的點,格式:{ x: X座標, y: Y座標 }
* @param {Array} poly 多邊形頂點,數組成員的格式同 p
* @return {String} 點 p 和多邊形 poly 的幾何關係
*/
function windingNumber(p, poly) {
var px = p.x,
py = p.y,
sum = 0
for(var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
var sx = poly[i].x,
sy = poly[i].y,
tx = poly[j].x,
ty = poly[j].y
// 點與多邊形頂點重合或在多邊形的邊上
if((sx - px) * (px - tx) >= 0 && (sy - py) * (py - ty) >= 0 && (px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
return 'on'
}
// 點與相鄰頂點連線的夾角
var angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)
// 確保夾角不超出取值範圍(-π 到 π)
if(angle >= Math.PI) {
angle = angle - Math.PI * 2
} else if(angle <= -Math.PI) {
angle = angle + Math.PI * 2
}
sum += angle
}
// 計算迴轉數並判斷點和多邊形的幾何關係
return Math.round(sum / Math.PI) === 0 ? 'out' : 'in'
}