Issue
Currently, I am creating a sort of physics simulation where a bunch of balls with different radii bounce against each other and the walls. To make it look better before I refactor, I would like to make the balls change color upon collision, but I'm not sure where I should add it in, or how.
Note: The color should be randomized by using
randomColor = Math.floor(Math.random() * 16777215).toString(16);
color = "#" + randomColor;
Note: Only the balls involved in the collision should change color. So if two balls touch, they should both change color, but no other.
Question: How can I make it so that after every collision, the balls change color?
function lineMessage(msg) {
document.querySelector('#myMessage').textContent += msg + '. ';
}
function groupMessage(msg) {
document.querySelector('#myMessage').innerHTML += msg + '<br/>';
}
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext("2d");
canvas.width = 1000;
canvas.height = 550;
const ballCount = document.querySelector('#ball-count');
const gravity = 0;
const wallLoss = 1;
let numBalls = 0; // approx as will not add ball if space can not be found
const minBallSize = 10;
const maxBallSize = 100;
const velMin = 1;
const velMax = 5;
const maxResolutionCycles = 100;
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1
//contact points of two circles radius r1, r2 moving along two lines (a,e)-(b,f) and (c,g)-(d,h)
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => {
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => { // find roots for quadratic
if (Math.abs(a) < 1e-6) {
return b != 0 ? [-c / b] : []
}
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)]
}
return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) { // only if moving towards the line
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x - (x1 - ny))) / d;
}
}
const balls = [];
const lines = [];
function Line(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity;
},
draw() {
ctx.moveTo(this.x + this.r, this.y);
ctx.arc(this.x, this.y, this.r, 0, Math.TAU);
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if (u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if (d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
)
if (times.length) {
if (times.length === 1) {
if (this.checkBallBallTime(times[0], time)) {
return times[0]
}
return;
}
if (times[0] <= times[1]) {
if (this.checkBallBallTime(times[0], time)) {
return times[0]
}
if (this.checkBallBallTime(times[1], time)) {
return times[1]
}
return
}
if (this.checkBallBallTime(times[1], time)) {
return times[1]
}
if (this.checkBallBallTime(times[0], time)) {
return times[0]
}
}
}
},
collideLine(l, time) {
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * wallLoss;
this.vy = (ny * u - this.vy) * wallLoss;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) {
const a = this;
const m1 = a.m;
const m2 = b.m;
const x = a.x - b.x
const y = a.y - b.y
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d
const u2 = (x * a.vy - y * a.vx ) / d
const u3 = (b.vx * x + b.vy * y) / d
const u4 = (x * b.vy - y * b.vx ) / d
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for (const b of balls) {
if (ball.doesOverlap(b)) {
return false
}
}
return true;
}
function create(bCount) {
//Instead of doing bCount = 1, you can also do create(1)
bCount = 1;
lines.push(new Line(-10, 10, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 10)).reverse());
lines.push((new Line(10, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 10, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
debugger
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(velMin, velMax);
const ball = new Ball(
Math.rand(maxBallSize + 10, canvas.width - maxBallSize - 10),
Math.rand(maxBallSize + 10, canvas.height - maxBallSize - 10),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(minBallSize, maxBallSize),
)
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while (resolving && e++ < maxResolutionCycles) { // too main ball may create very lone resolution cycle. e limits this
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for(const b of balls) {
idx1 = idx + 1;
while (idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for (const l of lines) {
const time = b.interceptLineTime(l, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx++;
}
if (resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) {
b.update()
}
ctx.strokeStyle = "#000";
ctx.beginPath();
for (const b of balls) {
b.draw()
}
for (const l of lines) {
l.draw()
}
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function addBall() {
numBalls++;
ballCount.innerHTML = numBalls;
//Instead of doing create(numBalls), you can do create(1)
create(numBalls);
}
mainLoop();
#canvas {
width: 1000px;
height: 550px
}
#myConsole {
background-color: black;
color: white;
min-height: 100px;
}
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE-edge">
<meta name="viewport", content="width=device-width, initial-scale=1.0">
<meta name="author" content="Christian Davis">
<link rel="stylesheet" href="styles.css">
<title>Bouncy Balls</title>
</head>
<body>
<button onclick="addBall()">Add Ball</button><br>
<div>Ball Count: <span id="ball-count">0</span></div>
<canvas id="canvas"></canvas>
<p id="myConsole">> <span id="myMessage"></span></p>
<script src="app2.js"></script>
</body>
</html>
Solution
This code generates a color for a ball which you just created:
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
a.color = "#" + randomColor;
b.color = "#" + randomColor;
Here in this function Ball(....)
the code generates a new color when 2 or more balls collide:
this.color = "#" + Math.floor(Math.random() * 16777215).toString(16);
Here is the correct version:
function lineMessage(msg) {
document.querySelector('#myMessage').textContent += msg + '. ';
}
function groupMessage(msg) {
document.querySelector('#myMessage').innerHTML += msg + '<br/>';
}
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext("2d");
canvas.width = 1000;
canvas.height = 550;
const ballCount = document.querySelector('#ball-count');
const gravity = 0;
const wallLoss = 1;
let numBalls = 0;
const minBallSize = 10;
const maxBallSize = 100;
const velMin = 1;
const velMax = 5;
const maxResolutionCycles = 100;
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randI = (min, max) => Math.random() * (max - min) + min | 0;
Math.randItem = arr => arr[Math.random() * arr.length | 0];
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => {
const A = a * a, B = b * b, C = c * c, D = d * d;
const E = e * e, F = f * f, G = g * g, H = h * h;
var R = (r1 + r2) ** 2;
const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
return Math.quadRoots(AA, BB, CC);
}
Math.quadRoots = (a, b, c) => {
if (Math.abs(a) < 1e-6) {
return b != 0 ? [-c / b] : [];
}
b /= a;
var d = b * b - 4 * (c / a);
if (d > 0) {
d = d ** 0.5;
return [0.5 * (-b + d), 0.5 * (-b - d)];
}
return d === 0 ? [0.5 * -b] : [];
}
Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2, r) => {
const xx = x2 - x1;
const yy = y2 - y1;
const d = vx * yy - vy * xx;
if (d > 0) {
const dd = r / (xx * xx + yy * yy) ** 0.5;
const nx = xx * dd;
const ny = yy * dd;
return (xx * (y - (y1 + nx)) - yy * (x - (x1 - ny))) / d;
}
}
const balls = [];
const lines = [];
function Line(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
Line.prototype = {
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
},
reverse() {
const x = this.x1;
const y = this.y1;
this.x1 = this.x2;
this.y1 = this.y2;
this.x2 = x;
this.y2 = y;
return this;
}
}
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
this.r = r;
this.m = m;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.color = "#" + Math.floor(Math.random() * 16777215).toString(16); // Initialize with a random color
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += gravity;
},
draw() {
ctx.fillStyle = this.color; // Set the fill color to the ball's color
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.TAU);
ctx.fill();
},
interceptLineTime(l, time) {
const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2, this.r);
if (u >= time && u <= 1) {
return u;
}
},
checkBallBallTime(t, minTime) {
return t > minTime && t <= 1;
},
interceptBallTime(b, time) {
const x = this.x - b.x;
const y = this.y - b.y;
const d = (x * x + y * y) ** 0.5;
if (d > this.r + b.r) {
const times = Math.circlesInterceptUnitTime(
this.x, this.y,
this.x + this.vx, this.y + this.vy,
b.x, b.y,
b.x + b.vx, b.y + b.vy,
this.r, b.r
)
if (times.length) {
if (times.length === 1) {
if (this.checkBallBallTime(times[0], time)) {
return times[0]
}
return;
}
if (times[0] <= times[1]) {
if (this.checkBallBallTime(times[0], time)) {
return times[0]
}
if (this.checkBallBallTime(times[1], time)) {
return times[1]
}
return
}
if (this.checkBallBallTime(times[1], time)) {
return times[1]
}
if (this.checkBallBallTime(times[0], time)) {
return times[0]
}
}
}
},
collideLine(l, time) {
const x1 = l.x2 - l.x1;
const y1 = l.y2 - l.y1;
const d = (x1 * x1 + y1 * y1) ** 0.5;
const nx = x1 / d;
const ny = y1 / d;
const u = (this.vx * nx + this.vy * ny) * 2;
this.x += this.vx * time;
this.y += this.vy * time;
this.vx = (nx * u - this.vx) * wallLoss;
this.vy = (ny * u - this.vy) * wallLoss;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) {
const a = this;
const m1 = a.m;
const m2 = b.m;
const x = a.x - b.x;
const y = a.y - b.y;
const d = (x * x + y * y);
const u1 = (a.vx * x + a.vy * y) / d;
const u2 = (x * a.vy - y * a.vx) / d;
const u3 = (b.vx * x + b.vy * y) / d;
const u4 = (x * b.vy - y * b.vx) / d;
const mm = m1 + m2;
const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;
a.x = a.x + a.vx * time;
a.y = a.y + a.vy * time;
b.x = b.x + b.vx * time;
b.y = b.y + b.vy * time;
b.vx = x * vu1 - y * u4;
b.vy = y * vu1 + x * u4;
a.vx = x * vu3 - y * u2;
a.vy = y * vu3 + x * u2;
a.x = a.x - a.vx * time;
a.y = a.y - a.vy * time;
b.x = b.x - b.vx * time;
b.y = b.y - b.vy * time;
// Change color upon collision
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
a.color = "#" + randomColor;
b.color = "#" + randomColor;
},
doesOverlap(ball) {
const x = this.x - ball.x;
const y = this.y - ball.y;
return (this.r + ball.r) > ((x * x + y * y) ** 0.5);
}
}
function canAdd(ball) {
for (const b of balls) {
if (ball.doesOverlap(b)) {
return false
}
}
return true;
}
function create(bCount) {
bCount = 1;
lines.push(new Line(-10, 10, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 10)).reverse());
lines.push((new Line(10, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 10, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
debugger
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(velMin, velMax);
const ball = new Ball(
Math.rand(maxBallSize + 10, canvas.width - maxBallSize - 10),
Math.rand(maxBallSize + 10, canvas.height - maxBallSize - 10),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(minBallSize, maxBallSize),
)
if (canAdd(ball)) {
balls.push(ball);
break;
}
}
}
}
function resolveCollisions() {
var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
while (resolving && e++ < maxResolutionCycles) {
resolving = false;
minObj = undefined;
minBall = undefined;
minTime = 1;
idx = 0;
for (const b of balls) {
idx1 = idx + 1;
while (idx1 < balls.length) {
const b1 = balls[idx1++];
const time = b.interceptBallTime(b1, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = b1;
minBall = b;
resolving = true;
}
}
}
for (const l of lines) {
const time = b.interceptLineTime(l, after);
if (time !== undefined) {
if (time <= minTime) {
minTime = time;
minObj = l;
minBall = b;
resolving = true;
}
}
}
idx++;
}
if (resolving) {
if (minObj instanceof Ball) {
minBall.collide(minObj, minTime);
} else {
minBall.collideLine(minObj, minTime);
}
after = minTime;
}
}
}
function mainLoop() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) {
b.update()
}
ctx.strokeStyle = "#000";
ctx.beginPath();
for (const b of balls) {
b.draw()
}
for (const l of lines) {
l.draw()
}
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function addBall() {
numBalls++;
ballCount.innerHTML = numBalls;
create(1);
}
mainLoop();
#canvas {
width: 1000px;
height: 550px
}
#myConsole {
background-color: black;
color: white;
min-height: 100px;
}
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE-edge">
<meta name="viewport", content="width=device-width, initial-scale=1.0">
<meta name="author" content="Christian Davis">
<link rel="stylesheet" href="styles.css">
<title>Bouncy Balls</title>
</head>
<body>
<button onclick="addBall()">Add Ball</button><br>
<div>Ball Count: <span id="ball-count">0</span></div>
<canvas id="canvas"></canvas>
<p id="myConsole">> <span id="myMessage"></span></p>
<script src="app2.js"></script>
</body>
</html>
Answered By - Mateuszprogrammer1302
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.