Mastering 2D Collision Detection: Vectors, AABB, OBB, and JavaScript Implementations
This article explains how to use simple shapes like circles and rectangles for fast 2D collision detection, reviews vector mathematics, provides a reusable Vector2d class with operations such as addition, subtraction, length, dot product and rotation, and shows concrete JavaScript code for circle‑circle, circle‑rectangle (both axis‑aligned and rotated) and rectangle‑rectangle collisions using AABB and OBB techniques.
Introduction
In 2D games, rectangles and circles are often used to replace complex shapes for intersection tests because they are the fastest to compute. A rectangle can be an Axis Aligned Bounding Box (AABB) when its sides are parallel to the axes, or an Oriented Bounding Box (OBB) when it is rotated. Different scenarios call for different solutions.
For example, a Pokémon sprite (a rectangular bounding box) and a Poké Ball (a bounding sphere) illustrate the choice of shape.
Vector
Vectors are a fundamental mathematical tool in collision detection; all subsequent calculations are performed using vectors, so we first review them.
Algebraic Representation of Vectors
A vector can be represented by its coordinates in a chosen coordinate system. For a free vector, moving its start point to the origin lets us describe it with a single point whose coordinates are the vector’s components.
<ol><li><code class="language-javascript">class Vector2d {
constructor(vx = 1, vy = 1) {
this.vx = vx;
this.vy = vy;
}
}
const vecA = new Vector2d(1, 2);
const vecB = new Vector2d(3, 1);
</code></li></ol>Vector Operations
Addition follows the parallelogram rule: the resulting vector’s x and y components are the sums of the operands’ components.
<ol><li><code class="language-javascript">static add(vec, vec2) {
const vx = vec.vx + vec2.vx;
const vy = vec.vy + vec2.vy;
return new Vector2d(vx, vy);
}
</code></li></ol>Subtraction yields a vector from the end of the second operand to the end of the first.
<ol><li><code class="language-javascript">static sub(vec, vec2) {
const vx = vec.vx - vec2.vx;
const vy = vec.vy - vec2.vy;
return new Vector2d(vx, vy);
}
</code></li></ol>The length (magnitude) is the square root of the sum of squares of its components.
<ol><li><code class="language-javascript">length() {
return Math.sqrt(this.vx * this.vx + this.vy * this.vy);
}
</code></li></ol>The dot (scalar) product multiplies corresponding components and sums them.
<ol><li><code class="language-javascript">static dot(vec, vec2) {
return vec.vx * vec2.vx + vec.vy * vec2.vy;
}
</code></li></ol>Rotation uses a rotation matrix.
<ol><li><code class="language-javascript">static rotate(vec, angle) {
const cosVal = Math.cos(angle);
const sinVal = Math.sin(angle);
const vx = vec.vx * cosVal - vec.vy * sinVal;
const vy = vec.vx * sinVal + vec.vy * cosVal;
return new Vector2d(vx, vy);
}
</code></li></ol>Circle
A circle is defined by its center (x, y) and radius r.
<ol><li><code class="language-javascript">class Circle {
constructor(x = 0, y = 0, r = 1) {
this.x = x;
this.y = y;
this.r = r;
}
get P() {
return new Vector2d(this.x, this.y); // center vector
}
}
</code></li></ol>Rectangle
A rectangle needs a center (x, y), width w, height h, and a rotation angle (degrees).
<ol><li><code class="language-javascript">export class Rect {
constructor(x = 0, y = 0, w = 1, h = 1, rotation = 0) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.rotation = rotation;
}
get C() {
return new Vector2d(this.x, this.y); // center vector
}
get A3() {
return new Vector2d(this.x + this.w / 2, this.y + this.h / 2); // top‑right vertex
}
get _rotation() {
return this.rotation / 180 * Math.PI; // convert to radians
}
}
</code></li></ol>Circle‑Circle Intersection
Two circles intersect if the distance between their centers is less than or equal to the sum of their radii.
<ol><li><code class="language-javascript">function circleCircleIntersect(circle1, circle2) {
const P1 = circle1.P;
const P2 = circle2.P;
const r1 = circle1.r;
const r2 = circle2.r;
const u = Vector2d.sub(P1, P2);
return u.length() <= r1 + r2;
}
</code></li></ol>Circle‑Rectangle Intersection
First handle the axis‑aligned case (AABB). The shortest distance from the circle centre to the rectangle is computed by clamping the difference vector to the rectangle’s half‑sizes.
<ol><li><code class="language-javascript">function rectCircleIntersect(rect, circle) {
const C = rect.C;
const P = circle.P;
const h = Vector2d.sub(P, C);
const v = new Vector2d(
Math.abs(h.vx),
Math.abs(h.vy)
);
const u = new Vector2d(
Math.max(v.vx - rect.w / 2, 0),
Math.max(v.vy - rect.h / 2, 0)
);
return u.lengthSquared() <= circle.r * circle.r;
}
</code></li></ol>If the rectangle is rotated, we rotate the circle centre in the opposite direction, turning the problem back into the axis‑aligned case.
<ol><li><code class="language-javascript">function p(rect, circle) {
if (rect.rotation % 360 === 0) return circle.P;
return Vector2d.add(
rect.C,
Vector2d.rotate(
Vector2d.sub(circle.P, rect.C),
-rect._rotation
)
);
}
function rectCircleIntersect(rect, circle) {
const rotation = rect.rotation;
const C = rect.C;
const r = circle.r;
const P = p(rect, circle);
const h = Vector2d.sub(P, C);
const v = new Vector2d(
Math.abs(h.vx),
Math.abs(h.vy)
);
const u = new Vector2d(
Math.max(v.vx - rect.w / 2, 0),
Math.max(v.vy - rect.h / 2, 0)
);
return u.lengthSquared() <= r * r;
}
</code></li></ol>Rectangle‑Rectangle Intersection (AABB)
Two axis‑aligned rectangles can be reduced to a point‑vs‑rectangle test by expanding one rectangle by the half‑sizes of the other.
<ol><li><code class="language-javascript">function AABBrectRectIntersect(rect1, rect2) {
const P = rect2.C;
const w2 = rect2.w;
const h2 = rect2.h;
const { w, h, x, y } = rect1;
const C = rect1.C;
const A3 = new Vector2d(
x + w / 2 + w2 / 2,
y + h / 2 + h2 / 2
); // new rectangle half‑size
const H = Vector2d.sub(A3, C);
const v = new Vector2d(
Math.abs(P.vx - C.vx),
Math.abs(P.vy - C.vy)
);
const u = new Vector2d(
Math.max(v.vx - H.vx, 0),
Math.max(v.vy - H.vy, 0)
);
return u.lengthSquared() === 0; // point inside expanded rectangle
}
</code></li></ol>Rectangle‑Rectangle Intersection (OBB – Separating Axis Theorem)
For oriented rectangles we use the Separating Axis Theorem (SAT). The four potential separating axes are the normals of each rectangle’s edges (two per rectangle). If a gap exists on any axis, the rectangles do not intersect.
<ol><li><code class="language-javascript">class Rect {
// ... same as before, plus vertex getters
get _A1() { return new Vector2d(this.x - this.w/2, this.y - this.h/2); }
get _A2() { return new Vector2d(this.x + this.w/2, this.y - this.h/2); }
get _A3() { return new Vector2d(this.x + this.w/2, this.y + this.h/2); }
get _A4() { return new Vector2d(this.x - this.w/2, this.y + this.h/2); }
get _axisX() { return new Vector2d(1, 0); }
get _axisY() { return new Vector2d(0, 1); }
get _rotation() { return this.rotation / 180 * Math.PI; }
get A1() { return this.rotation % 360 === 0 ? this._A1 : Vector2d.add(this.C, Vector2d.rotate(this._CA1, this._rotation)); }
// similarly A2, A3, A4 using rotated corner vectors
get axisX() { return this.rotation % 360 === 0 ? this._axisX : Vector2d.rotate(this._axisX, this._rotation); }
get axisY() { return this.rotation % 360 === 0 ? this._axisY : Vector2d.rotate(this._axisY, this._rotation); }
get vertexs() { return [this.A1, this.A2, this.A3, this.A4]; }
}
function OBBrectRectIntersect(rect1, rect2) {
const axes = [rect1.axisX, rect1.axisY, rect2.axisX, rect2.axisY];
for (const axis of axes) {
if (!cross(rect1, rect2, axis)) return false;
}
return true;
}
function cross(rect1, rect2, axis) {
const proj1 = rect1.vertexs.map(v => Vector2d.dot(v, axis)).sort((a,b)=>a-b);
const proj2 = rect2.vertexs.map(v => Vector2d.dot(v, axis)).sort((a,b)=>a-b);
const rect1Min = proj1[0];
const rect1Max = proj1[proj1.length-1];
const rect2Min = proj2[0];
const rect2Max = proj2[proj2.length-1];
return rect1Max >= rect2Min && rect2Max >= rect1Min;
}
</code></li></ol>Demos
Demo 1 (basic tests): https://rococolate.github.io/blog/gom/test1.html
Demo 2 (interactive drag‑and‑drop): https://rococolate.github.io/blog/gom/test2.html
References
Chapter 15: Collision Detection – http://blog.jmecn.net/chapter-15-collision-detection/
Fighting Game Essentials – http://daily.zhihu.com/story/4761397
How to determine overlap between a rectangle and a circle – https://www.zhihu.com/question/24251545
Common 2D Collision Detection – https://aotu.io/notes/2017/02/16/2d-collision-detection/index.html
OBB Collision Detection – https://www.cnblogs.com/iamzhanglei/archive/2012/06/07/2539751.html
Rotation matrix – https://en.wikipedia.org/wiki/Rotation_matrix
Dot product – https://zh.wikipedia.org/wiki/%E7%82%B9%E7%A7%AF
Vector – https://zh.wikipedia.org/wiki/%E5%90%91%E9%87%8F
WecTeam
WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
