为什么这个球在一个圆圈内不能正常弹跳?

时间:2016-02-07 22:15:05

标签: javascript math physics

请看这个小提琴:https://jsfiddle.net/sfarbota/wd5aa1wv/2/

我试图让球以正确的角度在圆圈内反弹而不会失去速度。我想我的碰撞检测有所下降,但我面临两个问题:

  1. 每次反弹时球都会减速,直到最终停止。
  2. 弹跳的角度似乎不正确。
  3. 这部分是基于这里给出的答案:https://stackoverflow.com/a/12053397/170309但我必须从Java翻译,并且从他们看起来无关紧要的例子中跳过几行。

    以下是代码:

    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; }
    

2 个答案:

答案 0 :(得分:4)

我的数学生锈了,所以我不太确定如何只使用点积来计算球的新轨迹,但我确定你可以用相关的trig函数计算它:使用atan2来计算碰撞点的角度和当前的轨迹角度,使用这两个来计算新的角度,并使用一对sincos乘以得到的速度新的x / y速度。

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

重要的部分是:

    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);
    }

另请注意,我已从setInterval切换到requestAnimationFrame,这样可确保每帧不超过一次更新。理想情况下,您希望根据自上次更新后经过的实际时间计算移动,而不是依赖它始终相同。

更新

使用点积:

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;
    }

答案 1 :(得分:1)

这是@jcaron 对点积代码的小升级。他做的速度矢量反射很完美,但是越界后会改变位置,没有考虑反弹过程中的移动。

下面的代码将考虑球在击中边界之前每帧移动的距离,并考虑反弹前后的移动来计算新位置。 https://jsfiddle.net/vm3wLk0z/

@jcaron 代码和升级后的代码之间的差异会在球速更高时更加明显。

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>