JavaScript之多小球非對心彈性碰撞

boborz發表於2019-03-03

引言

在學習JavaScript的時候,看到一個練習事例,就想到能不能做成類似Windows螢幕保護氣泡那種效果,經過不斷思考嘗試,最後做出的效果如下圖:

JavaScript之多小球非對心彈性碰撞

思路

其實上面的那種效果就是模擬理想物理環境下的多個小球非對心碰撞(對心碰撞是其特殊情況),所謂理想物理情況就是沒有外力作用的封閉系統,內部遵循動量守恆定律和能量守恆定律。假設小球A和小球B的質量分別為????,初始速度分別為????,碰撞後的速度分別為??'??',兩個小球的碰撞瞬間的狀態如下圖:

JavaScript之多小球非對心彈性碰撞

其中??????是兩小球沿球心連線方向上的分速度,??????是兩小球垂直球心連線方向上的分速度。碰撞後,由於兩小球在垂直球心連線方向上沒有力的相互作用,所以速度不變,還是??????,沿球心連線方向上的分速度為???'???'。運用以下物理公式:

能量守恆定律:
??•??²/2+??•??²/2=??•??'²/2+??•??'²/2

向量運算:
??²= ???²+???²
??²= ???²+???²
??'²= ???'²+???²
??'²= ???'²+???²

推導得出:
??•???²/2+??•???²/2=??•???'²/2+??•???'²/2

再聯合動量守恆定律:
??•???+??•???=??•???'+??•???'

推導得出:
???'=(???•(??-??)+2•??•???)/(??+??)
???'=(???•(??-??)+2•??•???)/(??+??)

最後合成碰撞後的速度??'??'就ok了!

程式碼實現

// 小球物件建構函式
function Ball(x, y, speedX, speedY, color, radius, density) {
  this.x = x;
  this.y = y;
  this.speedX = speedX;
  this.speedY = speedY;
  this.color = color;
  this.radius = radius;
  this.density = density;
}

// 繪製
Ball.prototype.draw = function () {
  ctx.beginPath();
  ctx.fillStyle = this.color;
  ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
  ctx.fill();
}

// 邊界碰撞檢測
Ball.prototype.borderCollisionDetect = function () {
  if ((this.x + this.radius) >= width && this.speedX > 0) {
    this.speedX *= -1;
  }

  if ((this.x - this.radius) <= 0 && this.speedX < 0) {
    this.speedX *= -1;
  }

  if ((this.y + this.radius) >= height && this.speedY > 0) {
    this.speedY *= -1;
  }

  if ((this.y - this.radius) <= 0 && this.speedY < 0) {
    this.speedY *= -1;
  }
}
複製程式碼
// 建立小球物件
function createBalls() {
  require(['utils'], function (utils) {
    while (balls.length < 8) {
      var ball = new Ball(
        utils.random(0, width),  // x
        utils.random(0, height), // y
        utils.random(1, 8),     // speedX
        utils.random(1, 8),     // speedY
        'rgb('+utils.random(0, 255) +','+ utils.random(0, 255)+','+ utils.random(0, 255) +')',
        30,                     // radius
        1                       // density
      );
      balls.push(ball);
    }
  });
}
複製程式碼
// 更新小球速度和位置
function update() {
  for (let i = 0; i < balls.length; i++) {
    balls[i].borderCollisionDetect();

    for (let j = i + 1; j < balls.length; j++) {
      if (ballsCollisionDetect(balls[i], balls[j])) {
        collide(balls[i], balls[j]);
      }
    }

    // 更新位置
    balls[i].x += balls[i].speedX;
    balls[i].y += balls[i].speedY;
  }
}
複製程式碼
// 碰撞檢測
function ballsCollisionDetect(ball1, ball2) {
  //  當前距離
  var dx = ball1.x - ball2.x;
  var dy = ball1.y - ball2.y;
  var distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));

  //  預測下一時刻會不會碰撞
  let dx_next = ball1.x + ball1.speedX - ball2.x - ball2.speedX;
  let dy_next = ball1.y + ball1.speedY - ball2.y - ball2.speedY;
  let distance_next = Math.sqrt(Math.pow(dx_next, 2) + Math.pow(dy_next, 2));

  if (distance_next < ball1.radius + ball2.radius && distance_next < distance) {
    return true;
  }
  return false;
}

複製程式碼
// 更新碰撞後的狀態
function collide(ball1, ball2) {
  require(['Vector2d'], function (Vector2d) {
    // 初始速度向量
    let speed_ball1_initial = new Vector2d(ball1.speedX, ball1.speedY);
    let speed_ball2_initial = new Vector2d(ball2.speedX, ball2.speedY);

    // 球心方向單位向量
    let s = new Vector2d(ball2.x - ball1.x, ball2.y - ball1.y);
    s = s.normalize();

    // 垂直球心方向單位向量
    let t = s.rotate(Math.PI / 2);

    // 速度在球心向量上的分速度投影
    let speed_ball1_initial_sc = speed_ball1_initial.dotProduct(s)/s.length();
    let speed_ball2_initial_sc = speed_ball2_initial.dotProduct(s)/s.length();

    // 速度在垂直球心向量上的分速度投影
    let speed_ball1_initial_tc = speed_ball1_initial.dotProduct(t)/t.length();
    let speed_ball2_initial_tc = speed_ball2_initial.dotProduct(t)/t.length();

    // 碰撞後球心方向上的分速度
    let speed_ball1_final_sc = (speed_ball1_initial_sc * (ball1.density * Math.pow(ball1.radius,3) - ball2.density * Math.pow(ball2.radius,3)) + 2 * (ball2.density * Math.pow(ball2.radius,3)) * speed_ball2_initial_sc)
     / (ball1.density * Math.pow(ball1.radius,3) + ball2.density * Math.pow(ball2.radius,3));
    let speed_ball2_final_sc = (speed_ball2_initial_sc * (ball2.density * Math.pow(ball2.radius,3) - ball1.density * Math.pow(ball1.radius,3)) + 2 * (ball1.density * Math.pow(ball1.radius,3)) * speed_ball1_initial_sc)
     / (ball1.density * Math.pow(ball1.radius,3) + ball2.density * Math.pow(ball2.radius,3));

    // 碰撞後球心方向上的分速度向量
    let speed_ball1_final_s = s.scale(speed_ball1_final_sc);
    let speed_ball2_final_s = s.scale(speed_ball2_final_sc);

    // 碰撞後垂直球心方向上的分速度向量
    let speed_ball1_final_t = t.scale(speed_ball1_initial_tc);
    let speed_ball2_final_t = t.scale(speed_ball2_initial_tc);

    // 結束速度向量
    let speed_ball1_final = speed_ball1_final_s.add(speed_ball1_final_t);
    let speed_ball2_final = speed_ball2_final_s.add(speed_ball2_final_t);

    // 更新速度
    ball1.speedX = speed_ball1_final.x;
    ball1.speedY = speed_ball1_final.y;

    ball2.speedX = speed_ball2_final.x;
    ball2.speedY = speed_ball2_final.y;
  });
}
複製程式碼

缺陷

本程式碼是通過window.requestAnimationFrame()方法迴圈執行來實現動畫效果的,它的回撥次數是每秒60次,所以對於一些速度"過快"的小球,會在撞擊邊界時出現"撞出去一小部分"的情況。還有本程式碼只考慮了兩個小球相撞的情況,沒有考慮三個以上小球同時相撞的場景。

結語

本程式碼是學習JavaScript時的實戰演練,能加深對這門語言的理解和掌握。完整程式碼詳見GitHub地址

參考連結

www.lyblog.net/detail/397.…
www.cnblogs.com/kenkofox/ar…
tina0152.blog.163.com/blog/static…
www.51testing.com/html/66/n-8…

相關文章