SasukeRinnegan
SasukeRinnegan

Reputation: 129

Bounding Rectangles under Sprites HTML5 Canvas

I've uploaded a couple of images to act as sprites for a 2D HTML5 Canvas car game. I've been trying to do collision detection with just using the coordinates of the sprites, but it does not work smoothly. I've heard mention of bounding rectangles before, and to my knowledge, they are invisible rectangles under sprites that aid in collision detection (correct me if I'm wrong).

I've seen some things online like Element.getBoundingClientRect(). Can anyone help me put some bounding rectangles under my sprites because I am clueless and I can't find any basic tutorial online.

Js Code: Jsbin link: http://jsbin.com/muzulutaci/2/edit?

var canvas = document.getElementById('background');
var context = canvas.getContext('2d');

//================
//ENTER: USER CAR
//================

//Uploading car sprite
var usercar = new Image();
usercar.src = "http://www.iconshock.com/img_jpg/BETA/communications/jpg/128/car_icon.jpg";

//Setting properties of car
var x = 450;
var y = 730;
var speed = 10;
var angle = -90;
var mod = 0;

function drawUserCar() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    context.save();
    context.translate(x, y);
    context.rotate(Math.PI / 180 * angle);
    context.drawImage(usercar, -(usercar.width / 2), -(usercar.height / 2));
    context.restore();

    obstacleCar1();
}

//Interval for animation
var moveInterval = setInterval(function () {
    drawUserCar();
}, 30);

//=====================
//ENTER: OBSTACLE CAR 1
//=====================

//Uploading obstacle car
var obstcar = new Image();
obstcar.src = "http://www.iconshock.com/img_jpg/BETA/communications/jpg/128/car_icon.jpg";

//Setting properties of obstacle car
var x1 = 450;
var y1 = 300;
var speed1 = 5;
var angle1 = 90;
var mod1 = 0;

function obstacleCar1() {

          x1 += (speed1 * mod1) * Math.cos(Math.PI / 180 * angle1);
          y1 += (speed1 * mod1) * Math.sin(Math.PI / 180 * angle1);

          context.save();
          context.translate(x1, y1);
          context.rotate(Math.PI / 180 * angle1);
          context.drawImage(obstcar, -(obstcar.width / 1), -(obstcar.height / 1));
          context.restore();

          }  

Upvotes: 2

Views: 1142

Answers (1)

markE
markE

Reputation: 105015

Collision detection between rotated rectangles is mathematically complicated and comes in several flavors:

Detect if the 2 rotated rectangles are intersecting (colliding)

To detect if 2 rotated rectangles are colliding (but not detect where they are colliding), you can use the Separating Axis Theorm. A nice explanation is here: http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169

Detect where the rotated rectangles are colliding and apply rebound to the rectangles

To apply rebound when 2 rotated rectangles are colliding requires some complicated physics. Perhaps the easiest path to do this is to use a physics library like Box2dJS. Here's a nice demo of Box2dJS showing colliding rectangles: http://box2d-js.sourceforge.net/index2.html

Example code and a Demo:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;

var BB=canvas.getBoundingClientRect();
var offsetX=BB.left;
var offsetY=BB.top;

var isDown=false;
var startX;
var startY;

var PI=Math.PI;

var car1Rect,car2Rect;
var cars=[];

var Closure=(function(){
  // ctor
  function Closure(x,y,imageObject){
    var iw=imageObject.width;
    var ih=imageObject.height;
    this.img=imageObject;
    this.x=x;
    this.y=y;
    this.w=iw;
    this.h=iw;
    this.cx=x+iw/2;
    this.cy=y+ih/2;
    this.radius=Math.sqrt(iw*iw+ih*ih)/2;
    this.rotation=0;
    this.corners=[];
    this.isDragging=false;
    this.collisionType=0;

    // corner angles
    var w2=iw/2;
    var h2=ih/2;
    this.negHalfWidth=-w2;
    this.negHalfHeight=-h2;
    this.cornerAngles=[
      Math.atan2(-h2,-w2),    // top-left
      Math.atan2(h2,-w2),     // top-right
      Math.atan2(h2,w2),      // bottom-right
      Math.atan2(-h2,w2)      // bottom-left
    ];

    this.rotateTo(0);

  }
  //
  Closure.prototype.draw=function(){   
    this.drawImage();
    this.drawBB(true);
    this.drawBoundingCircle(true);
  };
  Closure.prototype.moveBy=function(dx,dy){
    this.cx+=dx;
    this.cy+=dy;
  };
  Closure.prototype.rotateTo=function(angle){
    this.rotation=angle;
    this.setCorners();
  };
  Closure.prototype.setCorners=function(){
    this.corners.length=0;
    for(var i=0;i<this.cornerAngles.length;i++){
      var a=this.cornerAngles[i]+this.rotation;
      var x=this.radius*Math.cos(a);
      var y=this.radius*Math.sin(a);
      this.corners.push({x:x,y:y});
    }    
  };
  Closure.prototype.drawBB=function(withStroke){
    var p=this.corners;
    var cx=this.cx;
    var cy=this.cy;
    ctx.beginPath();
    ctx.moveTo(cx+p[0].x,cy+p[0].y);
    for(var i=1;i<p.length;i++){
      ctx.lineTo(cx+p[i].x,cy+p[i].y);
    }
    ctx.closePath();
    if(withStroke){
      switch(this.collisionType){
        case 0:ctx.strokeStyle='gray';break;
        case 1:ctx.strokeStyle='green';break;
        case 2:ctx.strokeStyle='red';break;
      }
      ctx.stroke();
    }
  };
  Closure.prototype.drawBoundingCircle=function(withStroke){
    var p=this.corners;
    var cx=this.cx;
    var cy=this.cy;
    ctx.beginPath();
    ctx.arc(this.cx,this.cy,this.radius,0,PI*2);
    ctx.closePath();
    if(withStroke){
      switch(this.collisionType){
        case 0:ctx.strokeStyle='gray';break;
        case 1:ctx.strokeStyle='red';break;
        case 2:ctx.strokeStyle='red';break;
      }        
      ctx.stroke();
    }
  };
  Closure.prototype.drawImage=function(){
    ctx.globalAlpha=0.50;
    ctx.translate(this.cx,this.cy);
    ctx.rotate(this.rotation);
    ctx.drawImage(this.img,this.negHalfWidth,this.negHalfHeight);
    ctx.setTransform(1,0,0,1,0,0);
    ctx.globalAlpha=1.00;
  };
  //    Closure.prototype.=function(){};
  return(Closure);
})();


function calculateCollisionType(r1,r2){

  // rough but fast circular bounds hit-test
  var dx=r2.cx-r1.cx;
  var dy=r2.cy-r1.cy;
  var rr=r1.radius+r2.radius;
  if(dx*dx+dy*dy>rr*rr){
    r1.collisionType=0; // no collision
    r2.collisionType=0; // no collision
    return(false);
  }   

  // hit-test the bounding rectangles
  if(RectanglesIntersect(r1,r2)){
    r1.collisionType=2; // bounding rectangles collide
    r2.collisionType=2;        
  }else{
    r1.collisionType=1; // circular bounds collide
    r2.collisionType=1;
  }

  return(true);
}


$car1Angle=$('#car1Angle');
$car2Angle=$('#car2Angle');
$car1Angle.val(0);
$car2Angle.val(0);

var carCount=2;
var car1=new Image();
car1.onload=start;
car1.src="https://dl.dropboxusercontent.com/u/139992952/multple/car1.png";
var car2=new Image();
car2.onload=start;
car2.src="https://dl.dropboxusercontent.com/u/139992952/multple/car2.png";
function start(){

  if(--carCount>0){return;}

  car1Rect=new Closure(50,100,car1);
  cars.push(car1Rect);
  car2Rect=new Closure(50,250,car2);
  cars.push(car2Rect);

  $("#canvas").mousedown(function(e){handleMouseDown(e);});
  $("#canvas").mousemove(function(e){handleMouseMove(e);});
  $("#canvas").mouseup(function(e){handleMouseUpOut(e);});
  $("#canvas").mouseout(function(e){handleMouseUpOut(e);});

  $car1Angle.change(function(){
    car1Rect.rotateTo($(this).val()*PI/180);
    draw();
  });
  $car2Angle.change(function(){
    car2Rect.rotateTo($(this).val()*PI/180);
    draw();
  });

  calculateCollisionType(car1Rect,car2Rect);
  draw();

  $('#testCollision').click(function(){
    log(RectanglesIntersect(car1Rect,car2Rect));
  });

}

function draw(){
  ctx.clearRect(0,0,cw,ch);      
  car2Rect.draw();
  car1Rect.draw();
}



function handleMouseDown(e){
  e.preventDefault();
  e.stopPropagation();

  startX=parseInt(e.clientX-offsetX);
  startY=parseInt(e.clientY-offsetY);

  isDown=false;
  for(var i=0;i<cars.length;i++){
    var c=cars[i];
    c.drawBB(false);
    if(ctx.isPointInPath(startX,startY)){
      c.isDragging=true;
      isDown=true;
    }
  }
}

function handleMouseUpOut(e){
  e.preventDefault();
  e.stopPropagation();

  isDown=false;
  for(var i=0;i<cars.length;i++){
    cars[i].isDragging=false;
  }
}

function handleMouseMove(e){
  if(!isDown){return;}

  e.preventDefault();
  e.stopPropagation();

  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);

  var dx=mouseX-startX;
  var dy=mouseY-startY;
  startX=mouseX;
  startY=mouseY;

  for(var i=0;i<cars.length;i++){
    var c=cars[i];
    if(c.isDragging){ c.moveBy(dx,dy); }
  }

  calculateCollisionType(car1Rect,car2Rect);
  draw();

}


///////////////////////////////////////
// Attribution for RectanglesIntersect() & isProjectedAxisCollision()
// https://github.com/jozefchutka/YCanvas/blob/master/YCanvasLibrary/libs/yoz/sk/yoz/math/FastCollisions.as
//

function RectanglesIntersect(r1,r2){

  // rotated rectangle hit-test
  var cx,cy,c;
  //
  cx=r1.cx;
  cy=r1.cy;
  c=r1.corners;
  //
  var r1p1x=cx+c[0].x;
  var r1p2x=cx+c[1].x;
  var r1p3x=cx+c[2].x;
  var r1p4x=cx+c[3].x;
  //
  var r1p1y=cy+c[0].y;
  var r1p2y=cy+c[1].y;
  var r1p3y=cy+c[2].y;
  var r1p4y=cy+c[3].y;
  //
  cx=r2.cx;
  cy=r2.cy;
  c=r2.corners;
  //
  var r2p1x=cx+c[0].x;
  var r2p2x=cx+c[1].x;
  var r2p3x=cx+c[2].x;
  var r2p4x=cx+c[3].x;
  //
  var r2p1y=cy+c[0].y;
  var r2p2y=cy+c[1].y;
  var r2p3y=cy+c[2].y;
  var r2p4y=cy+c[3].y;

  //
  if(!isProjectedAxisCollision(r1p1x, r1p1y, r1p2x, r1p2y, 
                               r2p1x, r2p1y, r2p2x, r2p2y, r2p3x, r2p3y, r2p4x, r2p4y))
    return false;

  if(!isProjectedAxisCollision(r1p2x, r1p2y, r1p3x, r1p3y, 
                               r2p1x, r2p1y, r2p2x, r2p2y, r2p3x, r2p3y, r2p4x, r2p4y))
    return false;

  if(!isProjectedAxisCollision(r2p1x, r2p1y, r2p2x, r2p2y, 
                               r1p1x, r1p1y, r1p2x, r1p2y, r1p3x, r1p3y, r1p4x, r1p4y))
    return false;

  if(!isProjectedAxisCollision(r2p2x, r2p2y, r2p3x, r2p3y, 
                               r1p1x, r1p1y, r1p2x, r1p2y, r1p3x, r1p3y, r1p4x, r1p4y))
    return false;
  //
  return true;
}

function isProjectedAxisCollision( b1x, b1y, b2x, b2y, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y){
  var x1, x2, x3, x4;
  var y1, y2, y3, y4;
  if(b1x == b2x){

    x1 = x2 = x3 = x4 = b1x;
    y1 = p1y;
    y2 = p2y;
    y3 = p3y;
    y4 = p4y;

    if(b1y > b2y)
    {
      if((y1 > b1y && y2 > b1y && y3 > b1y && y4 > b1y) || 
         (y1 < b2y && y2 < b2y && y3 < b2y && y4 < b2y))
        return false;
    }
    else
    {
      if((y1 > b2y && y2 > b2y && y3 > b2y && y4 > b2y) ||
         (y1 < b1y && y2 < b1y && y3 < b1y && y4 < b1y))
        return false;
    }
    return true;
  }
  else if(b1y == b2y){

    x1 = p1x;
    x2 = p2x;
    x3 = p3x;
    x4 = p4x;
    y1 = y2 = y3 = y4 = b1y;

  }else{

    var a = (b1y - b2y) / (b1x - b2x);
    var ia = 1 / a;
    var t1 = b2x * a - b2y;
    var t2 = 1 / (a + ia);

    x1 = (p1y + t1 + p1x * ia) * t2;
    x2 = (p2y + t1 + p2x * ia) * t2;
    x3 = (p3y + t1 + p3x * ia) * t2;
    x4 = (p4y + t1 + p4x * ia) * t2;

    y1 = p1y + (p1x - x1) * ia;
    y2 = p2y + (p2x - x2) * ia;
    y3 = p3y + (p3x - x3) * ia;
    y4 = p4y + (p4x - x4) * ia;
  }

  if(b1x > b2x){

    if((x1 > b1x && x2 > b1x && x3 > b1x && x4 > b1x) ||
       (x1 < b2x && x2 < b2x && x3 < b2x && x4 < b2x))
      return false;

  }else{

    if((x1 > b2x && x2 > b2x && x3 > b2x && x4 > b2x) ||
       (x1 < b1x && x2 < b1x && x3 < b1x && x4 < b1x))
      return false;
  }

  return true;
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
Red car angle:&nbsp<input id=car1Angle type=range min=0 max=360 value=0><br>
Gold car angle:&nbsp<input id=car2Angle type=range min=0 max=360 value=0><br>
<h4>Use sliders above to rotate cars.<br>Drag cars closer.<br>Bounding Circles turn green if they collide.<br>Bounding Rectangles turn green if they collide.</h4>
<br>
<canvas id="canvas" width=400 height=500></canvas>

The demo above is best viewed in full-screen mode

Upvotes: 3

Related Questions