简体   繁体   中英

Ball to rectangle collision detection canvas javascript

Recently i've embarked on learning canvas animations and have been following a few tutorials etc online. However the maths aspect i've started to hit a few boundaries with. One paticular issue involves reversing the x and y coordinates of a ball once it hits a div element inside the canvas. A picture representation can be seen below of the expected desired movements of the ball.

腔图像

As you can see, i would like the balls x and y cordinates change based on the side of the div thats touched.

At the present moment the ball does bounce of the sides of the canvas correctly and also does bounce correctly off of the left and right side of the main div. However once the ball hits the top or bottom of the main div, the logic breaks. My code for the project can be seen below.

 var canvas = document.querySelector('canvas'); var content = document.querySelector('.main-content h1'); var contentPosition = content.getBoundingClientRect(); canvas.width = window.innerWidth; canvas.height = window.innerHeight; var radius = 10; var maxRadius = 20; var mouse = { x: undefined, y: undefined } var colour = [ '#FF530D', '#E82C0C', '#FF0000', '#E80C7A', '#FF0DFF' ]; window.addEventListener('mousemove', function(event){ mouse.x = event.x; mouse.y = event.y; }); //c = context var c = canvas.getContext('2d'); function circle(x, y, dx, dy, radius){ this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.radius = radius; this.minRadius = Math.floor(Math.random() * 10 + 1); this.fillColour = colour[Math.floor(Math.random() * colour.length )]; this.draw = function(){ c.beginPath(); c.arc(this.x, this.y, this.radius, 0, Math.PI*2); c.fillStyle = this.fillColour; c.fill(); c.closePath(); } this.update = function(){ if(this.x + this.radius > innerWidth || this.x - this.radius < 0){ this.dx =- this.dx; } if(this.y + this.radius >= innerHeight || this.y - this.radius < 0){ this.dy =- this.dy; } if(this.x + this.radius > contentPosition.left && this.x - this.radius < contentPosition.right && this.y + this.radius > contentPosition.top && this.y - this.radius < contentPosition.bottom){ this.dx =- this.dx; } if(this.x - mouse.x < 50 && (this.x - mouse.x) > -50 && this.y - mouse.y < 50 && this.y - mouse.y > -50){ if(this.radius < maxRadius){ this.radius +=2; } } else if(this.radius > this.minRadius){ this.radius -= 2; } this.x += this.dx; this.y += this.dy; this.draw(); } } circleArray = []; function getDistance(x1, y1, x2, y2){ var xDistance = x2 - x1; var yDistance = y2 - y1; return Math.sqrt( Math.pow(xDistance, 2) + Math.pow(yDistance, 2)); } function randomIntFromInterval(min,max) { return Math.floor(Math.random()*(max-min+1)+min); } function init(){ for(i = 0; i < 100; i++){ x = randomIntFromInterval(radius, innerWidth - radius); y = randomIntFromInterval(radius, innerHeight - radius); dx = (Math.random() - 0.5) * 2; dy = (Math.random() - 0.5) * 2; circleArray.push(new circle(x, y, dx, dy, radius)); } } function animate(){ requestAnimationFrame(animate); c.clearRect(0, 0, innerWidth, innerHeight); for(i = 0; i < circleArray.length; i++){ circleArray[i].update(); } } init(); animate(); 
 canvas { position: fixed; z-index: -9999; } body { margin: 0; } .main-content{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .main-content h1{ font-weight: 700; font-size: 40px; text-shadow: 0 0 2px #464646; } 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="http://code.jquery.com/jquery-3.3.1.slim.js" integrity="sha256-fNXJFIlca05BIO2Y5zh1xrShK3ME+/lYZ0j+ChxX2DA=" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <link rel="stylesheet" href="styles.css"> <title>Canvas Test</title> </head> <body> <canvas></canvas> <div class="main-content"> <h1 class="text-center">Example Text For Main Div</h1> </div> <script src="canvas.js"></script> </body> </html> 

The above demonstrates the whole working logic. The code that detects the collision with the main div from the above snippet is as below.

 if(this.x + this.radius > contentPosition.left && this.x - this.radius < contentPosition.right  && this.y + this.radius > contentPosition.top && this.y - this.radius < contentPosition.bottom){
               this.dx =- this.dx;
            }

At the present moment, only the x coordinate of the ball changes once the left or right side of the div is hit by the ball. I need this adapted to also include the change of Y coordinate if the top or bottom of the main div is hit.

Any advice on how to correct my code or any knowledge of online maths based material which will point to the answer would greatly be appreciated.

Thanks in advance

Update

updated circle projected path based on code given in answer

在此处输入图片说明

Updated to notify the grid issue has been fixed and to include a snippet showing the solution in action.

Based off of the given image, I am assuming the circles will always start outside the rectangle and that the rectangle will always be axis-aligned. You can separate the x and y negations per the corresponding pair of walls. The solution below tests for the moment that a circle finds itself midway through a side of the div.

// Aliases to avoid thinking of offsetting positions
var circleTop = this.y - this.radius;
var circleBottom = this.y + this.radius;
var circleLeft = this.x - this.radius;
var circleRight = this.x + this.radius;

// For uniformity with circle
var rectTop = contentPosition.top;
var rectBottom = contentPosition.bottom;
var rectLeft = contentPosition.left;
var rectRight = contentPosition.right;

// Circle penetration on the div's left and right walls
if (((circleRight > rectLeft && circleLeft < rectLeft) ||
    (circleLeft < rectRight && circleRight > rectRight)) &&
    circleTop < rectBottom && circleBottom > rectTop) {

    this.dx = -this.dx;
}

// Circle penetration on the div's top and bottom walls
if (((circleBottom > rectTop && circleTop < rectTop) ||               
    (circleTop < rectBottom && circleBottom > rectBottom)) &&
    circleLeft < rectRight && circleRight > rectLeft) {

    this.dy = -this.dy;
}

The following is your code with mine transplanted in.

 var canvas = document.querySelector('canvas'); var content = document.querySelector('.main-content h1'); var contentPosition = content.getBoundingClientRect(); // For uniformity with circle var rectTop = contentPosition.top; var rectBottom = contentPosition.bottom; var rectLeft = contentPosition.left; var rectRight = contentPosition.right; canvas.width = window.innerWidth; canvas.height = window.innerHeight; var radius = 10; var maxRadius = 20; var mouse = { x: undefined, y: undefined } var colour = [ '#FF530D', '#E82C0C', '#FF0000', '#E80C7A', '#FF0DFF' ]; window.addEventListener('mousemove', function(event){ mouse.x = event.x; mouse.y = event.y; }); //c = context var c = canvas.getContext('2d'); function circle(x, y, dx, dy, radius){ this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.radius = radius; this.minRadius = Math.floor(Math.random() * 10 + 1); this.fillColour = colour[Math.floor(Math.random() * colour.length )]; this.draw = function(){ c.beginPath(); c.arc(this.x, this.y, this.radius, 0, Math.PI*2); c.fillStyle = this.fillColour; c.fill(); c.closePath(); } this.update = function(){ if(this.x + this.radius > innerWidth || this.x - this.radius < 0){ this.dx =- this.dx; } if(this.y + this.radius >= innerHeight || this.y - this.radius < 0){ this.dy =- this.dy; } // Aliases to avoid thinking of offsetting positions var circleTop = this.y - this.radius; var circleBottom = this.y + this.radius; var circleLeft = this.x - this.radius; var circleRight = this.x + this.radius; // Circle penetration on the div's left and right walls if (((circleRight > rectLeft && circleLeft < rectLeft) || (circleLeft < rectRight && circleRight > rectRight)) && circleTop < rectBottom && circleBottom > rectTop) { this.dx = -this.dx; } // Circle penetration on the div's top and bottom walls if (((circleBottom > rectTop && circleTop < rectTop) || (circleTop < rectBottom && circleBottom > rectBottom)) && circleLeft < rectRight && circleRight > rectLeft) { this.dy = -this.dy; } if(this.x - mouse.x < 50 && (this.x - mouse.x) > -50 && this.y - mouse.y < 50 && this.y - mouse.y > -50){ if(this.radius < maxRadius){ this.radius +=2; } } else if(this.radius > this.minRadius){ this.radius -= 2; } this.x += this.dx; this.y += this.dy; this.draw(); } } circleArray = []; function getDistance(x1, y1, x2, y2){ var xDistance = x2 - x1; var yDistance = y2 - y1; return Math.sqrt( Math.pow(xDistance, 2) + Math.pow(yDistance, 2)); } function randomIntFromInterval(min,max) { return Math.floor(Math.random()*(max-min+1)+min); } function init(){ for(i = 0; i < 100; i++){ x = randomIntFromInterval(radius, innerWidth - radius); y = randomIntFromInterval(radius, innerHeight - radius); dx = (Math.random() - 0.5) * 2; dy = (Math.random() - 0.5) * 2; circleArray.push(new circle(x, y, dx, dy, radius)); } } function animate(){ requestAnimationFrame(animate); c.clearRect(0, 0, innerWidth, innerHeight); for(i = 0; i < circleArray.length; i++){ circleArray[i].update(); } } init(); animate(); 
 canvas { position: fixed; z-index: -9999; } body { margin: 0; } .main-content{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .main-content h1{ font-weight: 700; font-size: 40px; text-shadow: 0 0 2px #464646; } 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="http://code.jquery.com/jquery-3.3.1.slim.js" integrity="sha256-fNXJFIlca05BIO2Y5zh1xrShK3ME+/lYZ0j+ChxX2DA=" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <link rel="stylesheet" href="styles.css"> <title>Canvas Test</title> </head> <body> <canvas></canvas> <div class="main-content"> <h1 class="text-center">Example Text For Main Div</h1> </div> <script src="canvas.js"></script> </body> </html> 

It should be noted that if your circle's speed (your this.dx and this.dy ) is large enough, and your div and circle are small enough, it is possible for the circle to pass through the div's wall(s) without triggering the collision conditions above. In video game collision programming, this issue is called tunneling and has more involved algorithms for mitigating the effect such as projecting the shape-in-motion ahead in time and testing against its shadow.

For more complex situations, you may find some more involved collision algorithms helpful such as the Separating Axis Theorem (SAT algorithm) for when your shapes are no longer axis-aligned.

The problem you deal with is that you need to know where the circle crossed the boundary of the box: via a horizontal border or a vertical border. Otherwise you may end of flipping the wrong variable's sign.

It becomes easier if you check which of the four sides of the box the circle currently is closest to, and let that side determine the bouncing effect.

Also, I would not just flip the sign, but explicitly state what the sign should be, so to avoid situations where you keep flipping back and forth and get those wobbly circle walks.

Here is how it could look:

if (this.x + this.radius > innerWidth || this.x - this.radius < 0) {
   this.dx = (this.x < this.radius || -1) * Math.abs(this.dx);
}
if (this.y + this.radius > innerHeight || this.y - this.radius < 0) {
   this.dy = (this.y < this.radius || -1) * Math.abs(this.dy);
}
if (this.y + this.radius > contentPosition.top 
        && this.y - this.radius < contentPosition.bottom
        && this.x + this.radius > contentPosition.left 
        && this.x - this.radius < contentPosition.right) {
    // Choose which side of the box is closest to the circle's centre
    var dists = [Math.abs(this.x - contentPosition.left),
                 Math.abs(this.x - contentPosition.right),
                 Math.abs(this.y - contentPosition.top),
                 Math.abs(this.y - contentPosition.bottom)];
    // Get minimum value's index in array
    var i = dists.indexOf(Math.min.apply(Math, dists)); 
    // ... that will be the side that dictates the bounce
    if (i < 2) {
        this.dx = (i || -1) * Math.abs(this.dx);
    } else {
        this.dy = (i > 2 || -1) * Math.abs(this.dy);
    }
}

In this snippet I also added some code to avoid that the initial position of a circle is within the box:

 var canvas = document.querySelector('canvas'); var content = document.querySelector('.main-content h1'); var contentPosition = content.getBoundingClientRect(); canvas.width = window.innerWidth; canvas.height = window.innerHeight; var radius = 10; var maxRadius = 20; var mouse = { x: undefined, y: undefined } var colour = [ '#FF530D', '#E82C0C', '#FF0000', '#E80C7A', '#FF0DFF' ]; window.addEventListener('mousemove', function(event){ mouse.x = event.x; mouse.y = event.y; }); //c = context var c = canvas.getContext('2d'); function circle(x, y, dx, dy, radius){ this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.radius = radius; this.minRadius = Math.floor(Math.random() * 10 + 1); this.fillColour = colour[Math.floor(Math.random() * colour.length )]; this.draw = function(){ c.beginPath(); c.arc(this.x, this.y, this.radius, 0, Math.PI*2); c.fillStyle = this.fillColour; c.fill(); c.closePath(); } this.update = function(){ if (this.x + this.radius > innerWidth || this.x - this.radius < 0) { this.dx = (this.x < this.radius || -1) * Math.abs(this.dx); } if (this.y + this.radius > innerHeight || this.y - this.radius < 0) { this.dy = (this.y < this.radius || -1) * Math.abs(this.dy); } if (this.y + this.radius > contentPosition.top && this.y - this.radius < contentPosition.bottom && this.x + this.radius > contentPosition.left && this.x - this.radius < contentPosition.right) { // Choose which side of the box is closest to the circle's centre var dists = [Math.abs(this.x - contentPosition.left), Math.abs(this.x - contentPosition.right), Math.abs(this.y - contentPosition.top), Math.abs(this.y - contentPosition.bottom)]; var i = dists.indexOf(Math.min.apply(Math, dists)); // Get minimum value's index in array // ... that will be the side that dictates the bounce if (i < 2) { this.dx = (i || -1) * Math.abs(this.dx); } else { this.dy = (i > 2 || -1) * Math.abs(this.dy); } } if(this.x - mouse.x < 50 && (this.x - mouse.x) > -50 && this.y - mouse.y < 50 && this.y - mouse.y > -50){ if(this.radius < maxRadius){ this.radius +=2; } } else if(this.radius > this.minRadius){ this.radius -= 2; } this.x += this.dx; this.y += this.dy; this.draw(); } } circleArray = []; function getDistance(x1, y1, x2, y2){ var xDistance = x2 - x1; var yDistance = y2 - y1; return Math.sqrt( Math.pow(xDistance, 2) + Math.pow(yDistance, 2)); } function randomIntFromInterval(min,max){ return Math.floor(Math.random()*(max-min+1)+min); } function init(){ for(var i = 0; i < 100; i++){ do { // repeat until not in box var x = randomIntFromInterval(radius, innerWidth - radius); var y = randomIntFromInterval(radius, innerHeight - radius); } while (x + radius > contentPosition.left && x - radius < contentPosition.right && y + radius > contentPosition.top && y - radius < contentPosition.bottom); var dx = (Math.random() - 0.5) * 2; var dy = (Math.random() - 0.5) * 2; circleArray.push(new circle(x, y, dx, dy, radius)); } } function animate(){ requestAnimationFrame(animate); c.clearRect(0, 0, innerWidth, innerHeight); for(var i = 0; i < circleArray.length; i++){ circleArray[i].update(); } } init(); animate(); 
 canvas { position: fixed; z-index: -9999; } body { margin: 0; } .main-content{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .main-content h1{ font-weight: 700; font-size: 40px; text-shadow: 0 0 2px #464646; border: 1px solid } 
 <canvas></canvas> <div class="main-content"> <h1 class="text-center">Example Text For Main Div</h1> </div> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM