Attention: Repository : https://github.com/shuffleandko/circle-collision-without-vector. The correctness of this tutorial is not guaranteed, the context may be updated without notifications, and the author is not responsible for any consequences that copy or run the codes in the tutorial, if any.
Go to chapter:
 Move to section:

Chapter 1 : Introduction


1.1 Introduction of the tutorial

At the coming chapters, we would build a simple physics engine that simulates ball collision (circle-circle collision) using HTML canvas and Javascript step by step. While there are already many tutorials about how to build a physics engine in scratch, we would like to implement it with our angle of view so that everyone could have one more way to understand the principle of circle collision. We would implement it with the following features:

  • Implementing without using vectors, but using rotation formulae directly
  • Implementing without defining new Javascript classes (just use objects directly)
  • Using trigonometry (sinθ, cosθ and tanθ) and square root as an intemidate, but would eliminate all trigonometric functions and square root functions finally
  • Keep all the codes in a single .html file

To be simple, we would implement elastic collision only and the objects would have no friction. Also, the tutorial would keep the codes in a single .html, hope it would help beginners to view and understand the codes more easily!

The final product contains the following features:

  • Movable circle
  • Fixed circle
  • Fixed polygon
  • Line segment


1.2 Product preview

The code has both debug and non-debug version, which the debug version has some extra drawings such as positions and velocities. The non debug final product would look like the following:

  • Non-debug version:

  • Debug version:

The code of the canvas above (non-debug version):

<canvas id="myCanvas" width="400px" height="400px"></canvas>
<script>
  const circleArray = [
    {    
      cx:80,
      cy:80,
      r:30,
      m:Math.PI*30*30, //mass = area
      vx:90,
      vy:30
    },
    {    
      cx:-40,
      cy:-80,
      r:40,
      m:Math.PI*40*40, //mass = area
      vx:90,
      vy:-30
    },
    {    
      cx:0,
      cy:80,
      r:10,
      m:Math.PI*10*10, //mass = area
      vx:0,
      vy:0
    }
  ];

  const fixedCircleArray = [
    {    
      cx:0,
      cy:0,
      r:200
    },
    {    
      cx:0,
      cy:-150,
      r:20
    }
  ];

  const fixedPolygonArray = [
    {
      x:[140,90,60,110],
      y:[10,40,-10,-40],
      isClosed:true
    },
    {
      x:[-90,-150,-90],
      y:[60,0,-60],
      isClosed:false
    }
  ];

  const canvas = document.getElementById("myCanvas");
  const ctx = canvas.getContext("2d");
  ctx.font="12px sans-serif";
  ctx.textAlign="center";
  const frameRate=60;

  const intervalId=setInterval(function(){
    //circle position update
    for(const c of circleArray){
        c.cx += c.vx * 1 / frameRate;
        c.cy += c.vy * 1 / frameRate;
    }

    for(const fp of fixedPolygonArray){
      //fixed point and circle check collision
      for(let i=0;i < fp.x.length;i++){
        for(const c2 of circleArray){
          const dx=c2.cx-fp.x[i];
          const dy=c2.cy-fp.y[i];
          const vxFactor = c2.vx * dx + c2.vy * dy;
          if( dx * dx + dy * dy <= c2.r * c2.r && vxFactor < 0){ //apply collision only if 2 circles touch and the direction of speed is correct
            c2.vx -= 2 * dx * vxFactor / (dx * dx + dy * dy);
            c2.vy -= 2 * dy * vxFactor / (dx * dx + dy * dy);
          }
        }
      }
      //line segment and circle collision
      for(let i = 0 , j = 1 ; fp.isClosed ? i < fp.x.length : i < fp.x.length - 1 ; i++ , j = (i+1) % fp.x.length){
        const lx = fp.x[j] - fp.x[i];
        const ly = fp.y[j] - fp.y[i];
        for(const c2 of circleArray){
          if((c2.cx - fp.x[i]) * lx > -(c2.cy - fp.y[i]) * ly && (c2.cx - fp.x[j]) * lx < -(c2.cy - fp.y[j]) * ly){
            const cxx1 = (c2.cx - fp.x[i]) * ly - (c2.cy - fp.y[i]) * lx;
            if(cxx1 * cxx1 <= c2.r * c2.r * (lx * lx + ly * ly)){ //check if the circle touches the line segment
            const relVx = c2.vx * ly - c2.vy * lx;
              if((cxx1 > 0 && relVx < 0) || (0 > cxx1 && relVx > 0)){ //check if the circle is approaching to the line segment
                c2.vx -= 2 * ly * relVx / (ly * ly + lx * lx);
                c2.vy += 2 * lx * relVx / (ly * ly + lx * lx);
              }
            }
          }
        }
      }
    }

    //fixed circle and circle check collision
    for(const fc of fixedCircleArray){
      for(const c2 of circleArray){
        const dx=c2.cx-fc.cx;
        const dy=c2.cy-fc.cy;
        const vxFactor = c2.vx * dx + c2.vy * dy;
        if( (dx * dx + dy * dy >= fc.r * fc.r && dx * dx + dy * dy <= (fc.r + c2.r)*(fc.r + c2.r) && vxFactor < 0) || (dx * dx + dy * dy <= fc.r * fc.r && dx * dx + dy * dy >= (fc.r - c2.r)*(fc.r - c2.r) && vxFactor > 0)){ //apply collision only if 2 circles touch and the direction of speed is correct
          c2.vx -= 2 * dx * vxFactor / (dx * dx + dy * dy);
          c2.vy -= 2 * dy * vxFactor / (dx * dx + dy * dy);
        }
      }
    }

    //circle check collision : find if d is smaller than (or equals to) r1+r2
    for(let i = 0; i < circleArray.length ; i++){
      const c1 = circleArray[i];
      for(let j = i; j < circleArray.length ; j++){
        const c2 = circleArray[j];
        const dx=c2.cx-c1.cx;
        const dy=c2.cy-c1.cy;
        if(dx * dx + dy * dy <= (c1.r + c2.r)*(c1.r + c2.r)){ //find if d is smaller than (or equals to) r1+r2
          const rel_c2vx = c2.vx - c1.vx;
          const rel_c2vy = c2.vy - c1.vy;
          if(rel_c2vx * dx + rel_c2vy * dy < 0){ //apply collision only when 2 circles are colliding instead of separating
            c1.vx += 2 * dx * c2.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
            c1.vy += 2 * dy * c2.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
            c2.vx -= 2 * dx * c1.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
            c2.vy -= 2 * dy * c1.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
          }
        }
      }
    }

    //clear all drawings
    ctx.clearRect(0,0,canvas.width,canvas.height);

    //draw x and y axis
    ctx.strokeStyle="lightgray";
    ctx.beginPath();
    ctx.moveTo(0,canvas.height/2);
    ctx.lineTo(canvas.width,canvas.height/2);
    ctx.moveTo(canvas.width/2,0);
    ctx.lineTo(canvas.width/2,canvas.height);
    ctx.stroke();
    ctx.strokeStyle="black";

    //draw border
    ctx.beginPath();
    ctx.rect(0,0,canvas.width,canvas.height);
    ctx.stroke();

    //draw fixed point and line segment
    for(const fp of fixedPolygonArray){
      //draw fixed point
      for(let i=0;i < fp.x.length;i++){
        ctx.beginPath();
        ctx.fillStyle=ctx.strokeStyle="gray";
        ctx.arc(canvas.width/2+fp.x[i],canvas.height/2-fp.y[i],2,0,2*Math.PI);
        ctx.fill();
      }
      //draw line segment
      for(let i = 0 , j = 1 ; fp.isClosed ? i < fp.x.length : i < fp.x.length - 1 ; i++ , j = (i+1) % fp.x.length){
        ctx.beginPath();
        ctx.moveTo(canvas.width/2+fp.x[i],canvas.height/2-fp.y[i]);
        ctx.lineTo(canvas.width/2+fp.x[j],canvas.height/2-fp.y[j]);
        ctx.stroke();
      }
    }

    //draw fixed circle
    for(let i=0;i < fixedCircleArray.length;i++){
        const fc=fixedCircleArray[i];
        ctx.fillStyle=ctx.strokeStyle="gray";
        ctx.beginPath();
        ctx.arc(canvas.width/2+fc.cx,canvas.height/2-fc.cy,fc.r,0,2*Math.PI);
        ctx.stroke();
    }

    //draw circle
    for(let i=0;i < circleArray.length;i++){
        const c=circleArray[i];
        ctx.fillStyle=ctx.strokeStyle="black";
        ctx.beginPath();
        ctx.arc(canvas.width/2+c.cx,canvas.height/2-c.cy,c.r,0,2*Math.PI);
        ctx.stroke();
    }
  },1000/frameRate);
</script>
 

That looks funny, right? Lets move to next chapters to start coding!