Search code examples
javascriptmathphysics

Why is this ball inside a circle not bouncing properly?


Please see this Fiddle: https://jsfiddle.net/sfarbota/wd5aa1wv/2/

I am trying to make the ball bounce inside the circle at the correct angles without losing speed. I think I have the collision detection down, but I am facing 2 issues:

  1. The ball slows down with each bounce, until eventually stopping.
  2. The angles at which it bounces appear to be incorrect.

This is partially based off of the answer given here: https://stackoverflow.com/a/12053397/170309 but I had to translate from Java and also skipped a few lines from their example that seemed irrelevant.

Here is the code:

JavaScript:

function getBall(xVal, yVal, dxVal, dyVal, rVal, colorVal) {
  var ball = {
    x: xVal,
    lastX: xVal,
    y: yVal,
    lastY: yVal,
    dx: dxVal,
    dy: dyVal,
    r: rVal,
    color: colorVal,
    normX: 0,
    normY: 0
  };

  return ball;
}

var canvas = document.getElementById("myCanvas");
var xLabel = document.getElementById("x");
var yLabel = document.getElementById("y");
var dxLabel = document.getElementById("dx");
var dyLabel = document.getElementById("dy");
var vLabel = document.getElementById("v");
var normXLabel = document.getElementById("normX");
var normYLabel = document.getElementById("normY");

var ctx = canvas.getContext("2d");

var containerR = 200;
canvas.width = containerR * 2;
canvas.height = containerR * 2;
canvas.style["border-radius"] = containerR + "px";

var balls = [
  //getBall(canvas.width / 2, canvas.height - 30, 2, -2, 20, "#0095DD"),
  //getBall(canvas.width / 3, canvas.height - 50, 3, -3, 30, "#DD9500"),
  //getBall(canvas.width / 4, canvas.height - 60, -3, 4, 10, "#00DD95"),
  getBall(canvas.width / 2, canvas.height / 5, -1.5, 3, 40, "#DD0095")
];

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (var i = 0; i < balls.length; i++) {
    var curBall = balls[i];
    ctx.beginPath();
    ctx.arc(curBall.x, curBall.y, curBall.r, 0, Math.PI * 2);
    ctx.fillStyle = curBall.color;
    ctx.fill();
    ctx.closePath();
    curBall.lastX = curBall.x;
    curBall.lastY = curBall.y;
    curBall.x += curBall.dx;
    curBall.y += curBall.dy;
    if (containerR <= curBall.r + Math.sqrt(Math.pow(curBall.x - containerR, 2) + Math.pow(curBall.y - containerR, 2))) {
      curBall.normX = (curBall.x + curBall.r) - (containerR);
      curBall.normY = (curBall.y + curBall.r) - (containerR);
      var normD = Math.sqrt(Math.pow(curBall.x, 2) + Math.pow(curBall.y, 2));
      if (normD == 0)
        normD = 1;
      curBall.normX /= normD;
      curBall.normY /= normD;
      var dotProduct = (curBall.dx * curBall.normX) + (curBall.dy * curBall.normY);
      curBall.dx = -2 * dotProduct * curBall.normX;
      curBall.dy = -2 * dotProduct * curBall.normY;
    }

    xLabel.innerText = "x: " + curBall.x;
    yLabel.innerText = "y: " + curBall.y;
    dxLabel.innerText = "dx: " + curBall.dx;
    dyLabel.innerText = "dy: " + curBall.dy;
    vLabel.innerText = "v: " + curBall.dy / curBall.dx;
    normXLabel.innerText = "normX: " + curBall.normX;
    normYLabel.innerText = "normY: " + curBall.normY;
  }
}

setInterval(draw, 10);

HTML:

<canvas id="myCanvas"></canvas>
<div id="x"></div>
<div id="y"></div>
<div id="dx"></div>
<div id="dy"></div>
<div id="v"></div>
<div id="normX"></div>
<div id="normY"></div>

CSS:

canvas { background: #eee; }

Solution

  • My math is rusty, so I'm not quite sure how you could compute the new trajectory of the ball using just a dot product, but I'm sure you can compute it with the relevant trig functions: use atan2 to compute the angle to the collision point and the current trajectory angle, use those two to compute the new angle, and a pair of sin and cos multiplied by the speed to get the new x/y speeds.

    jsFiddle: https://jsfiddle.net/jacquesc/wd5aa1wv/6/

    The important part is:

        var dx = curBall.x - containerR;
        var dy = curBall.y - containerR;
        if (Math.sqrt(dx * dx + dy * dy) >= containerR - curBall.r) {
          // current speed
          var v = Math.sqrt(curBall.dx * curBall.dx + curBall.dy * curBall.dy);
          // Angle from center of large circle to center of small circle,
          // which is the same as angle from center of large cercle
          // to the collision point
          var angleToCollisionPoint = Math.atan2(-dy, dx);
          // Angle of the current movement
          var oldAngle = Math.atan2(-curBall.dy, curBall.dx);
          // New angle
          var newAngle = 2 * angleToCollisionPoint - oldAngle;
          // new x/y speeds, using current speed and new angle
          curBall.dx = -v * Math.cos(newAngle);
          curBall.dy = v * Math.sin(newAngle);
        }
    

    Also note I switched from setInterval to requestAnimationFrame, which will make sure there's no more than one update per frame. Ideally you would want to compute the movement based on the actual time elapsed since the last update rather than rely on it being always the same.

    Update

    Using dot products:

    jsFiddle: https://jsfiddle.net/jacquesc/wd5aa1wv/9/

        var dx = curBall.x - containerR;
        var dy = curBall.y - containerR;
        var distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
    
        if (distanceFromCenter >= containerR - curBall.r) {
          var normalMagnitude = distanceFromCenter;
          var normalX = dx / normalMagnitude;
          var normalY = dy / normalMagnitude;
          var tangentX = -normalY;
          var tangentY = normalX;
          var normalSpeed = -(normalX * curBall.dx + normalY * curBall.dy);
          var tangentSpeed = tangentX * curBall.dx + tangentY * curBall.dy;
          curBall.dx = normalSpeed * normalX + tangentSpeed * tangentX;
          curBall.dy = normalSpeed * normalY + tangentSpeed * tangentY;
        }