7

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; }
Community
  • 1
  • 1
sfarbota
  • 2,619
  • 1
  • 22
  • 30
  • 1
    Not an answer to your question: don't use `setInterval`. Use `requestAnimationFrame` instead. – jcaron Feb 08 '16 at 00:01

2 Answers2

5

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;
    }
Community
  • 1
  • 1
jcaron
  • 17,302
  • 6
  • 32
  • 46
  • Thanks for the quick reply! This will work just fine. If you're curious, this document talks about how to calculate trajectory based on dot product, which is theoretically simpler: http://www.vobarian.com/collisions/2dcollisions2.pdf – sfarbota Feb 08 '16 at 01:17
  • I'll leave this question open for a day or two to see if anyone can figure it out without trig functions, but if not, I'll mark this as correct. – sfarbota Feb 08 '16 at 01:18
  • 1
    Updated using dot products. – jcaron Feb 08 '16 at 01:51
  • I'll leave it as an exercice to check which one is actually most efficient. – jcaron Feb 08 '16 at 01:51
  • Thanks again!! I did a little performance testing and it seems that the dot product version is indeed quicker to process (at least in Chrome). Here is the trig version with performance tracking: https://jsfiddle.net/sfarbota/wd5aa1wv/10/ and here is the dot product version: https://jsfiddle.net/sfarbota/wd5aa1wv/11/ – sfarbota Feb 08 '16 at 03:43
  • While I love how clear and well written your code is, I must point out it's a huge approximation. It's inverting the speed vector whenever the position goes beyond the border (that shouldn't be allowed after all), it's not considering the distance the ball would move to actually hit (somewhere between the current position the next position) and also without considering the distance the ball would move after hitting the border. – rafaelcastrocouto Jul 25 '21 at 21:25
1

This is a small upgrade to the dot product code by @jcaron. The speed vector reflection he made is perfect but it will change position after the border is crossed and it don't consider the movement during the bounce.

The code below will consider the distance the ball will move each frame before hitting the border and calculated the new position considering the movement before and after the bounce. https://jsfiddle.net/vm3wLk0z/

The difference between @jcaron code and the upgraded one will be more more visible when the ball speed is higher.

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;
}
function circleLineInters (r, h, k, m, n) {
  // circle: (x - h)^2 + (y - k)^2 = r^2
  // line: y = m * x + n
  // r: circle radius
  // h: x value of circle centre
  // k: y value of circle centre
  // m: slope
  // n: y-intercept

  // get a, b, c values
  var a = 1 + Math.pow(m,2);
  var b = -h * 2 + (m * (n - k)) * 2;
  var c = Math.pow(h,2) + Math.pow(n - k,2) - Math.pow(r,2);

  // get discriminant
  var d = Math.pow(b,2) - 4 * a * c;
  if (d >= 0) {
    // insert into quadratic formula
    var intersections = [
      (-b + Math.sqrt(Math.pow(b,2) - 4 * a * c)) / (2 * a),
      (-b - Math.sqrt(Math.pow(b,2) - 4 * a * c)) / (2 * a)
    ];
    if (d == 0) {
      // only 1 intersection
      return [intersections[0]];
    }
    return intersections;
  }
  // no intersection
  return [];
}

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 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, -2, 26, 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();
    // move
    curBall.lastX = curBall.x;
    curBall.lastY = curBall.y;
    if (curBall.xt) { // bounce
      curBall.x = curBall.xt;
      curBall.xt = false;
    } else curBall.x += curBall.dx;
    if (curBall.yt) { // bounce
      curBall.y = curBall.yt;
      curBall.yt = false;
    } else curBall.y += curBall.dy;
    // bounce
    var nextx = curBall.x + curBall.dx,
        nexty = curBall.y + curBall.dy;
    var ndx = nextx - containerR;
    var ndy = nexty - containerR;
    var distanceFromCenter = Math.sqrt(ndx * ndx + ndy * ndy);
    var rad = containerR - curBall.r;
    if (distanceFromCenter >= rad) {
      var s =  Math.sqrt(curBall.dx * curBall.dx + curBall.dy * curBall.dy);
      // calc collision point
      // intersetion between line [(x,y)(x+dx,y+dx)] 
      // and circle [r = rad, c = (R,R)]
      // m = rise = y2-y1/x2-x1 = ys/xs
      var m1 = curBall.dy / curBall.dx;
      // y = mx+n ... n = y-mx
      var n1 = nexty - m1 * nextx;
      var inters = circleLineInters(rad, containerR, containerR, m1, n1);
      // possible intersections 0,1,2
      // 0 inters can't hit, do nothing
      // 1 inters tangent, only possible outside
      if (inters.length == 2) { // line crosses the circle
        var hitx = inters[0];
        // choose inters x using the trajetory direction 
        if (curBall.dx < 0) hitx = inters[1];
        // calc hity with linear formula y = mx + n
        var hity = m1 * hitx + n1;
        curBall.xt = hitx;
        curBall.yt = hity;
        //update speed vectors
        var dx = curBall.xt - containerR;
        var dy = curBall.yt - containerR;
        var df = Math.sqrt(dx * dx + dy * dy);
        var normalX = dx / df;
        var normalY = dy / df;
        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;
        // move cell to reflected position
        var ra = Math.atan2(curBall.dy, curBall.dx);
        var cdx = hitx - curBall.x;
        var cdy = hity - curBall.y;
        var collDist = Math.sqrt(cdx * cdx + cdy * cdy);
        var rd = s - collDist;
        curBall.xt = curBall.xt + rd * Math.cos(ra);
        curBall.yt = curBall.yt + rd * Math.sin(ra);
      }
    }
  }
  requestAnimationFrame(draw);
}

draw();
canvas {
  background: #eee;
  border-radius: 50%;
}
<canvas id="myCanvas"></canvas>
rafaelcastrocouto
  • 11,781
  • 3
  • 38
  • 63