i have a simple physics engine to detect collisions between circles and paddles, right now i can set the paddle to be controlled by the mouse or not. but i want to be able to control one of the circles by mouse instead. at the bottom where bat[i].update() will control the specified paddle by that index. but i want to be able to control the circle by the mouse instead to further expand this engine. is there any way i can do this?
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false }
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 0;
var balls = []
var bats = []
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx, style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) };
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function distance(x1, y1, x2, y2) {
const xDist = x2 - x1;
const yDist = y2 - y1;
return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
}
// the ball
class ball {
constructor() {
this.r = 25
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 15
this.dy = 15
this.mass = 1
this.maxSpeed = 15
this.style = {
lineWidth: 12,
strokeStyle: "green"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if (y > canvas.height - this.r) {
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if (y < this.r) {
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if (x > canvas.width - this.r) {
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if (x < this.r) {
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if (speed > this.maxSpeed) { // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed - this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
class player {
constructor() {
this.r = 50
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 0.2
this.dy = 0.2
this.mass = 1
this.maxSpeed = 1000
this.style = {
lineWidth: 12,
strokeStyle: "blue"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
const ballShadow = {
r: 50,
x: 50,
y: 50,
dx: 0.2,
dy: 0.2,
}
//bat
class bat {
constructor(x, y, w, h) {
this.x = x
this.y = y
this.dx = 0
this.dy = 0
this.width = w
this.height = h
this.style = {
lineWidth: 2,
strokeStyle: "black",
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.strokeRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
function doBatBall(bat, ball) {
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if (s.x < 0) {
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if (s.y < 0) {
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if (s.dx > 0 && s.dy > 0) { return } // ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy) // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x + (s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y + (s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if (s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR) { // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if (!hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB) { // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if (!hit) { // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if (dist > radSqr) { return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x, cpy - s.y);
// is the distance greater than the ball speed then its a miss
if (d > ballSpeed) { return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed
s.dy *= ballSpeed
hit = true;
}
// if the ball hit the bat restore absolute position
if (hit) {
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.dx - otherParticle.dx;
const yVelocityDiff = particle.dy - otherParticle.dy;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate({ x: particle.dx, y: particle.dy }, angle);
const u2 = rotate({ x: otherParticle.dx, y: otherParticle.dy }, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
};
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
};
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.dx = vFinal1.x;
particle.dy = vFinal1.y;
otherParticle.dx = vFinal2.x;
otherParticle.dy = vFinal2.y;
}
}
for (let i = 0; i < 10; i++) {
balls.push(new ball())
}
balls.push(new player())
//x,y,w,h
bats.push(new bat((window.innerWidth / 2) - 150, window.innerHeight / 2, 10, 100))
bats.push(new bat((window.innerWidth / 2) + 150, window.innerHeight / 2, 10, 100))
// main update function
function update(timer) {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// move bat and ball
for (var i = 0; i < balls.length; i++) {
for (var j = 0; j < bats.length; j++) {
doBatBall(bats[j], balls[i])
}
balls[i].update()
balls[i].draw(ctx)
}
bats.forEach(bat => {
//bat.update();
bat.draw(ctx);
})
// check for bal bat contact and change ball position and trajectory if needed
// draw ball and bat
requestAnimationFrame(update);
}
requestAnimationFrame(update);
<body>
<canvas></canvas>
</body>
The balls cannot have 0 speed, but player can. Because of that, it fails at dividing by zero:
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
Simply add || 1 to the end of the line should fix it.
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false }
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 0;
var balls = []
var bats = []
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx, style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) };
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function distance(x1, y1, x2, y2) {
const xDist = x2 - x1;
const yDist = y2 - y1;
return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
}
// the ball
class ball {
constructor() {
this.r = 25
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 15
this.dy = 15
this.mass = 1
this.maxSpeed = 15
this.style = {
lineWidth: 12,
strokeStyle: "green"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if (y > canvas.height - this.r) {
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if (y < this.r) {
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if (x > canvas.width - this.r) {
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if (x < this.r) {
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if (speed > this.maxSpeed) { // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed - this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
class player {
constructor() {
this.r = 50
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 0.2
this.dy = 0.2
this.mass = 1
this.maxSpeed = 1000
this.width = this.r
this.height = this.r
this.style = {
lineWidth: 12,
strokeStyle: "blue"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
/* change */
/*
x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);
*/
x < this.width && (x = this.width);
y < this.height && (y = this.height);
x > canvas.width - this.width && (x = canvas.width - this.width);
y > canvas.height - this.height && (y = canvas.height - this.height);
/* end change */
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
const ballShadow = {
r: 50,
x: 50,
y: 50,
dx: 0.2,
dy: 0.2,
}
//bat
class bat {
constructor(x, y, w, h) {
this.x = x
this.y = y
this.dx = 0
this.dy = 0
this.width = w
this.height = h
this.style = {
lineWidth: 2,
strokeStyle: "black",
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.strokeRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
function doBatBall(bat, ball) {
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if (s.x < 0) {
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if (s.y < 0) {
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if (s.dx > 0 && s.dy > 0) { return } // ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy) // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x + (s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y + (s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if (s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR) { // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if (!hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB) { // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if (!hit) { // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
/* change */
// const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed) || 1;
/* end change */
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if (dist > radSqr) { return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x, cpy - s.y);
// is the distance greater than the ball speed then its a miss
if (d > ballSpeed) { return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed
s.dy *= ballSpeed
hit = true;
}
// if the ball hit the bat restore absolute position
if (hit) {
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.dx - otherParticle.dx;
const yVelocityDiff = particle.dy - otherParticle.dy;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate({ x: particle.dx, y: particle.dy }, angle);
const u2 = rotate({ x: otherParticle.dx, y: otherParticle.dy }, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
};
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
};
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.dx = vFinal1.x;
particle.dy = vFinal1.y;
otherParticle.dx = vFinal2.x;
otherParticle.dy = vFinal2.y;
}
}
for (let i = 0; i < 10; i++) {
balls.push(new ball())
}
balls.push(new player())
//x,y,w,h
bats.push(new bat((window.innerWidth / 2) - 150, window.innerHeight / 2, 10, 100))
bats.push(new bat((window.innerWidth / 2) + 150, window.innerHeight / 2, 10, 100))
// main update function
function update(timer) {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// move bat and ball
for (var i = 0; i < balls.length; i++) {
for (var j = 0; j < bats.length; j++) {
doBatBall(bats[j], balls[i])
}
balls[i].update()
balls[i].draw(ctx)
}
bats.forEach(bat => {
//bat.update();
bat.draw(ctx);
})
// check for bal bat contact and change ball position and trajectory if needed
// draw ball and bat
requestAnimationFrame(update);
}
requestAnimationFrame(update);
<body>
<canvas></canvas>
</body>
Adding your player to the array of balls is not what you want to do. Keep that object separate. Create your player as a variable and in your loop just draw and update the player
let player = new Player();
function update(timer) {
...
player.update();
player.draw(ctx);
...
}
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false }
function mouseEvents(e) {
mouse.x = e.x;
mouse.y = e.y;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 0;
var balls = []
var bats = []
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx, style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) };
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function distance(x1, y1, x2, y2) {
const xDist = x2 - x1;
const yDist = y2 - y1;
return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
}
// the ball
class ball {
constructor() {
this.r = 25
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 15
this.dy = 15
this.mass = 1
this.maxSpeed = 15
this.style = {
lineWidth: 12,
strokeStyle: "green"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if (y > canvas.height - this.r) {
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if (y < this.r) {
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if (x > canvas.width - this.r) {
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if (x < this.r) {
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if (speed > this.maxSpeed) { // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed - this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
class Player {
constructor() {
this.r = 50
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 0.2
this.dy = 0.2
this.mass = 1
this.maxSpeed = 1000
this.style = {
lineWidth: 12,
strokeStyle: "blue"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
/*x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);*/
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
for (var i = 0; i < balls.length; i++) {
//if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
const ballShadow = {
r: 50,
x: 50,
y: 50,
dx: 0.2,
dy: 0.2,
}
//bat
class bat {
constructor(x, y, w, h) {
this.x = x
this.y = y
this.dx = 0
this.dy = 0
this.width = w
this.height = h
this.style = {
lineWidth: 2,
strokeStyle: "black",
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.strokeRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
function doBatBall(bat, ball) {
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if (s.x < 0) {
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if (s.y < 0) {
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if (s.dx > 0 && s.dy > 0) { return } // ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy) // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x + (s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y + (s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if (s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR) { // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if (!hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB) { // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if (!hit) { // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if (dist > radSqr) { return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x, cpy - s.y);
// is the distance greater than the ball speed then its a miss
if (d > ballSpeed) { return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed
s.dy *= ballSpeed
hit = true;
}
// if the ball hit the bat restore absolute position
if (hit) {
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.dx - otherParticle.dx;
const yVelocityDiff = particle.dy - otherParticle.dy;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate({ x: particle.dx, y: particle.dy }, angle);
const u2 = rotate({ x: otherParticle.dx, y: otherParticle.dy }, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
};
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
};
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.dx = vFinal1.x;
particle.dy = vFinal1.y;
otherParticle.dx = vFinal2.x;
otherParticle.dy = vFinal2.y;
}
}
for (let i = 0; i < 10; i++) {
balls.push(new ball())
}
let player = new Player();
//x,y,w,h
bats.push(new bat((window.innerWidth / 2) - 150, window.innerHeight / 2, 10, 100))
bats.push(new bat((window.innerWidth / 2) + 150, window.innerHeight / 2, 10, 100))
// main update function
function update(timer) {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// move bat and ball
for (var i = 1; i < balls.length; i++) {
for (var j = 0; j < bats.length; j++) {
doBatBall(bats[j], balls[i])
}
balls[i].update()
balls[i].draw(ctx)
}
player.update();
player.draw(ctx);
bats.forEach(bat => {
//bat.update();
bat.draw(ctx);
})
// check for bal bat contact and change ball position and trajectory if needed
// draw ball and bat
requestAnimationFrame(update);
}
update();
<canvas></canvas>
Keep in mind the mouse is not exactly in the center of the circle because you are not accounting for the canvas position in the mousemove function. Also you have something limiting your player objects ability to move over the whole canvas.
EDIT:
This is what was causing your player to not be able to move over the whole canvas so comment it out.
/* x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);*/
And your collision is slightly off because in you player class you are checking distance against this.r * 2. I would change it to
for (var i = 0; i < balls.length; i++) {
//if (this === balls[i]) continue //not really needed
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r + balls[i].r) {
resolveCollision(this, balls[i])
}
Related
There is a falling balls code https://jsfiddle.net/d1e8x7wk/,
which is generated using canvas
Code also added to this editor
window.addEventListener('load', () => {
//---------------------------------------
// Set up ball options
//---------------------------------------
const imgBalls = [
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png'
]
let ballCount = imgBalls.length, // How many balls
DAMPING = 0.4, // Damping
GRAVITY = 0.01, // Gravity strength
SPEED = 5, // Ball speed
ballAdditionTime = 100, // How fast are balls added
ballSrc = imgBalls, // Ball image source
topOffset = 400, // Adjust this for initial ball spawn point
xOffset = 0, // left offset
yOffset = 0, // bottom offset
ballDensity = 20, // How dense are the balls
ball_1_size = 200, // Ball 1 size
ball_2_size = 180, // Ball 2 size
ball_3_size = 62, // Ball 6 size
canvasWidth = 1500, // Canvas width
canvasHeight = 1000, // Canvas height
stackBall = true, // Stack the balls (or false is overlap)
ballsLoaded = 0,
stopAnimation = false;
//---------------------------------------
// Canvas globals
//---------------------------------------
let canvas,
ctx,
TWO_PI = Math.PI * 2,
balls = [],
vel_x,
vel_y;
let rect = {
x: 0,
y: 0,
w: canvasWidth,
h: canvasHeight
};
//---------------------------------------
// do the animation
//---------------------------------------
window.requestAnimFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, ballAdditionTime);
};
//---------------------------------------
// set up the ball
//---------------------------------------
const Ball = function(x, y, radius, num) {
this.x = x;
this.y = y;
this.px = x;
this.py = y;
this.fx = 0;
this.fy = 0;
this.radius = radius;
this.num = num;
this.angle = 0;
// Different ball sizes
let random = Math.round(Math.random() * imgBalls.length)
if (random === 0) {
this.width = ball_1_size;
this.height = ball_1_size;
if (stackBall) {
this.radius = ball_1_size / 2;
}
} else if (random === 1 || random === 2) {
this.width = ball_2_size;
this.height = ball_2_size;
if (stackBall) {
this.radius = ball_2_size / 2;
}
} else {
this.width = ball_3_size;
this.height = ball_3_size;
if (stackBall) {
this.radius = ball_3_size / 2;
}
}
ctx.rotate(this.angle * Math.PI / 180);
};
//---------------------------------------
// Apply the physics
//---------------------------------------
Ball.prototype.apply_force = function(delta) {
delta *= delta;
this.fy += GRAVITY;
this.x += this.fx * delta;
this.y += this.fy * delta;
this.fx = this.fy = 0;
};
//---------------------------------------
// Newtonian motion algorithm
//---------------------------------------
Ball.prototype.velocity = function() {
var nx = this.x * 2 - this.px;
var ny = this.y * 2 - this.py;
this.px = this.x;
this.py = this.y;
this.x = nx;
this.y = ny;
};
//---------------------------------------
// Ball prototype
//---------------------------------------
Ball.prototype.draw = function(ctx) {
img = new Image();
img.src = imgBalls[this.num];
if (stackBall) {
ctx.drawImage(
img,
this.x - this.radius - xOffset,
this.y - this.radius - xOffset,
this.width,
this.height
);
} else {
ctx.drawImage(
img,
this.x - xOffset,
this.y - yOffset,
this.width,
this.height
);
}
};
//---------------------------------------
// resolve collisions (ball on ball)
//---------------------------------------
let resolve_collisions = function(ip) {
let i = balls.length;
while (i--) {
let ball_1 = balls[i];
let n = balls.length;
while (n--) {
if (n == i) continue;
let ball_2 = balls[n];
let diff_x = ball_1.x - ball_2.x;
let diff_y = ball_1.y - ball_2.y;
let length = diff_x * diff_x + diff_y * diff_y;
let dist = Math.sqrt(length);
let real_dist = dist - (ball_1.radius + ball_2.radius);
if (real_dist < 0) {
let vel_x1 = ball_1.x - ball_1.px;
let vel_y1 = ball_1.y - ball_1.py;
let vel_x2 = ball_2.x - ball_2.px;
let vel_y2 = ball_2.y - ball_2.py;
let depth_x = diff_x * (real_dist / dist);
let depth_y = diff_y * (real_dist / dist);
ball_1.x -= depth_x * 0.5;
ball_1.y -= depth_y * 0.5;
ball_2.x += depth_x * 0.5;
ball_2.y += depth_y * 0.5;
if (ip) {
let pr1 = (DAMPING * (diff_x * vel_x1 + diff_y * vel_y1)) / length;
let pr2 = (DAMPING * (diff_x * vel_x2 + diff_y * vel_y2)) / length;
vel_x1 += pr2 * diff_x - pr1 * diff_x;
vel_x2 += pr1 * diff_x - pr2 * diff_x;
vel_y1 += pr2 * diff_y - pr1 * diff_y;
vel_y2 += pr1 * diff_y - pr2 * diff_y;
ball_1.px = ball_1.x - vel_x1;
ball_1.py = ball_1.y - vel_y1;
ball_2.px = ball_2.x - vel_x2;
ball_2.py = ball_2.y - vel_y2;
}
}
}
}
};
//---------------------------------------
// Bounce off the walls
//---------------------------------------
let check_walls = function() {
let i = balls.length;
while (i--) {
let ball = balls[i];
if (ball.x < ball.radius) {
let vel_x = ball.px - ball.x;
ball.x = ball.radius;
ball.px = ball.x - vel_x * DAMPING;
} else if (ball.x + ball.radius > canvas.width) {
vel_x = ball.px - ball.x;
ball.x = canvas.width - ball.radius;
ball.px = ball.x - vel_x * DAMPING;
}
// Ball is new. So don't do collision detection until it hits the canvas. (with an offset to stop it snapping)
if (ball.y > 100) {
if (ball.y < ball.radius) {
let vel_y = ball.py - ball.y;
ball.y = ball.radius;
ball.py = ball.y - vel_y * DAMPING;
} else if (ball.y + ball.radius > canvas.height) {
vel_y = ball.py - ball.y;
ball.y = canvas.height - ball.radius;
ball.py = ball.y - vel_y * DAMPING;
}
}
}
};
//---------------------------------------
// Add a ball to the canvas
//---------------------------------------
let add_ball = function(num) {
let x = Math.random() * canvas.width;
let y = Math.random() * canvas.height;
let r = 30 + Math.random() * ballDensity;
let diff_x = x;
let diff_y = y;
let dist = Math.sqrt(diff_x * diff_x + diff_y * diff_y);
balls.push(new Ball(x, y, r, num));
};
//---------------------------------------
// iterate balls
//---------------------------------------
let update = function() {
let iter = 1;
let delta = SPEED / iter;
while (iter--) {
let i = balls.length;
while (i--) {
balls[i].apply_force(delta);
balls[i].velocity();
}
resolve_collisions();
check_walls();
i = balls.length;
while (i--) {
balls[i].velocity();
let ball = balls[i];
}
resolve_collisions();
check_walls();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
i = balls.length;
while (i--) {
balls[i].draw(ctx);
}
requestAnimFrame(update);
};
//---------------------------------------
// Set up the canvas object
//---------------------------------------
function doBalls() {
stopAnimation = false;
canvas = document.getElementById("balls");
ctx = canvas.getContext("2d");
let $canvasDiv = document.querySelector(".section");
function respondCanvas() {
canvas.height = $canvasDiv.clientHeight;
canvas.width = $canvasDiv.clientWidth;
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
respondCanvas();
ballAdd();
}
function ballAdd() {
let count = 0;
let timer = setInterval(function() {
addBallTimer();
}, 100);
let addBallTimer = function() {
add_ball(count % ballCount);
count++;
if (count === ballCount) {
stopTimer();
}
};
let stopTimer = function() {
clearInterval(timer);
};
update();
}
doBalls();
});
.section {
position: relative;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000;
}
<div class="section">
<canvas id="balls"></canvas>
</div>
I'm trying to make a rotation the balls
const Ball = function(x, y, radius, num) {
this.x = x;
this.y = y;
this.px = x;
this.py = y;
this.fx = 0;
this.fy = 0;
this.radius = radius;
this.num = num;
this.angle = 0;
// Different ball sizes
let random = Math.round(Math.random() * imgBalls.length)
if (random === 0) {
this.width = ball_1_size;
this.height = ball_1_size;
if (stackBall) {
this.radius = ball_1_size / 2;
}
} else if (random === 1 || random === 2) {
this.width = ball_2_size;
this.height = ball_2_size;
if (stackBall) {
this.radius = ball_2_size / 2;
}
} else {
this.width = ball_3_size;
this.height = ball_3_size;
if (stackBall) {
this.radius = ball_3_size / 2;
}
}
ctx.rotate(this.angle * Math.PI / 180);
};
After reading the information,
I wrote this code, but it does not work for me
ctx.rotate(this.angle * Math.PI / 180);
I must have misunderstood how to do this, tell me how to properly rotate the balls
Thanks in advance
You will need to
increase the angle at some point in your code
apply a rotation transform to rotate around each ball center before each drawImage call
ctx.save();
ctx.translate(+this.x, +this.y);
ctx.rotate(this.angle++ * Math.PI / 180);
ctx.translate(-this.x, -this.y);
ctx.drawImage
ctx.restore()
ctx.save() and ctx.restore() is called to restore the canvas transform after each draw call.
Please read
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate#rotating_a_shape_around_its_center
for details on applying transforms to Canvas.
I am trying to write aa small physics demo using Javascript. I have multiple balls that bounce off each other just fine, but things go wrong when I try to add gravity.
I am trying to conserve the momentum once they hit, but when I add constant gravity to each one, the physics start to break down.
Here is what I have in terms of code:
class Ball {
constructor ({
x,
y,
vx,
vy,
radius,
color = 'red',
}) {
this.x = x
this.y = y
this.vx = vx
this.vy = vy
this.radius = radius
this.color = color
this.mass = 1
}
render (ctx) {
ctx.save()
ctx.fillStyle = this.color
ctx.strokeStyle = this.color
ctx.translate(this.x, this.y)
ctx.strokeRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2)
ctx.beginPath()
ctx.arc(0, 0, this.radius, Math.PI * 2, false)
ctx.closePath()
ctx.fill()
ctx.restore()
return this
}
getBounds () {
return {
x: this.x - this.radius,
y: this.y - this.radius,
width: this.radius * 2,
height: this.radius * 2
}
}
}
const intersects = (rectA, rectB) => {
return !(rectA.x + rectA.width < rectB.x ||
rectB.x + rectB.width < rectA.x ||
rectA.y + rectA.height < rectB.y ||
rectB.y + rectB.height < rectA.y)
}
const checkWall = (ball) => {
const bounceFactor = 0.5
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius
ball.vx *= -bounceFactor
}
if (ball.x - ball.radius < 0) {
ball.x = ball.radius
ball.vx *= -bounceFactor
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius
ball.vy *= -1
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius
ball.vy *= -bounceFactor
}
}
const rotate = (x, y, sin, cos, reverse) => {
return {
x: reverse ? x * cos + y * sin : x * cos - y * sin,
y: reverse ? y * cos - x * sin : y * cos + x * sin
}
}
const checkCollision = (ball0, ball1, dt) => {
const dx = ball1.x - ball0.x
const dy = ball1.y - ball0.y
const dist = Math.sqrt(dx * dx + dy * dy)
const minDist = ball0.radius + ball1.radius
if (dist < minDist) {
//calculate angle, sine, and cosine
const angle = Math.atan2(dy, dx)
const sin = Math.sin(angle)
const cos = Math.cos(angle)
//rotate ball0's position
const pos0 = {x: 0, y: 0}
//rotate ball1's position
const pos1 = rotate(dx, dy, sin, cos, true)
//rotate ball0's velocity
const vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true)
//rotate ball1's velocity
const vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true)
//collision reaction
const vxTotal = (vel0.x - vel1.x)
vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
(ball0.mass + ball1.mass)
vel1.x = vxTotal + vel0.x
const absV = Math.abs(vel0.x) + Math.abs(vel1.x)
const overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x)
pos0.x += vel0.x / absV * overlap
pos1.x += vel1.x / absV * overlap
//rotate positions back
const pos0F = rotate(pos0.x, pos0.y, sin, cos, false)
const pos1F = rotate(pos1.x, pos1.y, sin, cos, false)
//adjust positions to actual screen positions
ball1.x = ball0.x + pos1F.x
ball1.y = ball0.y + pos1F.y
ball0.x = ball0.x + pos0F.x
ball0.y = ball0.y + pos0F.y
//rotate velocities back
const vel0F = rotate(vel0.x, vel0.y, sin, cos, false)
const vel1F = rotate(vel1.x, vel1.y, sin, cos, false)
ball0.vx = vel0F.x
ball0.vy = vel0F.y
ball1.vx = vel1F.x
ball1.vy = vel1F.y
}
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let oldTime = 0
canvas.width = innerWidth
canvas.height = innerHeight
document.body.appendChild(canvas)
const log = document.getElementById('log')
const balls = new Array(36).fill(null).map(_ => new Ball({
x: Math.random() * innerWidth,
y: Math.random() * innerHeight,
vx: (Math.random() * 2 - 1) * 5,
vy: (Math.random() * 2 - 1) * 5,
radius: 20,
}))
requestAnimationFrame(updateFrame)
function updateFrame (ts) {
const dt = ts - oldTime
oldTime = ts
ctx.clearRect(0, 0, innerWidth, innerHeight)
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// ADD GRAVITY HERE
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// CHECK FOR COLLISIONS HERE
checkCollision(ball0, ball1, dt)
}
}
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
// const dist = ball2.x - ball1.x
// if (Math.abs(dist) < ball1.radius + ball2.radius) {
// const vxTotal = ball1.vx - ball2.vx
// ball1.vx = ((ball1.mass - ball2.mass) * ball1.vx + 2 * ball2.mass * ball2.vx) / (ball1.mass + ball2.mass)
// ball2.vx = vxTotal + ball1.vx
// ball1.x += ball1.vx
// ball2.x += ball2.vx
// }
// ball.vy += 0.5
// ball.x += ball.vx
// ball.y += ball.vy
//
// ball.render(ctx)
requestAnimationFrame(updateFrame)
}
* { margin: 0; padding: 0; }
As you can see, I have checkCollision helper method, which calculates the kinetic energy and new velocities of a ball once it has collided with another ball. My update loop looks like this:
// add velocities to balls position
// check if its hitting any wall and bounce it back
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// Add constant gravity to the vertical velocity
// When balls stack up on each other at the bottom, the gravity is still applied and my
// "checkCollision" method freaks out and the physics start to explode
ball.vy += 0.8
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// Check collisions between two balls
checkCollision(ball0, ball1, dt)
}
}
// Finally render the ball on-screen
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
How do I calculate aa gravity, while preventing the physics from exploding when the balls start stacking on top of each other?
It seems that the gravity force is colliding with the "checkCollision" method. The checkCollision method tries to move them back in place, but the constant gravity overwrites it and continues pulling them down.
EDIT: After some reading I understand some Verlet integration is in order, but am having difficulties with wrapping my head around it.
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// This line needs to be converted to verlet motion?
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
Balls do not overlap
There is a fundamental flaw in the collision testing due to the fact that the collisions are calculated only when 2 balls overlap. In the real world this never happens.
The result of "collide on overlap" when many balls are interacting, will result in behavior that does not conserve the total energy of the system.
Resolve by order of collision
You can resolve collisions such that balls never overlap however the amount of processing is indeterminate per frame, growing exponentially as the density of balls increases.
The approach is to locate the first collision between balls in the time between frames. Resolve that collision and then with the new position of that collision find the next collision closest in time forward from the last. Do that until there are no pending collisions for that frame. (There is more to it than that) The result is that the simulation will never be in the impossible state where balls overlap.
Check out my Pool simulator on CodePen that uses this method to simulate pool balls. The balls can have any speed and always resolve correctly.
Verlet integration.
However you can reduce the noise using the overlapping collisions by using verlet integration which will keep the total energy of the balls at a more stable level.
To do that we introduce 2 new properties of the ball, px, py that hold the previous position of the ball.
Each frame we calculate the balls velocity as the difference between the current position and the new position. That velocity is used to do the calculations for the frame.
When a ball changes direction (hits wall or another ball) we also need to change the balls previous position to match where it would have been on the new trajectory.
Use constant time steps.
Using time steps based on time since last frame will also introduce noise and should not be used in the overlap collision method.
Reduce time, increase iteration
To further combat the noise you need to slow the overall speed of the balls to reduce the amount they overlay and thus more closely behave as if they collided at the point ballA.radius + ballB.radius apart. Also you should test every ball against every other ball, not just ball against balls above it in the balls array.
To keep the animation speed up you solve the ball V ball V wall collisions a few times per frame. The example does 5. The best value depends on the total energy of the balls, the level of noise that is acceptable, and the CPU power of the device its running on.
Accuracy matters
Your collision function is also way out there. I had a quick look and it did not look right. I added an alternative in the example.
When a ball hits a wall it does so at some time between the frames. You must move the ball away from the wall by the correct distance. Not doing so is like simulating a ball that sticks to a wall a tiny bit each time it hits, further diverging from what really happens.
Example
This is a rewrite of your original code. Click canvas to add some energy.
const ctx = canvas.getContext("2d");
const BOUNCE = 0.75;
const resolveSteps = 5;
var oldTime = 0;
const $setOf = (count, fn = (i) => i) => {var a = [], i = 0; while (i < count) { a.push(fn(i++)) } return a };
const $rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const $randP = (min = 1, max = min + (min = 0), p = 2) => Math.random() ** p * (max - min) + min;
var W = canvas.width, H = canvas.height;
const BALL_COUNT = 80;
const BALL_RADIUS = 15, BALL_MIN_RADIUS = 6;
const GRAV = 0.5 / resolveSteps;
requestAnimationFrame(updateFrame);
canvas.addEventListener("click", () => {
balls.forEach(b => {
b.px = b.x + (Math.random() * 18 - 9);
b.py = b.y + (Math.random() * -18);
})
});
class Ball {
constructor({x, y, vx, vy, radius}) {
this.x = x;
this.y = y;
this.px = x - vx;
this.py = y - vy;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.mass = radius * radius * Math.PI * (4 / 3); // use sphere volume as mass
}
render(ctx) {
ctx.moveTo(this.x + this.radius, this.y);
ctx.arc(this.x, this.y, this.radius, Math.PI * 2, false);
}
move() {
this.vx = this.x - this.px;
this.vy = this.y - this.py;
this.vy += GRAV;
this.px = this.x;
this.py = this.y;
this.x += this.vx;
this.y += this.vy;
this.checkWall();
}
checkWall() {
const ball = this;
const top = ball.radius;
const left = ball.radius;
const bottom = H - ball.radius;
const right = W - ball.radius;
if (ball.x > right) {
const away = (ball.x - right) * BOUNCE;
ball.x = right - away;
ball.vx = -Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
} else if (ball.x < left) {
const away = (ball.x - left) * BOUNCE;
ball.x = left + away;
ball.vx = Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
}
if (ball.y > bottom) {
const away = (ball.y - bottom) * BOUNCE;
ball.y = bottom - away;
ball.vy = -Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
} else if (ball.y < top) {
const away = (ball.y - top) * BOUNCE;
ball.y = top + away;
ball.vy = Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
}
}
collisions() {
var b, dx, dy, nx, ny, cpx, cpy, p, d, i = 0;
var {x, y, vx, vy, px, py, radius: r, mass: m} = this;
while (i < balls.length) {
b = balls[i++];
if (this !== b) {
const rr = r + b.radius;
if (x + rr > b.x && x < b.x + rr && y + rr > b.y && y < b.y + rr) {
dx = x - b.x;
dy = y - b.y;
d = (dx * dx + dy * dy) ** 0.5;
if (d < rr) {
nx = (b.x - x) / d;
ny = (b.y - y) / d;
p = 2 * (vx * nx + vy * ny - b.vx * nx - b.vy * ny) / (m + b.mass);
cpx = (x * b.radius + b.x * r) / rr;
cpy = (y * b.radius + b.y * r) / rr;
x = cpx + r * (x - b.x) / d;
y = cpy + r * (y - b.y) / d;
b.x = cpx + b.radius * (b.x - x) / d;
b.y = cpy + b.radius * (b.y - y) / d;
px = x - (vx -= p * b.mass * nx);
py = y - (vy -= p * b.mass * ny);
b.px = b.x - (b.vx += p * m * nx);
b.py = b.y - (b.vy += p * m * ny);
}
}
}
}
this.x = x;
this.y = y;
this.px = px;
this.py = py;
this.vx = vx;
this.vy = vy;
this.checkWall();
}
}
const balls = (() => {
return $setOf(BALL_COUNT, () => new Ball({
x: $rand(BALL_RADIUS, W - BALL_RADIUS),
y: $rand(BALL_RADIUS, H - BALL_RADIUS),
vx: $rand(-2, 2),
vy: $rand(-2, 2),
radius: $randP(BALL_MIN_RADIUS, BALL_RADIUS, 4),
}));
})();
function updateFrame(ts) {
var i = 0, j = resolveSteps;
ctx.clearRect(0, 0, W, H);
while (i < balls.length) { balls[i++].move() }
while (j--) {
i = 0;
while (i < balls.length) { balls[i++].collisions(balls) }
}
ctx.fillStyle = "#0F0";
ctx.beginPath();
i = 0;
while (i < balls.length) { balls[i++].render(ctx) }
ctx.fill();
requestAnimationFrame(updateFrame)
}
<canvas id="canvas" width="400" height="180" style="border:1px solid black;"></canvas>
<div style="position: absolute; top: 10px; left: 10px;">Click to stir</div>
I am trying to make a raycasting game. Everything is rendered correctly except when the ray is facing up (angle > PI) or facing right(angle > 0.5PI and < 1.5PI) lines are drawn on walls. I am not sure what is causing it to happen but I know that the lines are not affected by the rotation of the player only by the players position. I tried rounding the rays position but that did not help.
I don't have enough reputation to post images.
Right and up ray walls.
(https://i.imgur.com/DKlfzM3.png)
The walls up close.
(https://i.imgur.com/orP89sT.png)
Left and down ray walls.
(https://i.imgur.com/YVggx51.png)
Code:
let rayX, rayY, rayAngle, rayDeltaX, rayDeltaY
for (let i = 0; i < this.screen.width; i ++) {
rayAngle = this.angle - this.fov / 2 + i * (this.fov / this.screen.width)
if (rayAngle < 0) {
rayAngle += Math.PI * 2
}
else if (rayAngle > Math.PI * 2) {
rayAngle -= Math.PI * 2
}
rayX = this.x
rayY = this.y
let stepY
if (rayAngle > Math.PI) {
stepY = -this.tileSize
rayY = Math.floor(rayY / this.tileSize) * this.tileSize - 1
}
else {
stepY = this.tileSize
rayY = Math.floor(rayY / this.tileSize) * this.tileSize + this.tileSize
}
rayX = this.x + (rayY - this.y) / Math.tan(rayAngle)
rayDeltaY = stepY
rayDeltaX = stepY / Math.tan(rayAngle)
while(true) {
if (this.Map.map[Math.floor(rayY / this.tileSize) * this.Map.width + Math.floor(rayX / this.tileSize)] == '#') {
break
}
rayX += rayDeltaX
rayY += rayDeltaY
}
let rayHorizontalX = rayX
let rayHorizontalY = rayY
let rayDistanceHorizontal = Math.sqrt((this.x - rayHorizontalX) ** 2 + (this.y - rayHorizontalY) ** 2)
rayX = this.x
rayY = this.y
let stepX
if (rayAngle > 0.5 * Math.PI && rayAngle < 1.5 * Math.PI) {
stepX = -this.tileSize
rayX = Math.floor(rayX / this.tileSize) * this.tileSize - 1
}
else {
stepX = this.tileSize
rayX = Math.floor(rayX / this.tileSize) * this.tileSize + this.tileSize
}
rayY = this.y + (rayX - this.x) * Math.tan(rayAngle)
rayDeltaY = stepX * Math.tan(rayAngle)
rayDeltaX = stepX
while(true) {
if (this.Map.map[Math.floor(rayY / this.tileSize) * this.Map.width + Math.floor(rayX / this.tileSize)] == '#') {
break
}
rayX += rayDeltaX
rayY += rayDeltaY
}
let rayVerticalX = rayX
let rayVerticalY = rayY
let rayDistanceVertical = Math.sqrt((this.x - rayVerticalX) ** 2 + (this.y - rayVerticalY) ** 2)
let rayFinalDistance
if (rayDistanceHorizontal < rayDistanceVertical) {
rayFinalDistance = rayDistanceHorizontal
ctx.fillStyle = 'darkblue'
}
else {
rayFinalDistance = rayDistanceVertical
ctx.fillStyle = 'blue'
}
let rayCorrectedDistance = rayFinalDistance * Math.cos(rayAngle - this.angle)
let lineHeight = this.tileSize * (this.screen.width / 2 / Math.tan(this.fov / 2)) / rayCorrectedDistance
let lineBottom = this.projectionPlane.centerY + lineHeight * 0.5
let lineTop = this.projectionPlane.height - lineBottom
ctx.fillRect(i, lineTop, 1, lineHeight)
}
Any help would be appreciated.
I fixed the problem. When the ray was facing up or right I was substracting 1 from the rounded position of the ray to get the first intersection. Substracting a smaller number like 0.001 fixed it.
As far as I understood it seems that even after changing the color when the collision is detected it reverts back to blue due to the else statement when it is compared between other circle and they are not colliding. So how would you solve this so that that the instance when the collision between any circle occurs it changes to red
collision detection
this.update = function() {
for (let i = 0; i < circles.length; i++) {
if (this !== circles[i] && getDistance(this.x, this.y, circles[i].x, circles[i].y) <= 200 * 200) {
this.c = 'red';
circles[i].c = 'red';
resolveCollision(this, circles[i]);
} else {
this.c = 'blue';
circles[i].c = 'blue';
}
}
//wall deflection
if (this.x - this.r <= 0 || this.x + this.r >= innerWidth)
this.v.x *= -1
if (this.y - this.r <= 0 || this.y + this.r >= innerHeight)
this.v.y *= -1
this.x += this.v.x;
this.y += this.v.y;
this.draw();
};
//deflection amongst other circles
function resolveCollision(circle, othercircle) {
const xVelocityDiff = circle.v.x - othercircle.v.x;
const yVelocityDiff = circle.v.y - othercircle.v.y;
const xDist = othercircle.x - circle.x;
const yDist = othercircle.y - circle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = -Math.atan2(othercircle.y - circle.y, othercircle.x - circle.x);
const m1 = circle.m;
const m2 = othercircle.m;
const u1 = rotate(circle.v, angle);
const u2 = rotate(othercircle.v, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
}
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
}
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
circle.v.x = vFinal1.x;
circle.v.y = vFinal1.y;
othercircle.v.x = vFinal2.x;
othercircle.v.y = vFinal2.y;
}
}
Semaphores
Use a semaphore that holds the collision state of the circle.
Thus in your Circle.prototype would have something like these functions and properties
Circle.prototype = {
collided: false, // when true change color
draw() {
ctx.strokeStyle = this.collided ? "red" : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = true;
}
}
}
Counters
Or you may want to only have the color change last for some time. You can modify the semaphore and use it as a counter. On collision set it to the number of frames to change color for.
Circle.prototype = {
collided: 0,
draw() {
ctx.strokeStyle = this.collided ? (this.collided--, "red") : "blue";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
},
...
...
// in update
update() {
// when collision is detected set semaphore
if (collision) {
this.collided = 60; // 1 second at 60FPS
}
}
}
Example
This example is taken from another answer I did earlier this year.
As there is a lot of code I have highlighted the relevant code with
/*= ANSWER CODE ==============================================================
...
=============================================================================*/
The example uses counters and changes color for 30 frames after a collision with another ball or wall.
I did not use a semaphore as all the balls would be red within a second.
canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
mathExt(); // creates some additional math functions
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 10; // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 6;
const MAX_BALL_SIZE = 30;
const VEL_MIN = 1;
const VEL_MAX = 5;
const MAX_RESOLUTION_CYCLES = 100; // Put too many balls (or too large) in the scene and the
// number of collisions per frame can grow so large that
// it could block the page.
// If the number of resolution steps is above this value
// simulation will break and balls can pass through lines,
// get trapped, or worse. LOL
const SHOW_COLLISION_TIME = 30;
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;
/*= ANSWER CODE ==============================================================*/
this.collided = 0;
/*============================================================================*/
}
Ball.prototype = {
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += GRAVITY;
},
draw() {
/*= ANSWER CODE ==============================================================*/
ctx.strokeStyle = this.collided ? (this.collided--, "#F00") : "#00F";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
ctx.stroke();
/* ============================================================================*/
},
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) {
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_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) * WALL_LOSS;
this.vy = (ny * u - this.vy) * WALL_LOSS;
this.x -= this.vx * time;
this.y -= this.vy * time;
},
collide(b, time) { // b is second ball
/*= ANSWER CODE ==============================================================*/
this.collided = SHOW_COLLISION_TIME;
b.collided = SHOW_COLLISION_TIME;
/*============================================================================*/
const a = this;
const m1 = a.m;
const m2 = b.m;
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;
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;
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) {
lines.push(new Line(-10, 20, ctx.canvas.width + 10, 5));
lines.push((new Line(-10, ctx.canvas.height - 2, ctx.canvas.width + 10, ctx.canvas.height - 30)).reverse());
lines.push((new Line(30, -10, 4, ctx.canvas.height + 10)).reverse());
lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 30, ctx.canvas.height + 10));
while (bCount--) {
let tries = 100;
while (tries--) {
const dir = Math.rand(0, Math.TAU);
const vel = Math.rand(VEL_MIN, VEL_MAX);
const ball = new Ball(
Math.rand(MAX_BALL_SIZE + 30, canvas.width - MAX_BALL_SIZE - 30),
Math.rand(MAX_BALL_SIZE + 30, canvas.height - MAX_BALL_SIZE - 30),
Math.cos(dir) * vel,
Math.sin(dir) * vel,
Math.rand(MIN_BALL_SIZE, MAX_BALL_SIZE),
);
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++ < MAX_RESOLUTION_CYCLES) { // 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;
}
}
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
resolveCollisions();
for (const b of balls) { b.update() }
for (const b of balls) { b.draw() }
ctx.lineWidth = 1;
ctx.strokeStyle = "#000";
ctx.beginPath();
for(const l of lines) { l.draw() }
ctx.stroke();
requestAnimationFrame(mainLoop);
}
function mathExt() {
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) [where (,) is coord (x,y)]
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, 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;
}
}
}
<canvas id="canvas"></canvas>
const collided = {
color: 'red',
get current() {
return this.color
},
set current(clr) {
if (this.color === 'red') {
this.color = 'blue'
} else {
this.color = 'red'
}
}
}
this.update= function(){
for(let i=0;i<circles.length;i++){
if(this!==circles[i] && getDistance(this.x,this.y,circles[i].x,circles[i].y)<=200*200){
this.c=collided.current
collided.current = this.c
circles[i].c=collided.current
resolveCollision(this,circles[i]);
}
// ...
}
}
The trick is to use use getters and setters to ensure that the most recently used color value is never reapplied
First, split the if:
// psudo-code
if this is not circles[i], then
if overlapping, then
do something
else
do something else
else
do nothing
Then, fix if this is not circles[i]:
{x:100,y:100}!={x:100,y:100}, so
either add id to circles and compare ids (my recommendation), or,
compare .xs and .ys (less desired - what if they are equal but not same circle?), or,
use JSON.stringify(a)==JSON.stringify(b).
I would add .overlapping and before the loop, I'd add another loop setting all .overlapping to false, then, withing the modified original loop, I'd check if .overlapping is false, and then if there is a collision, I'd set .overlapping to true for both.
Another way would be to create an array to hole overlapping circles' .ids and check if that array includes the current loop item's .id.
Summary:
In attempt to make a simple collision detection system between a moving rect and a falling circle, I would like to make it more realistic.
Main question:
-The main thing I would like to solve is detecting when the circle Object is hitting the corner of the rect and in return having the circle bounce based off of that angle.
The Code:
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle();
}
function draw() {
background(75);
obstacle.display();
obstacle.update();
for (var i = 0; i < balls.length; i++) {
balls[i].display();
if (!RectCircleColliding(balls[i], obstacle)){
balls[i].update();
balls[i].edges();
}
//console.log(RectCircleColliding(balls[i], obstacle));
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY));
}
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
}
this.edges = function() {
if (this.y >= height - this.r) {
this.y = height - this.r;
this.velocity = this.velocity * -1;
this.gravity = this.gravity * 1.1;
}
}
}
function Obstacle() {
this.x = width - width;
this.y = height / 2;
this.w = 200;
this.h = 25;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
function RectCircleColliding(Ball, Obstacle) {
// define obstacle borders
var oRight = Obstacle.x + Obstacle.w;
var oLeft = Obstacle.x;
var oTop = Obstacle.y;
var oBottom = Obstacle.y + Obstacle.h;
//compare ball's position (acounting for radius) with the obstacle's border
if (Ball.x + Ball.r > oLeft) {
if (Ball.x - Ball.r < oRight) {
if (Ball.y + Ball.r > oTop) {
if (Ball.y - Ball.r < oBottom) {
let oldY = Ball.y;
Ball.y = oTop - Ball.r;
Ball.velocity = Ball.velocity * -1;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return (true);
}
}
}
}
return false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>
Expected output:
I would like to see the falling circles bounce off the rectangle with respect to where they are hitting on the rectangle.
If the circles hit the corners they should bounce differently versus hitting dead center.
Prerequisite
The ball's velocity must be a vector (XY components), not just a single number.
1. Determine if the circle might hit a side or a corner
Obtain the components of the vector from the center of the rectangle to the circle, and check it against the rectangle's dimensions:
// Useful temporary variables for later use
var hx = 0.5 * obstacle.w;
var hy = 0.5 * obstacle.h;
var rx = obstacle.x + hx;
var ry = obstacle.y + hy;
// displacement vector
var dx = ball.x - rx;
var dy = ball.y - ry;
// signs
var sx = dx < -hx ? -1 : (dx > hx ? 1 : 0);
var sy = dy < -hy ? -1 : (dy > hy ? 1 : 0);
If both sx, sy are non-zero, the ball might hit a corner, otherwise it might hit a side.
2. Determine whether the circle collides
Multiply each sign by the respective half-dimension:
// displacement vector from the nearest point on the rectangle
var tx = sx * (Math.abs(dx) - hx);
var ty = sy * (Math.abs(dy) - hy);
// distance from p to the center of the circle
var dc = Math.hypot(tx, ty);
if (dc <= ball.r) {
/* they do collide */
}
3. Determine the collision normal vector
(tx, ty) are the components of the normal vector, but only if the ball's center is outside the rectangle:
// epsilon to account for numerical imprecision
const EPSILON = 1e-6;
var nx = 0, ny = 0, nl = 0;
if (sx == 0 && sy == 0) { // center is inside
nx = dx > 0 ? 1 : -1;
ny = dy > 0 ? 1 : -1;
nl = Math.hypot(nx, ny);
} else { // outside
nx = tx;
ny = ty;
nl = dc;
}
nx /= nl;
ny /= nl;
4. Resolve any "penetration"
(No immature jokes please)
This ensures that the ball will never penetrate into the surface of the rectangle, which improves the visual quality of the collisions:
ball.x += (ball.r - dc) * nx;
ball.y += (ball.r - dc) * ny;
5. Resolve the collision
If the circle is travelling in the direction of the normal, don't resolve the collision as the ball might stick to the surface:
// dot-product of velocity with normal
var dv = ball.vx * nx + ball.vy * ny;
if (dv >= 0.0) {
/* exit and don't do anything else */
}
// reflect the ball's velocity in direction of the normal
ball.vx -= 2.0 * dv * nx;
ball.vy -= 2.0 * dv * ny;
Working JS snippet
const GRAVITY = 250.0;
function Ball(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
this.vx = 0;
this.vy = 0;
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.collidePage = function(b) {
if (this.vy > 0 && this.y + this.r >= b) {
this.y = b - this.r;
this.vy = -this.vy;
}
}
this.updatePosition = function(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
}
this.updateVelocity = function(dt) {
this.vy += GRAVITY * dt;
}
}
function Obstacle(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle(0, height / 2, 200, 25);
}
const DT = 0.05;
function draw() {
background(75);
obstacle.update();
obstacle.display();
for (var i = 0; i < balls.length; i++) {
balls[i].updatePosition(DT);
balls[i].collidePage(height);
ResolveRectCircleCollision(balls[i], obstacle);
balls[i].updateVelocity(DT);
balls[i].display();
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY, 15));
}
const EPSILON = 1e-6;
function ResolveRectCircleCollision(ball, obstacle) {
var hx = 0.5 * obstacle.w, hy = 0.5 * obstacle.h;
var rx = obstacle.x + hx, ry = obstacle.y + hy;
var dx = ball.x - rx, dy = ball.y - ry;
var sx = dx < -hx ? -1 : (dx > hx ? 1 : 0);
var sy = dy < -hy ? -1 : (dy > hy ? 1 : 0);
var tx = sx * (Math.abs(dx) - hx);
var ty = sy * (Math.abs(dy) - hy);
var dc = Math.hypot(tx, ty);
if (dc > ball.r)
return false;
var nx = 0, ny = 0, nl = 0;
if (sx == 0 && sy == 0) {
nx = dx > 0 ? 1 : -1; ny = dy > 0 ? 1 : -1;
nl = Math.hypot(nx, ny);
} else {
nx = tx; ny = ty;
nl = dc;
}
nx /= nl; ny /= nl;
ball.x += (ball.r - dc) * nx; ball.y += (ball.r - dc) * ny;
var dv = ball.vx * nx + ball.vy * ny;
if (dv >= 0.0)
return false;
ball.vx -= 2.0 * dv * nx; ball.vy -= 2.0 * dv * ny;
return true;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
I'll try to present a solution, which keeps as may from the original code as possible. The solution intends to be a evolution of the code presented in the question.
Add a sideward movement (selv.sideV) to the Ball object, which is initialized by 0:
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.sideV = 0
// ...
}
Move the ball to the side in update, by the sideward movement and reduce the sideward movment:
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
this.x += this.sideV;
this.sideV *= 0.98;
}
Create function for an intersection test of 2 boxes:
function IsectRectRect(l1, r1, t1, b1, l2, r2, t2, b2) {
return l1 < r2 && l2 < r1 && t1 < b2 && t2 < b1;
}
And a function which can calculate the refection vector R of an incident vector V to a the normal vector of surface N (the reflection like a billiard ball):
function reflect( V, N ) {
R = V.copy().sub(N.copy().mult(2.0 * V.dot(N)));
return R;
}
When the Ball collides with the Obstacle, then you've to handle 3 situations.
The Ball fully hits the top of the Obstacle: IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB)
The Ball hits the left edge of the Obstacle: IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB)
The Ball hits the right edge of the Obstacle: IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB)
In each case the normal vector for the reflection has to be calculated. This is the vector from either the top or the edge of the Obstacle to the center of the Ball.
Use the function reflect to bounce the Ball on the Obstacle:
function RectCircleColliding(Ball, Obstacle) {
let oL = Obstacle.x;
let oR = Obstacle.x + Obstacle.w;
let oT = Obstacle.y;
let oB = Obstacle.y + Obstacle.h;
let bL = Ball.x - Ball.r;
let bR = Ball.x + Ball.r;
let bT = Ball.y - Ball.r;
let bB = Ball.y + Ball.r;
let isect = false;
let hitDir = createVector(0, 1);
if ( IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB) ) {
isect = true;
} else if ( IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oL, oT))
isect = hitDir.mag() < Ball.r;
} else if ( IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oR, oT))
isect = hitDir.mag() < Ball.r;
}
if ( isect ) {
let dir = createVector(Ball.sideV, Ball.velocity);
R = reflect(dir, hitDir.normalize());
Ball.velocity = R.y;
Ball.sideV = R.x;
let oldY = Ball.y;
Ball.y = oT - Ball.r;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return true;
}
return false;
}
See the example, where I applied the changes to your original code:
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle();
}
function draw() {
background(75);
obstacle.display();
obstacle.update();
for (var i = 0; i < balls.length; i++) {
balls[i].display();
if (!RectCircleColliding(balls[i], obstacle)){
balls[i].update();
balls[i].edges();
}
//console.log(RectCircleColliding(balls[i], obstacle));
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY));
}
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.sideV = 0
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
this.x += this.sideV;
this.sideV *= 0.98;
}
this.edges = function() {
if (this.y >= height - this.r) {
this.y = height - this.r;
this.velocity = this.velocity * -1;
this.gravity = this.gravity * 1.1;
}
}
}
function Obstacle() {
this.x = width - width;
this.y = height / 2;
this.w = 200;
this.h = 25;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
function IsectRectRect(l1, r1, t1, b1, l2, r2, t2, b2) {
return l1 < r2 && l2 < r1 && t1 < b2 && t2 < b1;
}
function reflect( V, N ) {
R = V.copy().sub(N.copy().mult(2.0 * V.dot(N)));
return R;
}
function RectCircleColliding(Ball, Obstacle) {
let oL = Obstacle.x;
let oR = Obstacle.x + Obstacle.w;
let oT = Obstacle.y;
let oB = Obstacle.y + Obstacle.h;
let bL = Ball.x - Ball.r;
let bR = Ball.x + Ball.r;
let bT = Ball.y - Ball.r;
let bB = Ball.y + Ball.r;
let isect = false;
let hitDir = createVector(0, 1);
if ( IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB) ) {
isect = true;
} else if ( IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oL, oT))
isect = hitDir.mag() < Ball.r;
} else if ( IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oR, oT))
isect = hitDir.mag() < Ball.r;
}
if ( isect ) {
let dir = createVector(Ball.sideV, Ball.velocity);
R = reflect(dir, hitDir.normalize());
Ball.velocity = R.y;
Ball.sideV = R.x;
let oldY = Ball.y;
Ball.y = oT - Ball.r;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return true;
}
return false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>