Application example

The script is based on a simple ball collision simulation.

When a ball hit the wall, it triggers a vibration. The script uses the smartphone accelerometer to change the direction of the gravity force.


Press button to reset.


var canvas = document.querySelector('canvas');
var pen = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
var numBalls = 1;
var grav = [0, -0.1];
function Ball(x, y, dx, dy, r) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.r = r;
  this.color = 'hsl(' + Math.random() * 360 + ',90%,50%)';
  this.draw = function () {
    pen.fillStyle = this.color;
    pen.beginPath();
    pen.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
    pen.fill();
  };
  this.update = function () {
    this.x += this.dx;
    this.y += this.dy;
    this.dx += grav[0];
    this.dy -= grav[1];
    if (this.x > W - this.r) {
      this.x = W - this.r;
      if (this.dx > 4)
      navigator.vibrate(10);
      // We made the phone vibrate because it had sufficent velocity
      this.dx *= -0.5;
    } else if (this.x < this.r) {
      this.x = this.r;
      if (this.dx < 4)
      navigator.vibrate(10);
      this.dx *= -0.5;
    }
    if (this.y > H - this.r) {
      this.y = H - this.r;
      if (this.dy > 4)
      navigator.vibrate(10);
      this.dy *= -0.5;
    } else if (this.y < this.r) {
      this.y = this.r + 1;
      if (this.dy < 4)
      navigator.vibrate(10);
      this.dy *= -0.5;
    }
    this.draw();
  };
}
var balls = [];
function reset() {
  balls = [];
  for (var i = 0; i < numBalls; i++) {
    var x = Math.random() * W;
    var y = Math.random() * H;
    var r = Math.random() * 20 + 10;
    balls.push(new Ball(x, y, Math.random() * 10 - 5, Math.random() * 10 - 5, r));
  }
}
reset();
var output = document.querySelector('.output');
function handleOrientation(event) {
  var x = event.beta;  // In degree in the range [-180,180]
  var y = event.gamma; // In degree in the range [-90,90]
  output.innerHTML  = "beta : " + x + "\n";
  output.innerHTML += "gamma: " + y + "\n";
  if (x >  90) { x =  90};
  if (x < -90) { x = -90};
  grav[1] = -x/90;
  grav[0] = y/90;

}

window.addEventListener('deviceorientation', handleOrientation, false);

window.addEventListener('keydown', function (key) {
  if (key.code === 'Space') {
    reset();
  }
});
var mouseDown = false;
var cooldown = 0;
var mouse = {
  x: undefined,
  y: undefined };
canvas.addEventListener('mousedown', function (event) {
  mouseDown = true;
});
canvas.addEventListener('mouseup', function (event) {
  mouseDown = false;
});
canvas.addEventListener('mousemove', function (event) {
  mouse.x = event.x - 15;
  mouse.y = event.y - 15;
});
function addball() {
  var r = Math.random() * 20 + 10;
    balls.push(new Ball(0, 0, Math.random() * 10 - 5, Math.random() * 10 - 5, r));
}

function animateb() {
  pen.clearRect(0, 0, W, H);
  cooldown++;
  if (mouseDown && cooldown > 2) {
    var r = Math.random() * 20 + 10;
    balls.push(new Ball(mouse.x, mouse.y, Math.random() * 10 - 5, Math.random() * 10 - 5, r));
    cooldown = 0;
  }
  for (var ball of balls) {
    ball.update();
    for (var ball2 of balls) {//Not the most efficient way to check every pair, but this is just a rough version
      if (ball !== ball2) {
        var collision = checkCollision(ball, ball2);
        if (collision[0]) {
          adjustPositions(ball, ball2, collision[1]);
          resolveCollision(ball, ball2);
        }
      }
    }
  }
  requestAnimationFrame(animateb);
}
animateb();
function checkCollision(ballA, ballB) {
  var rSum = ballA.r + ballB.r;
  var dx = ballB.x - ballA.x;
  var dy = ballB.y - ballA.y;
  return [rSum * rSum > dx * dx + dy * dy, rSum - Math.sqrt(dx * dx + dy * dy)];
}
function resolveCollision(ballA, ballB) {
  var relVel = [ballB.dx - ballA.dx, ballB.dy - ballA.dy];
  var norm = [ballB.x - ballA.x, ballB.y - ballA.y];
  var mag = Math.sqrt(norm[0] * norm[0] + norm[1] * norm[1]);
  norm = [norm[0] / mag, norm[1] / mag];
  var velAlongNorm = relVel[0] * norm[0] + relVel[1] * norm[1];
  if (velAlongNorm > 0)
  return;
  var bounce = 0.7;
  var j = -(1 + bounce) * velAlongNorm;
  j /= 1 / ballA.r + 1 / ballB.r;
  var impulse = [j * norm[0], j * norm[1]];
  ballA.dx -= 1 / ballA.r * impulse[0];
  ballA.dy -= 1 / ballA.r * impulse[1];
  ballB.dx += 1 / ballB.r * impulse[0];
  ballB.dy += 1 / ballB.r * impulse[1];
  vit = relVel[0] + relVel[1]
}
function adjustPositions(ballA, ballB, depth) {//Inefficient implementation for now
  const percent = 0.2;
  const slop = 0.01;
  var correction = Math.max(depth - slop, 0) / (1 / ballA.r + 1 / ballB.r) * percent;
  var norm = [ballB.x - ballA.x, ballB.y - ballA.y];
  var mag = Math.sqrt(norm[0] * norm[0] + norm[1] * norm[1]);
  norm = [norm[0] / mag, norm[1] / mag];
  correction = [correction * norm[0], correction * norm[1]];
  ballA.x -= 1 / ballA.r * correction[0];
  ballA.y -= 1 / ballA.r * correction[1];
  ballB.x += 1 / ballB.r * correction[0];
  ballB.y += 1 / ballB.r * correction[1];
}
Previous