如下內容轉載自Crape的文章《web頁面上的旋轉矩形碰撞》javascript
做者:Crapehtml
連接:https://juejin.im/post/5eede991e51d45740950c946java
來源:掘金web
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。算法
前言
本文主要是總結一下web頁面中的旋轉矩形的碰撞檢測,碰撞算法自己並不難,只是須要注意web座標系在計算中的影響。碰撞檢測應該是在遊戲等場景中很常見且基礎的功能,本文記錄了在JavaScript API GL遇到了這類碰撞問題的調研和實現的過程。數組
需求場景
用戶在地圖上實現MultiLabel文本標註覆蓋物時,會因爲兩個label座標過近,或者地圖的旋轉、縮放產生的變化而相互重疊。目前label的背景色均爲透明且暫時還不支持配置,文字重疊以後識別度降低不少,就計劃先實現label之間的避讓功能。檢測到兩個label碰撞時,根據優先級選擇隱藏其中的一個,保證文字的可讀性。ide
肯定算法
在JSAPI GL中,label並非在三維空間中的,而是繪製在屏幕上的,只是會根據用戶視角的移動實時計算出label在屏幕座標中所處的位置,而後在每一幀中進行繪製。label實際上就是一行文字,咱們能夠把它用一個矩形包圍起來,當作總體計算,由於每一個字之間的相對位置並不會變,這樣一來label的碰撞檢測實際上能夠轉化爲二維空間內的矩形碰撞。工具
通常的橫平豎直的矩形檢測碰撞很簡單,只要想清楚有哪些狀況便可,不在這裏贅述。可是用戶能夠對label進行旋轉和偏移操做,普通的檢測方法就不適用了,若是強行把label用一個大的水平矩形包裹起來再計算,精度損失會不少,因此調研了一下旋轉矩形的碰撞檢測方法。post
比較常見的一種方式是經過分離軸定律(SAT:Separating Axis Theorem)來計算,分離軸定義:兩個凸多邊形物體,若是能找到一個軸,使得兩個物體在該軸上的投影互不重疊,那麼這兩個物體就沒有發生碰撞,這條軸能夠稱爲分離軸。ui
通常不會遍歷全部角度的軸,而是檢測垂直於多邊形每條邊的軸,由於在這些軸上咱們能夠取到極值。對於矩形來講能夠進一步簡化,由於一個矩形的4條軸內有2個是重複的,因此只須要檢測矩形互相垂直的兩條邊對應的軸就能夠了。
進行判斷的具體方式有兩種:一是把每一個矩形的4個頂點投影到一個軸上,算出該矩形最長的連線距離,判斷兩個矩形的投影是否重疊;二是將兩個矩形的半徑距離投影到軸上,而後把兩個矩形中心點的連線投影到通一個軸上,判斷兩個矩形的半徑投影之和與中心點連線投影的大小。
本文采用第二種方式計算,首先搞清楚投影的概念,引入向量來進行計算:
咱們能夠用單位向量來表示垂直於邊線的軸,這樣一個向量在軸線上的投影長度能夠用該向量與投影軸上的單位向量的點積來表示。如上圖,A點座標爲(xa, ya),OB爲線段OA在x軸上的投影,x軸的單位向量爲(1, 0),OA · x軸單位向量 = (xa, ya) · (1, 0) = xa * 1 + ya * 0 = xa。
// 若是用數組[x ,y]表示一個向量,則兩個向量的點積結果能夠表示爲 function dot(vectorA, vectorB) { return Math.abs(vectorA[0] * vectorB[0] + vectorA[1] * vectorB[1]); }
而後就是如何表示矩形兩個軸的單位向量,假設矩形以自身的中心點爲原點,逆時針旋轉θ,其兩條相鄰邊的軸的單位向量以下圖所示:
單位圓的半徑爲1,因此單位向量OA爲 (cosθ, sinθ),另外一條邊的單位向量與OA垂直,爲(-sinθ, cosθ),這兩個單位向量的點積爲0。但這裏有一個很是重要的注意點:web頁面中的座標系與咱們平時使用的座標系不一樣,x軸正方向不變,y軸的正方向向下。我在最開始實現算法的過程當中忽略了這個問題,致使碰撞結果不對,調試了半天才發現緣由。在實際計算中,咱們所使用的座標都是web屏幕座標系下的,軸的正方向與經常使用的不一樣,因此兩個單位向量應該分別表示爲 (cosθ, -sinθ), (sinθ, cosθ),以下圖所示:
而後就是計算矩形的半徑投影,首先明確下半徑投影的概念,能夠理解爲矩形中心點到一個頂點的向量,在軸上的投影長度。其實就是,矩形在X軸上最遠處的交點,數學上意義就是2條檢測軸的投影之和。
兩個矩形檢測的過程當中,以其中一個矩形的檢測軸爲座標系,投影另一個矩形的檢測軸。如上圖所示,藍色線段爲左邊矩形的半徑投影,黃色線段爲右邊矩形檢測軸。咱們須要把右邊2條檢測軸投影到藍色線段所在X軸的單位向量(即左邊矩形的檢測軸單位向量),獲得投影比例,而後乘以檢測軸長度(即矩形長、寬的一半),可計算出右邊矩形的半徑投影。紅色線段則是兩個矩形中心點的連線,一樣須要計算它在藍色線段所在X軸的投影長度,若是中心點連線的投影長度大於兩個矩形的半徑投影之和,那麼在這條軸上兩個矩形沒有碰撞,不然發生碰撞。
檢測最終是否碰撞,須要對四個分離軸都檢測一次,在任何一個軸上沒有碰撞,則兩個矩形就沒有碰撞。
實現
實際實現的過程當中進行了簡單的旋轉矩形類,可根據實際業務需求調整,例如添加縮放、偏移等參數
class Rect { constructor(options) { const {center, height, width, angle} = options; this.centerPoint = [center.x, center.y]; this.halfHeight = height / 2; this.halfWidth = width / 2; this.setRotation(angle); } getProjectionRadius(axis) { // 計算半徑投影 const projectionAxisX = this.dot(axis, this.axisX); const projectionAxisY = this.dot(axis, this.axisY); return this.halfWidth * projectionAxisX + this.halfHeight * projectionAxisY; } dot(vectorA, vectorB) { // 向量點積 return Math.abs(vectorA[0] * vectorB[0] + vectorA[1] * vectorB[1]); } setRotation(angle) { // 計算兩個檢測軸的單位向量 const deg = (angle / 180) * Math.PI; this.axisX = [Math.cos(deg), -Math.sin(deg)]; this.axisY = [Math.sin(deg), Math.cos(deg)]; return this; } isCollision(check) { const centerDistanceVertor = [ this.centerPoint[0] - check.centerPoint[0], this.centerPoint[1] - check.centerPoint[1] ]; const axes = [ // 兩個矩形一共4條檢測軸 this.axisX, this.axisY, check.axisX, check.axisY ]; for (let i = 0, len = axes.length; i < len; i++) { if (this.getProjectionRadius(axes[i]) + check.getProjectionRadius(axes[i]) <= this.dot(centerDistanceVertor, axes[i])) { return false; // 任意一條軸沒碰上,就是沒碰撞 } } return true; } }
使用時每一個矩形實例化一個Rect類,而後調用實例上的isCollision
方法,參數傳入另外一個矩形的實例,最後返回一個boolean
類型的碰撞結果。
總結
封裝的這個類比較簡單,沒有涉及到裏面參數改變的問題,有須要的話能夠再完善。實現過程當中注意下web座標系的問題就能夠了。矩形應該是最簡單的一種,其餘凸多邊形的檢測會複雜一些,有興趣的話能夠本身嘗試一下。
本文參考如下blog: https://blog.csdn.net/tom_221x/article/details/38457757 https://aotu.io/notes/2017/02/16/2d-collision-detection/index.html
畫圖工具爲 GeoGebra sketch
實際效果能夠在騰訊位置服務官網的示例中嘗試https://lbs.qq.com/webDemoCenter/glAPI/glMarker/labelCollision