title: Circle to circle collision resolution
body: <p>I'm trying to resolve circle to circle collisions. The problem is, the balls never settle to the bottom -- they continue to jiggle around. What can I do to make the balls settle to the bottom at the end of their cycle (without jiggling)?</p>
  
  <p>CodePen: <a href="https://codepen.io/KriegersVan/pen/gOpXzwE?editors=0010" rel="nofollow noreferrer">https://codepen.io/KriegersVan/pen/gOpXzwE?editors=0010</a></p>
  
  <pre><code>var _bouncingBalls = (function() {
        document.getElementById("canvas").width = window.innerWidth;
        document.getElementById("canvas").height = window.innerHeight;
  
        function _randomNumBetween(min, max) {
          return Math.floor(Math.random() * (max - min + 1) + min);
        }
  
        function _negativeOrPositiveOne() {
          return Math.random() &lt; 0.50 ? -1 : 1;
        }
  
        function _random10Chars() {
          return Math.random().toString(36).substr(2, 10);
        }
  
        var _balls = {
          numOf: 10,
          colors: ["#6a00ff", "#9500ff", "#d000ff", "#ff00ff", "#00f2ff", "#f59b42", "#e9f542"],
          particles: [],
          ctx: document.getElementById("canvas").getContext("2d"),
          lastFrameTime: 0,
          startTime: 0,
          inProgress: false,
          init: function(x, y) {
            for (var i = 0; i &lt; this.numOf; i++) {
              this.particles.push({
                x: x+(i*(window.innerWidth * 0.02)), //So they don't overlap at the beginning
                y: y,
                vy: Math.random() * _negativeOrPositiveOne(),
                vx: 2 * Math.random() * _negativeOrPositiveOne(),
                color: this.colors[_randomNumBetween(0, this.colors.length - 1)],
                radius: window.innerWidth * 0.008,
                gravity: 0.70,
                damping: 0.50,
                id: _random10Chars() + _random10Chars()
              });
            }
  
            this.startTime = window.performance.now();
  
            if (!this.inProgress) {
              this.inProgress = true;
              window.requestAnimationFrame(this.render);
            }
          },
          isBallCollision: function(particle) {
            //https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
            for (var j = 0; j &lt; _balls.particles.length; j++) {
              var comparisonParticle = _balls.particles[j];
              var dx = particle.x - comparisonParticle.x;
              var dy = particle.y - comparisonParticle.y;
              var distance = Math.sqrt(dx * dx + dy * dy);
  
              if (particle.id === comparisonParticle.id) {
                continue;
              }
  
             var minDist = comparisonParticle.radius + particle.radius; 
              if (distance &lt; minDist) {
                //https://www.youtube.com/watch?v=nlwtgvZCz0k
                var distance_x = dx;
                var distance_y = dy;
                var radii_sum = particle.radius+comparisonParticle.radius;
                var length = Math.sqrt(distance_x * distance_x + distance_y * distance_y) || 1;
                var unit_x = distance_x/length;
                var unit_y = distance_y/length;
  
                particle.x = comparisonParticle.x+(radii_sum+1)*unit_x;
                particle.y = comparisonParticle.y+(radii_sum+1)*unit_y;
  
                return comparisonParticle;
              }
            }
  
            return null;
          },
          isBottomHit: function(obj, comparisonObj){
            return obj.y+obj.radius &gt; comparisonObj.y-comparisonObj.radius &amp;&amp;
                   obj.y+obj.radius &lt; comparisonObj.y+comparisonObj.radius;
          },
          isTopHit: function(obj, comparisonObj){
            return obj.y-obj.radius &lt; comparisonObj.y+comparisonObj.radius &amp;&amp;
                   obj.y-obj.radius &gt; comparisonObj.y-comparisonObj.radius;
          },
          isLeftHit: function(obj, comparisonObj){
            return obj.x-obj.radius &lt; comparisonObj.x+comparisonObj.radius &amp;&amp;
                   obj.x-obj.radius &gt; comparisonObj.x-comparisonObj.radius;
          },
          isRightHit: function(obj, comparisonObj){
            return obj.x+obj.radius &gt; comparisonObj.x-comparisonObj.radius &amp;&amp;
                   obj.x+obj.radius &lt; comparisonObj.x+comparisonObj.radius;
          },
          bottomReached: function(obj) {
            return (obj.y + obj.radius) &gt;= window.innerHeight;
          },
          topReached: function(obj) {
            return (obj.y - obj.radius) &lt;= 0;
          },
          leftSideReached: function(obj) {
            return (obj.x - obj.radius) &lt;= 0;
          },
          rightSideReached: function(obj) {
            return (obj.x + obj.radius) &gt;= window.innerWidth;
          },
          draw: function(deltaNum, timestamp) {
            var r = _balls;
            var ctx = r.ctx;
            var elapsed = timestamp - r.startTime;
  
            for (var i = 0; i &lt; r.particles.length; i++) {
              var obj = r.particles[i];
  
              ctx.beginPath();
              ctx.fillStyle = obj.color;
              ctx.moveTo(
                obj.x,
                obj.y
              );
              ctx.arc(
                obj.x,
                obj.y,
                obj.radius,
                0,
                2 * Math.PI,
                false
              );
              ctx.fill();
              ctx.closePath();
  
              obj.y += (obj.vy * deltaNum);
  
              obj.vy += (obj.gravity * deltaNum);
  
              obj.x += (obj.vx * deltaNum);
  
              var otherBall = r.isBallCollision(obj);
  
              if (otherBall) {
                  if (r.isBottomHit(obj, otherBall)){
                    //Bounce up
                    obj.vy = -Math.abs(obj.vy*obj.damping);
                  } else if (r.isTopHit(obj, otherBall)){
                    //bounce down
                    obj.vy = Math.abs(obj.vy*obj.damping); 
                  }
  
                  if (r.isLeftHit(obj, otherBall)){
                    //Bounce right
                    obj.vx = Math.abs(obj.vx*obj.damping);
                  } else if (r.isRightHit(obj, otherBall)){
                    //Bounce left
                    obj.vx = -Math.abs(obj.vx*obj.damping); 
                  }
              } 
  
              if (r.bottomReached(obj)) {
                obj.y = window.innerHeight - obj.radius;
  
                if (obj.vy &gt; 5){
                  //Bounce
                  obj.vy = -(obj.vy * 0.70);
                } else {
                  //Stop bouncing
                  obj.vy = 0;
  
                  if (otherBall) {
                    obj.vx = -(obj.vx * obj.damping);
                  }
  
                  obj.vx += obj.vx &gt; 0 ? -0.008 : 0.008;
  
                  if (obj.vx &lt; 0.010 &amp;&amp; obj.vx &gt; -0.010) {
                    obj.vx = 0;
                  }
                }
  
              } else if (r.topReached(obj)) {
                obj.vy = Math.abs(obj.vy);
              } 
  
              if (r.rightSideReached(obj)) {
                obj.vx = -obj.vx;
              } else if (r.leftSideReached(obj)) {
                obj.vx = Math.abs(obj.vx);
              }             
             }
            },
            render: function(timestamp) {
              var a = _balls;
              var delta = timestamp - (a.lastFrameTime || timestamp);
  
              a.lastFrameTime = timestamp;
  
              if (a.particles.length) {
                a.ctx.clearRect(
                  0,
                  0,
                  window.innerWidth,
                  window.innerHeight
                );
                a.draw(delta / 16.6, timestamp);
              }
  
              window.requestAnimationFrame(a.render);
            }
          };
  
  
  
  
  
          var _mouseControl = (function() {
            var _el = document.getElementById("canvas");
  
            _el.addEventListener("click", function(e) {
              _balls.init.call(_balls, e.clientX, e.clientY);
            });
  
          }());
  
  
        }());
  
  </code></pre>