【前提】需要了解向量相关知识,例如叉积、点积的概念
一、完整代码及样式示例
1.1. 效果图
①未碰撞
②碰撞
③包含
1.2. 完整代码示例
【注】APP查看文章时,不知道是什么原因,在文章中操作这个示例会对DOM的判定有影响,但是点击右上角的
查看详情后,在详情页的实例才是正常效果
二、实现方法及逻辑解析
2.1. 计算逻辑图解
2.2. 核心代码逻辑解读
2.2.1. 检测两条线段是否相交
『❗❗❗注意❗❗❗』:当两条线段处于同一直线上时,不论两条线段是否相交,叉积的值最后都会是0,所以需要基于这种情况再做一次判断。
【常量】线段AB:A(-5, 9)、B(3, 1),线段CD:C(-11, 9)、D(14, -1)。
向量A->B(8, -8)、向量C->D(25, -10)、向量C->A(6, 0)、向量C->B(14, -8)、向量D->A(-19, 10)、向量D->B(-11, 2)、向量A->C(-6, 0)、向量A->D(19, -10)、向量B->C(-14, 8)、向量B->D(11, -2)。
【变量】用于替代线段CD来测试叉积的线段MN:M(2, 5)、N(5, 10)。向量M->N(3, 5)、向量M->A(-7, 4)、向量M->B(1, -4)、向量N->A(-10, -1)、向量N->B(-2, -9)、向量A->M(7, -4)、向量A->N(10, 1)、向量B->M(-1, 4)、向量B->N(2, 9)。
其主要逻辑思路是互相判断相比较的两个点是否在自身点的两侧,若两个线段的结果都是对应的点在自身两侧,则证明两条线段相交:
-
向量C->A与向量C->B的叉积:值<0代表向量C->B在向量C->A的顺时针方向(下方),值=0代表两个向量共线,值>0代表向量C->B在向量C->A的逆时针方向(上方)向量C->A×向量C->B= (6 * -8) - (0 * 14) = -48向量M->A×向量M->B= (-7 * -4) - (4 * 1) = 24 -
向量D->A与向量D->B的叉积:值<0代表向量D->B在向量D->A的顺时针方向(下方),值=0代表两个向量共线,值>0代表向量D->B在向量D->A的逆时针方向(上方)向量D->A×向量D->B= (-19 * 2) - (-11 * 10) = 72向量N->A×向量N->B= (-10 * -9) - (-2 * -1) = 88 -
向量A->C与向量A->D的叉积:值<0代表向量A->D在向量A->C的顺时针方向(下方),值=0代表两个向量共线,值>0代表向量A->D在向量A->C的逆时针方向(上方)向量A->C×向量A->D= (-6 * -10) - (19 * 0) = 60向量A->M×向量A->N= (7 * 1) - (10 *- 4) = 47 -
向量B->C与向量B->D的叉积:值<0代表向量B->D在向量B->C的顺时针方向(下方),值=0代表两个向量共线,值>0代表向量B->D在向B->C的逆时针方向(上方)向量B->C×向量B->D= (-14 * -2) - (11 * 8) = -60向量B->M×向量B->N= (-1 * 9) - (2 * 4) = -17
总结
步骤一 × 步骤二:值<0代表C、D两点在线段AB的两侧,值=0代表C、D两点至少有一点在直线AB上,值>0代表C、D两点在线段AB的同侧
步骤三 × 步骤四:值<0代表A、B两点在线段CD的两侧,值=0代表A、B两点至少有一点在直线CD上,值>0代表A、B两点在线段CD的同侧
故步骤一 × 步骤二 <= 0 且步骤三 × 步骤四 <= 0 代表 线段AB和线段CD相交或重合
计算详解
(向量C->A×向量C->B) * (向量D->A×向量D->B) < 0 C、D两点在线段AB的两侧
(向量A->C×向量A->D) * (向量B->C×向量B->D) < 0 A、B两点在线段CD的两侧
(向量M->A×向量M->B) * (向量N->A×向量N->B) > 0 M、N两点在线段AB的同侧
(向量A->M×向量A->N) * (向量B->M×向量B->N) < 0 A、B两点在线段MN的两侧
依照上面的计算可以得出结论:线段AB和线段CD相交,线段AB和线段MN不相交
2.2.2 检测点是否在矩形内
【常量】矩形的四个点位坐标=>v0(-6, 4)、 v1(5, 4)、 v2(5, -3)、 v3(-6, -3),以及需要检测的Q(x, y)点。向量v1->v0(-11, 0)、向量v3->v0(0, 7)。
【变量】下面将使用6个点来模拟Q点位置,来判断点是否在矩形内:M0(-9, 2)、M1(-2, 2)、M2(7, 2)、N0(-4, 7)、N1(-4, 2)、N2(-4, -4)。向量M0->v0(3, 2)、向量M1->v0(-4, 2)、向量M2->v0(-13, 2)、向量N0->v0(-2, -3)、向量N1->v0(-2, 2)、向量N2->v0(-2, 8)。
检测主要利用了向量的点击和投影比较来判断点是否在矩形内,主要比较步骤是:
-
向量Q->v0与向量v1->v0点积:值>0代表Q点在v0右侧,值<0代表Q点在v0左侧,值=0代表Q的x值与v0的x值相同向量M0->v0*向量v1->v0= (3 * -11) + (2 * 0) = -33向量M1->v0*向量v1->v0= (-4 * -11) + (2 * 0) = 44向量M2->v0*向量v1->v0= (-13 * -11) + (2 * 0) = 143向量N0->v0*向量v1->v0= (-2 * -11) + (-3 * 0) = 22向量N1->v0*向量v1->v0= (-2 * -11) + (2 * 0) = 22向量N2->v0*向量v1->v0= (-2 * -11) + (8 * 0) = 22 -
向量Q->v0在向量v1->v0上投影的长度与向量v1->v0的模进行比较:若长度小于模长则Q点位于v1的左侧,若大于模长则位于v1的右侧,若等于模长则Q的x值和v1的x值相同(
向量M0->v0*向量v1->v0) - (向量v1->v0*向量v1->v0) = -33 - 121 = -154(
向量M1->v0*向量v1->v0) - (向量v1->v0*向量v1->v0) = 44 - 121 = -77(
向量M2->v0*向量v1->v0) - (向量v1->v0*向量v1->v0) = 143 - 121 = 22(
向量N0->v0*向量v1->v0) - (向量v1->v0*向量v1->v0) = 22 - 121 = -99(
向量N1->v0*向量v1->v0) - (向量v1->v0*向量v1->v0) = 22 - 121 = -99(
向量N2->v0*向量v1->v0) - (向量v1->v0*向量v1->v0) = 22 - 121 = -99 -
向量Q->v0与向量v3->v0点积:值>0代表Q点在v3的下方,值<0代表Q点在v0的上方,值=0代表Q的y值与v0的y值相同向量N0->v0*向量v3->v0= (-2 * 0) + (-3 * 7) = -21向量N1->v0*向量v3->v0= (-2 * 0) + (2 * 7) = 14向量N2->v0*向量v3->v0= (-2 * 0) + (8 * 7) = 56向量M0->v0*向量v3->v0= (3 * 0) + (2 * 7) = 14向量M1->v0*向量v3->v0= (-4 * 0) + (2 * 7) = 14向量M2->v0*向量v3->v0= (-13 * 0) + (2 * 7) = 14 -
向量Q->v0与向量v3->v0上投影的长度与向量v3->v0的模进行比较:若长度小于模长则Q点位于v3的上方,若大于模长则位于v3的下方,若等于模长则Q的y值和v3的y值相同(
向量N0->v0*向量v3->v0) - (向量v3->v0*向量v3->v0) = -21 - 49 = -70(
向量N1->v0*向量v3->v0) - (向量v3->v0*向量v3->v0) = 14 - 49 = -35(
向量N2->v0*向量v3->v0) - (向量v3->v0*向量v3->v0) = 56 - 49 = 7(
向量M0->v0*向量v3->v0) - (向量v3->v0*向量v3->v0) = 14 - 49 = -35(
向量M1->v0*向量v3->v0) - (向量v3->v0*向量v3->v0) = 14 - 49 = -35(
向量M2->v0*向量v3->v0) - (向量v3->v0*向量v3->v0) = 14 - 49 = -35
【总结】
步骤一<=0+步骤二<=模长= Q点在v0和v1点之间
步骤三<=0+步骤四<=模长= Q点在v0和v3点之间
故步骤一>=0+步骤二<=模长+步骤三>=0+步骤四<=模长= Q点在以v0、v1、v2、v3组成的矩形内:
① 仅有M1满足在v0和v3点之间
0 < 向量M1->v0 * 向量v1->v0 < 向量v1->v0 * 向量v1->v0
② M0、M1、M2均满足在v0和v3点之间
0 < 向量M1->v0 * 向量v3->v0 < 向量v3->v0 * 向量v3->v0
③ N0、N1、N2均满足在v0和v1点之间
0 < 向量N1->v0 * 向量v1->v0 < 向量v1->v0 * 向量v1->v0
④ 仅有N1满足在v0和v3点之间
0 < 向量N1->v0 * 向量v3->v0 < 向量v3->v0 * 向量v3->v0
通过上述计算发现仅有M1和N1点符合Q点在rect内的变量可选项
三、核心代码(TS及JS)
3.1. TS封装的类
interface RectOptionsInter {
x: number;
y: number;
width: number;
height: number;
style?: { rotation?: number; padding?: number[] };
}
const defStyle: { rotation: number; padding: number[] } = {
rotation: 0,
padding: [0, 0, 0, 0],
};
class Rect {
x?: number; // 矩形左上角点的x坐标
y?: number; // 矩形左上角点的y坐标
width?: number; // 矩形的宽度
height?: number; // 矩形的高度
style?: { rotation?: number; padding?: number[] }; // 矩形样式信息,包含旋转角度和内边距
rotation?: number; // 矩形的旋转角度
_vertexes?: Vertex[]; // 矩形当前四个点的坐标[v0, v1, v2, v3]
_borders?: Line[]; // 矩形当前四条边的数组信息[{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
/**
* 矩形类的构造函数
* @param {RectOptionsInter} options 矩形的初始参数,包括位置、大小和样式
*/
constructor({
x,
y,
width,
height,
style = {
rotation: 0,
padding: [0, 0, 0, 0],
},
}: RectOptionsInter) {
this.init({ x, y, width, height, style });
}
init({
x,
y,
width,
height,
style = {
rotation: 0,
padding: [0, 0, 0, 0],
},
}: RectOptionsInter): void {
if (!style.padding) {
style.padding = defStyle.padding;
}
if (style.rotation === undefined) {
style.rotation = defStyle.rotation;
}
this.x = x - style.padding[3];
this.y = y - style.padding[0];
this.width = width + style.padding[1] + style.padding[3];
this.height = height + style.padding[0] + style.padding[2];
this.style = style;
this.rotation = style.rotation;
this._vertexes = this.getVertexes(); // 矩形当前四个点的坐标[v0, v1, v2, v3]
this._borders = this.getBorders(); // 矩形当前四条边的数组信息[{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
}
setRectData({
x,
y,
width,
height,
style = { rotation: 0, padding: [0, 0, 0, 0] },
}: RectOptionsInter): void {
this.init({ x, y, width, height, style });
}
getRectData(): {
x?: number;
y?: number;
width?: number;
height?: number;
style?: { rotation?: number; padding?: number[] };
} {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
style: this.style,
};
}
getVertexes() {
if (
this.x === undefined ||
this.y === undefined ||
this.width === undefined ||
this.height === undefined ||
this.rotation === undefined
) {
throw new Error(
"Rect properties x, y, width, and height must be defined."
);
}
const c = new Vertex(this.x + this.width / 2, this.y + this.height / 2); // 矩形中心坐标
const v0 = new Vertex(this.x, this.y).rotate(c, this.rotation);
const v1 = new Vertex(this.x + this.width, this.y).rotate(c, this.rotation);
const v2 = new Vertex(this.x + this.width, this.y + this.height).rotate(
c,
this.rotation
);
const v3 = new Vertex(this.x, this.y + this.height).rotate(
c,
this.rotation
);
return [v0, v1, v2, v3]; // 以矩形左上角点为起始(v0),依次顺时针得到v1, v2, v3
}
getBorders(): Line[] {
const vertexes = this.getVertexes(); // 获取到举行四个点的坐标,即vertexes = [v0, v1, v2, v3]
return [
// 返回四条线段的信息,[{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
new Line(vertexes[0], vertexes[1]),
new Line(vertexes[1], vertexes[2]),
new Line(vertexes[2], vertexes[3]),
new Line(vertexes[3], vertexes[0]),
];
}
/**
* 识别矩形相交的函数
* @param {Rect} rect
* @returns {{ collision: boolean; state: string }} 返回值为一个对象,包含两个属性:collision和state
* collision: 布尔值,表示是否发生碰撞
* state: 字符串,表示碰撞的状态,可以是"碰撞"、"包含"或"未碰撞"
* 这里的相交关系是指:两矩形之间有交集或一个矩形完全包含在另一个矩形内
* 进行比较的rect包含信息有height, rotation, width, x, y, _vertexes, _borders,即:
* height, rotation, width, x, y, [v0, v1, v2, v3], [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
*/
detectIntersect(rect: Rect): { collision: boolean; state: string } {
// 先检测相交关系;相交和内含相比是大概率
const borders0 = this.getBorders(); // 自身rect的四条边的信息rect0: [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
const borders1 = rect.getBorders(); // 进行比较的rect的四条边的信息rect1: [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
for (let i = 0; i < borders0.length; i++) {
for (let j = 0; j < borders1.length; j++) {
if (borders0[i].intersect(borders1[j])) {
// return true;
return { collision: true, state: "碰撞" };
}
}
}
if (this.contain(rect) || rect.contain(this)) {
// return true;
return { collision: true, state: "包含" };
}
// return false;ß
return { collision: false, state: "未碰撞" };
}
/**
* rect之间是否是包含关系
* @param {Rect} rect
* @returns {boolean} 返回值为true则代表自身rect包含进行比较的rect,若为false则反之
* 这里的包含关系是指:自身rect的四个点都在进行比较的rect内部
* 比较的rect包含信息有height, rotation, width, x, y, _vertexes, _borders,即:
* height, rotation, width, x, y, [v0, v1, v2, v3], [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
*/
contain(rect: Rect): boolean {
const rectVertexes = rect.getVertexes(); // 这里的rectVertexes代表比较的rect四个点的信息[v0, v1, v2, v3]
for (let i = 0; i < rectVertexes.length; i++) {
if (!this.isPointIn(rectVertexes[i])) {
return false;
}
}
return true;
}
/**
* 顶点是否在矩形内部
* @param {Vertex} vertex
*/
isPointIn(vertex: Vertex): boolean {
const [v0, v1, v2, v3] = this.getVertexes(); // 这里的[v0, v1, v2, v3]是自身rect四个点的信息
const v0_v = v0.sub(vertex);
const v0_1 = v0.sub(v1);
const v0_3 = v0.sub(v3);
/**
* 0 <= v0_v.dot(v0_1) 值为true则代表 向量vertex->v0 与 向量v1->v0 的夹角<=90°,即点vertex在v0点的右侧或和v0的x值相同,false代表点vertex在v0的左侧
* v0_v.dot(v0_1) <= v0_1.dot(v0_1) 值为true则代表点vertex在v1的左侧或和v1的x值相同,false代表点vertex在v1的右侧(可以理解为 向量vertex->v0 投影在 向量 v1->v0 上的长度与向量v1->v0的模来比较,若小于则在v1左侧,等于则和v1的x的值相同,大于则在v1的右侧)
* 0 <= v0_v.dot(v0_3) 值为true则代表 向量vertex->0 与向量v3->v0 的夹角<=90°,即点vertex在v0点的下方或和v0点的y值相同,false代表点vertex在v0的上方
* v0_v.dot(v0_3) <= v0_3.dot(v0_3)值为true则代表点vertex在v3的上方或和v3的y值相同,false代表点vertex在v3的下方(可以理解为 向量vertex->v3 投影在 向量 v3->v0 上的长度与向量v3->v0的模来比较,若小于则在v3上方,等于则和v3的y的值相同,大于则在v3的下方)
*/
return (
0 <= v0_v.dot(v0_1) &&
v0_v.dot(v0_1) <= v0_1.dot(v0_1) &&
0 <= v0_v.dot(v0_3) &&
v0_v.dot(v0_3) <= v0_3.dot(v0_3)
);
}
}
// 顶点
class Vertex {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
/**
* 绕点origin旋转
* @param {Vertex} origin
* @param {Number} radian
* @returns {Vertex}
*/
rotate(origin: Vertex, radian: number): Vertex {
const x =
(this.x - origin.x) * Math.cos(-radian) -
(this.y - origin.y) * Math.sin(-radian) +
origin.x;
const y =
(this.x - origin.x) * Math.sin(-radian) +
(this.y - origin.y) * Math.cos(-radian) +
origin.y;
return new Vertex(x, y);
}
/**
* 点积
* @param {Vertex} vertex
*/
dot(vertex: Vertex): number {
return this.x * vertex.x + this.y * vertex.y;
}
/**
* 相减
*/
sub(vertex: Vertex): Vertex {
return new Vertex(this.x - vertex.x, this.y - vertex.y);
}
/**
* 点之间的距离
*/
distance(vertex: Vertex): number {
return Math.sqrt(
Math.pow(this.x - vertex.x, 2) + Math.pow(this.y - vertex.y, 2)
);
}
}
// 线
class Line {
v0: Vertex;
v1: Vertex;
constructor(v0: Vertex, v1: Vertex) {
this.v0 = v0;
this.v1 = v1;
}
/**
* @param {Line} line
* @return {*boolean} 返回值为true则表示两条线段相交,为false则表示两条线段不相交
* line: 进行比较的rect的一条边的信息 {v0: v0, v1: v1}
* this.v0, this.v1: 自身rect对应的一条边的信息
*/
intersect(line: Line): boolean {
if (this.collinearIntersect({ v0: this.v0, v1: this.v1 }, line)) {
return false;
}
let s0 = this.area(this.v0, this.v1, line.v0); // 判断this.v0和this.v1在line.v0的位置,若s0<0则向量line.v0->this.v1在向量line.v0->this.v0的顺时针方向,s0>0则在逆时针方向,s0=0则共线或平行
let s1 = this.area(this.v0, this.v1, line.v1); // 判断this.v0和this.v1在line.v1的位置,若s1<0则向量line.v1->this.v1在向量line.v1->this.v0的顺时针方向,s1>0则在逆时针方向,s1=0则共线或平行
let s2 = this.area(line.v0, line.v1, this.v0); // 判断line.v0和line.v1在this.v0的位置,若s2<0则向量this.v0->line.v1在向量this.v0->line.v0的顺时针方向,s2>0则在逆时针方向,s2=0则共线或平行
let s3 = this.area(line.v0, line.v1, this.v1); // 判断line.v0和line.v1在this.v1的位置,若s3<0则向量this.v1->line.v1在向量this.v1->line.v0的顺时针方向,s3>0则在逆时针方向,s3=0则共线或平行
// s0 * s1 < 0代表着线段line的两个顶点在 this.v0、this.v1组成的线段 的两侧,s0 * s1 = 0则代表线段line至少有一个顶点在 this.v0、this.v1组成的线段 上面,s0 * s1 > 0则代表线段line在 this.v0、this.v1组成的线段同侧
// s2 * s3 < 0代表着this.v0和this.v1在线段line的两侧。s2 * s3 = 0则代表着this.v0和this.v1至少有一个点在线段line上面,s2 * s3 > 0则代表着this.v0和this.v1在线段line的同侧
// (s0 * s1 <= 0) && (s2 * s3 <= 0)表示 this.v0和this.v1组成的线段 与线段line的关系,值为true则表示两条线段相交,为false则表示两条线段不相交
return s0 * s1 <= 0 && s2 * s3 <= 0;
}
/** * 判断两条线段是否共线
* @param {Line} v1
* @param {Line} v2
* @returns {boolean} 返回值为true则表示两条线段共线
* v1: 自身rect对应的一条边的信息
* v2: 进行比较的rect的一条边的信息
*/
collinearIntersect(
line1: { v0: Vertex; v1: Vertex },
line2: { v0: Vertex; v1: Vertex }
): boolean {
// 参数化直线(假设向量已共线,直接使用x坐标或y坐标作为参数)
const getParamRange = (vec: {
v0: Vertex;
v1: Vertex;
}): {
x: { min: number; max: number };
y: { min: number; max: number };
} => {
return {
x: {
min: Math.min(vec.v0.x, vec.v1.x),
max: Math.max(vec.v0.x, vec.v1.x),
},
y: {
min: Math.min(vec.v0.y, vec.v1.y),
max: Math.max(vec.v0.y, vec.v1.y),
},
};
};
const straightLine1 = this.getStraightLine(line1.v0, line1.v1);
const straightLine2 = this.getStraightLine(line2.v0, line2.v1);
// 如果两个向量处于同一直线上时,则进行比较是否相交
if (
straightLine1.k === straightLine2.k &&
straightLine1.b === straightLine2.b
) {
const range1 = getParamRange(line1);
const range2 = getParamRange(line2);
return (
Math.max(range1.x.min, range2.x.min) <=
Math.min(range1.x.max, range2.x.max) ||
Math.max(range1.y.min, range2.y.min) <=
Math.min(range1.y.max, range2.y.max)
);
} else {
return false;
}
}
/**
* 这个函数主要是用来求向量ca和向量cb的叉积
* @param {Vertex} a
* @param {Vertex} b
* @param {Vertex} c
*/
area(a: Vertex, b: Vertex, c: Vertex): number {
return (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);
}
/**
* 获取两点之间的直线方程
* @param {Vertex} point1
* @param {Vertex} point2
* @returns {{ k: number | null; b: number | null }} 返回值为一个对象,包含两个属性:k和b
* k: 斜率,若两点的x坐标相同则为null
* b: 截距,若两点的x坐标相同则为null
*/
getStraightLine(
point1: { x: number; y: number },
point2: { x: number; y: number }
): { k: number | null; b: number | null } {
if (point1.y === point2.y) {
return { k: 0, b: point1.y };
} else if (point1.x === point2.x) {
return { k: null, b: null };
} else {
const k = (point1.y - point2.y) / (point1.x - point2.x);
const b = point1.y - point1.x * k;
return {
k,
b,
};
}
}
}
export { Rect };
3.2. JS封装的类
const defStyle = {
rotation: 0,
padding: [0, 0, 0, 0],
};
class Rect {
/**
* 矩形类的构造函数
* @param {RectOptionsInter} options 矩形的初始参数,包括位置、大小和样式
*/
constructor({
x,
y,
width,
height,
style = {
rotation: 0,
padding: [0, 0, 0, 0],
},
}) {
this.init({ x, y, width, height, style });
}
init({
x,
y,
width,
height,
style = {
rotation: 0,
padding: [0, 0, 0, 0],
},
}) {
if (!style.padding) {
style.padding = defStyle.padding;
}
if (style.rotation === undefined) {
style.rotation = defStyle.rotation;
}
this.x = x - style.padding[3];
this.y = y - style.padding[0];
this.width = width + style.padding[1] + style.padding[3];
this.height = height + style.padding[0] + style.padding[2];
this.style = style;
this.rotation = style.rotation;
this._vertexes = this.getVertexes(); // 矩形当前四个点的坐标[v0, v1, v2, v3]
this._borders = this.getBorders(); // 矩形当前四条边的数组信息[{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
}
setRectData({
x,
y,
width,
height,
style = { rotation: 0, padding: [0, 0, 0, 0] },
}) {
this.init({ x, y, width, height, style });
}
getRectData() {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
style: this.style,
};
}
getVertexes() {
if (
this.x === undefined ||
this.y === undefined ||
this.width === undefined ||
this.height === undefined ||
this.rotation === undefined
) {
throw new Error(
"Rect properties x, y, width, and height must be defined."
);
}
const c = new Vertex(this.x + this.width / 2, this.y + this.height / 2); // 矩形中心坐标
const v0 = new Vertex(this.x, this.y).rotate(c, this.rotation);
const v1 = new Vertex(this.x + this.width, this.y).rotate(c, this.rotation);
const v2 = new Vertex(this.x + this.width, this.y + this.height).rotate(
c,
this.rotation
);
const v3 = new Vertex(this.x, this.y + this.height).rotate(
c,
this.rotation
);
return [v0, v1, v2, v3]; // 以矩形左上角点为起始(v0),依次顺时针得到v1, v2, v3
}
getBorders() {
const vertexes = this.getVertexes(); // 获取到举行四个点的坐标,即vertexes = [v0, v1, v2, v3]
return [
// 返回四条线段的信息,[{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
new Line(vertexes[0], vertexes[1]),
new Line(vertexes[1], vertexes[2]),
new Line(vertexes[2], vertexes[3]),
new Line(vertexes[3], vertexes[0]),
];
}
/**
* 识别矩形相交的函数
* @param {Rect} rect
* @returns {{ collision: boolean; state: string }} 返回值为一个对象,包含两个属性:collision和state
* collision: 布尔值,表示是否发生碰撞
* state: 字符串,表示碰撞的状态,可以是"碰撞"、"包含"或"未碰撞"
* 这里的相交关系是指:两矩形之间有交集或一个矩形完全包含在另一个矩形内
* 进行比较的rect包含信息有height, rotation, width, x, y, _vertexes, _borders,即:
* height, rotation, width, x, y, [v0, v1, v2, v3], [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
*/
detectIntersect(rect) {
// 先检测相交关系;相交和内含相比是大概率
const borders0 = this.getBorders(); // 自身rect的四条边的信息rect0: [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
const borders1 = rect.getBorders(); // 进行比较的rect的四条边的信息rect1: [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
for (let i = 0; i < borders0.length; i++) {
for (let j = 0; j < borders1.length; j++) {
if (borders0[i].intersect(borders1[j])) {
// return true;
return { collision: true, state: "碰撞" };
}
}
}
if (this.contain(rect) || rect.contain(this)) {
// return true;
return { collision: true, state: "包含" };
}
// return false;ß
return { collision: false, state: "未碰撞" };
}
/**
* rect之间是否是包含关系
* @param {Rect} rect
* @returns {boolean} 返回值为true则代表自身rect包含进行比较的rect,若为false则反之
* 这里的包含关系是指:自身rect的四个点都在进行比较的rect内部
* 比较的rect包含信息有height, rotation, width, x, y, _vertexes, _borders,即:
* height, rotation, width, x, y, [v0, v1, v2, v3], [{v0: v0, v1: v1}, {v0: v1, v1: v2}, {v0: v2, v1: v3}, {v0: v3, v1: v0}]
*/
contain(rect) {
const rectVertexes = rect.getVertexes(); // 这里的rectVertexes代表比较的rect四个点的信息[v0, v1, v2, v3]
for (let i = 0; i < rectVertexes.length; i++) {
if (!this.isPointIn(rectVertexes[i])) {
return false;
}
}
return true;
}
/**
* 顶点是否在矩形内部
* @param {Vertex} vertex
*/
isPointIn(vertex) {
const [v0, v1, v2, v3] = this.getVertexes(); // 这里的[v0, v1, v2, v3]是自身rect四个点的信息
const v0_v = v0.sub(vertex);
const v0_1 = v0.sub(v1);
const v0_3 = v0.sub(v3);
/**
* 0 <= v0_v.dot(v0_1) 值为true则代表 向量vertex->v0 与 向量v1->v0 的夹角<=90°,即点vertex在v0点的右侧或和v0的x值相同,false代表点vertex在v0的左侧
* v0_v.dot(v0_1) <= v0_1.dot(v0_1) 值为true则代表点vertex在v1的左侧或和v1的x值相同,false代表点vertex在v1的右侧(可以理解为 向量vertex->v0 投影在 向量 v1->v0 上的长度与向量v1->v0的模来比较,若小于则在v1左侧,等于则和v1的x的值相同,大于则在v1的右侧)
* 0 <= v0_v.dot(v0_3) 值为true则代表 向量vertex->0 与向量v3->v0 的夹角<=90°,即点vertex在v0点的下方或和v0点的y值相同,false代表点vertex在v0的上方
* v0_v.dot(v0_3) <= v0_3.dot(v0_3)值为true则代表点vertex在v3的上方或和v3的y值相同,false代表点vertex在v3的下方(可以理解为 向量vertex->v3 投影在 向量 v3->v0 上的长度与向量v3->v0的模来比较,若小于则在v3上方,等于则和v3的y的值相同,大于则在v3的下方)
*/
return (
0 <= v0_v.dot(v0_1) &&
v0_v.dot(v0_1) <= v0_1.dot(v0_1) &&
0 <= v0_v.dot(v0_3) &&
v0_v.dot(v0_3) <= v0_3.dot(v0_3)
);
}
}
// 顶点
class Vertex {
constructor(x, y) {
this.x = x;
this.y = y;
}
/**
* 绕点origin旋转
* @param {Vertex} origin
* @param {Number} radian
* @returns {Vertex}
*/
rotate(origin, radian) {
const x =
(this.x - origin.x) * Math.cos(-radian) -
(this.y - origin.y) * Math.sin(-radian) +
origin.x;
const y =
(this.x - origin.x) * Math.sin(-radian) +
(this.y - origin.y) * Math.cos(-radian) +
origin.y;
return new Vertex(x, y);
}
/**
* 点积
* @param {Vertex} vertex
*/
dot(vertex) {
return this.x * vertex.x + this.y * vertex.y;
}
/**
* 相减
*/
sub(vertex) {
return new Vertex(this.x - vertex.x, this.y - vertex.y);
}
/**
* 点之间的距离
*/
distance(vertex) {
return Math.sqrt(
Math.pow(this.x - vertex.x, 2) + Math.pow(this.y - vertex.y, 2)
);
}
}
// 线
class Line {
constructor(v0, v1) {
this.v0 = v0;
this.v1 = v1;
}
/**
* @param {Line} line
* @return {*boolean} 返回值为true则表示两条线段相交,为false则表示两条线段不相交
* line: 进行比较的rect的一条边的信息 {v0: v0, v1: v1}
* this.v0, this.v1: 自身rect对应的一条边的信息
*/
intersect(line) {
if (this.collinearIntersect({ v0: this.v0, v1: this.v1 }, line)) {
return false;
}
let s0 = this.area(this.v0, this.v1, line.v0); // 判断this.v0和this.v1在line.v0的位置,若s0<0则向量line.v0->this.v1在向量line.v0->this.v0的顺时针方向,s0>0则在逆时针方向,s0=0则共线或平行
let s1 = this.area(this.v0, this.v1, line.v1); // 判断this.v0和this.v1在line.v1的位置,若s1<0则向量line.v1->this.v1在向量line.v1->this.v0的顺时针方向,s1>0则在逆时针方向,s1=0则共线或平行
let s2 = this.area(line.v0, line.v1, this.v0); // 判断line.v0和line.v1在this.v0的位置,若s2<0则向量this.v0->line.v1在向量this.v0->line.v0的顺时针方向,s2>0则在逆时针方向,s2=0则共线或平行
let s3 = this.area(line.v0, line.v1, this.v1); // 判断line.v0和line.v1在this.v1的位置,若s3<0则向量this.v1->line.v1在向量this.v1->line.v0的顺时针方向,s3>0则在逆时针方向,s3=0则共线或平行
// s0 * s1 < 0代表着线段line的两个顶点在 this.v0、this.v1组成的线段 的两侧,s0 * s1 = 0则代表线段line至少有一个顶点在 this.v0、this.v1组成的线段 上面,s0 * s1 > 0则代表线段line在 this.v0、this.v1组成的线段同侧
// s2 * s3 < 0代表着this.v0和this.v1在线段line的两侧。s2 * s3 = 0则代表着this.v0和this.v1至少有一个点在线段line上面,s2 * s3 > 0则代表着this.v0和this.v1在线段line的同侧
// (s0 * s1 <= 0) && (s2 * s3 <= 0)表示 this.v0和this.v1组成的线段 与线段line的关系,值为true则表示两条线段相交,为false则表示两条线段不相交
return s0 * s1 <= 0 && s2 * s3 <= 0;
}
/** * 判断两条线段是否共线
* @param {Line} v1
* @param {Line} v2
* @returns {boolean} 返回值为true则表示两条线段共线
* v1: 自身rect对应的一条边的信息
* v2: 进行比较的rect的一条边的信息
*/
collinearIntersect(line1, line2) {
// 参数化直线(假设向量已共线,直接使用x坐标或y坐标作为参数)
const getParamRange = (vec) => {
return {
x: {
min: Math.min(vec.v0.x, vec.v1.x),
max: Math.max(vec.v0.x, vec.v1.x),
},
y: {
min: Math.min(vec.v0.y, vec.v1.y),
max: Math.max(vec.v0.y, vec.v1.y),
},
};
};
const straightLine1 = this.getStraightLine(line1.v0, line1.v1);
const straightLine2 = this.getStraightLine(line2.v0, line2.v1);
// 如果两个向量处于同一直线上时,则进行比较是否相交
if (
straightLine1.k === straightLine2.k &&
straightLine1.b === straightLine2.b
) {
const range1 = getParamRange(line1);
const range2 = getParamRange(line2);
return (
Math.max(range1.x.min, range2.x.min) <=
Math.min(range1.x.max, range2.x.max) ||
Math.max(range1.y.min, range2.y.min) <=
Math.min(range1.y.max, range2.y.max)
);
} else {
return false;
}
}
/**
* 这个函数主要是用来求向量ca和向量cb的叉积
* @param {Vertex} a
* @param {Vertex} b
* @param {Vertex} c
*/
area(a, b, c) {
return (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);
}
/**
* 获取两点之间的直线方程
* @param {Vertex} point1
* @param {Vertex} point2
* @returns {{ k: number | null; b: number | null }} 返回值为一个对象,包含两个属性:k和b
* k: 斜率,若两点的x坐标相同则为null
* b: 截距,若两点的x坐标相同则为null
*/
getStraightLine(point1, point2) {
if (point1.y === point2.y) {
return { k: 0, b: point1.y };
} else if (point1.x === point2.x) {
return { k: null, b: null };
} else {
const k = (point1.y - point2.y) / (point1.x - point2.x);
const b = point1.y - point1.x * k;
return {
k,
b,
};
}
}
}
export { Rect };