2010-11-20

Project "HoT MetaL JaZz" - Part 3

In our last episode, we demonstrated how to draw images to our canvas. Now we will dig deeper into the HTML5 Canvas drawing methods by looking at the path-drawing methods, and learn how to scale images.

Drawing straight lines is easy. We start with [context].beginPath(), plot our paths, apply a stroke and/or fill, then call [context].closePath() to finish up. For example, to draw a simple triangle, you could use code like the following. Open gamecore.js, and replace your current draw() method with the following:

GameCore.prototype.draw = function() {
  this.bufferContext.fillStyle = "rgb(0, 0, 0)";
  this.bufferContext.fillRect(0,
                              0,
                              this.bufferCanvasWidth,
                              this.bufferCanvasHeight);

  this.bufferContext.beginPath();
  this.bufferContext.moveTo(100, 100);
  this.bufferContext.lineTo(200, 100);
  this.bufferContext.lineTo(100, 200);
  this.bufferContext.lineTo(100, 100);

  this.bufferContext.fillStyle = "rgb(127, 127, 127)";
  this.bufferContext.fill();
  this.bufferContext.strokeStyle = "rgb(255, 255, 255)";
  this.bufferContext.stroke();
  this.bufferContext.closePath();

  this.primaryContext.clearRect(0,
                                0,
                                this.primaryCanvasWidth,
                                this.primaryCanvasHeight);

  this.primaryContext.drawImage(this.bufferCanvas,
                                0,
                                0,
                                this.primaryCanvasWidth,
                                this.primaryCanvasHeight);

  setTimeout("game.draw()", this.frameSpeed);
}

The first difference is in the first line - we set the fillStyle to black explicitly, to ensure that we always get a black fill since we'll be changing the context's fillStyle in this example.

Then comes the call, [context].beginPath(). This simple call is used to indicate that we are starting to plot a path. After that, we use [context].moveTo() to move our "cursor" to the indicated X/Y coordinate, without plotting any paths. Three lines calling [context].lineTo() plot the path for a triangular shape. We set our fillStyle to gray, call [context].fill(), and this fills our triangle shape with the color gray. We then set our strokeStyle to white, and call [context].stroke() to draw the lines between points. A call to [context].closePath() lets the context know that this path is complete.

Of course, we can draw curves fairly easily, too. We can draw a triangle that looks "pinched", with each side bending inward toward the center of the shape. To do this, we replace our [context].lineTo() calls with the following:

this.bufferContext.quadraticCurveTo(125, 125, 200, 100);
this.bufferContext.quadraticCurveTo(125, 125, 100, 200);
this.bufferContext.quadraticCurveTo(125, 125, 100, 100);

Look out, those are some sharp points on that triangle!

Quadratic curves are simple: The first two arguments define the coordinate to which the middle of the line will be "pulled", and the second pair of arguments define that start and end points of the path.

Of course, bezier curves are no problem for us, either. Change the code for drawing your triangle to the following:

this.bufferContext.bezierCurveTo(250,  50,  50,  50, 200, 100);
this.bufferContext.bezierCurveTo(125, 275, 275, 125, 100, 200);
this.bufferContext.bezierCurveTo( 50,  50,  50, 250, 100, 100);

The main difference between quadratic curves and bezier curves are that quadratic curves have one "pull" point, while bezier curves have two. Once you master these curves, you can make some very intriquing designs.

As a side note, you can assign an image as a fillStyle for a path. Doing so is quite simple, involving a call to [context].createPattern(). The function takes two arguments: The first is the image you want to use, and the second is a string that determines the pattern of repetition. Valid values for this argument are "repeat", "repeat-x", "repeat-y", and "no-repeat".

There are some other path-plotting functions: arcTo(float x1, float y1, float x2, float y2, float radius); and arc(float x, float y, float radius, float startAngle, float endAngle, boolean anticlockwise). Experiment with these to learn how to create things like pie charts, etc. Oh, and don't forget the rect(float x, float y, float width, float height) function!

Before we move on to scaling our game canvas, let's touch on one last detail about paths - there is a function, [context].isPointInPath(float x, float y) that will tell you whether the specified point is contained within the path you are currently plotting. It returns true if the point is contained within the path, false if otherwise. Use this for collision detection against complex shapes.

Now that you have a good understanding of paths, we can move on to something a little different. Let's say you want to scale your game canvas to fit the size of the browser window. Furthermore, you want to scale the image with the canvas, so that players with bigger screens see the same amount of view area, just stretched to fit.

If that's something you're interested in, you're in luck - it's pretty easy to do this, so let's get to work on that. Firstly, go back to your skeleton HTML file. We will generally leave this alone for the most part, but there are times where it will need a slight tweak. In the <body> tag, add the following attribute shown in boldface:

<body onload="startGame();" onresize="game.resize();">

This makes the browser call our GameCore's .resize() function, which is:

GameCore.prototype.resize = function() {
  var winInnerWidth = window.innerWidth;
  var winInnerHeight = window.innerHeight;

  this.primaryCanvas.width = winInnerWidth;
  this.primaryCanvas.height = winInnerHeight;

  this.primaryCanvasWidth = this.primaryCanvas.width;
  this.primaryCanvasHeight = this.primaryCanvas.height;
}

This function simply resizes the primary canvas, without touching the buffer canvas. Thus, we can keep the same amount of data on-screen independent of the size of the game's primary canvas. The buffer just "stretches" to the size of the primary canvas.

Remember that this function is called whenever the window is resized, so you may want to add an extra call to the GameCore.resize() method at the end of your GameCore.init() method, just before the call to GameCore.loadData().

Here's an exercise: Add a minimum and maximum size to which the primary canvas will scale. For extra credit, have the primary canvas retain its original aspect ratio. Hint: The aspect ratio would be the canvas width divided by the canvas height.

That about does it for laying the groundwork. In the next section, we'll begin to take what we've learned to help us build a simple framework for our game. In the meantime, there's a really nice "HTML5 Canvas Cheat Sheet" you should definitely check out.

Until next time, have fun!

No comments:

Post a Comment