Some Canvas Experiments

This post is more than 2 years old.

I spent some time yesterday reading an excellent article on a simple Canvas-based version of the old Snake game: Create a mobile version of Snake with HTML5 canvas and JavaScript. This article, by Eoin McGrath, does a great job explaining how he used Canvas to animate the game. (If you've never played Snake before, think Tron light cycles, single player, and not as cool.) I've been meaning to work on some simple games with Canvas, and while there are some very cool frameworks out there (EaselJS and Impact for example), I wanted to play around a bit with the raw code before I started punting some of the grunt work to other libraries. What follows is a series of experiments based on McGrath's core code set. Be gentle - this is my first time.

I began by creating a ball that would bounce around. I know - not rocket science. McGrath's original code animated the snake and had it die as soon as it hit a wall. For my logic I needed to simply make the ball bounce. Here's the code for version 1:

<!DOCTYPE HTML> <html>

&lt;head&gt;
	&lt;script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"&gt;&lt;/script&gt;
	&lt;script&gt;
	$(document).ready(function() {

		var canvas = $("#gamearea")[0];
		canvas.width = 400;
		canvas.height = 400;
		var ctx = canvas.getContext("2d");


	    var draw = {
	        clear: function () {
	            ctx.clearRect(0, 0, canvas.width, canvas.height);
	        },    
	 
	        rect: function (x, y, w, h, col) {
	            ctx.fillStyle = col;
	            ctx.fillRect(x, y, w, h);
	        },
	       
	      circle: function (x, y, radius, col) {
	          ctx.fillStyle = col;
	          ctx.beginPath();
	          ctx.arc(x, y, radius, 0, Math.PI*2, true);
	          ctx.closePath();
	          ctx.fill();
	      },
	 
	        text: function (str, x, y, size, col) {
	            ctx.font = 'bold ' + size + 'px monospace';
	            ctx.fillStyle = col;
	            ctx.fillText(str, x, y);
	        }
	    };

		var ballOb = function() {

			this.init = function() {
				this.speed = 8;
				this.x = 19;
				this.y = 89;
				this.w = this.h = 10;
				this.col = "darkgreen";
				this.xdir = this.ydir = 1;
			}

			this.move = function() {

				if ((this.x-this.w) &lt; 0 || this.x &gt; (canvas.width - this.w)) this.xdir*=-1;
				if ((this.y-this.w) &lt; 0 || this.y &gt; (canvas.height - this.h)) this.ydir*=-1;
				
				this.x += (this.xdir * this.speed);
			    this.y += (this.ydir * this.speed);
		
			}

			this.draw = function () {
				draw.circle(this.x, this.y, this.w, this.col);
			}
			

		}

		var ball = new ballOb();
		ball.init();
		
		function loop() {
			draw.clear();
			ball.move();
			ball.draw();
		}
	
		setInterval(loop, 30);
	})
	&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
	&lt;canvas id="gamearea" style="background-color:red"&gt;&lt;/canvas&gt;
	
&lt;/body&gt;

</html>

You can demo this here: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index.html. Notice the wall hit isn't exactly perfect. I made it a bit better as I went on. I apologize for the horrible color schemes. I wanted something very clear to see.

In the next iteration, I added a paddle object and added event listeners so I could move the paddle:

<!DOCTYPE HTML> <html>

&lt;head&gt;
	&lt;script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"&gt;&lt;/script&gt;
	&lt;script&gt;
	var ball;
	var paddle;

	$(document).ready(function() {

		
		var canvas = $("#gamearea")[0];
		canvas.width = 400;
		canvas.height = 400;
		var ctx = canvas.getContext("2d");
		var input = {
			left: false,
			right: false
		};

		$(window).keydown(function(e) {
	       switch (e.keyCode) {
	            case 37: input.left = true; break;                            
	            case 39: input.right = true; break;                            
	       } 
		});

		$(window).keyup(function(e) {
	       switch (e.keyCode) {
	            case 37: input.left = false; break;                            
	            case 39: input.right = false; break;                            
	       } 
		});


	    var draw = {
	        clear: function () {
	            ctx.clearRect(0, 0, canvas.width, canvas.height);
	        },    
	 
	        rect: function (x, y, w, h, col) {
	            ctx.fillStyle = col;
	            ctx.fillRect(x, y, w, h);
	        },
	       
	      circle: function (x, y, radius, col) {
	          ctx.fillStyle = col;
	          ctx.beginPath();
	          ctx.arc(x, y, radius, 0, Math.PI*2, true);
	          ctx.closePath();
	          ctx.fill();
	      },
	 
	        text: function (str, x, y, size, col) {
	            ctx.font = 'bold ' + size + 'px monospace';
	            ctx.fillStyle = col;
	            ctx.fillText(str, x, y);
	        }
	    };

		var ballOb = function() {

			this.init = function() {
				this.speed = 8;
				this.x = 19;
				this.y = 89;
				this.w = this.h = 10;
				this.col = "darkgreen";
				this.xdir = this.ydir = 1;
			}

			this.move = function() {

				if (this.x &lt; 0 || this.x &gt; (canvas.width-this.w)) this.xdir*=-1;
				if (this.y &lt; 0 || this.y &gt; (canvas.height-this.h)) this.ydir*=-1;
				
				this.x += (this.xdir * this.speed);
			    this.y += (this.ydir * this.speed);
				
				//handle hitting the edge
				if(this.x-this.w &lt; 0) { this.x = 0+this.w; this.xdir=1 }
				if(this.x+this.w &gt; canvas.width) { this.x = canvas.width-this.w; this.xdir= -1 }
				if(this.y-this.w &lt; 0) { this.y = 0+this.w; this.ydir=1 }					
				if(this.y+this.w &gt; canvas.height) { this.y = canvas.height-this.w; this.ydir=-1 }			
			}

			this.draw = function () {
				draw.circle(this.x, this.y, this.w, this.col);
			}
			

		}

		var paddleOb = function() {

			this.init = function() {
				this.speed = 8;
				this.w = 0.25 * canvas.width;
				this.h = 20;
				this.x = 10;
				this.y = canvas.height - this.h - 10;
				this.col = "white";
				//this.xdir = this.ydir = 1;
			}

			this.move = function() {
				if(input.left) {
					this.x -= this.speed;
					if(this.x &lt; 0) this.x=0;
				}
				if(input.right) {
					this.x += this.speed;
					if((this.x+this.w) &gt; canvas.width) this.x=canvas.width-this.w;
				}
			}

			this.draw = function () {
				draw.rect(this.x, this.y, this.w, this.h,this.col);
			}
			

		}
		
		ball = new ballOb();
		ball.init();
		
		paddle = new paddleOb();
		paddle.init();
		
		function loop() {
			draw.clear();
			ball.move();
			ball.draw();
			paddle.draw();
			paddle.move();
		}
	
		setInterval(loop, 30);
	})
	&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
	&lt;canvas id="gamearea" style="background-color:red"&gt;&lt;/canvas&gt;
	
&lt;/body&gt;

</html>

And here is the demo: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index2.html. Note that this version has better detection for hitting the walls.

So next I added support for noticing when I hit the paddle and keeping score. Nothing too crazy - just a call to the collides method McGrath created for this own game.

<!DOCTYPE HTML> <html>

&lt;head&gt;
	&lt;script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"&gt;&lt;/script&gt;
	&lt;script&gt;
	var ball;
	var paddle;

	$(document).ready(function() {

		
		var canvas = $("#gamearea")[0];
		canvas.width = 400;
		canvas.height = 400;
		var score = 0;
		
		var ctx = canvas.getContext("2d");
		var input = {
			left: false,
			right: false
		};

		$(window).keydown(function(e) {
	       switch (e.keyCode) {
	            case 37: input.left = true; break;                            
	            case 39: input.right = true; break;                            
	       } 
		});

		$(window).keyup(function(e) {
	       switch (e.keyCode) {
	            case 37: input.left = false; break;                            
	            case 39: input.right = false; break;                            
	       } 
		});


	    var draw = {
	        clear: function () {
	            ctx.clearRect(0, 0, canvas.width, canvas.height);
	        },    
	 
	        rect: function (x, y, w, h, col) {
	            ctx.fillStyle = col;
	            ctx.fillRect(x, y, w, h);
	        },
	       
	      circle: function (x, y, radius, col) {
	          ctx.fillStyle = col;
	          ctx.beginPath();
	          ctx.arc(x, y, radius, 0, Math.PI*2, true);
	          ctx.closePath();
	          ctx.fill();
	      },
	 
	        text: function (str, x, y, size, col) {
	            ctx.font = 'bold ' + size + 'px monospace';
	            ctx.fillStyle = col;
	            ctx.fillText(str, x, y);
	        }
	    };

		var ballOb = function() {

			this.init = function() {
				this.speed = 8;
				this.x = 19;
				this.y = 89;
				this.w = this.h = 10;
				this.col = "darkgreen";
				this.xdir = this.ydir = 1;
			}

			this.move = function() {

				if (this.x &lt; 0 || this.x &gt; (canvas.width-this.w)) this.xdir*=-1;
				if (this.y &lt; 0 || this.y &gt; (canvas.height-this.h)) this.ydir*=-1;
				
				this.x += (this.xdir * this.speed);
			    this.y += (this.ydir * this.speed);
				
				//handle hitting the edge
				if(this.x-this.w &lt; 0) { this.x = 0+this.w; this.xdir=1 }
				if(this.x+this.w &gt; canvas.width) { this.x = canvas.width-this.w; this.xdir= -1 }
				if(this.y-this.w &lt; 0) { this.y = 0+this.w; this.ydir=1 }					
				if(this.y+this.w &gt; canvas.height) { this.y = canvas.height-this.w; this.ydir=-1; score++ }	
				
				//handle hitting paddle
				if(this.collides(paddle)) this.ydir = -1;		
			}

			this.draw = function () {
				draw.circle(this.x, this.y, this.w, this.col);
			}

			this.collides = function(obj) {
			
				// this sprite's rectangle
				this.left = this.x;
				this.right = this.x + this.w;
				this.top = this.y;
				this.bottom = this.y + this.h;
				
				// other object's rectangle
				// note: we assume that obj has w, h, w & y properties
				obj.left = obj.x;
				obj.right = obj.x + obj.w;
				obj.top = obj.y;
				obj.bottom = obj.y + obj.h;
				
				// determine if not intersecting
				if (this.bottom &lt; obj.top) { return false; }
				if (this.top &gt; obj.bottom) { return false; }
				
				if (this.right &lt; obj.left) { return false; }
				if (this.left &gt; obj.right) { return false; }
				
				// otherwise, it's a hit
				return true;
			};
					

		}

		var paddleOb = function() {

			this.init = function() {
				this.speed = 8;
				this.w = 0.25 * canvas.width;
				this.h = 20;
				this.x = 10;
				this.y = canvas.height - this.h - 10;
				this.col = "white";
				//this.xdir = this.ydir = 1;
			}

			this.move = function() {
				if(input.left) {
					this.x -= this.speed;
					if(this.x &lt; 0) this.x=0;
				}
				if(input.right) {
					this.x += this.speed;
					if((this.x+this.w) &gt; canvas.width) this.x=canvas.width-this.w;
				}
			}

			this.draw = function () {
				draw.rect(this.x, this.y, this.w, this.h,this.col);
			}
			

		}
		
		ball = new ballOb();
		ball.init();
		
		paddle = new paddleOb();
		paddle.init();
		
		function loop() {
			draw.clear();
			ball.move();
			ball.draw();
			paddle.draw();
			paddle.move();
			draw.text("Score: "+score, 10, 20, 20);
		}
	
		setInterval(loop, 30);
	})
	&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
	&lt;canvas id="gamearea" style="background-color:red"&gt;&lt;/canvas&gt;
	
&lt;/body&gt;

</html>

And this demo may be found here: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index3.html

I then got mean and built this one: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index4.html Don't click. Seriously. I warned you.

Finally I went the extra step of doing a Google search and making the graphics not quite so ugly.

<!DOCTYPE HTML> <html>

&lt;head&gt;
	&lt;script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"&gt;&lt;/script&gt;
	&lt;script&gt;
	var ball;
	var paddle;

	$(document).ready(function() {

		
		var canvas = $("#gamearea")[0];
		canvas.width = 400;
		canvas.height = 400;
		var score = 0;
		
		var ctx = canvas.getContext("2d");
		var input = {
			left: false,
			right: false
		};

		$(window).keydown(function(e) {
	       switch (e.keyCode) {
	            case 37: input.left = true; break;                            
	            case 39: input.right = true; break;                            
	       } 
		});

		$(window).keyup(function(e) {
	       switch (e.keyCode) {
	            case 37: input.left = false; break;                            
	            case 39: input.right = false; break;                            
	       } 
		});


	    var draw = {
	        clear: function () {
	            ctx.clearRect(0, 0, canvas.width, canvas.height);
	        },    
	 
	        rect: function (x, y, w, h, col) {
	            ctx.fillStyle = col;
	            ctx.fillRect(x, y, w, h);
	        },
	       
	      circle: function (x, y, radius, col) {
	          ctx.fillStyle = col;
	          ctx.beginPath();
	          ctx.arc(x, y, radius, 0, Math.PI*2, true);
	          ctx.closePath();
	          ctx.fill();
	      },
	 
	        text: function (str, x, y, size, col) {
	            ctx.font = 'bold ' + size + 'px monospace';
	            ctx.fillStyle = col;
	            ctx.fillText(str, x, y);
	        }
	    };

		var ballOb = function() {

			this.init = function() {
				this.speed = 10;
				this.x = 19;
				this.y = 89;
				this.w = this.h = 10;
				this.col = "green";
				this.xdir = this.ydir = 1;
			}

			this.move = function() {

				if (this.x &lt; 0 || this.x &gt; (canvas.width-this.w)) this.xdir*=-1;
				if (this.y &lt; 0 || this.y &gt; (canvas.height-this.h)) this.ydir*=-1;
				
				this.x += (this.xdir * this.speed);
			    this.y += (this.ydir * this.speed);
				
				//handle hitting the edge
				if(this.x-this.w &lt; 0) { this.x = 0+this.w; this.xdir=1 }
				if(this.x+this.w &gt; canvas.width) { this.x = canvas.width-this.w; this.xdir= -1 }
				if(this.y-this.w &lt; 0) { this.y = 0+this.w; this.ydir=1 }					
				if(this.y+this.w &gt; canvas.height) { this.y = canvas.height-this.w; this.ydir=-1; score++ }	
				
				//handle hitting paddle
				if(this.collides(paddle)) this.ydir = -1;		
			}

			this.draw = function () {
				draw.circle(this.x, this.y, this.w, this.col);
			}

			this.collides = function(obj) {
			
				// this sprite's rectangle
				this.left = this.x;
				this.right = this.x + this.w;
				this.top = this.y;
				this.bottom = this.y + this.h;
				
				// other object's rectangle
				// note: we assume that obj has w, h, w & y properties
				obj.left = obj.x;
				obj.right = obj.x + obj.w;
				obj.top = obj.y;
				obj.bottom = obj.y + obj.h;
				
				// determine if not intersecting
				if (this.bottom &lt; obj.top) { return false; }
				if (this.top &gt; obj.bottom) { return false; }
				
				if (this.right &lt; obj.left) { return false; }
				if (this.left &gt; obj.right) { return false; }
				
				// otherwise, it's a hit
				return true;
			};
					

		}

		var paddleOb = function() {

			this.init = function() {
				this.speed = 8;
				this.w = 0.25 * canvas.width;
				this.h = 20;
				this.x = 10;
				this.y = canvas.height - this.h - 10;
				this.col = "white";
				//this.xdir = this.ydir = 1;
			}

			this.move = function() {
				if(input.left) {
					this.x -= this.speed;
					if(this.x &lt; 0) this.x=0;
				}
				if(input.right) {
					this.x += this.speed;
					if((this.x+this.w) &gt; canvas.width) this.x=canvas.width-this.w;
				}
			}

			this.draw = function () {
				draw.rect(this.x, this.y, this.w, this.h,this.col);
			}
			

		}
		
		ball = new ballOb();
		ball.init();
		
		paddle = new paddleOb();
		paddle.init();
		
		function loop() {
			draw.clear();
			ball.move();
			ball.draw();
			paddle.draw();
			paddle.move();
			draw.text("Score: "+score, 10, 20, 20);
		}
	
		setInterval(loop, 30);
	})
	&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
	&lt;canvas id="gamearea" style="background:url(brick.jpg)"&gt;&lt;/canvas&gt;
	&lt;p&gt;
	Brick wall picture credit: &lt;a href="http://www.flickr.com/photos/richard_wasserman/5510266273/"&gt;Nutch Bicer&lt;/a&gt;
	&lt;/p&gt;
&lt;/body&gt;

</html>

You can find this final demo here: http://www.coldfusionjedi.com/demos/2011/nov/15_2/index5.html.

Enjoy. I've got an idea for a simple zombie game name. Yes - I love my job. I really love my job.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Phillip Senn posted on 11/17/2011 at 2:53 AM

Surely this spells the end for Flash ;)

Comment 2 by Devin posted on 11/17/2011 at 3:12 AM

@Phillip,

Since every one of Ray's demo URLs displayed a blank white screen in the latest version of IE, I don't understand how HTML5 has not made Flash MORE popular.

Comment 3 by Raymond Camden posted on 11/17/2011 at 3:24 AM

I think failing in IE is a win. ;)

Comment 4 by anon posted on 11/17/2011 at 3:25 AM

Cool. Could you show how to add gesture support to the paddle ? So we could, say touch drag it on a tablet.

Thanks

Comment 5 by Raymond Camden posted on 11/17/2011 at 3:26 AM

@anon - my plan was to put this on a device tomorrow.

Comment 6 by Raymond Camden posted on 11/17/2011 at 3:29 AM

I know why it failed in IE - forgot:

<!DOCTYPE HTML>

adding it to my demos now.

Comment 7 by Shiva posted on 11/17/2011 at 12:22 PM

Hey Ray,

Cool Stuff! Seems like the above code still doesn't work with old age browsers like IE8 and lower versions.

of-course, Failing in IE is win ;)

Though canvas are natively not supported in IE 8 as mentioned in the http://en.wikipedia.org/wik...

When i use the below JS, works fine with me in IE 8
http://canvas-text.googleco...

<!--[if IE]><script type="text/javascript" src="excanvas.js"></script><![endif]-->

Thanks,
Shiva

Comment 8 by Pete posted on 11/23/2011 at 12:53 PM

Why does the score counter go up when you miss the ball?

I know it's only a demo... but it still bothers me!

Comment 9 by Raymond Camden posted on 11/23/2011 at 8:15 PM

It's like golf.