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

Why is this ball inside a circle not bouncing properly?

请看这个Fiddle:https://jsfiddle.net/sfarbota/wd5aa1wv/2/

我正在尝试让球以正确的角度在圆圈内弹跳而不损失速度。我想我已经关闭了碰撞检测,但我面临两个问题:

  1. 球每次弹跳都会减慢速度,直到最终停止。
  2. 弹跳的角度似乎不正确。

这部分基于此处给出的答案: 但我不得不从 Java 翻译过来,并且还跳过了他们示例中似乎无关紧要的几行。

代码如下:

Java脚本:

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

我的数学很生疏,所以我不太确定如何仅使用点积来计算球的新轨迹,但我相信您可以使用相关的三角函数来计算它:使用 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;
    }

这是@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>