<div id="container_div">
  <div id="button_div">
    <button id="release_button">
      <img id="add_ball_icon" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/409445/add_ball_icon.svg">
    </button> 
  </div>
  <div id="canvas_div">
    <canvas id="canvas"></canvas>
  </div> 
</div>



<!-- BUS DERBY AD -->
<!-- <div id="bus_derby_ad_div">
  <a href="https://codepen.io/matthewmain/pen/YJwoVy" target="_blank">
    <img id="bus_derby_ad" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/409445/bus_derby_ad.svg">
  </a>
</div>
<style>
  #bus_derby_ad_div {
    position: fixed;
    width: 10%;
    bottom: 3%;
    right: 3%;
  }
  #bus_derby_ad {
    width: 100%;
    opacity: .75;
  }
  #bus_derby_ad:hover {
    opacity: 1;
  }
</style> -->
body {
	position: absolute;
	margin: 0;
	padding: 0;
	width: 100%;
	height: 100%;
}

#container_div {
  position: absolute;
  top: 45%;
  left: 50%;
  transform: translate(-50%,-50%);
}

#button_div {
  position: absolute;
  width: 100%;
  height: 20%; 
}

#release_button {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 40%;
  width: 15%;
  border: none;
  border-radius: 4px;
  background: #999999;
  font-family: monaco;
  font-size: 14px;
  text-align: center;
}

#release_button:hover {
  background: #888888;
}

*:focus {
  outline: none;
}

#add_ball_icon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 37%;
}

#canvas_div {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 78%;
}

#canvas {
  position: absolute;
  right: 50%;
  top: 50%;
  transform: translate(50%,-50%);
  border: solid 3px #555555;
  border-radius: 10px;
}


///////////////////////////////////////////////////
////////////       VERLET MARBLES       ///////////
///////////////////////////////////////////////////



///---INITIALIZATION---///


//environment
var container = document.getElementById("container_div");
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var marbles = [];
var marbleCount = 0;

//scaling
scaleToWindow(); 
var cw = canvas.width;
var ch = canvas.height;

//settings
var gravity = 0.5;  // as rate of y-velocity increase per frame
var airFriction = 0.999;  // as proportion of previous velocity after frame refresh
var groundFriction = 0.999;  // as proportion of previous x velocity
var bounceLoss = 0.5;  // as proportion of previous velocity after bouncing
var precision = 15;  // number of position-refinement iterations

//animation
for (var i=0; i<15; i++) { releaseMarble() };  // releases marbles at beginning



///---FUNCTIONS---///

//scaling
function scaleToWindow() {
  if (window.innerWidth > window.innerHeight) {
    container.style.height = window.innerHeight*.8+"px";
    container.style.width = container.style.height;
  } else {
    container.style.width = window.innerWidth*.8+"px";
    container.style.height = container.style.width;
  }
  canvas.width = document.getElementById("canvas_div").clientWidth;
  canvas.height = document.getElementById("canvas_div").clientHeight;
}

//marble constructor
function Marble(radius, color, current_x, current_y, previous_x, previous_y) {
  this.r = radius;
  this.color = color;
  this.cx = current_x;
  this.cy = current_y;
  this.px = previous_x;
  this.py = previous_y;
  marbleCount += 1;
  this.id = marbleCount;
};

//creates and releases a new marble
function releaseMarble() {
  var radius = randNumBetween(10,30);
  var color = randomColor();
  var current_x = randNumBetween(0,cw);
  var current_y = 0;
  var previous_x = randNumBetween(current_x-20,current_x+20);
  var previous_y = randNumBetween(current_y-20,current_y);
  marbles.push( new Marble(radius, color, current_x, current_y, previous_x, previous_y) );
}

//generates a random integer from a defined range
function randNumBetween(min,max) {
  return Math.floor(Math.random()*(max-min+1))+min;
}

//generates a random hex color
function randomColor() {
  var color = "#";
  var hex = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
  for (var i=0; i<6; i++) { color += hex[randNumBetween(0,15)] };
  return color;
}

//gets distance between two marbles' centers 
function centerDistance(marble_1, marble_2) {
  var x_diff = marble_2.cx - marble_1.cx;
  var	y_diff = marble_2.cy - marble_1.cy;
  return Math.sqrt( x_diff*x_diff + y_diff*y_diff);  //(pythagorean theorum)
}

//gets distance between two marbles' surfaces
function surfaceDistance(marble_1, marble_2) {  
  var center_dist = centerDistance(marble_1,marble_2);
  return center_dist - (marble_1.r + marble_2.r);
}

//updates marble positions using verlet velocity
function moveMarbles() {
  for (var i=0; i<marbles.length; i++) { 
    var m = marbles[i];  // marble
    var xv = (m.cx - m.px) * airFriction;  // marble x velocity
    var yv = (m.cy - m.py) * airFriction;  // marble y velocity
    //(slows marbles when rolling on ground)
    if (m.py >= ch-m.r-1 && m.py-m.r <= ch) { xv *= groundFriction } 
    //(updates previous coordinates as current coordinates)
    m.px = m.cx;
    m.py = m.cy;
    //(updates current coordinates by adding velocity & gravity)
    m.cx += xv;
    m.cy += yv;
    m.cy += gravity;
  }
}

//checks whether marble hits another marble, if so they both bounce off of each other
function resolveMarbleCollisions() {
  for (var i=0; i<marbles.length; i++) { 
    var m1 = marbles[i];  // first marble
    var xv1 = (m1.cx - m1.px);  // marble 1 x velocity
    var yv1 = (m1.cy - m1.py);  // marble 1 y velocity
    for (var j=0; j<marbles.length; j++) {
      var m2 = marbles[j];  // second marble
      var xv2 = (m2.cx - m2.px);  // marble 2 x velocity
      var yv2 = (m2.cy - m2.py);  // marble 2 y velocity
      var cd = centerDistance(m1,m2);
      var sd = surfaceDistance(m1,m2);
      //if marbles are overlapping (and marble is not self),
      if (sd < 0 && m1.id != m2.id) { 
        //get depth of overlap for x & y (based on ratio of sd/cd to x & y depth/diff),
        var x_diff = m1.cx - m2.cx;  // x difference between marbles
        var y_diff = m1.cy - m2.cy;  // y difference between marbles
        var x_depth = x_diff * sd / cd;  // x depth of overlap
        var y_depth = y_diff * sd / cd;  // y depth of overlap
        //and move marbles' x & y values back by half of shared depth each (altering velocities)
        m1.cx -= x_depth * 0.5;
        m1.cy -= y_depth * 0.5;
        m2.cx += x_depth * 0.5;
        m2.cy += y_depth * 0.5;
      }
    }
  }
}

//checks whether marble hits a wall, if so marble bounces off wall
function resolveWallCollisions() {
  for (var i=0; i<marbles.length; i++) { 
    var m = marbles[i];  // marble
    var xv = (m.cx - m.px);  // marble x velocity
    var yv = (m.cy - m.py);  // marble y velocity
    //if marble crosses a wall, move it back and invert velocity
    if (m.cx > cw - m.r) { m.cx = cw - m.r; m.px = m.cx + xv * bounceLoss; }
    if (m.cx < 0 + m.r) { m.cx = 0 + m.r; m.px = m.cx + xv * bounceLoss; } 
    if (m.cy > ch - m.r) { m.cy = ch - m.r; m.py = m.cy + yv * bounceLoss; } 
    if (m.cy < 0 + m.r) { m.cy = 0 + m.r; m.py = m.cy + yv * bounceLoss; }
  }  
}

//renders marbles on canvas
function renderMarbles() {
  ctx.clearRect(0, 0, cw, ch);
  for(var i=0; i<marbles.length; i++) {
    var m = marbles[i];  // marble  
    var grd = ctx.createRadialGradient(m.cx, m.cy, m.r*.4, m.cx, m.cy, m.r*3);
    grd.addColorStop(0, m.color);
    grd.addColorStop(1, "#000000");
    ctx.fillStyle = grd;
    ctx.beginPath();
    ctx.arc(m.cx, m.cy, m.r, 0, Math.PI*2);
    ctx.fill();
    
  }
}

//updates animation frame continuously
function update() {
  moveMarbles();
  for (var i=0; i<precision; i++) {
    resolveMarbleCollisions();
    resolveWallCollisions();
  }
  renderMarbles();
  window.requestAnimationFrame(update);
}

update();



///---EVENTS---///

//scaling
window.addEventListener('resize', scaleToWindow);

//releasing new marbles
document.getElementById("release_button").addEventListener('click', releaseMarble);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.