如下内容转载自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