如何检测矩形是否碰撞💥

1,457 阅读23分钟

前提】需要了解向量相关知识,例如叉积、点积的概念

一、完整代码及样式示例

1.1. 效果图

①未碰撞

未碰撞.png

②碰撞

碰撞.png

③包含

包含.png

1.2. 完整代码示例

【注】APP查看文章时,不知道是什么原因,在文章中操作这个示例会对DOM的判定有影响,但是点击右上角的查看详情后,在详情页的实例才是正常效果

二、实现方法及逻辑解析

2.1. 计算逻辑图解

逻辑解析图.png

2.2. 核心代码逻辑解读

2.2.1. 检测两条线段是否相交

『❗❗❗注意❗❗❗』:当两条线段处于同一直线上时,不论两条线段是否相交,叉积的值最后都会是0,所以需要基于这种情况再做一次判断。

坐标-1.png 【常量】线段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)

其主要逻辑思路是互相判断相比较的两个点是否在自身点的两侧,若两个线段的结果都是对应的点在自身两侧,则证明两条线段相交:

  1. 向量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

  2. 向量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

  3. 向量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

  4. 向量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)

检测主要利用了向量的点击投影比较来判断点是否在矩形内,主要比较步骤是:

  1. 向量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

  2. 向量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

  3. 向量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

  4. 向量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 };