Canvas

Perhaps no other HTML5 feature is as powerful as the canvas API with regards to game development for the web platform. Although we may have every other feature currently in the specification, as well as any forthcoming feature that the browser can possibly support, it would be nearly impossible to produce a high quality, engaging, fun game using HTML and JavaScript. The canvas API allows us to create 2D, as well as 3D graphics right on the browser. It also lets us manipulate the graphical data stored in the canvas context, down to the individual pixel level.

One major difference between a canvas graphic and an SVG graphic, apart from the fact that SVG graphics are vector-based, and canvas graphics are always raster graphics, is that the canvas is a single HTML element, and everything drawn in it is, for all practical purposes, non-existent to the browser. Thus, any event handling on individual entities drawn on a canvas must be handled at the application level. There are generic events on the canvas that we can observe and respond to, such as clicks, move events, and keyboard events. Beyond that, we are free to do as we please.

Beyond the shape-based drawing that we can do on an HTML5 canvas, there are three major use cases for the API. We can create 2D, sprite-based games, full-blown 3D games (using WebGL with the help of the canvas), and manipulating photographs. The last use case mentioned: photo manipulation, is especially interesting. The API has a very handy function that allows us to not only export the data in the canvas as a PNG or JPG image, but it also supports various types of compression. That means, we can draw on a canvas, load graphics on it (for example photographs), manipulate that data at a pixel level (for example apply Photoshop-like filters to it), rotate, stretch, scale, and otherwise, play with the data. Then, the API allows us to export that data as a compressed file that can be saved to the file system.

For the purposes of this book, we'll focus on the aspects of the canvas API that we can best use for game development. Although WebGL is a very exciting aspect of the canvas element, but we will cover a very basic introduction to it in Chapter 6, Adding Features to Your Game. For other capabilities available on the canvas API, we will cover them briefly with a few examples in the following section.

How to use it

The first thing we need to understand about the canvas element is that there are two parts to it. One is the physical canvas element, and the other is the rendering context through which we can draw to the canvas. As of this writing, there are two rendering contexts that we can use in modern browsers, namely, CanvasRenderingContext2D and WebGLRenderingContext.

To obtain a reference to the rendering context of a canvas, we call a factory method on the canvas element itself.

var canvasA = document.createElement("canvas");
var ctx2d = canvas.getContext("2d");
ctx2d instanceof CanvasRenderingContext2D; // True

var canvasB = document.createElement("canvas");
var ctx3d = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
ctx3d instanceof WebGLRenderingContext; // True

Note that the use of a fallback context is aimed at the prefixed experimentalwebgl context. As of this writing, most browsers that support WebGL will do so through the experimental tag.

The rest of the section will relate exclusively to the CanvasRenderingContext2D API. While it is technically possible to do everything that the 2D canvas context can, using the 3D canvas context of WebGL, the only thing that these two APIs have in common is their link to the HTML5 canvas element. WebGL is an entire programming language in and of itself, and a single chapter dedicated to it would not be enough to even scratch the surface.

Now, a very important aspect of the 2D rendering context is its coordinate space. Similar to most coordinate system in computers, the origin is located at the top left corner of the canvas. The horizontal axis increases to the right, while the vertical axis increases downwards. The size of the grid held in memory to represent the canvas is determined by the physical size of the canvas that generates the rendering context, and not the styled size of the canvas. This is a key principle that can't be emphasized enough. By default, a canvas is 300 x 150 pixels. Even if we resize the canvas through Cascading Style Sheets (CSS), the rendering context that it generates will be that size (unless we physically resize the canvas, of course). Once the rendering context has been created, it cannot be resized.

<style>
canvas {
   border: 3px solid #ddd;
   width: 500px;
   height: 300px;
}
</style>

<script>
   var canvas = document.createElement("canvas");
   var ctx = canvas.getContext("2d");

   document.body.appendChild(canvas);

   alert(ctx.canvas.width);
</script>
How to use it

The border was added in order to make the canvas somewhat visible to us, as by default, the canvas is transparent.

You will observe that the CSS rule is indeed applied to the canvas element, even though the canvas' real size is still the default 300 x 150 pixels. If we were to draw a circle in the middle of that canvas, the circle would seem distorted, because the actual coordinate space where the circle is actually drawn, would be stretched by the styling applied to the canvas.

clearRect

The first drawing function we'll look at is clearRect. All that this function does is, clear a rectangular area of the canvas. This function is called on the context object, as do all drawing calls that we'll be making on the 2D canvas. The four parameters it takes, represent, in order, the x and y offset from the canvas' origin, plus a width and a height distance to clear. Keep in mind that unlike other popular drawing APIs in other programming languages, the last two parameters are not measured from the origin—they are displacement distances from the point specified by the first two parameters.

var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");

// Clear the entire canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);

// Only clear the half inside area of the canvas
ctx.clearRect(canvas.width * 0.25, canvas.height * 0.25,
   canvas.width * 0.5, canvas.height * 0.5);

// Clear a square 100x100 at the lower right bottom of the canvas
ctx.clearRect(canvas.width - 100, canvas.height - 100, 100, 100);

Normally, when rendering many frames every second, we'd be calling this function to clear out the entire canvas before drawing the next frame. Luckily, in most JavaScript engines, this function performs fairly well; so that we don't need to worry too much about optimizing the precise area to clear out on a regular basis.

Fill and stroke

When drawing native objects such as lines, paths, text, and other shapes, we'll deal with the concept of strokes and fills; just as in SVG, a stroke refers to the outline of a primitive (such as a border or sorts), and the fill is the content that covers the inside of the shape.

The way we can change the color that is used to fill a shape, or the color used to stroke the shape, is by assigning any color to the fillStyle or strokeStyle properties. The color can be any string valid for a CSS color.

// Short hand hex colors are fine
ctx.fillStyle = "#c00";
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Keyword colors are fine, though not as precise
ctx.strokeStyle = "white";

ctx.lineWidth = 10;
ctx.strokeRect(25, 25, 100, 100);
ctx.strokeRect(175, 25, 100, 100);

// Alpha transparency is also allowed
ctx.fillStyle = "rgba(100, 255, 100, 0.8)";

ctx.fillRect(5, 50, canvas.width - 10, 50);
Fill and stroke

Any valid CSS color string can be assigned to color properties in the 2D rendering context, including colors with opacity.

Note

Pay special attention to the fact that the rendering context acts much like a state machine. Once you set a fill or stroke style, as well as any other property, that property will maintain that value until you change it.

Also, note that each subsequent drawing call that you issue, draws on top of whatever is already on the canvas. Thus, we can layer shapes and images by carefully arranging the drawing calls in just the right order.

Lines

Drawing lines is as easy as calling the function lineTo, which only takes two parameters, indicating the point where the line is going to. Subsequent calls to lineTo will draw a line to the point specified by the function call, starting the line at the last point where the line was. More specifically, the line starts where the current drawing pointer is.

By default, the pointer is not defined anywhere, so drawing a line to some other point makes little sense. To help with that, we can make use of the function moveTo, which moves the drawing pointer without drawing anything.

Finally, any calls to lineTo only set the points in memory. In order to eventually draw the line, we need to make a quick call to the stroke function. Once this call is made, whatever attributes are currently set (such as line width and stroke style), will be drawn. Thus, changing line properties before actually stroking the line does little good, and can negatively influence performance.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

// This call is completely useless
ctx.strokeStyle = "#c0c";
ctx.lineWidth = 5;

ctx.moveTo(0, 0);
ctx.lineTo(100, 100);
ctx.lineTo(canvas.width, 0);

// This call is also useless because the line hasn't been drawn yet
ctx.strokeStyle = "#ca0";
ctx.moveTo(10, canvas.height - 10);
ctx.lineTo(canvas.width - 10, canvas.height * 0.5);

// This color is applied to every line drawn so far
ctx.strokeStyle = "#f5a";

// The line is finally drawn here
ctx.stroke();
Lines

Shapes are only drawn after the call to stroke(), at which point the current style attributes are used.

Shapes

There are a couple of different shapes we can draw very effortlessly. These are rectangles and circles. While, there is no circle function as there is a rect function for drawing rectangles. There is, however, an arc function, from which we can draw circles.

The rect function takes four parameters, exactly as fillRect. arc takes an x and a y coordinate, followed by a radius, a starting angle (in radians, not degrees), an ending angle, and a Boolean, specifying, if the arc is to be drawn clockwise. To draw a circle, we can just draw an arc that goes from 0 to PI times 2, which is the same as 360 degrees.

ctx.fillStyle = "#fff";
ctx.strokeStyle = "#c0c";

ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.rect(10, 10, 50, 50);
ctx.rect(75, 50, 50, 50);

ctx.moveTo(180, 100);
ctx.arc(180, 100, 30, 1, 3, true);

ctx.moveTo(225, 40);
ctx.arc(225, 40, 20, 0, Math.PI * 2, false);

ctx.stroke();
Shapes

Arcs (including circles) are drawn from their center and not from some point on their outline.

Text

Drawing text on an HTML5 canvas is also pretty straightforward. The function fillText takes a string (the text to be drawn), and an x and y coordinate, where the text begins to draw. Additionally, we can style the text the same way that text can be styled through CSS. This can be done by setting the text style property string to the font attribute.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.fillStyle = "#f00";
ctx.font = "2.5em 'Times New Roman'";

ctx.fillText("I Love HTML5!", 20, 75);

Transformations

The canvas API also defines a few transformation functions that allow us to translate, scale, and rotate the context's coordinate system. After transforming the coordinate system, we can draw onto the canvas just as we normally would, and the transformations would apply.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Now the origin is at point 50x50
ctx.translate(50, 50);

ctx.fillStyle = "#f00";
ctx.fillRect(0, 0, 50, 50);

Rotation and scaling also works the same way. The scale function takes a value to scale the coordinate system by, on each axis. The rotation function takes a single parameter, which is the angle (in radian) to rotate the coordinate system by.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

// With transformations, order is very important
ctx.scale(2, 1);
ctx.translate(50, 50);
ctx.rotate(0.80);
ctx.translate(10, -20);

ctx.fillStyle = "#f00";
ctx.fillRect(0, 0, 50, 50);
Transformations

With transformations, order is very important.

Drawing images

Probably the most exciting and useful feature of the 2D canvas API from a game development perspective, is its ability to draw images onto it. Thankfully, for us, there are several ways to draw a regular JPG, GIF, or PNG image right on the canvas, including functions that handle scaling images from source to destination.

One other note that we need to make about the canvas element is that it follows the same origin policy. That means, in order for us to be able to draw an image onto a canvas context, the script attempting to draw the image must be served from the same domain (along with the same protocol and port number) as the image. Any attempt to load an image from a different domain into the canvas context and the browser will throw an exception.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

var img = new Image();
img.onload = function(){
   ctx.drawImage(img, 0, 0, this.width, this.height);
};

img.src = "img/html5-logo.png";

The simplest call to draw an image only takes five parameters. The first is a reference to an image. The next two parameters are the x and y position where that image will be drawn onto the canvas, and the last two parameters are the width and height to paint the image onto the canvas. If the last two parameters don't maintain the aspect ratio of the original image, the result will be distortion, rather than clipping. Also, note that, if the original image is larger than the canvas, or if the image is drawn from an offset such that part of the image runs off the canvas, that extra data will simply not be drawn (obviously), and the canvas will just ignore those pixels outside the viewable area:

Drawing images

The HTML5 logo drawn inside a canvas rendering context.

A very important observation is that if the browser has not yet finished downloading the image resource from the server by the time the call is made to drawImage, the canvas will simply not draw anything, since the image passed to be drawn onto it has not loaded yet. In the case where we draw the same image onto the canvas multiple times per second using a game loop of some sort, this is not really a problem, because whenever the image finally loads, the next pass through the game loop will successfully draw the image. However, in cases where the call to draw the image is only done once (as in the example above), we only get one chance to draw the image. Thus, it is very important that we don't make that call until the image is in fact loaded into memory, and ready to be drawn into the canvas.

In order to ensure that the call to draw the image into the canvas only happens after the image has been fully downloaded from the server, we can simply register a callback function on the load event of the image. This way, as soon as the image is done downloading, the browser can fire that callback, and the call can finally be made to draw the image. This way, we can be sure that the image will indeed be ready by the time we want to have it rendered in the canvas.

There is also another version of the same function, which takes into account, scaling from source to destination. In the case above, the source image is larger than the canvas. Instead of resizing the image using a photo editing software program, we can instead tell the canvas to draw the whole image into a smaller area of the canvas. The scaling is done by the canvas automatically. We could also draw the image into a larger area than the image itself, but doing so will result in pixilation depending on how much we scale the image.

The parameters for this function are the source image, the source x and y coordinates (in other words, where to start sampling the source image relative to the image itself), the source width and height (in other words, how much to sample the source image), and the destination x and y, followed by width and height.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

var img = new Image();
img.onload = function(){

   ctx.drawImage(img,
      // Sample part of the upper left corner of the source image
      35, 60, this.width / 2, this.height / 2,

      // And draw it onto the entire canvas, even if it distorts the image
      0, 0, canvas.width, canvas.height);
};

img.src = "img/html5-logo.png";
Drawing images

Part of the HTML5 logo drawn inside a canvas rendering context, with some intentional stretching.

Manipulating pixels

Now that we know how to draw images into a canvas, let's take things to the next step, and work with the individual pixels drawn in the canvas. There are two functions that we can use in order to accomplish this. One function allows us to retrieve the pixel data from the canvas context, and the other lets us put a pixel buffer back into the canvas context. Additionally, there is a function that allows us to retrieve the pixel data as a data URL, meaning, that we can save the image data from the canvas right to the user's file system, just as we can with a regular image from an <img /> tag.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

var img = new Image();
img.onload = function(){
   ctx.drawImage(img, 35, 60, this.width / 2, this.height / 2, 0, 0, canvas.width, canvas.height);

   // Extract pixel data from canvas context
   var pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);

   pixels instanceof ImageData; // True
   pixels.data instanceof Uint8ClampedArray; // True
   pixels.width == canvas.width; // True
   pixels.height == canvas.height; // True

   // Insert pixel data into canvas context
   ctx.putImageData(pixels, 0, 0);
};

img.src = "img/html5-logo.png";

To get the pixel data representing whatever is currently drawn in the canvas, we can use the function getImageData. The four parameters are the x and y offset on the source image, along with the width and height to be extracted. Note that the output from this function is an object of type ImageData, which has three attributes, namely, width, height, and a typed array with the actual pixel information. As mentioned earlier in the chapter, this typed array is of type Uint8ClampedArray, where each element can only be an integer with a value between 0 and 255 inclusive.

The pixel data is a buffer of length (canvas.width x canvas.height x 4). That is, each four elements represent one pixel, representing the red, green, blue, and alpha channels of the pixel in this order. Thus, in order to manipulate an image through this canvas API, we perform various calculations on this pixel buffer, which we can then put back in the canvas using the putImageData function.

The three parameters of putImageData are the ImageData object, along with the x and y offset on the destination canvas. From there, the canvas will render the image data as far as it can, clipping any extra data that would otherwise be drawn outside the canvas.

As an example of what we can do with an image, we'll take the HTML5 logo that we drew into the canvas, and apply a gray scale function to the pixel data representing it. If this sounds like a complex task, fear not. While there are several different formulas to turn a color image into gray scale, the easiest way to do this is to simply average the red, green, and blue values of each pixel.

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);

var img = new Image();
img.onload = function(){
   ctx.drawImage(img, 35, 60, this.width / 2, this.height / 2, 0, 0, canvas.width, canvas.height);

   // Extract pixel data from canvas context
   var pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);

   // Iterate over every four elements, which together represent a single pixel
   for (var i = 0, len = pixels.data.length; i < len; i += 4) {
      var red = pixels.data[i];
      var green = pixels.data[i + 1];
      var blue = pixels.data[i + 2];
      var gray = (red + green + blue) / 3;

     // PS: Alpha channel can be accessed at pixels.data[i + 3]

      pixels.data[i] = gray;
      pixels.data[i + 1] = gray;
      pixels.data[i + 2] = gray;
   }

   // Insert pixel data into canvas context
   ctx.putImageData(pixels, 0, 0);
};

img.src = "img/html5-logo.png";
Manipulating pixels

Manipulating an image is no more complex than performing various calculations on each pixel of the pixel buffer that represents an image.

Finally, the way we can export the image from a canvas is as simple as calling the toDataURL function. Make a special note that this function is called on the canvas object, and not on the rendering context object. The toDataURL function of the canvas object takes two optional parameters, namely, a string representing the MIME type of the output image, and a float between 0.0 and 1.0, representing the quality of the output image. If the output image type is anything other than "image/jpeg", the quality parameter is ignored.

   ctx.putImageData(pixels, 0, 0);

   var imgUrl_LQ = canvas.toDataURL("image/jpeg", 0.0);
   var out = new Image();
   out.src = imgUrl_LQ;
   document.body.appendChild(out);

   var imgUrl_HQ = canvas.toDataURL("image/jpeg", 1.0);
   var out = new Image();
   out.src = imgUrl_HQ;
   document.body.appendChild(out);

   var imgUrl_raw = canvas.toDataURL("image/png");
   var out = new Image();
   out.src = imgUrl_raw;
   document.body.appendChild(out);
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.38.176