hover 背後的數學和圖形學

JunpengZ發表於2021-11-17

前端開發中,hover是最常見的滑鼠操作行為之一,用起來也很方便,CSS直接提供:hover偽類,js可以通過mouseover+mouseout事件模擬,甚至一些第三方庫/框架直接提供了 hover API ,比如 jQuery 的 hover() 函式。大部分前端開發者在使用這些很方便的方法時,可能並沒有思考過 hover 背後的實現原理。

hover 是跟 DOM 繫結的,常規 DOM 是一個個矩形(CSS 盒模型),滑鼠移動時瀏覽器需要判斷滑鼠指標座標是否在這個 DOM 的矩形範圍之內,根本上是一個數學問題,即判斷一個點是否位於一個矩形內

這是跟很簡單的計算,對比點座標和矩形四個角的座標就行了。

但是對於其他的幾種前端圖形技術來說,就不一定這麼簡單了,比如SVG、Canvas、WebGL,因為這幾種圖形技術中並非只有矩形這一種簡單圖形。

SVG

SVG 除了 <rect> 矩形之外,還有<circle><line>等代表某種特定圖形的元素,以及<arc><path>這類繪製任意圖形的元素。

SVG 實現 hover 的方式跟普通 HTML 並無二致,SVG 本身就是一種特異的 HTML,可以直接使用絕大部分 DOM API 和 CSS 選擇器。

Canvas 2D

Canvas 2D(下文簡稱Canvas)是比 SVG 更底層的圖形技術,只有 rect 這一種特定圖形,其他的圖形都是通過使用直線、弧線、貝塞爾曲線等路徑 API 繪製出來。

Canvas 繪製的圖形都是在一個 <canvas> 元素內,並不能向 DOM 或 SVG 一樣使用 CSS 偽類或js事件實現某個圖形的hover效果。為解決這個問題, Canvas 提供了isPointInPath() API 來判斷某個點是否位於某個閉合路徑之內,不過這個 API 並不是很好用,這個方法時掛載到繪製上下文 context上的,只能判斷某個點是否位於當前繪製的路徑內,這是個非常操蛋的限制。所以在 Canvas 2D 技術領域也通常會借鑑 WebGL 的實現方案,即通過數學方法判斷一個點是否位於一個不規則多邊形內

WebGL

WebGL 是比 Canvas 2D 更底層的圖形技術,可以說是現階段前端領域最底層、最接近圖形學的圖形技術。

未來可以期待一下 WebGPU。

WebGL 中只有點、線段、三角形三種基本圖元,所有視覺可見的形狀都是以這三種圖元組成。其實主要是三角形,包括絕大多數的線和點也是由三角形組成。因為 WebGL 1.0 不支援寬於1畫素的線段,而且折線還要考慮各種 join 效果。

WebGL 2.0 支援寬於 1畫素的線段。

WebGL 中實現某個圖形的 hover 以及click、mouseover、mouseout等滑鼠事件的根本就是上文提到的判斷一個點是否位於一個不規則多邊形內。這是一個純粹的幾何數學問題,理論上有很多種解法,其中在工程領域使用最普遍的是射線法,這是目前綜合計算複雜度和效能消耗的最優解之一。

射線法的原理是以待判斷的點座標畫一條水平的直線,然後判斷這條直接與多邊形各條邊的交點數量,如果是奇數則代表點在多邊形內,如果是偶數則代表點在多邊形之外。射線法可以適用於任意多邊形,包括有洞(hole)的多邊形,具體的推導過程就不貼了,感興趣的話可以自己查一下相關資料。

射線法涉及以下三個問題:

  1. 如何獲取多邊形的各條邊的端座標?
  2. 如果多邊形的某條邊是曲線怎麼辦?
  3. 如何判斷兩條線段有交點?

如何獲取多邊形的各條邊的端座標?

這其實並不是一個圖形繪製領域的問題,而是資料製備領域的問題。以一個簡單圖形舉例:

上圖中的六邊形是由四個三角形組成,前端從服務端拿到的資料一般只包括六邊形的6個頂點座標,即v1 - v6,而且這6個座標點是按照順時針排列(如果有hole,則hole的頂點是逆時針排列),如下:

[v1,v2,v3,v4,v5,v6]

前端拿到頂點陣列後需要使用三角剖分演算法將其切割成4個三角形,最後才給到 WebGL 繪製。

也就是說,在資料製備階段就已經將多邊形的每個頂點座標確定了,然後依序兩兩相接就是多邊形的各條邊。

當然也不排除有的技術團隊在資料製備階段就進行了三角剖分,但這麼幹的比較少,因為剖分後資料量會增長很多,會帶來額外的儲存成本和網路通訊耗時。

如果多邊形的某條邊是曲線怎麼辦?

這是一個偽命題。WebGL 中不存在曲線,任意圖形都是通過點、線段、三角形三種圖元組合而成,即便視覺上是一個曲線或圓弧,本質上也是一個個三角形,只不過通過演算法處理讓人眼看不出明顯的折角。所以WebGL中的任何圖形本質上都是多邊形,既然是多邊形就可以按照上文的方案解決點與多邊形的相對位置判斷問題。

如何判斷兩條線段有交點?

明確了上面兩個問題之後,就只剩下判斷兩條線段是否相交這一個問題了。這同樣是個純粹的數學問題。

回顧上文提到的多邊形頂點資料製備,多邊形的邊是由相鄰兩個頂點相連而成,頂點是有序的,也就是說多邊形的每條邊都是有向線段,所以判斷兩條線段是否相交這個問題準確的說發應該是:判斷兩個有模向量是否相交

這就回到了高中數學哈哈。

第一個知識點是向量叉乘

t = 向量A x 向量B = |A||B|sin(a)

其中a是向量A和向量B的夾角。為了方便描述,我們把上述計算得到的結果賦值為t

嚴格的說,只有三維向量的叉乘才有幾何意義,兩個向量叉乘得到的是一個垂直於向量A和向量B、模為t的三維向量。二維向量的叉乘是從三維向量基礎上延展出來的,有以下幾何意義:

  1. t為向量A和向量B為相鄰邊的平行四邊形的面積;
  2. 如果t>0,那麼向量A正旋轉到向量B的角度小於180度;
  3. 如果t<0,那麼向量A正旋轉到向量B的角度大於180度;
  4. 如果t=0 ,那麼向量A和B平行。

判斷兩條線段是否相交用到了上述的規則2-4。先看下面這張圖:

如果線段AB和CD相交可以推匯出以下規則:

  1. 點A和點B分別位於線段CD的兩側;
  2. 點C和點D分別位於線段AB的兩側。

把第一條規則轉化成數學語言,用向量描述:

  1. 向量AC位於向量AB的逆時針方向;
  2. 向量AD位於向量AB的順時針方向;
  3. 向量BC位於向量BA的順時針方向;
  4. 向量BD位於向量BA的逆時針方向。

進一步轉化便是:

AB x AC > 0
AB x AD < 0
BA x BC < 0
BA x BD > 0

同理轉化第二條規則,不再贅述。

總結

本文簡單總結了前端常用的各種圖形技術實現hover效果的方法,水平有限,聊當拋磚引玉。前端很多常用的方法或API底層都很值得玩味,這不比八股文有意思?

相關文章