简体   繁体   中英

How to bounce an object within circle bounds?

I have a basic circle bouncing off the walls of a rectangle canvas (that I adapted from an example).

https://jsfiddle.net/n5stvv52/1/

The code to check for this kind of collision is somewhat crude, like so, but it works:

if (p.x > canvasWidth - p.rad) {
  p.x = canvasWidth - p.rad
  p.velX *= -1
}
if (p.x < p.rad) {
  p.x = p.rad
  p.velX *= -1
}
if (p.y > canvasHeight - p.rad) {
  p.y = canvasHeight - p.rad
  p.velY *= -1
}
if (p.y < p.rad) {
  p.y = p.rad
  p.velY *= -1
}

Where p is the item moving around.

However, the bounds of my canvas now need to be a circle, so I check collision with the following:

const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad

if (collision) {
  console.log('Out of circle bounds!')
}

When my ball hits the edges of the circle, the if (collision) statement executes as true and I see the log . So I can get it detected, but I'm unable to know how to calculate the direction it should then go after that.

Obviously comparing x to the canvas width isn't what I need because that's the rectangle and a circle is cut at the corners.

Any idea how I can update my if statements to account for this newly detected circle?

I'm absolutely terrible with basic trigonometry it seems, so please bear with me! Thank you.

So in order to do this you will indeed need some good ol' trig. The basic ingredients you'll need are:

  • The vector that points from the center of the circle to the collision point.
  • The velocity vector of the ball

Then, since things bounce with roughly an "equal and opposite angle", you'll need to find the angle difference between that velocity vector and the radius vector, which you can get by using a dot product.

Then do some trig to get a new vector that is that much off from the radius vector, in the other direction (this is your equal and opposite). Set that to be the new velocity vector, and you're good to go.

I know that's a bit dense, especially if you're rusty with your trig / vector math, so here's the code to get it going. This code could probably be simplified but it demonstrates the essential steps at least:

 function canvasApp (selector) { const canvas = document.querySelector(selector) const context = canvas.getContext('2d') const canvasWidth = canvas.width const canvasHeight = canvas.height const canvasRadius = canvasWidth / 2 const particleList = {} const numParticles = 1 const initVelMax = 1.5 const maxVelComp = 2.5 const randAccel = 0.3 const fadeColor = 'rgba(255,255,255,0.1)' let p context.fillStyle = '#050505' context.fillRect(0, 0, canvasWidth, canvasHeight) createParticles() draw() function createParticles () { const minRGB = 16 const maxRGB = 255 const alpha = 1 for (let i = 0; i < numParticles; i++) { const vAngle = Math.random() * 2 * Math.PI const vMag = initVelMax * (0.6 + 0.4 * Math.random()) const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB)) const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB)) const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB)) const color = `rgba(${r},${g},${b},${alpha})` const newParticle = { x: Math.random() * canvasWidth, y: Math.random() * canvasHeight, velX: vMag * Math.cos(vAngle), velY: vMag * Math.sin(vAngle), rad: 15, color } if (i > 0) { newParticle.next = particleList.first } particleList.first = newParticle } } function draw () { context.fillStyle = fadeColor context.fillRect(0, 0, canvasWidth, canvasHeight) p = particleList.first // random accleration p.velX += (1 - 2 * Math.random()) * randAccel p.velY += (1 - 2 * Math.random()) * randAccel // don't let velocity get too large if (p.velX > maxVelComp) { p.velX = maxVelComp } else if (p.velX < -maxVelComp) { p.velX = -maxVelComp } if (p.velY > maxVelComp) { p.velY = maxVelComp } else if (p.velY < -maxVelComp) { p.velY = -maxVelComp } px += p.velX py += p.velY // boundary const dx = px - canvasRadius const dy = py - canvasRadius const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad if (collision) { console.log('Out of circle bounds!') // Center of circle. const center = [Math.floor(canvasWidth/2), Math.floor(canvasHeight/2)]; // Vector that points from center to collision point (radius vector): const radvec = [px, py].map((c, i) => c - center[i]); // Inverse vector, this vector is one that is TANGENT to the circle at the collision point. const invvec = [-py, px]; // Direction vector, this is the velocity vector of the ball. const dirvec = [p.velX, p.velY]; // This is the angle in radians to the radius vector (center to collision point). // Time to rememeber some of your trig. const radangle = Math.atan2(radvec[1], radvec[0]); // This is the "direction angle", eg, the DIFFERENCE in angle between the radius vector // and the velocity vector. This is calculated using the dot product. const dirangle = Math.acos((radvec[0]*dirvec[0] + radvec[1]*dirvec[1]) / (Math.hypot(...radvec)*Math.hypot(...dirvec))); // This is the reflected angle, an angle that is "equal and opposite" to the velocity vec. const refangle = radangle - dirangle; // Turn that back into a set of coordinates (again, remember your trig): const refvec = [Math.cos(refangle), Math.sin(refangle)].map(x => x*Math.hypot(...dirvec)); // And invert that, so that it points back to the inside of the circle: p.velX = -refvec[0]; p.velY = -refvec[1]; // Easy peasy lemon squeezy! } context.fillStyle = p.color context.beginPath() context.arc(px, py, p.rad, 0, 2 * Math.PI, false) context.closePath() context.fill() p = p.next window.requestAnimationFrame(draw) } } canvasApp('#canvas') 
 <canvas id="canvas" width="500" height="500" style="border: 1px solid red; border-radius: 50%;"></canvas> 

DISCLAIMER: Since your initial position is random, this doens't work very well with the ball starts already outside of the circle. So make sure the initial point is within the bounds.

You don't need trigonometry at all. All you need is the surface normal, which is the vector from the point of impact to the center. Normalize it (divide both coordinates by the length), and you get the new velocity using

v' = v - 2 * (v • n) * n

Where v • n is the dot product:

v • n = vx * nx + vy * ny

Translated to your code example, that's

// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad

if (collision) {
  // the normal at the point of collision is -dx, -dy normalized
  var nx = -dx / nl
  var ny = -dy / nl
  // calculate new velocity: v' = v - 2 * dot(d, v) * n
  const dot = p.velX * nx + p.velY * ny
  p.velX = p.velX - 2 * dot * nx
  p.velY = p.velY - 2 * dot * ny
}

 function canvasApp(selector) { const canvas = document.querySelector(selector) const context = canvas.getContext('2d') const canvasWidth = canvas.width const canvasHeight = canvas.height const canvasRadius = canvasWidth / 2 const particleList = {} const numParticles = 1 const initVelMax = 1.5 const maxVelComp = 2.5 const randAccel = 0.3 const fadeColor = 'rgba(255,255,255,0.1)' let p context.fillStyle = '#050505' context.fillRect(0, 0, canvasWidth, canvasHeight) createParticles() draw() function createParticles() { const minRGB = 16 const maxRGB = 255 const alpha = 1 for (let i = 0; i < numParticles; i++) { const vAngle = Math.random() * 2 * Math.PI const vMag = initVelMax * (0.6 + 0.4 * Math.random()) const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB)) const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB)) const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB)) const color = `rgba(${r},${g},${b},${alpha})` const newParticle = { // start inside circle x: canvasWidth / 4 + Math.random() * canvasWidth / 2, y: canvasHeight / 4 + Math.random() * canvasHeight / 2, velX: vMag * Math.cos(vAngle), velY: vMag * Math.sin(vAngle), rad: 15, color } if (i > 0) { newParticle.next = particleList.first } particleList.first = newParticle } } function draw() { context.fillStyle = fadeColor context.fillRect(0, 0, canvasWidth, canvasHeight) // draw circle bounds context.fillStyle = "black" context.beginPath() context.arc(canvasRadius, canvasRadius, canvasRadius, 0, 2 * Math.PI, false) context.closePath() context.stroke() p = particleList.first // random accleration p.velX += (1 - 2 * Math.random()) * randAccel p.velY += (1 - 2 * Math.random()) * randAccel // don't let velocity get too large if (p.velX > maxVelComp) { p.velX = maxVelComp } else if (p.velX < -maxVelComp) { p.velX = -maxVelComp } if (p.velY > maxVelComp) { p.velY = maxVelComp } else if (p.velY < -maxVelComp) { p.velY = -maxVelComp } px += p.velX py += p.velY // boundary const dx = px - canvasRadius const dy = py - canvasRadius const nl = Math.sqrt(dx * dx + dy * dy) const collision = nl >= canvasRadius - p.rad if (collision) { // the normal at the point of collision is -dx, -dy normalized var nx = -dx / nl var ny = -dy / nl // calculate new velocity: v' = v - 2 * dot(d, v) * n const dot = p.velX * nx + p.velY * ny p.velX = p.velX - 2 * dot * nx p.velY = p.velY - 2 * dot * ny } context.fillStyle = p.color context.beginPath() context.arc(px, py, p.rad, 0, 2 * Math.PI, false) context.closePath() context.fill() p = p.next window.requestAnimationFrame(draw) } } canvasApp('#canvas') 
 <canvas id="canvas" width="176" height="176"></canvas> 

You can use the polar coordinates to normalize the vector:

var theta = Math.atan2(dy, dx)
var R = canvasRadius - p.rad

p.x = canvasRadius + R * Math.cos(theta)
p.y = canvasRadius + R * Math.sin(theta)

p.velX *= -1
p.velY *= -1

https://jsfiddle.net/d3k5pd94/1/

Update : The movement can be more natural if we add randomness to acceleration:

 p.velX *= Math.random() > 0.5 ? 1 : -1
 p.velY *= Math.random() > 0.5 ? 1 : -1

https://jsfiddle.net/1g9h9jvq/

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