Skip to content

Instantly share code, notes, and snippets.

@jeantimex
Created September 17, 2024 05:50
Show Gist options
  • Save jeantimex/e831b260353d4e658508621a9dc0a8c2 to your computer and use it in GitHub Desktop.
Save jeantimex/e831b260353d4e658508621a9dc0a8c2 to your computer and use it in GitHub Desktop.
const canvas = document.createElement("canvas");
canvas.width = 600;
canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
const BODIES = [];
const COLLISIONS = [];
let LEFT, UP, RIGHT, DOWN;
let friction = 0;
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
set(x, y) {
this.x = x;
this.y = y;
}
add(v) {
return new Vector(this.x + v.x, this.y + v.y);
}
subtr(v) {
return new Vector(this.x - v.x, this.y - v.y);
}
mag() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
mult(n) {
return new Vector(this.x * n, this.y * n);
}
normal() {
return new Vector(-this.y, this.x).unit();
}
unit() {
if (this.mag() === 0) {
return new Vector(0, 0);
} else {
return new Vector(this.x / this.mag(), this.y / this.mag());
}
}
drawVec(start_x, start_y, n, color) {
ctx.beginPath();
ctx.moveTo(start_x, start_y);
ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
}
static dot(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
static cross(v1, v2) {
return v1.x * v2.y - v1.y * v2.x;
}
}
class Matrix {
constructor(rows, cols) {
this.rows = rows;
this.cols = cols;
this.data = [];
for (let i = 0; i < this.rows; i++) {
this.data[i] = [];
for (let j = 0; j < this.cols; j++) {
this.data[i][j] = 0;
}
}
}
multiplyVec(vec) {
let result = new Vector(0, 0);
result.x = this.data[0][0] * vec.x + this.data[0][1] * vec.y;
result.y = this.data[1][0] * vec.x + this.data[1][1] * vec.y;
return result;
}
rotMx22(angle) {
this.data[0][0] = Math.cos(angle);
this.data[0][1] = -Math.sin(angle);
this.data[1][0] = Math.sin(angle);
this.data[1][1] = Math.cos(angle);
}
}
//classes storing the primitive shapes: LineShape, CircleShape, RectangleShape, Triangle
class LineShape {
constructor(x0, y0, x1, y1) {
this.vertex = [];
this.vertex[0] = new Vector(x0, y0);
this.vertex[1] = new Vector(x1, y1);
this.dir = this.vertex[1].subtr(this.vertex[0]).unit();
this.mag = this.vertex[1].subtr(this.vertex[0]).mag();
this.pos = new Vector(
(this.vertex[0].x + this.vertex[1].x) / 2,
(this.vertex[0].y + this.vertex[1].y) / 2
);
}
draw() {
ctx.beginPath();
ctx.moveTo(this.vertex[0].x, this.vertex[0].y);
ctx.lineTo(this.vertex[1].x, this.vertex[1].y);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.closePath();
}
}
class CircleShape {
constructor(x, y, r) {
this.vertex = [];
this.pos = new Vector(x, y);
this.r = r;
}
draw() {
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2 * Math.PI);
ctx.stroke();
//ctx.fillStyle = "red";
//ctx.fill();
ctx.closePath();
}
}
class RectangleShape {
constructor(x1, y1, x2, y2, w, slope = 0) {
this.vertex = [];
this.width = w;
this.slope = slope;
this.dir = new Vector(x2 - x1, y2 - y1).unit();
this.refDir = this.dir;
this.length = new Vector(x2 - x1, y2 - y1).mag();
this.pos = new Vector((x1 + x2) / 2, (y1 + y2) / 2);
this.angle = 0;
this.rotMat = new Matrix(2, 2);
this.updateVertices();
}
updateVertices() {
let slopeOffset = this.width * this.slope;
let halfLength = this.length / 2;
let halfWidth = this.width / 2;
this.vertex[0] = this.pos
.add(this.dir.mult(-halfLength))
.add(this.dir.normal().mult(-halfWidth));
this.vertex[1] = this.pos
.add(this.dir.mult(halfLength))
.add(this.dir.normal().mult(-halfWidth + slopeOffset));
this.vertex[2] = this.pos
.add(this.dir.mult(halfLength))
.add(this.dir.normal().mult(halfWidth + slopeOffset));
this.vertex[3] = this.pos
.add(this.dir.mult(-halfLength))
.add(this.dir.normal().mult(halfWidth));
}
draw() {
ctx.beginPath();
ctx.moveTo(this.vertex[0].x, this.vertex[0].y);
ctx.lineTo(this.vertex[1].x, this.vertex[1].y);
ctx.lineTo(this.vertex[2].x, this.vertex[2].y);
ctx.lineTo(this.vertex[3].x, this.vertex[3].y);
ctx.lineTo(this.vertex[0].x, this.vertex[0].y);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.closePath();
}
getVertices() {
this.rotMat.rotMx22(this.angle);
this.dir = this.rotMat.multiplyVec(this.refDir);
this.updateVertices();
return this.vertex;
}
}
class ConvexShape {
constructor(vertices) {
this.vertex = vertices.map((v) => new Vector(v.x, v.y));
this.pos = this.calculateCenter();
this.angle = 0;
this.rotMat = new Matrix(2, 2);
// Calculate initial direction (assuming the first vertex is the "front")
this.dir = this.vertex[0].subtr(this.pos).unit();
this.refDir = this.dir;
// Calculate reference diameters
this.refDiam = this.vertex.map((v) => v.subtr(this.pos));
}
calculateCenter() {
let sum = this.vertex.reduce(
(sum, vertex) => sum.add(vertex),
new Vector(0, 0)
);
return sum.mult(1 / this.vertex.length);
}
draw() {
ctx.beginPath();
ctx.moveTo(this.vertex[0].x, this.vertex[0].y);
for (let i = 1; i < this.vertex.length; i++) {
ctx.lineTo(this.vertex[i].x, this.vertex[i].y);
}
ctx.closePath();
ctx.strokeStyle = "black";
ctx.stroke();
}
getVertices() {
this.rotMat.rotMx22(this.angle);
this.dir = this.rotMat.multiplyVec(this.refDir);
this.vertex = this.refDiam.map((diam) => {
let rotated = this.rotMat.multiplyVec(diam);
return rotated.add(this.pos);
});
}
}
//Parent class of the bodies (Ball, Capsule, Box, Star, Wall)
class Body {
constructor(x, y) {
this.comp = [];
this.pos = new Vector(x, y);
this.m = 0;
this.inv_m = 0;
this.inertia = 0;
this.inv_inertia = 0;
this.elasticity = 1;
this.vel = new Vector(0, 0);
this.acc = new Vector(0, 0);
this.acceleration = 1;
this.angVel = 0;
this.player = false;
BODIES.push(this);
}
draw() {}
display() {}
reposition() {}
keyControl() {}
}
class Ball extends Body {
constructor(x, y, r, m) {
super();
this.comp = [new CircleShape(x, y, r)];
this.m = m;
if (this.m === 0) {
this.inv_m = 0;
} else {
this.inv_m = 1 / this.m;
}
}
draw() {
this.comp[0].draw();
}
display() {
this.vel.drawVec(this.pos.x, this.pos.y, 10, "green");
ctx.fillStyle = "black";
ctx.fillText("m = " + this.m, this.pos.x - 10, this.pos.y - 5);
ctx.fillText("e = " + this.elasticity, this.pos.x - 10, this.pos.y + 5);
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.comp[0].pos = this.comp[0].pos.add(this.vel);
}
keyControl() {
if (LEFT) {
this.acc.x = -this.acceleration;
}
if (UP) {
this.acc.y = -this.acceleration;
}
if (RIGHT) {
this.acc.x = this.acceleration;
}
if (DOWN) {
this.acc.y = this.acceleration;
}
if (!LEFT && !RIGHT) {
this.acc.x = 0;
}
if (!UP && !DOWN) {
this.acc.y = 0;
}
}
}
class Capsule extends Body {
constructor(x1, y1, x2, y2, r, m) {
super();
this.comp = [new CircleShape(x1, y1, r), new CircleShape(x2, y2, r)];
let recV1 = this.comp[1].pos.add(
this.comp[1].pos.subtr(this.comp[0].pos).unit().normal().mult(r)
);
let recV2 = this.comp[0].pos.add(
this.comp[1].pos.subtr(this.comp[0].pos).unit().normal().mult(r)
);
this.comp.unshift(
new RectangleShape(recV1.x, recV1.y, recV2.x, recV2.y, 2 * r)
);
this.m = m;
if (this.m === 0) {
this.inv_m = 0;
} else {
this.inv_m = 1 / this.m;
}
this.inertia =
(this.m *
((2 * this.comp[0].width) ** 2 +
(this.comp[0].length + 2 * this.comp[0].width) ** 2)) /
12;
if (this.m === 0) {
this.inv_inertia = 0;
} else {
this.inv_inertia = 1 / this.inertia;
}
}
draw() {
this.comp[0].draw();
this.comp[1].draw();
this.comp[2].draw();
}
keyControl() {
if (UP) {
this.acc = this.comp[0].dir.mult(-this.acceleration);
}
if (DOWN) {
this.acc = this.comp[0].dir.mult(this.acceleration);
}
if (LEFT) {
this.angVel = -0.1;
}
if (RIGHT) {
this.angVel = 0.1;
}
if (!UP && !DOWN) {
this.acc.set(0, 0);
}
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.comp[0].pos = this.comp[0].pos.add(this.vel);
this.angVel *= 1;
this.comp[0].angle += this.angVel;
this.comp[0].getVertices();
this.comp[1].pos = this.comp[0].pos.add(
this.comp[0].dir.mult(-this.comp[0].length / 2)
);
this.comp[2].pos = this.comp[0].pos.add(
this.comp[0].dir.mult(this.comp[0].length / 2)
);
}
}
class Box extends Body {
constructor(x1, y1, x2, y2, w, m, slope = 0) {
super();
this.comp = [new RectangleShape(x1, y1, x2, y2, w, slope)];
this.m = m;
if (this.m === 0) {
this.inv_m = 0;
} else {
this.inv_m = 1 / this.m;
}
this.inertia =
(this.m * (this.comp[0].width ** 2 + this.comp[0].length ** 2)) / 12;
if (this.m === 0) {
this.inv_inertia = 0;
} else {
this.inv_inertia = 1 / this.inertia;
}
}
draw() {
this.comp[0].draw();
}
keyControl() {
if (UP) {
this.acc = this.comp[0].dir.mult(-this.acceleration);
}
if (DOWN) {
this.acc = this.comp[0].dir.mult(this.acceleration);
}
if (LEFT) {
this.angVel = -0.1;
}
if (RIGHT) {
this.angVel = 0.1;
}
if (!UP && !DOWN) {
this.acc.set(0, 0);
}
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.comp[0].pos = this.comp[0].pos.add(this.vel);
this.angVel *= 1;
this.comp[0].angle += this.angVel;
this.comp[0].getVertices();
}
}
class ConvexBody extends Body {
constructor(vertices, m) {
super();
this.comp = [new ConvexShape(vertices)];
this.m = m;
if (this.m === 0) {
this.inv_m = 0;
this.inv_inertia = 0;
} else {
this.inv_m = 1 / this.m;
this.inertia = this.calculateInertia();
this.inv_inertia = 1 / this.inertia;
}
}
calculateInertia() {
const vertices = this.comp[0].vertex;
let totalArea = 0;
let totalInertia = 0;
// Choose an arbitrary point as the origin (e.g., the first vertex)
const origin = vertices[0];
for (let i = 1; i < vertices.length - 1; i++) {
const v1 = vertices[i].subtr(origin);
const v2 = vertices[i + 1].subtr(origin);
// Calculate the area of the triangle
const triangleArea = Math.abs(Vector.cross(v1, v2)) / 2;
// Calculate the centroid of the triangle
const centroid = origin.add(v1.add(v2).mult(1 / 3));
// Calculate the inertia of the triangle around its centroid
const a = v1.mag();
const b = v2.mag();
const c = v1.subtr(v2).mag();
const triangleInertia = ((a * a + b * b + c * c) * triangleArea) / 36;
// Use parallel axis theorem to get inertia around origin
const distanceSquared = centroid.subtr(origin).mag() ** 2;
const inertiaAroundOrigin =
triangleInertia + triangleArea * distanceSquared;
totalArea += triangleArea;
totalInertia += inertiaAroundOrigin;
}
// Scale the inertia by the mass
return (this.m / totalArea) * totalInertia;
}
draw() {
this.comp[0].draw();
}
keyControl() {
if (UP) {
this.acc = this.comp[0].dir.mult(-this.acceleration);
}
if (DOWN) {
this.acc = this.comp[0].dir.mult(this.acceleration);
}
if (LEFT) {
this.angVel = -0.1;
}
if (RIGHT) {
this.angVel = 0.1;
}
if (!UP && !DOWN) {
this.acc.set(0, 0);
}
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.comp[0].pos = this.comp[0].pos.add(this.vel);
this.angVel *= 1;
this.comp[0].angle += this.angVel;
this.comp[0].getVertices();
}
}
class Polygon extends Body {
constructor(x, y, sides, radius, m) {
super(x, y);
this.sides = sides;
this.radius = radius;
this.m = m;
// Calculate vertices
const vertices = this.calculateVertices(x, y, sides, radius);
// Create ConvexShape component
this.comp = [new ConvexShape(vertices)];
// Set mass and inertia
if (this.m === 0) {
this.inv_m = 0;
this.inv_inertia = 0;
} else {
this.inv_m = 1 / this.m;
// Approximating inertia as that of a circle
this.inertia = (this.m * this.radius * this.radius) / 2;
this.inv_inertia = 1 / this.inertia;
}
}
calculateVertices(x, y, sides, radius) {
let vertices = [];
for (let i = 0; i < sides; i++) {
let angle = (i / sides) * 2 * Math.PI;
let vx = x + radius * Math.cos(angle);
let vy = y + radius * Math.sin(angle);
vertices.push({ x: vx, y: vy });
}
return vertices;
}
draw() {
this.comp[0].draw();
}
keyControl() {
if (UP) {
this.acc = this.comp[0].dir.mult(-this.acceleration);
}
if (DOWN) {
this.acc = this.comp[0].dir.mult(this.acceleration);
}
if (LEFT) {
this.angVel = -0.1;
}
if (RIGHT) {
this.angVel = 0.1;
}
if (!UP && !DOWN) {
this.acc.set(0, 0);
}
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.comp[0].pos = this.comp[0].pos.add(this.vel);
this.angVel *= 1;
this.comp[0].angle += this.angVel;
this.comp[0].getVertices();
}
}
class Star extends Body {
constructor(x1, y1, r, m) {
super();
this.comp = [];
this.r = r;
let center = new Vector(x1, y1);
let upDir = new Vector(0, -1);
let p1 = center.add(upDir.mult(r));
let p2 = center
.add(upDir.mult(-r / 2))
.add(upDir.normal().mult((-r * Math.sqrt(3)) / 2));
let p3 = center
.add(upDir.mult(-r / 2))
.add(upDir.normal().mult((r * Math.sqrt(3)) / 2));
this.comp.push(new ConvexShape([p1, p2, p3]));
p1 = center.add(upDir.mult(-r));
p2 = center
.add(upDir.mult(r / 2))
.add(upDir.normal().mult((-r * Math.sqrt(3)) / 2));
p3 = center
.add(upDir.mult(r / 2))
.add(upDir.normal().mult((r * Math.sqrt(3)) / 2));
this.comp.push(new ConvexShape([p1, p2, p3]));
this.m = m;
if (this.m === 0) {
this.inv_m = 0;
} else {
this.inv_m = 1 / this.m;
}
this.inertia = (this.m * (2 * this.r) ** 2) / 12;
if (this.m === 0) {
this.inv_inertia = 0;
} else {
this.inv_inertia = 1 / this.inertia;
}
}
draw() {
this.comp[0].draw();
this.comp[1].draw();
}
keyControl() {
if (UP) {
this.acc = this.comp[0].dir.mult(-this.acceleration);
}
if (DOWN) {
this.acc = this.comp[0].dir.mult(this.acceleration);
}
if (LEFT) {
this.angVel = -0.1;
}
if (RIGHT) {
this.angVel = 0.1;
}
if (!UP && !DOWN) {
this.acc.set(0, 0);
}
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.angVel *= 1;
this.comp[0].pos = this.comp[0].pos.add(this.vel);
this.comp[0].angle += this.angVel;
this.comp[0].getVertices();
this.comp[1].pos = this.comp[0].pos;
this.comp[1].angle += this.angVel;
this.comp[1].getVertices();
}
}
class Trapezoid extends Body {
constructor(x, y, width, height, slope, m) {
super(x, y);
this.bottomWidth = width;
this.height = height;
this.slope = slope;
this.m = m;
// Calculate base2
const topWidth = width - 2 * height * slope;
// Calculate vertices
let vertices = this.calculateVertices(x, y, width, topWidth, height);
// Create ConvexShape component
this.comp = [new ConvexShape(vertices)];
// Set mass and inertia
if (this.m === 0) {
this.inv_m = 0;
this.inv_inertia = 0;
} else {
this.inv_m = 1 / this.m;
// Approximating inertia as that of a rectangle
this.inertia = (this.m * (width * width + height * height)) / 12;
this.inv_inertia = 1 / this.inertia;
}
}
calculateVertices(x, y, bottomWidth, topWidth, height) {
return [
{ x: x - bottomWidth / 2, y: y + height / 2 },
{ x: x + bottomWidth / 2, y: y + height / 2 },
{ x: x + topWidth / 2, y: y - height / 2 },
{ x: x - topWidth / 2, y: y - height / 2 },
];
}
draw() {
this.comp[0].draw();
}
keyControl() {
if (UP) {
this.acc = this.comp[0].dir.mult(-this.acceleration);
}
if (DOWN) {
this.acc = this.comp[0].dir.mult(this.acceleration);
}
if (LEFT) {
this.angVel = -0.1;
}
if (RIGHT) {
this.angVel = 0.1;
}
if (!UP && !DOWN) {
this.acc.set(0, 0);
}
}
reposition() {
this.acc = this.acc.unit().mult(this.acceleration);
this.vel = this.vel.add(this.acc);
this.vel = this.vel.mult(1 - friction);
this.comp[0].pos = this.comp[0].pos.add(this.vel);
this.angVel *= 1;
this.comp[0].angle += this.angVel;
this.comp[0].getVertices();
}
}
class Wall extends Body {
constructor(x1, y1, x2, y2) {
super();
this.comp = [new LineShape(x1, y1, x2, y2)];
}
draw() {
this.comp[0].draw();
}
}
//Collision manifold, consisting the data for collision handling
//Manifolds are collected in an array for every frame
class CollData {
constructor(o1, o2, normal, pen, cp) {
this.o1 = o1;
this.o2 = o2;
this.normal = normal;
this.pen = pen;
this.cp = cp;
}
penRes() {
let penResolution = this.normal.mult(
this.pen / (this.o1.inv_m + this.o2.inv_m)
);
this.o1.comp[0].pos = this.o1.comp[0].pos.add(
penResolution.mult(this.o1.inv_m)
);
this.o2.comp[0].pos = this.o2.comp[0].pos.add(
penResolution.mult(-this.o2.inv_m)
);
}
collRes() {
//1. Closing velocity
let collArm1 = this.cp.subtr(this.o1.comp[0].pos);
let rotVel1 = new Vector(
-this.o1.angVel * collArm1.y,
this.o1.angVel * collArm1.x
);
let closVel1 = this.o1.vel.add(rotVel1);
let collArm2 = this.cp.subtr(this.o2.comp[0].pos);
let rotVel2 = new Vector(
-this.o2.angVel * collArm2.y,
this.o2.angVel * collArm2.x
);
let closVel2 = this.o2.vel.add(rotVel2);
//2. Impulse augmentation
let impAug1 = Vector.cross(collArm1, this.normal);
impAug1 = impAug1 * this.o1.inv_inertia * impAug1;
let impAug2 = Vector.cross(collArm2, this.normal);
impAug2 = impAug2 * this.o2.inv_inertia * impAug2;
let relVel = closVel1.subtr(closVel2);
let sepVel = Vector.dot(relVel, this.normal);
let new_sepVel = -sepVel * Math.min(this.o1.elasticity, this.o2.elasticity);
let vsep_diff = new_sepVel - sepVel;
let impulse =
vsep_diff / (this.o1.inv_m + this.o2.inv_m + impAug1 + impAug2);
let impulseVec = this.normal.mult(impulse);
//3. Changing the velocities
this.o1.vel = this.o1.vel.add(impulseVec.mult(this.o1.inv_m));
this.o2.vel = this.o2.vel.add(impulseVec.mult(-this.o2.inv_m));
this.o1.angVel += this.o1.inv_inertia * Vector.cross(collArm1, impulseVec);
this.o2.angVel -= this.o2.inv_inertia * Vector.cross(collArm2, impulseVec);
}
}
//Event listeners for the arrow keys
function userInput() {
canvas.addEventListener("keydown", function (e) {
if (e.keyCode === 37) {
LEFT = true;
}
if (e.keyCode === 38) {
UP = true;
}
if (e.keyCode === 39) {
RIGHT = true;
}
if (e.keyCode === 40) {
DOWN = true;
}
});
canvas.addEventListener("keyup", function (e) {
if (e.keyCode === 37) {
LEFT = false;
}
if (e.keyCode === 38) {
UP = false;
}
if (e.keyCode === 39) {
RIGHT = false;
}
if (e.keyCode === 40) {
DOWN = false;
}
});
}
function round(number, precision) {
let factor = 10 ** precision;
return Math.round(number * factor) / factor;
}
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function testCircleShape(x, y, color = "black") {
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
}
function closestPointOnLS(p, w1) {
let ballToWallStart = w1.start.subtr(p);
if (Vector.dot(w1.dir, ballToWallStart) > 0) {
return w1.start;
}
let wallEndToBall = p.subtr(w1.end);
if (Vector.dot(w1.dir, wallEndToBall) > 0) {
return w1.end;
}
let closestDist = Vector.dot(w1.dir, ballToWallStart);
let closestVect = w1.dir.mult(closestDist);
return w1.start.subtr(closestVect);
}
//Separating axis theorem on two objects
//Returns with the details of the Minimum Translation Vector (or false if no collision)
function sat(o1, o2) {
let minOverlap = null;
let smallestAxis;
let vertexObj;
let axes = findAxes(o1, o2);
let proj1,
proj2 = 0;
let firstShapeAxes = getShapeAxes(o1);
for (let i = 0; i < axes.length; i++) {
proj1 = projShapeOntoAxis(axes[i], o1);
proj2 = projShapeOntoAxis(axes[i], o2);
let overlap =
Math.min(proj1.max, proj2.max) - Math.max(proj1.min, proj2.min);
if (overlap < 0) {
return false;
}
if (
(proj1.max > proj2.max && proj1.min < proj2.min) ||
(proj1.max < proj2.max && proj1.min > proj2.min)
) {
let mins = Math.abs(proj1.min - proj2.min);
let maxs = Math.abs(proj1.max - proj2.max);
if (mins < maxs) {
overlap += mins;
} else {
overlap += maxs;
axes[i] = axes[i].mult(-1);
}
}
if (overlap < minOverlap || minOverlap === null) {
minOverlap = overlap;
smallestAxis = axes[i];
if (i < firstShapeAxes) {
vertexObj = o2;
if (proj1.max > proj2.max) {
smallestAxis = axes[i].mult(-1);
}
} else {
vertexObj = o1;
if (proj1.max < proj2.max) {
smallestAxis = axes[i].mult(-1);
}
}
}
}
let contactVertex = projShapeOntoAxis(smallestAxis, vertexObj).collVertex;
//smallestAxis.drawVec(contactVertex.x, contactVertex.y, minOverlap, "blue");
if (vertexObj === o2) {
smallestAxis = smallestAxis.mult(-1);
}
return {
pen: minOverlap,
axis: smallestAxis,
vertex: contactVertex,
};
}
//Helping functions for the SAT below
//returns the min and max projection values of a shape onto an axis
function projShapeOntoAxis(axis, obj) {
setBallVerticesAlongAxis(obj, axis);
let min = Vector.dot(axis, obj.vertex[0]);
let max = min;
let collVertex = obj.vertex[0];
for (let i = 0; i < obj.vertex.length; i++) {
let p = Vector.dot(axis, obj.vertex[i]);
if (p < min) {
min = p;
collVertex = obj.vertex[i];
}
if (p > max) {
max = p;
}
}
return {
min: min,
max: max,
collVertex: collVertex,
};
}
//finds the projection axes for the two objects
function findAxes(o1, o2) {
let axes = [];
if (o1 instanceof CircleShape && o2 instanceof CircleShape) {
axes.push(o2.pos.subtr(o1.pos).unit());
return axes;
}
if (o1 instanceof CircleShape) {
axes.push(closestVertexToPoint(o2, o1.pos).subtr(o1.pos).unit());
}
if (o1 instanceof LineShape) {
axes.push(o1.dir.normal());
}
if (o1 instanceof RectangleShape) {
axes.push(o1.dir.normal());
axes.push(o1.dir);
}
if (o1 instanceof ConvexShape) {
for (let i = 0; i < o1.vertex.length; i++) {
let j = (i + 1) % o1.vertex.length;
axes.push(o1.vertex[j].subtr(o1.vertex[i]).normal());
}
}
if (o2 instanceof CircleShape) {
axes.push(closestVertexToPoint(o1, o2.pos).subtr(o2.pos).unit());
}
if (o2 instanceof LineShape) {
axes.push(o2.dir.normal());
}
if (o2 instanceof RectangleShape) {
axes.push(o2.dir.normal());
axes.push(o2.dir);
}
if (o2 instanceof ConvexShape) {
o2.getVertices();
for (let i = 0; i < o2.vertex.length; i++) {
let j = (i + 1) % o2.vertex.length;
axes.push(o2.vertex[j].subtr(o2.vertex[i]).normal());
}
}
if (o2 instanceof ConvexShape) {
for (let i = 0; i < o2.vertex.length; i++) {
let j = (i + 1) % o2.vertex.length;
axes.push(o2.vertex[j].subtr(o2.vertex[i]).normal());
}
}
return axes;
}
//iterates through an objects vertices and returns the one that is the closest to the given point
function closestVertexToPoint(obj, p) {
let closestVertex;
let minDist = null;
for (let i = 0; i < obj.vertex.length; i++) {
if (p.subtr(obj.vertex[i]).mag() < minDist || minDist === null) {
closestVertex = obj.vertex[i];
minDist = p.subtr(obj.vertex[i]).mag();
}
}
return closestVertex;
}
//returns the number of the axes that belong to an object
function getShapeAxes(obj) {
if (obj instanceof CircleShape || obj instanceof LineShape) {
return 1;
}
if (obj instanceof RectangleShape) {
return 2;
}
if (obj instanceof ConvexShape) {
return obj.vertex.length;
}
}
//the ball vertices always need to be recalculated based on the current projection axis direction
function setBallVerticesAlongAxis(obj, axis) {
if (obj instanceof CircleShape) {
obj.vertex[0] = obj.pos.add(axis.unit().mult(-obj.r));
obj.vertex[1] = obj.pos.add(axis.unit().mult(obj.r));
}
}
//Thats it for the SAT and its support functions
//Prevents objects to float away from the canvas
function putWallsAroundCanvas() {
let edge1 = new Wall(0, 0, canvas.clientWidth, 0);
let edge2 = new Wall(
canvas.clientWidth,
0,
canvas.clientWidth,
canvas.clientHeight
);
let edge3 = new Wall(
canvas.clientWidth,
canvas.clientHeight,
0,
canvas.clientHeight
);
let edge4 = new Wall(0, canvas.clientHeight, 0, 0);
}
function generateRandomConvexVertices(
centerX,
centerY,
minVertices = 3,
maxVertices = 10,
minRadius = 20,
maxRadius = 50
) {
// Generate a random number of vertices
const numVertices =
Math.floor(Math.random() * (maxVertices - minVertices + 1)) + minVertices;
// Generate random angles
let angles = [];
for (let i = 0; i < numVertices; i++) {
angles.push(Math.random() * 2 * Math.PI);
}
angles.sort((a, b) => a - b);
// Generate vertices
let vertices = [];
for (let i = 0; i < numVertices; i++) {
const radius = Math.random() * (maxRadius - minRadius) + minRadius;
const x = centerX + radius * Math.cos(angles[i]);
const y = centerY + radius * Math.sin(angles[i]);
vertices.push(new Vector(x, y));
}
return vertices;
}
//Game loop (60 frames per second)
function mainLoop(timestamp) {
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
userInput();
COLLISIONS.length = 0;
BODIES.forEach((b) => {
b.draw();
b.display();
if (b.player) {
b.keyControl();
}
b.reposition();
});
BODIES.forEach((b, index) => {
for (let bodyPair = index + 1; bodyPair < BODIES.length; bodyPair++) {
let bestSat = {
pen: null,
axis: null,
vertex: null,
};
for (let o1comp = 0; o1comp < BODIES[index].comp.length; o1comp++) {
for (let o2comp = 0; o2comp < BODIES[bodyPair].comp.length; o2comp++) {
if (
sat(BODIES[index].comp[o1comp], BODIES[bodyPair].comp[o2comp]).pen >
bestSat.pen
) {
bestSat = sat(
BODIES[index].comp[o1comp],
BODIES[bodyPair].comp[o2comp]
);
ctx.fillText("COLLISION", 500, 400);
}
}
}
if (bestSat.pen !== null) {
COLLISIONS.push(
new CollData(
BODIES[index],
BODIES[bodyPair],
bestSat.axis,
bestSat.pen,
bestSat.vertex
)
);
}
}
});
COLLISIONS.forEach((c) => {
c.penRes();
c.collRes();
});
requestAnimationFrame(mainLoop);
}
//Setting up the initial environment before starting the main loop
putWallsAroundCanvas();
//Creating 10 body object with random arguments
for (let addBody = 0; addBody < 7; addBody++) {
let x0 = randInt(100, canvas.clientWidth - 100);
let y0 = randInt(100, canvas.clientHeight - 100);
let x1 = x0 + randInt(-50, 50);
let y1 = y0 + randInt(-50, 50);
let x2 = x1 + randInt(-50, 50);
let y2 = y1 + randInt(-50, 50);
let r = randInt(10, 30);
let m = randInt(0, 10);
if (addBody % 7 === 0) {
let ballObj = new Ball(x0, y0, r, m);
}
if (addBody % 7 === 1) {
let boxObj = new Box(x0, y0, x1, y1, r + 30, m);
}
if (addBody % 7 === 2) {
let capsObj = new Capsule(x0, y0, x1, y1, r, m);
}
if (addBody % 7 === 3) {
let starObj = new Star(x0, y0, r + 20, m);
}
if (addBody % 7 === 4) {
let polygonObj = new Polygon(x0, y0, 5, r + 30, m);
}
if (addBody % 7 === 5) {
let trapezoidObj = new Trapezoid(x0, y0, 100, 100, 0.25, m);
}
if (addBody % 7 === 6) {
let vertices = generateRandomConvexVertices(100, 100, 5, 5);
let convexObj = new ConvexBody(vertices, m);
}
}
let playerBall = new Ball(320, 240, 10, 5);
playerBall.player = true;
requestAnimationFrame(mainLoop);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment