canvas內部元素不能像DOM元素一樣方便的添加互動事件監聽,因為canvas內不存在元素這個概念,他們只是canvas繪製出來的圖形。這對互動開發來說是個必經障礙,想要監聽圖形的點擊事件思路很簡單,只要監聽canvas元素本身的點擊事件,再判斷點擊座標位於哪一個圖形內部,就變相實現了圖形點擊事件。本文將介紹三種方法,判斷座標點是否位於某個canvas圖形內部。
約定本文介紹的三種方法適用於識別canvas內形狀不規則且位置無規律的圖形點擊事件,對於形狀規則或者位置有規律的場景,肯定有更簡便的實現,這裡不做討論。
像素法像素檢測法的想法是,將canvas中的多個圖形(如果有多個的話)分別離屏繪製,並用getImageData() 方法分別獲取到像素資料保存。當canvas元素監聽到點擊事件時,透過點擊座標可以直接推算出點擊發生在canvas上的第幾個像素,然後遍歷前面保存的圖形數據,看看這個像素的alpha值是不是0,如果是0說明落點不在目前圖形內,否則就說明點到了這個圖形。
根據點擊座標得到所點擊的像素序號的方法:
像素序號= (縱座標-1) * canvas寬度+ 橫座標
例如在寬度為5 的畫布上點選座標(3,3) ,根據上述公式得到像素序號為(3-1) * 5 + 3 = 18 ,如圖所示:
因為canvas導出的圖形資料是將每個像素以rgba 的順序存成4個數字組成的數組,所以想存取指定像素的alpha值,只要讀取這個數組的第pIndex * 4 + 3 個值就可以了,如果這個值不為0,表示該像素可見,也就是點選到了該圖形。
這個方法是我認為思路最直接、結果最準確、而且對圖形形狀沒有任何要求的方法,但這個方法有一個致命的局限,當圖形需要在畫布上移動時,要頻繁的創建數據緩存才能保證檢測結果準確,受到畫布尺寸和圖形數量的影響, getImageData() 方法的效能會成為嚴重的瓶頸。所以如果canvas圖形是靜態的,這個方法非常適合,否則就不適合用這個方法了。
角度法角度判斷法的原理很容易理解,如果一個點在多邊形內部,則該點與多邊形所有頂點兩兩構成的夾角,相加應該剛好等於360°。
計算過程可以轉變為以下三個步驟:
1.已知多邊形頂點與已知座標,將座標與頂點兩兩組合成三點隊列
2. 已知三點求夾角,可使用餘玄定理
3.判斷夾角和是否360°
每一步都很簡單,實作如下:
//計算兩點距離const getDistence = function (p1, p2) { return Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y))};//角度法判斷點在多邊形內部const checkPointInPolyline = (point, polylinePoints) => { let totalA = 0; const A = point; for (let i = 0; i < polylinePoints.length; i++) { let B, C; if (i === polylinePoints.length - 1) { B = { x: polylinePoints[ i][0], y: polylinePoints[i][1] }; C = { x: polylinePoints[0][0], y: polylinePoints[0][1] }; } else { B = { x: polylinePoints[i][0], y: polylinePoints[i][1] }; C = { x: polylinePoints[i + 1][0] , y: polylinePoints[i + 1][1] }; } //計算角度const angleA = Math.acos((Math.pow(getDistence(A, C), 2) + Math.pow(getDistence(A, B), 2) - Math.pow(getDistence(B, C), 2)) / (2 * getDistence(A, C) * getDistence(A, B))) totalA += angleA } //判斷角度總和return totalA === 2 * Math.PI}這個方法有一個局限性,就是圖形必須是凸多邊形。如果不是凸多邊形需要先切割成凸多邊形再計算,這就比較複雜了。
類似的思路還有面積法,如果一個點在多邊形內部,那麼該點與多邊形所有頂點兩兩構成的三角形,面積相加應該等於多邊形的面積,首先計算多邊形的面積就很麻煩,所以這種方法可以直接pass掉。
射線法射線法是我講不清道理但非常好用的方法,只要判斷點與多邊形一側的交點個數為奇數,則點在多邊形內部。要注意的是,只要數任何一邊的焦點個數就可以,例如左側。這個方法不限制多邊形的類型,凸多邊形、凹多邊形甚至環形都可以。
實作起來也非常簡單:
const checkPointInPolyline = (point, polylinePoints) => { //射線法let leftSide = 0; const A = point; for (let i = 0; i < polylinePoints.length; i++) { let B, C; if (i = == polylinePoints.length - 1) { B = { x: polylinePoints[i][0], y: polylinePoints[i][1] }; C = { x: polylinePoints[0][0], y: polylinePoints[0][1] }; } else { B = { x: polylinePoints[i][0], y : polylinePoints[i][1] }; C = { x: polylinePoints[i + 1][0], y: polylinePoints[i + 1][1] }; } //判斷左側相交let sortByY = [By, Cy].sort((a,b) => ab) if (sortByY[0] < Ay && sortByY[1] > Ay) { if(Bx<Ax || Cx < Ax){ leftSide++ } } } return leftSide % 2 === 1}射線法有一種特殊情況,當點在多變形的一邊上時需要特殊處理。但在工程上我認為也可以不處理,因為如果使用者剛好點在圖形的邊界上,那麼程式認為他沒有點到也講的過去。
總結以上三種方法都可以實現canvas中不規則圖形的點擊檢測。其中,像素法的優點在於不挑形狀,而且在靜態場景中有一定的性能優勢;角度法應該說只有理論價值,實用性不佳;工程中最實用的當屬射線法,局限性小,實現簡單,多數時候只要知道射線法就可以了。