Canvas collisions
- Click and drag a figure
- Double click on canvas area to add new figure
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
// Constructor for Shape objects to hold data for all drawn objects.
// For now they will just be defined as rectangles.
function Shape(type, description) { // x, y, w, h, fill
this.x = description.x || 3;
this.y = description.y || 3;
this.fill = description.fill || '#AAAAAA';
this.colliding = false;
if(type == "circle") {
this.r = description.r || 3;
this.type = 'circle';
} else {
this.w = description.w || 1;
this.h = description.h || 1;
this.type = 'rectangle';
}
}
Shape.prototype.draw = function(ctx) {
ctx.globalAlpha = 1;
if (this.colliding) { ctx.globalAlpha = 0.33; }
if(this.type == "circle") {
ctx.beginPath();
ctx.arc(
this.x,
this.y,
this.r,
0,
2 * Math.PI,
false
);
ctx.fillStyle = this.fill || '#AAAAAA';
ctx.fill();
} else {
ctx.beginPath();
ctx.fillStyle = this.fill;
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
Shape.prototype.contains = function(mx, my) {
var result = false;
if (this.type == 'circle') {
var dx = this.x - mx;
var dy = this.y - my;
result = (dx * dx + dy * dy) <= this.r * this.r;
} else {
// make sure the Mouse X,Y fall in the area between
// the shape's X and (X + Width) and its Y and (Y + Height)
result = (this.x <= mx) && (this.x + this.w >= mx) && (this.y <= my) && (this.y + this.h >= my);
}
return result;
}
function CanvasState(canvas) {
this.canvas = canvas;
this.width = canvas.width;
this.height = canvas.height;
this.ctx = canvas.getContext('2d');
// This complicates things a little but fixes mouse co-ordinate problems
// when there's a border or padding. See getMouse for more detail
var stylePaddingLeft, stylePaddingTop, styleBorderLeft, styleBorderTop;
if (document.defaultView && document.defaultView.getComputedStyle) {
this.stylePaddingLeft =
parseInt(document.defaultView.getComputedStyle(canvas, null)['paddingLeft'], 10) || 0;
this.stylePaddingTop =
parseInt(document.defaultView.getComputedStyle(canvas, null)['paddingTop'], 10) || 0;
this.styleBorderLeft =
parseInt(document.defaultView.getComputedStyle(canvas, null)['borderLeftWidth'], 10) || 0;
this.styleBorderTop =
parseInt(document.defaultView.getComputedStyle(canvas, null)['borderTopWidth'], 10) || 0;
}
// Some pages have fixed-position bars (like the stumbleupon bar) at the top or left of the page
// They will mess up mouse coordinates and this fixes that
var html = document.body.parentNode;
this.htmlTop = html.offsetTop;
this.htmlLeft = html.offsetLeft;
// keep track of state !
this.valid = false; // when set to false, the canvas will redraw everything
this.shapes = []; // the collection of things to be drawn
this.dragging = false; // Keep track of when we are dragging
// the current selected object. In the future we could turn this into an array for multiple selection
this.selection = null;
this.dragoffx = 0; // See mousedown and mousemove events for explanation
this.dragoffy = 0;
// This is an example of a closure!
// Right here "this" means the CanvasState. But we are making events on the Canvas itself,
// and when the events are fired on the canvas the variable "this" is going to mean the canvas!
// Since we still want to use this particular CanvasState in the events we have to save a reference to it.
// This is our reference!
var myState = this;
//fixes a problem where double clicking causes text to get selected on the canvas
canvas.addEventListener('selectstart', function(e) { e.preventDefault(); return false; }, false);
// Up, down, and move are for dragging
canvas.addEventListener('mousedown', function(e) {
var mouse = myState.getMouse(e);
var mx = mouse.x;
var my = mouse.y;
var shapes = myState.shapes;
var l = shapes.length;
for (var i = l-1; i >= 0; i--) {
if (shapes[i].contains(mx, my)) {
var mySel = shapes[i];
// Keep track of where in the object we clicked
// so we can move it smoothly (see mousemove)
myState.dragoffx = mx - mySel.x;
myState.dragoffy = my - mySel.y;
myState.dragging = true;
myState.selection = mySel;
myState.valid = false;
return;
}
}
// havent returned means we have failed to select anything.
// If there was an object selected, we deselect it
if (myState.selection) {
myState.selection = null;
myState.valid = false; // Need to clear the old selection border
}
}, true);
canvas.addEventListener('mousemove', function(e) {
if (myState.dragging){
var mouse = myState.getMouse(e);
// We don't want to drag the object by its top-left corner, we want to drag it
// from where we clicked. Thats why we saved the offset and use it here
myState.selection.x = mouse.x - myState.dragoffx;
myState.selection.y = mouse.y - myState.dragoffy;
myState.valid = false; // Something's dragging so we must redraw
}
}, true);
canvas.addEventListener('mouseup', function(e) {
myState.dragging = false;
}, true);
// double click for making new shapes
canvas.addEventListener('dblclick', function(e) {
var mouse = myState.getMouse(e);
var color = 'rgb('+getRandomInt(0,250)+','+getRandomInt(0,250)+','+getRandomInt(0,250)+')';
var types = [0,'circle'];
var type = types[Math.floor(Math.random()*types.length)];
if (type == 'circle') {
myState.addShape(
new Shape( 'circle', {
x:mouse.x - 10,
y:mouse.y - 10,
r:getRandomInt(15,35),
fill:color
}
)
);
} else {
myState.addShape(
new Shape( 'rectangle', {
x:mouse.x - 10,
y:mouse.y - 10,
w:getRandomInt(15,40),
h:getRandomInt(15,40),
fill:color
}
)
);
}
}, true);
// options !
this.selectionColor = 'black';
this.selectionWidth = 3;
this.interval = 16;
this.ctx.setLineDash([3,3]); // set dashes for stroke
setInterval(function() { myState.draw(); }, myState.interval);
}
CanvasState.prototype.addShape = function(shape) {
this.shapes.push(shape);
this.valid = false;
}
CanvasState.prototype.clear = function() {
this.ctx.clearRect(0, 0, this.width, this.height);
}
CanvasState.prototype.RectCircleColliding = function(circle, rect) {
// vertical & horizontal (distX/distY) distances between the circle and rectangle center
var distX = Math.abs(circle.x - rect.x - rect.w / 2);
var distY = Math.abs(circle.y - rect.y - rect.h / 2);
// If the distance is greater than halfCircle + halfRect, then they are too far apart to be colliding
if (distX > (rect.w / 2 + circle.r)) { return false; }
if (distY > (rect.h / 2 + circle.r)) { return false; }
// If the distance is less than halfRect then they are definitely colliding
if (distX <= (rect.w / 2)) { return true; }
if (distY <= (rect.h / 2)) { return true; }
// Test for collision at rect corner
// Think of a line from the rect center to any rect corner
// Now extend that line by the radius of the circle
// If the circle’s center is on that line they are colliding at exactly that rect corner
// Using Pythagoras formula to compare the distance between circle and rect centers
var dx = distX - rect.w / 2;
var dy = distY - rect.h / 2;
return (dx * dx + dy * dy <= (circle.r * circle.r));
}
// While draw is called as often as the INTERVAL variable demands,
// It only ever does something if the canvas gets invalidated by our code
CanvasState.prototype.draw = function() {
// if our state is invalid, redraw and validate!
if (!this.valid) {
var ctx = this.ctx;
var shapes = this.shapes;
var rcc = this.RectCircleColliding;
this.clear();
// add stuff you want drawn in the background all the time here
shapes.forEach(function(shape, index) {
shapes[index].colliding = false;
});
// todo: reduce repetition or move outside
shapes.forEach(function(this_shape, this_index) {
shapes.forEach(function(that_shape, that_index) {
if (this_index == that_index) return;
var collision = false;
// circle - circle
if (this_shape.type == 'circle' && that_shape.type == 'circle') {
var dx = this_shape.x - that_shape.x;
var dy = this_shape.y - that_shape.y;
var distance = Math.sqrt(dx * dx + dy * dy);
collision = (distance < this_shape.r + that_shape.r);
}
// circle - rect / rect - circle
else if (
(this_shape.type == 'circle' && that_shape.type == 'rectangle') ||
(this_shape.type == 'rectangle' && that_shape.type == 'circle')
) {
var circle = (this_shape.type == 'circle') ? this_shape : that_shape;
var rect = (this_shape.type == 'rectangle') ? this_shape : that_shape;
collision = rcc(circle,rect);
}
// rect - rect
else {
collision = (
this_shape.x < that_shape.x + that_shape.w &&
this_shape.x + this_shape.w > that_shape.x &&
this_shape.y < that_shape.y + that_shape.h &&
this_shape.h + this_shape.y > that_shape.y
) ? true : false;
}
if (collision) {
shapes[this_index].colliding = true;
shapes[that_index].colliding = true;
}
})
});
// draw all shapes
var l = shapes.length;
for (var i = 0; i < l; i++) {
var shape = shapes[i];
// We can skip the drawing of elements that have moved off the screen:
if (shape.x > this.width || shape.y > this.height ||
shape.x + shape.w < 0 || shape.y + shape.h < 0) continue;
shapes[i].draw(ctx);
}
// draw selection
// right now this is just a stroke along the edge of the selected Shape
if (this.selection != null) {
ctx.strokeStyle = this.selectionColor;
ctx.lineWidth = this.selectionWidth;
var mySel = this.selection;
if (mySel.type == 'circle') {
ctx.beginPath();
ctx.arc(
mySel.x,
mySel.y,
mySel.r,
0,
2 * Math.PI,
false
);
ctx.stroke();
} else {
ctx.strokeRect(mySel.x,mySel.y,mySel.w,mySel.h);
}
}
// add stuff you want drawn on top all the time here
this.valid = true;
// console.log([shapes,this.selection])
}
}
// Creates an object with x and y defined, set to the mouse position relative to the state's canvas
// If you wanna be super-correct this can be tricky, we have to worry about padding and borders
CanvasState.prototype.getMouse = function(e) {
var element = this.canvas, offsetX = 0, offsetY = 0, mx, my;
// Compute the total offset
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
// Add padding and border style widths to offset
// Also add the html offsets in case there's a position:fixed bar
offsetX += this.stylePaddingLeft + this.styleBorderLeft + this.htmlLeft;
offsetY += this.stylePaddingTop + this.styleBorderTop + this.htmlTop;
mx = e.pageX - offsetX;
my = e.pageY - offsetY;
// We return a simple javascript object (a hash) with x and y defined
return {x: mx, y: my};
}
// If you dont want to use <body onLoad='init()'>
// You could uncomment this init() reference and place the script reference inside the body tag
//init();
function init() {
var s = new CanvasState(document.getElementById('canvas'));
s.addShape(new Shape('rectangle', {x:40,y:40,w:30,h:50,fill:'#ffff00'})); // The default is gray
s.addShape(new Shape('rectangle', {x:80,y:140,w:50,h:40,fill:'#ff0000'}));
// Lets make some partially transparent
s.addShape(new Shape('rectangle', {x:160,y:150,w:60,h:20,fill:'#ff8000'}));
s.addShape(new Shape('rectangle', {x:240,y:80,w:30,h:20,fill:'#80ff00'}));
s.addShape(new Shape('rectangle', {x:300,y:80,w:25,h:40,fill:'#ff00bf'}));
// circles
s.addShape(new Shape('circle', {x:70,y:250,r:40,fill:'#00bfff'}));
s.addShape(new Shape('circle', {x:140,y:230,r:30,fill:'#0040ff'}));
s.addShape(new Shape('circle', {x:210,y:80,r:20,fill:'#8000ff'}));
// s.addShape(new Shape('circle', {x:125,y:80,r:20,fill:'rgba(245, 22, 79, .7)'}));
}
init();
Dynamic Collisions
// setup canvas
var canvas = document.querySelector('#canvas2');
var ctx = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
// generate random number
function random(min,max) {
var num = Math.floor(Math.random()*(max-min)) + min;
return num;
}
// define Ball constructor
function Ball(x, y, velX, velY, color, size) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.color = color;
this.size = size;
}
// define ball draw method
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
};
// define ball update method
Ball.prototype.update = function() {
if( (this.x + this.size) >= width ) { this.velX = -(this.velX); }
if( (this.x - this.size) <= 0 ) { this.velX = -(this.velX); }
if( (this.y + this.size) >= height ) { this.velY = -(this.velY); }
if( (this.y - this.size) <= 0 ) { this.velY = -(this.velY); }
this.x += this.velX;
this.y += this.velY;
};
// define ball collision detection
Ball.prototype.collisionDetect = function() {
for(var j = 0; j < balls.length; j++) {
if(!(this === balls[j])) {
var dx = this.x - balls[j].x;
var dy = this.y - balls[j].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.size + balls[j].size) {
balls[j].color = this.color =
'rgb(' + random(0,255) + ',' + random(0,255) + ',' + random(0,255) +')';
}
}
}
};
// define array to store balls
var balls = [];
// define loop that keeps drawing the scene constantly
function loop() {
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(0,0,width,height);
while(balls.length < 10) {
var size = random(5,20);
var ball = new Ball(
// ball position always drawn at least one ball width
// away from the adge of the canvas, to avoid drawing errors
random(0 + size,width - size),
random(0 + size,height - size),
random(-3,3),
random(-3,3),
'rgb(' + random(0,255) + ',' + random(0,255) + ',' + random(0,255) +')',
size
);
balls.push(ball);
}
for(var i = 0; i < balls.length; i++) {
balls[i].draw();
balls[i].update();
balls[i].collisionDetect();
}
requestAnimationFrame(loop);
}
loop();
Back to Main Page