user3769916 user3769916 - 3 months ago 7
Javascript Question

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>




Answer

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();
  }
  function render(){
    can.fillStyle='rgba(0,0,0,0.5)',
    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>