html5中監(jiān)聽canvas內(nèi)部元素點(diǎn)擊事件的三種方法

canvas內(nèi)部元素不能像DOM元素一樣方便的添加交互事件監(jiān)聽,因?yàn)閏anvas內(nèi)不存在“元素”這個(gè)概念,他們僅僅是canvas繪制出來的圖形。這對于交互開發(fā)來說是一個(gè)必經(jīng)障礙,想要監(jiān)聽圖形的點(diǎn)擊事件思路很簡單,只要監(jiān)聽canvas元素本身的點(diǎn)擊事件,再判斷點(diǎn)擊坐標(biāo)位于哪一個(gè)圖形內(nèi)部,就變相實(shí)現(xiàn)了圖形點(diǎn)擊事件。本文將介紹三種方法,判斷坐標(biāo)點(diǎn)是否位于某個(gè)canvas圖形內(nèi)部。
約定
本文介紹的三種方法適用于識(shí)別canvas內(nèi)形狀不規(guī)則而且位置無規(guī)律的圖形點(diǎn)擊事件,對于形狀規(guī)則或者位置有規(guī)律的場景,肯定有更簡便的實(shí)現(xiàn),這里不做討論。
像素法
像素檢測法的思路是,將canvas中的多個(gè)圖形(如果有多個(gè)的話)分別離屏繪制,并用 getImageData() 方法分別獲取到像素?cái)?shù)據(jù)保存起來。當(dāng)canvas元素監(jiān)聽到點(diǎn)擊事件時(shí),通過點(diǎn)擊坐標(biāo)可以直接推算出點(diǎn)擊發(fā)生在canvas上的第幾個(gè)像素,然后遍歷前面保存的圖形數(shù)據(jù),看看這個(gè)像素的alpha值是不是0,如果是0說明落點(diǎn)不在當(dāng)前圖形內(nèi),否則就說明點(diǎn)到了這個(gè)圖形。
根據(jù)點(diǎn)擊坐標(biāo)得到所點(diǎn)擊的像素序號的方法:
像素序號 = (縱坐標(biāo)-1) * canvas寬度 + 橫坐標(biāo)
比如在寬度為 5 的畫布上點(diǎn)擊坐標(biāo) (3,3) ,根據(jù)上述公式得到像素序號是 (3-1) * 5 + 3 = 18 ,如圖所示:
因?yàn)閏anvas導(dǎo)出的圖形數(shù)據(jù)是將每個(gè)像素以 rgba 的順序存成4個(gè)數(shù)字組成的數(shù)組,所以想訪問指定像素的alpha值,只要讀取這個(gè)數(shù)組的第 pIndex * 4 + 3 個(gè)值就可以了,如果這個(gè)值不為0,說明該像素可見,也就是點(diǎn)擊到了該圖形。
這個(gè)方法是我認(rèn)為思路最直接、結(jié)果最準(zhǔn)確、而且對圖形形狀沒有任何要求的方法,但這個(gè)方法有一個(gè)致命的局限,當(dāng)圖形需要在畫布上移動(dòng)時(shí),要頻繁的創(chuàng)建數(shù)據(jù)緩存才能保證檢測結(jié)果準(zhǔn)確,受到畫布尺寸和圖形數(shù)量的影響, getImageData() 方法的性能會(huì)成為嚴(yán)重的瓶頸。所以如果canvas圖形是靜態(tài)的,這個(gè)方法非常適合,否則就不適合用這個(gè)方法了。
角度法
角度判斷法的原理很容易理解,如果一個(gè)點(diǎn)在多邊形內(nèi)部,則該點(diǎn)與多邊形所有頂點(diǎn)兩兩構(gòu)成的夾角,相加應(yīng)該剛好等于360°。
計(jì)算過程可以轉(zhuǎn)變?yōu)橐韵氯齻€(gè)步驟:
1.已知多邊形頂點(diǎn)和已知坐標(biāo),將坐標(biāo)與頂點(diǎn)兩兩組合成三點(diǎn)隊(duì)列
2. 已知三點(diǎn)求夾角,可以使用 余玄定理
3.判斷夾角之和是否360°
每一步都很簡單,實(shí)現(xiàn)如下:
//計(jì)算兩點(diǎn)距離 const getDistence = function (p1, p2) { return Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)) }; //角度法判斷點(diǎn)在多邊形內(nèi)部 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] }; } //計(jì)算角度 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 }
這個(gè)方法有一個(gè)局限性,就是圖形必須是 凸多邊形 。如果不是凸多邊形需要先切割成凸多邊形再計(jì)算,這就比較復(fù)雜了。
類似的思路還有面積法,如果一個(gè)點(diǎn)在多邊形內(nèi)部,那么該點(diǎn)與多邊形所有頂點(diǎn)兩兩構(gòu)成的三角形,面積相加應(yīng)該等于多邊形的面積,首先計(jì)算多邊形的面積就很麻煩,所以這種方法可以直接pass掉。
射線法
射線法是一個(gè)我講不清道理但非常好用的方法,只要判斷點(diǎn)與多邊形一側(cè)的交點(diǎn)個(gè)數(shù)為奇數(shù),則點(diǎn)在多邊形內(nèi)部。需要注意的是,只要數(shù)任何一側(cè)的焦點(diǎn)個(gè)數(shù)就可以,比如左側(cè)。這個(gè)方法不限制多邊形的類型,凸多邊形、凹多邊形甚至環(huán)形都可以。
實(shí)現(xiàn)起來也非常簡單:
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] }; } //判斷左側(cè)相交 let sortByY = [B.y, C.y].sort((a,b) => a-b) if (sortByY[0] < A.y && sortByY[1] > A.y){ if(B.x<A.x || C.x < A.x){ leftSide++ } } } return leftSide % 2 === 1 }
射線法有一種特殊情況,當(dāng)點(diǎn)在多變形的一條邊上時(shí)需要特殊處理。但在工程中我認(rèn)為也可以不處理,因?yàn)槿绻脩魟偤命c(diǎn)在圖形的邊界上,那么程序認(rèn)為他沒有點(diǎn)到也講的過去。
總結(jié)
以上三種方法都可以實(shí)現(xiàn)canvas中不規(guī)則圖形的點(diǎn)擊檢測。其中,像素法的優(yōu)勢在于不挑形狀,而且在靜態(tài)場景中有一定的性能優(yōu)勢;角度法應(yīng)該說只有理論價(jià)值,實(shí)用性不佳;工程中最實(shí)用的當(dāng)屬射線法,局限性小,實(shí)現(xiàn)簡單,多數(shù)時(shí)候只需要知道射線法就可以了。
相關(guān)文章
如何在Canvas上的圖形/圖像綁定事件監(jiān)聽的實(shí)現(xiàn)
這篇文章主要介紹了如何在Canvas上的圖形/圖像綁定事件監(jiān)聽的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著2020-09-16- 這篇文章主要介紹了如何在Canvas中添加事件的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)2019-05-21
- 這篇文章主要介紹了詳解Canvas事件綁定的相關(guān)資料,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-06-27
- 這篇文章主要介紹了HTML5 Canvas的事件處理介紹,本文講解了Canvas的限制、給Canvas元素綁定事件、isPointInPath方法、循環(huán)重繪和事件冒泡等內(nèi)容,需要的朋友可以參考下2015-04-24
一個(gè)不錯(cuò)的HTML5 Canvas多層點(diǎn)擊事件監(jiān)聽實(shí)例
寫到一個(gè)多層點(diǎn)擊事件的監(jiān)聽。覺得還是挺好玩的。于是把它從模塊中抽化出來了,喜歡的朋友可以參考下2014-04-29HTML5 Canvas鼠標(biāo)與鍵盤事件demo示例
本文的主要母的是演示HTML5 Canvas鼠標(biāo)事件,獲取Canvas對象上的鼠標(biāo)坐標(biāo),演示鍵盤事件通過鍵盤控制Canvas上對象移動(dòng),感興趣的朋友可以參考下哈,希望對大家有所幫助2013-07-04- 這篇文章主要介紹了詳解如何在Canvas中添加事件的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)2021-04-16