html
<body>
<head>
<link rel="stylesheet" type="text/css" href="./index.css">
</head>
<body>
<main>
<canvas id="gameboard"></canvas>
</main>
<script type="text/javascript" src="./index.js"></script>
</body>
</body>
css
* {
box-sizing: border-box;
padding: 0;
margin: 0;
font-family: sans-serif;
}
main {
width: 100vw;
height: 100vh;
background: hsl(0deg, 0%, 10%);
}
js
const canvas = document.getElementById("gameboard");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let width = canvas.width;
let height = canvas.height;
// 因為重力加速度的單位是 m/s/s缚去,而畫布以像素為單位,所以重力加速度應(yīng)保持一定的比例,
// 這里簡單的使用了 100 倍的重力加速度截驮。
const gravity = 980; // 重力加速度常量
// 小球的類
class Circle {
/**
* @param context 對象
* @param x 坐標(biāo)
* @param y 坐標(biāo)
* @param r 半徑
* @param vx x速度
* @param vy y速度
* @param mass 質(zhì)量
* @param cor 恢復(fù)系數(shù)
*
*/
constructor(context, x, y, r, vx, vy, mass = 1, cor = 1) {
this.context = context;
this.colliding = false;
this.x = x;
this.y = y;
this.r = r;
this.vx = vx;
this.vy = vy;
this.mass = mass;
this.cor = cor;
}
// 繪制小球
draw() {
this.context.fillStyle = this.colliding ? "hsl(300, 100%, 70%)" : "hsl(170, 100%, 50%)";
this.context.beginPath();
this.context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
this.context.fill();
}
/**
* 更新畫布
* @param {number} seconds
*/
update(seconds) {
this.vy += gravity * seconds; // 重力加速度
this.x += this.vx * seconds;
this.y += this.vy * seconds;
}
// 小球是否碰撞
isCircleCollided(other) {
let squareDistance = (this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y);
let squareRadius = (this.r + other.r) * (this.r + other.r);
return squareDistance <= squareRadius;
}
// 碰撞后將小球的狀態(tài)改為碰撞
checkCollideWith(other) {
if (this.isCircleCollided(other)) {
this.colliding = true;
other.colliding = true;
this.changeVelocityAndDirection(other); // 當(dāng)碰撞的時候去設(shè)置速度向量
}
}
// 檢測小球碰撞后的速度
changeVelocityAndDirection(other) {
// 創(chuàng)建兩小球的速度向量
let velocity1 = new Vector(this.vx, this.vy);
let velocity2 = new Vector(other.vx, other.vy);
// 獲取兩個圓心坐標(biāo)的差
let vNorm = new Vector(this.x - other.x, this.y - other.y);
// 獲取連心線方向的單位向量和切線方向上的單位向量
let unitVNorm = vNorm.normalize();
let unitVTan = new Vector(-unitVNorm.y, unitVNorm.x);
// 用點乘計算小球速度在這兩個方向上的投影
let v1n = velocity1.dot(unitVNorm);
let v1t = velocity1.dot(unitVTan);
let v2n = velocity2.dot(unitVNorm);
let v2t = velocity2.dot(unitVTan);
// 恢復(fù)系數(shù),取最小值
let cor = Math.min(this.cor, other.cor);
// 碰撞后的速度公式所需要的變量值了狮崩,直接用代碼把公式套用進去
// v1' = (v1(m1-m2) = 2m2v2) / (m1 + m2)
// v2' = (v2(m2-m1) = 2m1v1) / (m1 + m2)
// let v1nAfter = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass);
// let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass);
let v1nAfter = (this.mass * v1n + other.mass * v2n + cor * other.mass * (v2n - v1n)) / (this.mass + other.mass);
let v2nAfter = (this.mass * v1n + other.mass * v2n + cor * this.mass * (v1n - v2n)) / (this.mass + other.mass);
// 如果 v1nAfter 小于 v2nAfter其监,那么第 1 個小球和第 2 個小球會越來越遠,此時不用處理碰撞
if (v1nAfter < v2nAfter) {
return;
}
// 給碰撞后的速度加上方向则果,計算在連心線方向和切線方向上的速度,只需要讓速度標(biāo)量跟連心線單位向量和切線單位向量相乘
let v1VectorNorm = unitVNorm.multiply(v1nAfter);
let v1VectorTan = unitVTan.multiply(v1t);
let v2VectorNorm = unitVNorm.multiply(v2nAfter);
let v2VectorTan = unitVTan.multiply(v2t);
// 最后把連心線上的速度向量和切線方向的速度向量進行加法操作漩氨,就能獲得碰撞后小球的速度向量
let velocity1After = v1VectorNorm.add(v1VectorTan);
let velocity2After = v2VectorNorm.add(v2VectorTan);
this.vx = velocity1After.x;
this.vy = velocity1After.y;
other.vx = velocity2After.x;
other.vy = velocity2After.y;
}
}
// 畫布
class Gameboard {
constructor() {
this.startTime;
this.init();
}
init() {
this.circles = [
new Circle(ctx, 30, 50, 30, -100, 390, 30, 0.7),
new Circle(ctx, 60, 180, 20, 180, -275, 20, 0.7),
new Circle(ctx, 120, 100, 60, 120, 262, 100, 0.3),
new Circle(ctx, 150, 180, 10, -130, 138, 10, 0.7),
new Circle(ctx, 190, 210, 10, 138, -280, 10, 0.7),
new Circle(ctx, 220, 240, 10, 142, 350, 10, 0.7),
new Circle(ctx, 100, 260, 10, 135, -460, 10, 0.7),
new Circle(ctx, 120, 285, 10, -165, 370, 10, 0.7),
new Circle(ctx, 140, 290, 10, 125, 230, 10, 0.7),
new Circle(ctx, 160, 380, 10, -175, -180, 10, 0.7),
new Circle(ctx, 180, 310, 10, 115, 440, 10, 0.7),
new Circle(ctx, 100, 310, 10, -195, -325, 10, 0.7),
new Circle(ctx, 60, 150, 10, -138, 420, 10, 0.7),
new Circle(ctx, 70, 430, 45, 135, -230, 45, 0.7),
new Circle(ctx, 250, 290, 40, -140, 335, 40, 0.7),
];
window.requestAnimationFrame(this.process.bind(this));
}
process(now) {
if (!this.startTime) {
this.startTime = now;
}
let seconds = (now - this.startTime) / 1000;
this.startTime = now;
for (let i = 0; i < this.circles.length; i++) {
this.circles[i].update(seconds);
}
this.checkEdgeCollision();
this.checkCollision();
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < this.circles.length; i++) {
this.circles[i].draw(ctx);
}
window.requestAnimationFrame(this.process.bind(this));
}
// 重置碰撞狀態(tài)
checkCollision() {
this.circles.forEach((circle) => (circle.colliding = false));
for (let i = 0; i < this.circles.length; i++) {
for (let j = i + 1; j < this.circles.length; j++) {
this.circles[i].checkCollideWith(this.circles[j]);
}
}
}
// 檢測邊界碰撞
checkEdgeCollision() {
const cor = 0.8; // 設(shè)置恢復(fù)系統(tǒng)
this.circles.forEach((circle) => {
// 左右碰壁
if(circle.x < circle.r) {
circle.vx = -circle.vx * cor;
circle.x = circle.r;
} else if(circle.x > width - circle.r) {
circle.vx = -circle.vx * cor;
circle.x = width - circle.r;
}
// 上下碰壁
if(circle.y < circle.r) {
circle.vy = -circle.vy * cor;
circle.y = circle.r;
} else if(circle.y > height - circle.r) {
circle.vy = -circle.vy * cor;
circle.y = height - circle.r;
}
})
}
}
new Gameboard();
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
/**
* 向量加法
* @param {Vector} v
*/
add(v) {
return new Vector(this.x + v.x, this.y + v.y);
}
/**
* 向量減法
* @param {Vector} v
*/
substract(v) {
return new Vector(this.x - v.x, this.y - v.y);
}
/**
* 向量與標(biāo)量乘法
* @param {Vector} s
*/
multiply(s) {
return new Vector(this.x * s, this.y * s);
}
/**
* 向量與向量點乘(投影)
* @param {Vector} v
*/
dot(v) {
return this.x * v.x + this.y * v.y;
}
/**
* 向量標(biāo)準(zhǔn)化(除去長度)
* @param {number} distance
*/
normalize() {
let distance = Math.sqrt(this.x * this.x + this.y * this.y);
return new Vector(this.x / distance, this.y / distance);
}
}