Search code examples
javascriptcanvascollision

canvas circle detect collision and response


I am a beginner in canvas animation, I have tried a simple example code to do a circle collision test. Before that I have tried to search around internet but I can not understand what is the logic behind. Below is the code what i get so far, the problem is some of the circle they did the collision but after that they stick together or become overlay each other, not sure if anything I have missed or wrong in the logic behind?

    window.requestAnimationFrame= (function(){
      return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
      function(callback){
        window.setTimeout(callback, 1000 / 60);
      };
    })();

    (function(){
      var c= document.getElementsByTagName('canvas')[0],
          can= c.getContext('2d'),
          ppl= [],
          count= 20;
      
      function resize(){
        c.width= window.innerWidth,
        c.height= window.innerHeight,
        can.fillStyle='#000000',
        can.fillRect(0,0,c.width,c.height)
      }
      
      function pplD(){
        var tf,pplnew={
          x: Math.floor(Math.random()*c.width),
          y: Math.floor(Math.random()*c.height),
          size: 20,//Math.random()*4+8,
          vx: (Math.random()-0.5)*4+2,
          vy: (Math.random()-0.5)*4+2
        }
            
        for(var i=0;i<ppl.length;i++){
          tf= coli(pplnew,ppl[i]);
          if(tf){
            pplD();
            return 
          } 
        }
        
        return pplnew;
      }
      
      function canF(){
        for(var i=0;i<count;i++){
          ppl.push(new pplD)
        }
        requestAnimationFrame(render)
      }
      
      function coli(a,b){
        var dis= a.size+b.size,
            disx= (a.x-b.x), disy= (a.y-b.y),
            disxy= Math.sqrt((disx*disx)+(disy*disy)),
            c;
        
        if(disxy<dis){
        
          if((a.vx>0 && b.vx>0) || (a.vx<0 && b.vx<0)){
            c= a.vx, a.vx= b.vx, b.vx= c
          }else{
            a.vx*= -1, b.vx*= -1
          }
          if((a.vy>0 && b.vy>0) || (a.vy<0 && b.vy<0)){
            c= a.vy, a.vy= b.vy, b.vy= c
          }else{
            a.vy*= -1, b.vy*= -1
          }
          
          return true;
        }
        return false;
      }
      
      function drawppl(d,p){
        
        var tf;
        
        for(var i=0;i<ppl.length;i++){
          if(i==p) continue;
          tf= coli(d,ppl[i]);
        }
        
        if(d.x+d.size>c.width || d.x-d.size<0) d.vx=d.vx*-1;
        if(d.y+d.size>c.height || d.y-d.size<0) d.vy=d.vy*-1;
        
        d.x+= d.vx,
        d.y+= d.vy;
        
        can.fillStyle= '#a6e22e';
        can.beginPath();
        can.arc(d.x, d.y, d.size, 0, Math.PI*2, true);
        can.closePath();
        can.fill()
      }
      
      function render(){
        can.fillStyle='rgba(0,0,0,0.2)',
        can.fillRect(0,0,c.width,c.height)
        
        for(var i=0;i<ppl.length;i++){
          drawppl(ppl[i],i)
        }
        requestAnimationFrame(render)
      }
      
      window.onresize= resize;
      resize();
      canF()
    })();
html,body {
  width:100%;
  height:100%;
  margin:0;
  padding:0;
  border:0;
}
<canvas></canvas>


Solution

  • You should to resolve future, not current colisions.
    Math can be some difficult, but all your need - to take future positions in account.
    Result velocity can't to be just negative of source, too.

    I correct your function accordingly to this article

    window.requestAnimationFrame= (function(){
      return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
      function(callback){
        window.setTimeout(callback, 1000 / 60);
      };
    })();
    
    (function(){
      var c= document.getElementsByTagName('canvas')[0],
          can = c.getContext('2d'),
          ppl = [],
          count = 12;
      
      function resize(){
        c.width  = window.innerWidth;
        c.height = window.innerHeight;
        // console.log('Container', {x: c.width, y: c.height});
        can.fillStyle='#000000',
        can.fillRect(0,0,c.width,c.height)
      }
      
      function pplD(){
        var r = Math.random()*20+10;
        var tf,pplnew={
          x: Math.floor(Math.random()*c.width-2*r)+3*r,
          y: Math.floor(Math.random()*c.height-2*r)+3*r,
          r: r,
          m: r/30, // mass
          vx: (Math.random()-0.5)*4+2,
          vy: (Math.random()-0.5)*4+2
        }
            
        for(var i=0;i<ppl.length;i++){
          resA(pplnew,ppl[i]);
        }
        return pplnew;
      }
      
      function canF(){
        for(var i=0;i<count;i++){
          ppl.push(new pplD)
        }
        // make heavy one
        ppl[0].m = 5;
        requestAnimationFrame(render)
      }
      
      function distance(a, b) {
        return Math.sqrt(Math.pow(b.x - a.x, 2) +  Math.pow(b.y - a.y, 2));
      }
      function midpoint(a, b) {
        return {
          x: (a.x + b.x) / 2,
          y: (a.y + b.y) / 2
        };
      }
      function resA(a, b) {
        var mid = midpoint(a, b);
        var dist = distance(a, b);
        if (dist > a.r+b.r) return;
        a.x = mid.x + (a.r+b.r) * (a.x - b.x) / dist;
        a.y = mid.y + (a.r+b.r) * (a.y - b.y) / dist;
      }
    
      function staticStaticResolve(a, b) {
        var mid = midpoint(a, b);
        var dist = distance(a, b);
        if (dist > a.r+b.r) return;
        a.x = mid.x + a.r * (a.x - b.x) / dist;
        a.y = mid.y + a.r * (a.y - b.y) / dist;
        b.x = mid.x + b.r * (b.x - a.x) / dist;
        b.y = mid.y + b.r * (b.y - a.y) / dist;
      }
    
    
      function coli(a, b) {
        var a1 = {
          x: a.x + a.vx,
          y: a.y + a.vy
        };
        var b1 = {
          x: b.x + b.vx,
          y: b.y + b.vy
        };
        var d = distance(a1, b1);
        if (d > a.r+b.r) return;
        var n = {
          x: (b1.x - a1.x) / d,
          y: (b1.y - a1.y) / d
        };
    
        var p = 2 * (a.vx*n.x + a.vy*n.y - (b.vx*n.x + b.vy*n.y)) / (a.m + b.m);
    
        a.vx = a.vx - p * b.m * n.x;
        a.vy = a.vy - p * b.m * n.y;
        b.vx = b.vx + p * a.m * n.x;
        b.vy = b.vy + p * a.m * n.y;
      }
      function drawppl(d, p){
    
        for(var i=0;i<ppl.length;i++){
          if(i==p) continue;
          coli(d,ppl[i]);
        }
    
        if(d.x+d.r>c.width)
           d.vx=-Math.abs(d.vx);
        if(d.x-d.r<0)
           d.vx=Math.abs(d.vx);
        if(d.y+d.r>c.height)
           d.vy=-Math.abs(d.vy);
        if(d.y-d.r<0)
           d.vy=Math.abs(d.vy);
        
        d.x += d.vx,
        d.y += d.vy;
        
        can.beginPath();
        can.arc(d.x, d.y, d.r, 0, Math.PI*2, true);
        can.closePath();
        //can.fillStyle= d.m > 3 ? '#ff6666' : '#66ff66';
        //can.fill();
        can.lineWidth = 3;
        can.strokeStyle= d.m > 3 ? '#ff6666' : '#66ff66';
        can.stroke();
      }
      function render(){
        can.fillStyle='rgba(0,0,0,0.2)',
        can.fillRect(0,0,c.width,c.height)
        
        for(var i=0;i<ppl.length;i++){
          drawppl(ppl[i],i)
        }
        requestAnimationFrame(render)
      }
      
      window.onresize= resize;
      resize();
      canF();
    })();
    html,body {
      width:100%;
      height:100%;
      margin:0;
      padding:0;
      border:0;
    }
    <canvas></canvas>

    Let's try to resolve "eating" issue.
    We just will resolve them as static, when they are too near.

    window.requestAnimationFrame= (function(){
      return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
      function(callback){
        window.setTimeout(callback, 1000 / 60);
      };
    })();
    
    (function(){
      var c= document.getElementsByTagName('canvas')[0],
          can = c.getContext('2d'),
          ppl = [],
          count = 12;
      
      function resize(){
        c.width  = window.innerWidth;
        c.height = window.innerHeight;
        // console.log('Container', {x: c.width, y: c.height});
        can.fillStyle='#000000';
        can.fillRect(0,0,c.width,c.height)
      }
      
      function pplD(){
        var r = Math.random()*20+10;
        var tf,pplnew={
          x: Math.floor(Math.random()*c.width-2*r)+3*r,
          y: Math.floor(Math.random()*c.height-2*r)+3*r,
          r: r,
          m: r/30, // mass
          vx: (Math.random()-0.5)*4+2,
          vy: (Math.random()-0.5)*4+2
        }
            
        for(var i=0;i<ppl.length;i++){
          resA(pplnew,ppl[i]);
        }
        return pplnew;
      }
      
      function canF(){
        for(var i=0;i<count;i++){
          ppl.push(new pplD)
        }
        // make heavy one
        ppl[0].m = 5;
        requestAnimationFrame(render)
      }
      
      function distance(a, b) {
        return Math.sqrt(Math.pow(b.x - a.x, 2) +  Math.pow(b.y - a.y, 2));
      }
      function midpoint(a, b) {
        return {
          x: (a.x + b.x) / 2,
          y: (a.y + b.y) / 2
        };
      }
      function resA(a, b) {
        var mid = midpoint(a, b);
        var dist = distance(a, b);
        if (dist > a.r+b.r) return;
        a.x = mid.x + (a.r+b.r) * (a.x - b.x) / dist;
        a.y = mid.y + (a.r+b.r) * (a.y - b.y) / dist;
      }
    
      function staticStaticResolve(a, b) {
        var mid = midpoint(a, b);
        var dist = distance(a, b);
        if (dist > a.r+b.r) return;
        a.x = mid.x + a.r * (a.x - b.x) / dist;
        a.y = mid.y + a.r * (a.y - b.y) / dist;
        b.x = mid.x + b.r * (b.x - a.x) / dist;
        b.y = mid.y + b.r * (b.y - a.y) / dist;
      }
    
    
      function coli(a, b) {
        var a1 = {
          x: a.x + a.vx,
          y: a.y + a.vy
        };
        var b1 = {
          x: b.x + b.vx,
          y: b.y + b.vy
        };
        var d = distance(a1, b1);
        if (d > a.r+b.r) return;
        if (d < Math.min(a.r, b.r)) {
          staticStaticResolve(a, b);
        }
        var n = {
          x: (b1.x - a1.x) / d,
          y: (b1.y - a1.y) / d
        };
    
        var p = 2 * (a.vx*n.x + a.vy*n.y - (b.vx*n.x + b.vy*n.y)) / (a.m + b.m);
    
        a.vx = a.vx - p * b.m * n.x;
        a.vy = a.vy - p * b.m * n.y;
        b.vx = b.vx + p * a.m * n.x;
        b.vy = b.vy + p * a.m * n.y;
      }
      function drawppl(d, p){
    
        for(var i=0;i<ppl.length;i++){
          if(i==p) continue;
          coli(d,ppl[i]);
        }
    
        if(d.x+d.r>c.width)
           d.vx=-Math.abs(d.vx);
        if(d.x-d.r<0)
           d.vx=Math.abs(d.vx);
        if(d.y+d.r>c.height)
           d.vy=-Math.abs(d.vy);
        if(d.y-d.r<0)
           d.vy=Math.abs(d.vy);
        
        d.x += d.vx,
        d.y += d.vy;
        
        can.beginPath();
        can.arc(d.x, d.y, d.r, 0, Math.PI*2, true);
        can.closePath();
        //can.fillStyle= d.m > 3 ? '#ff6666' : '#66ff66';
        //can.fill();
        can.lineWidth = 2;
        can.strokeStyle= d.m > 3 ? '#ff6666' : '#66ff66';
        can.stroke();
      }
      var times = [];
      function render(){
    
        times = times.filter(t => t>Date.now()-1000);
        times.push(Date.now());
    
        can.fillStyle='rgba(0,0,0,0.5)',
        can.fillRect(0,0,c.width,c.height)
    
        can.font = "30px Arial";
        can.fillStyle='#ffffff';
        can.fillText("FPS: "+ times.length,10,50);
        
        for(var i=0;i<ppl.length;i++){
          drawppl(ppl[i],i)
        }
        requestAnimationFrame(render)
      }
      
      window.onresize= resize;
      resize();
      canF();
    })();
    html,body {
      width:100%;
      height:100%;
      margin:0;
      padding:0;
      border:0;
    }
    <canvas></canvas>