You are previewing HTML5 Canvas.

HTML5 Canvas

Cover of HTML5 Canvas by Steve Fulton... Published by O'Reilly Media, Inc.
  1. HTML5 Canvas
    1. SPECIAL OFFER: Upgrade this ebook with O’Reilly
    2. A Note Regarding Supplemental Files
    3. Preface
      1. Running the Examples in the Book
      2. What You Need to Know
      3. How This Book Is Organized
      4. Conventions Used in This Book
      5. Using Code Examples
      6. We’d Like to Hear from You
      7. Safari® Books Online
      8. Acknowledgments
    4. 1. Introduction to HTML5 Canvas
      1. The Basic HTML Page
      2. Basic HTML We Will Use in This Book
      3. The Document Object Model (DOM) and Canvas
      4. JavaScript and Canvas
      5. HTML5 Canvas “Hello World!”
      6. Debugging with Console.log
      7. The 2D Context and the Current State
      8. The HTML5 Canvas Object
      9. Another Example: Guess The Letter
      10. What’s Next
    5. 2. Drawing on the Canvas
      1. The Basic File Setup for This Chapter
      2. The Basic Rectangle Shape
      3. The Canvas State
      4. Using Paths to Create Lines
      5. Advanced Path Methods
      6. Compositing on the Canvas
      7. Simple Canvas Transformations
      8. Filling Objects with Colors and Gradients
      9. Filling Shapes with Patterns
      10. Creating Shadows on Canvas Shapes
      11. What’s Next
    6. 3. The HTML5 Canvas Text API
      1. Displaying Basic Text
      2. Setting the Text Font
      3. Text and the Canvas Context
      4. Text with Gradients and Patterns
      5. Width, Height, Scale, and toDataURL() Revisited
      6. Final Version of Text Arranger
      7. What’s Next
    7. 4. Images on the Canvas
      1. The Basic File Setup for This Chapter
      2. Image Basics
      3. Simple Cell-Based Sprite Animation
      4. Advanced Cell-Based Animation
      5. Applying Rotation Transformations to an Image
      6. Creating a Grid of Tiles
      7. Zooming and Panning an Image
      8. Pixel Manipulation
      9. Copying from One Canvas to Another
      10. What’s Next
    8. 5. Math, Physics, and Animation
      1. Moving in a Straight Line
      2. Bouncing Off Walls
      3. Curve and Circular Movement
      4. Simple Gravity, Elasticity, and Friction
      5. Easing
      6. What’s Next?
    9. 6. Mixing HTML5 Video and Canvas
      1. HTML5 Video Support
      2. Converting Video Formats
      3. Basic HTML5 Video Implementation
      4. Preloading Video in JavaScript
      5. Video and the Canvas
      6. Video on the Canvas Examples
      7. Animation Revisited: Moving Videos
      8. What’s Next?
    10. 7. Working with Audio
      1. The Basic <audio> Tag
      2. Audio Formats
      3. Audio Tag Properties, Functions, and Events
      4. Playing a Sound with No Audio Tag
      5. Creating a Canvas Audio Player
      6. Case Study in Audio: Space Raiders Game
      7. What’s Next
    11. 8. Canvas Game Essentials
      1. Why Games in HTML5?
      2. Our Basic Game HTML5 File
      3. Our Game’s Design
      4. Game Graphics: Drawing with Paths
      5. Animating on the Canvas
      6. Applying Transformations to Game Graphics
      7. Game Graphic Transformations
      8. Game Object Physics and Animation
      9. A Basic Game Framework
      10. Putting It All Together
      11. The player Object
      12. Geo Blaster Game Algorithms
      13. The Geo Blaster Basic Full Source
      14. Rock Object Prototype
      15. What’s Next
    12. 9. Combining Bitmaps and Sound
      1. Geo Blaster Extended
      2. Creating a Dynamic Tile Sheet at Runtime
      3. A Simple Tile-Based Game
      4. What’s Next
    13. 10. Mobilizing Games with PhoneGap
      1. Going Mobile!
      2. Creating the iOS Application with PhoneGap
      3. Beyond the Canvas
      4. What’s Next
    14. 11. Further Explorations
      1. 3D with WebGL
      2. Multiplayer Applications with ElectroServer 5
      3. Conclusion
    15. Index
    16. About the Authors
    17. Colophon
    18. SPECIAL OFFER: Upgrade this ebook with O’Reilly
O'Reilly logo

Video on the Canvas Examples

In the last section, we learned that the video playing on the canvas and the video embedded with the <video> tag are, in fact, the same video. It took a lot more code to play the video on the canvas than it did to embed and play the video in JavaScript. So, this begs the question: why load video onto the canvas at all?

Well, sometimes simply displaying a video and playing it is not enough. You might want events to occur as the video is playing, or perhaps you want to use transformations on it, use it in a game, create custom video controls, or animate it and move it on the canvas.

The following five examples will show you in very specific detail why the canvas can be an exciting way to display video.

Using the currentTime Property to Create Video Events

The first way we will use video in conjunction with Canvas is to use the currentTime property of a playing video to trigger events. Recall that the currentTime property is updated as the video plays, and it shows the video’s elapsed running time.

For our example, we are going to create a dynamic object in JavaScript containing the following properties:

time

The elapsed time to trigger the event

message

A text message to display on the canvas

x

The x position of the text message

y

The y position of the text message

First, we will create an array of these objects and place them into a variable named messages. We will then create four events (messages that will appear) that will take place at the elapsed currentTime of 0, 1, 4, and 8 seconds:

var messages = new Array();
   messages[0] = {time:0,message:"", x:0 ,y:0};
   messages[1] = {time:1,message:"This Is Muir Beach!", x:90 ,y:200};
   messages[2] = {time:4,message:"Look At Those Waves!", x:240 ,y:240};
   messages[3] = {time:8,message:"Look At Those Rocks!", x:100 ,y:100};

To display the messages, we will call a for:next loop inside our drawScreen() function. Inside the loop, we test each message in the messages array to see whether the currentTime property of the video is greater than the time property of the message. If so, we know that it is OK to display the message. We then display the message on the canvas using the fillStyle property and fillText() function of the Canvas context, producing the results shown in Figure 6-8:

for (var i = 0; i < messages.length ; i++) {
         var tempMessage = messages[i];
         if (videoElement.currentTime > tempMessage.time) {
            context.font = "bold 14px sans";
            context.fillStyle = "#FFFF00";
            context.fillText  (tempMessage.message,  tempMessage.x ,tempMessage.y);
         }
      }
Canvas video displaying text overlay events

Figure 6-8. Canvas video displaying text overlay events

Of course, this is a very simple way to create events. The various text messages will not disappear after others are created, but that is just a small detail. The point of this exercise is that, with code like this, you could do almost anything with a running video. You could pause the video, show an animation, and then continue once the animation is done. Or you could pause to ask the user for input and then load a different video. Essentially, you can make the video completely interactive in any way you choose. The model for these events could be very similar to the one we just created.

Example 6-8 provides the full code listing for this application.

Example 6-8. Creating simple video events

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH6EX8: Creating Simple Video Events</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
var videoElement;
var videoDiv;
function eventWindowLoaded() {

   videoElement = document.createElement("video");
   videoDiv = document.createElement('div');
   document.body.appendChild(videoDiv);
   videoDiv.appendChild(videoElement);
   videoDiv.setAttribute("style", "display:none;");
   var videoType = supportedVideoFormat(videoElement);
   if (videoType == "") {
      alert("no video support");
      return;
   }
   videoElement.setAttribute("src", "muirbeach." + videoType);
   videoElement.addEventListener("canplaythrough",videoLoaded,false);

}
function supportedVideoFormat(video) {
   var returnExtension = "";
   if (video.canPlayType("video/webm") =="probably" || 
       video.canPlayType("video/webm") == "maybe") {
         returnExtension = "webm";
   } else if(video.canPlayType("video/mp4") == "probably" || 
       video.canPlayType("video/mp4") == "maybe") {
         returnExtension = "mp4";
   } else if(video.canPlayType("video/ogg") =="probably" || 
       video.canPlayType("video/ogg") == "maybe") {
         returnExtension = "ogg";
   }

   return returnExtension;

}

function canvasSupport () {
     return Modernizr.canvas;
}


function videoLoaded() {
   canvasApp();

}

function canvasApp() {

  if (!canvasSupport()) {
          return;
        }

  function  drawScreen () {

      //Background
      context.fillStyle = '#ffffaa';
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = '#000000';
      context.strokeRect(5,  5, theCanvas.width−10, theCanvas.height−10);
      //video
      context.drawImage(videoElement , 85, 30);
      // Text
      context.fillStyle = "#000000";
      context.font = "10px sans";
      context.fillText  ("Duration:" + videoElement.duration,  10 ,280);
      context.fillText  ("Current time:" + videoElement.currentTime,  260 ,280);
      context.fillText  ("Loop: " + videoElement.loop,  10 ,290);
      context.fillText  ("Autoplay: " + videoElement.autoplay,  80 ,290);
      context.fillText  ("Muted: " + videoElement.muted,  160 ,290);
      context.fillText  ("Controls: " + videoElement.controls,  240 ,290);
      context.fillText  ("Volume: " + videoElement.volume,  320 ,290);

      //Display Message
      for (var i =0; i < messages.length ; i++) {
         var tempMessage = messages[i];
         if (videoElement.currentTime > tempMessage.time) {
            context.font = "bold 14px sans";
            context.fillStyle = "#FFFF00";
            context.fillText  (tempMessage.message,  tempMessage.x ,tempMessage.y);
         }
      }

   }

   var messages = new Array();
   messages[0] = {time:0,message:"", x:0 ,y:0};
   messages[1] = {time:1,message:"This Is Muir Beach!", x:90 ,y:200};
   messages[2] = {time:4,message:"Look At Those Waves!", x:240 ,y:240};
   messages[3] = {time:8,message:"Look At Those Rocks!", x:100 ,y:100};

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");
   videoElement.play();

   setInterval(drawScreen, 33);

}

</script>

</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">

<canvas id="canvasOne" width="500" height="300">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

Canvas Video Transformations: Rotation

Showing a static video on the screen is one thing, but transforming it on the screen using alpha transparency and rotations is quite another. These types of transformations can be easily applied to video on the canvas in much the same way as you would apply them to an image or a drawing object.

In this example, we will create a video that rotates clockwise. To achieve this effect, we first create a variable, rotation, which we will use to hold the current values of the rotation property that we will apply to the video. We create this variable outside of the drawScreen() function, inside canvasApp():

var rotation = 0;

The drawScreen() function is where all the real action takes place for this example. First, we need to save the current canvas context so we can restore it after we perform the transformation. We covered this in depth in Chapter 2, but here’s a quick refresher. Transformations on the canvas are global in nature, which means they affect everything. Since the canvas works in immediate mode, there is no stack of objects to manipulate. Instead, we need to save the canvas context before the transformation, apply the transformation, and then restore the saved context afterward.

First, we save it:

context.save();

Next, we reset the context transformation to the identity, which clears anything that was set previously:

context.setTransform(1,0,0,1,0,0);

Then we need to set up some variables that will be used for the rotation calculation. The x and y variables set the upper-left location of the video on the canvas. The videoWidth and videoHeight variables will be used to help rotate the video from the center:

var x = 100;
var y = 100;
var videoWidth=320;
var videoHeight=240;

Now it is time to use the rotation variable, which represents the angle that we rotated the video on the canvas. It starts at 0, and we will increase it every time drawScreen() is called. However, the context.rotate() method requires an angle to be converted to radians when passed as its lone parameter. The following line of code converts the value in the rotation variable to radians, and stores it in a variable named angleInRadians:

var angleInRadians = rotation * Math.PI / 180;

We need to find the video’s center on the canvas so we can start our rotation from that point. We find the x value by taking our videoX variable and adding half the width of the video. We find the y value by taking our videoY variable and adding half the height of the video. We supply both of those values as parameters to the context.translate() function so the rotation will begin at that point. We need to do this because we are not rotating the video object—we are rotating the entire canvas in relation to the displayed video:

context.translate(x+.5*videoWidth, y+.5*videoHeight);

The rest of the code is really straightforward. First, we call the rotate() function of the context, passing our angle (converted to radians) to perform the rotation:

context.rotate(angleInRadians);

Then we call drawImage(), passing the video object, and the x,y positions of where we want the video to be displayed. This is a bit tricky but should make sense. Since we used the context.translate() function to move to the center of the video, we now need to place it in the upper-left corner. To find that corner, we need to subtract half the width to find the x position, and half the height to find the y position:

context.drawImage(videoElement ,-.5*videoWidth, -.5*videoHeight);

Finally, we restore the canvas we saved before the transformation started, and we update the rotation variable so that we will have a new angle on the next call to drawScreen():

context.restore();
rotation++;

Now the video should rotate at 1 degree clockwise per call to drawScreen() while fading onto the canvas. You can easily increase the speed of the rotation by changing the value that you input for the rotation variable in the last line in the drawScreen() function.

Here is the code for the final drawScreen() function for this example:

function  drawScreen () {

      //Background
      context.fillStyle = '#ffffaa';
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = '#000000';
      context.strokeRect(5,  5, theCanvas.width−10, theCanvas.height−10);
      //video
      //*** Start rotation calculation
      context.save();
      context.setTransform(1,0,0,1,0,0); 

      var angleInRadians = rotation * Math.PI / 180;
      var x = 100;
      var y = 100;
      var videoWidth=320;
      var videoHeight=240;
      context.translate(x+.5*videoWidth, y+.5*videoHeight); 
      context.rotate(angleInRadians);
      //****
      context.drawImage(videoElement ,-.5*videoWidth, -.5*videoHeight);
      //*** restore screen
      context.restore();
      rotation++;
      //***
}

Figure 6-9 shows what the video will look like when rotating on the canvas. You can see the full code for this in Example 6-9.

Canvas video rotation

Figure 6-9. Canvas video rotation

Example 6-9. Rotating a video

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH6EX9: Video Rotation Transform</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
var videoElement;
var videoDiv;
function eventWindowLoaded() {

   videoElement = document.createElement("video");
   videoDiv = document.createElement('div');
   document.body.appendChild(videoDiv);
   videoDiv.appendChild(videoElement);
   videoDiv.setAttribute("style", "display:none;");
   var videoType = supportedVideoFormat(videoElement);
   if (videoType == "") {
      alert("no video support");
      return;
   }
   videoElement.setAttribute("src", "muirbeach." + videoType);
   videoElement.addEventListener("canplaythrough",videoLoaded,false);

}

function supportedVideoFormat(video) {
   var returnExtension = "";
   if (video.canPlayType("video/webm") =="probably" || 
       video.canPlayType("video/webm") == "maybe") {
         returnExtension = "webm";
   } else if(video.canPlayType("video/mp4") == "probably" || 
       video.canPlayType("video/mp4") == "maybe") {
         returnExtension = "mp4";
   } else if(video.canPlayType("video/ogg") =="probably" || 
       video.canPlayType("video/ogg") == "maybe") {
         returnExtension = "ogg";
   }

   return returnExtension;

}

function canvasSupport () {
     return Modernizr.canvas;
}


function videoLoaded() {
   canvasApp();

}

function canvasApp() {

  if (!canvasSupport()) {
          return;
        }

   //*** set rotation value
   var rotation = 0;
   //***

  function  drawScreen () {

      //Background
      context.fillStyle = '#ffffaa';
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = '#000000';
      context.strokeRect(5,  5, theCanvas.width-10, theCanvas.height-10);
      //video
      //*** Start rotation calculation
      context.save();
      context.setTransform(1,0,0,1,0,0); 

      var angleInRadians = rotation * Math.PI / 180;
      var x = 100;
      var y = 100;
      var videoWidth=320;
      var videoHeight=240;
      context.translate(x+.5*videoWidth, y+.5*videoHeight); 
      context.rotate(angleInRadians);
      //****
      context.drawImage(videoElement ,-.5*videoWidth, -.5*videoHeight);
      //*** restore screen
      context.restore();
      rotation++;
      //***

   }

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");
   videoElement.setAttribute("loop", "true");     videoElement.play();
   setInterval(drawScreen, 33);

}

</script>

</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">

<canvas id="canvasOne" width="500" height="500">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

Canvas Video Puzzle

Now we arrive at the most involved example of this section. We are going to create a puzzle game based on the video we have displayed on the canvas, illustrated in Figure 6-10. Here are the steps showing how the game will operate:

  1. We will load the video onto the canvas but not display it.

  2. We will decide how many parts we want to be in our puzzle.

  3. We will create a board array that holds all the puzzle pieces.

  4. The pieces will be displayed in a 4×4 grid.

  5. We will randomize the pieces on the board to mix up the puzzle.

  6. We will add an event listener for the mouse button.

  7. We will set an interval to call drawScreen().

  8. We will wait for the user to click a puzzle piece.

  9. While we are waiting, the various parts of the video will play just as though they were one video.

  10. When a user clicks a puzzle piece, it will highlight in yellow.

  11. If the user has selected two pieces, we will swap their positions.

  12. The user will attempt to put the puzzle back together so she can see the video as it was created.

Setting up the game

To start, we are going to set up some variables that will define the game’s playfield. Here is a rundown of the variables and how they will be used:

rows

The numbers of rows in the grid of puzzle pieces

cols

The number of columns in the grid of puzzle pieces

xPad

The space, in pixels, between each column

yPad

The space, in pixels, between each row

startXOffset

The number of pixels from the left of the canvas to the location where we will start drawing the grid of puzzle pieces

startYOffset

The number of pieces from the top of the canvas to the location where we will start drawing the grid of puzzle pieces

partWidth

The width of each puzzle piece

partHeight

The height of each puzzle piece

board

A two-dimensional array that holds the puzzle pieces

Video puzzle

Figure 6-10. Video puzzle

The following code includes values for each variable:

var rows = 4;
var cols = 4;
var xPad = 10;
var yPad = 10;
var startXOffset = 10;
var startYOffset = 10;
var partWidth = videoElement.width/cols;
var partHeight = videoElement.height/rows;
var board = new Array();

Next we need to initialize the board array and fill it with some dynamic objects that represent each piece of the puzzle. We loop through the number of cols in the board and create rows amount of dynamic objects in each one. The dynamic objects we are creating have these properties:

finalCol

The final column-resting place of the piece when the puzzle is complete. We use this value to figure out what part of the video cut out to make this piece.

finalRow

The final row-resting place of the piece when the puzzle is complete. We use this value to figure out what part of the video cut out to make this piece.

selected

A Boolean that is initially set to false. We will use this to see whether we should highlight a piece or switch two pieces when the user clicks a piece.

Notice that we use two nested for:next loops to fill the board array with these objects. Familiarize yourself with this construct because we use it many times in this game. Two nested loops used like this are particularly useful for games and apps that require a 2D grid in order to be displayed and manipulated:

for (var i = 0; i < cols; i++) {
      board[i] = new Array();
      for (var j =0; j < rows; j++) {
         board[i][j] = { finalCol:i,finalRow:j,selected:false };
      }
}

Now that we have the board array initialized, we call randomizeBoard() (we will discuss this function shortly), which mixes up the puzzle by randomly placing the pieces on the screen. We finish the setup section of the game by adding an event listener for the mouseup event (when the user releases the mouse button), and by setting an interval to call drawScreen() every 33 milliseconds:

board = randomizeBoard(board);

theCanvas.addEventListener("mouseup",eventMouseUp, false);

setInterval(drawScreen, 33);

Randomizing the puzzle pieces

The randomizeBoard() function requires you to pass in the board variable so we can operate on it. We’ve set up the function this way so it will be portable to other applications.

To randomize the puzzle pieces, we first need to set up an array named newBoard that will hold the randomized puzzle pieces. newBoard will be what we call a parallel array. Its purpose is to become the original array—but randomized. We then create a local cols variable and initialize it to the length of the board array that was passed in to the function, and a local rows variable, initialized to the length of the first column—board[0]—in the array. This works because all of our rows and columns are the same length, so the number of rows in the first column is the same as all the others. We now have the building blocks required to randomize the pieces:

function randomizeBoard(board) {
    var newBoard = new Array();
    var cols = board.length;
    var rows = board[0].length

Next, we loop through every column and row, randomly choosing a piece from the board array and moving it into newBoard:

      for (var i = 0; i < cols; i++) {

Note

We use two nested for:next loops here once again.

Every time we come to an iteration of the outer nested loop, we create a new array that we will fill up in the second nested loop. Then we drop into that nested loop. The found variable will be set to true when we have found a random location to place the piece in the newBoard array. The rndRow and rndCol variables hold the random values we will create to try and find a random location for the puzzle pieces:

newBoard[i] = new Array();
         for (var j =0; j < rows; j++) {
            var found = false;
            var rndCol = 0;
            var rndRow = 0;

Now we need to find a location in newBoard in which to put the puzzle piece from the board array. We use a while() loop that continues to iterate as long as the found variable is false. To find a piece to move, we randomly choose a row and column, and then use them to see whether that space (board[rndCol][rndRow]) is set to false. If it is not false, we have found a piece to move to the newBoard array. We then set found equal to true so we can get out of the while() loop and move to the next space in newBoard that we need to fill:

            while (!found) {
               var rndCol = Math.floor(Math.random() * cols);
               var rndRow = Math.floor(Math.random() * rows);
               if (board[rndCol][rndRow] != false) {
                  found = true;
               }
            }

Finally, we move the piece we found in board to the current location we are filling in newBoard. Then, we set the piece in the board array to false so that when we test for the next piece, we won’t try to use the same piece we just found. When we are done filling up newBoard, we return it as the newly randomized array of puzzle pieces:

            newBoard[i][j] = board[rndCol][rndRow];
            board[rndCol][rndRow] = false;
         }

      }

      return newBoard;

   }

Drawing the screen

The drawScreen() function is the heart of this application. It is called on an interval, and then used to update the video frames and to draw the puzzle pieces on the screen. A good portion of drawScreen() looks like applications we have built many times already in this book. When it begins, we draw the background and a bounding box on the screen:

function drawScreen () {

      //Background
      context.fillStyle = '#303030';
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = '#FFFFFF';
      context.strokeRect(5,  5, theCanvas.width−10, theCanvas.height−10);

However, the primary work of this function is—you guessed it—another set of two nested for:next loops that draw the puzzle pieces onto the canvas. This set needs to do three things:

  1. Draw a grid of puzzle pieces on the canvas based on their placement in the board two-dimensional array.

  2. Find the correct part of the video to render for each piece based on the finalCol and finalRow properties we set in the dynamic object for each piece.

  3. Draw a yellow box around the piece that has its selected property set to true.

We start our loop by finding the x and y (imageX, imageY) locations to “cut” the puzzle piece from the video object. We do this by taking the finalRow and finalCol properties of the dynamic piece objects we created, and multiplying them by the partWidth and partHeight, respectively. We then have the origin point (top-left x and y locations) for the piece of the video to display:

for (var c = 0; c < cols; c++) {
   for (var r = 0; r < rows; r++) {

      var tempPiece = board[c][r];
      var imageX = tempPiece.finalCol*partWidth;
      var imageY = tempPiece.finalRow*partHeight;

Now that we know the origin point of the video we will display for a particular piece of the puzzle, we need to know where it will be placed on the canvas. While the code below might look confusing, it’s really just simple arithmetic. To find the x location (placeX) of a piece, multiply the partWidth times the current iterated column (c), then add the current iterated column multiplied by the xPad (the number of pixels between each piece), and add the startXOffset, which is the x location of the upper-left corner of the entire board of pieces. Finding placeY is very similar, but you use the current row (r), yPad, and partHeight in the calculation:

var placeX = c*partWidth+c*xPad+startXOffset;
var placeY = r*partHeight+r*yPad+startYOffset;

Now it’s time to draw the piece on the canvas. We need to “cut” out the part of the video that we will display for each piece of the puzzle (we won’t actually cut anything though). We will again use the drawImage() function, as we have many other times already. However, now we use the version of drawImage() that accepts nine parameters:

videoElement

The image that we are going to display; in this case, it is the video.

imageX

The x location of the upper-right order of the part of the image to display.

imageY

The y location of the upper-right order of the part of the image to display.

partWidth

The width from the x location of the rectangle to display.

partHeight

The height from the y location of the rectangle to display.

placeX

The x location to place the image on the canvas.

placeY

The y location to place the image on the canvas.

partWidth

The width of the image as displayed on the canvas.

partHeight

The height of the image as displayed on the canvas.

We’ve already discussed how we calculated most of these values, so it is just a matter of knowing the drawImage() API function and plugging in the variables:

context.drawImage(videoElement, imageX, imageY, partWidth, partHeight, 
    placeX, placeY, partWidth, partHeight);

There is one last thing we are going to do in this function. If a puzzle piece is marked as “selected” (the selected Boolean property is true), we will draw a yellow box around the piece:

         if (tempPiece.selected) {

            context.strokeStyle = '#FFFF00';
            context.strokeRect( placeX,  placeY, partWidth, partHeight);

         }
      }
   }

}

Detecting mouse interactions and the canvas

Recall that back in the canvasApp() function, we set an event listener for the mouseup action with the event handler function set to eventMouseUp. We now need to create that function:

theCanvas.addEventListener("mouseup",eventMouseUp, false);

The first thing we do in the eventMouseUp() function is test to find the x and y locations of the mouse pointer when the button was pressed. We will use those coordinates to figure out whether the user clicked on any of the puzzle pieces.

Since some browsers support the layerX/layerY properties of the event object, and others support the offsetX/offsetY properties, we need to support both. No matter which one is set, we will use those properties to set our mouseX and mouseY variables to the x and y locations of the mouse pointer:

function eventMouseUp(event) {

    var mouseX;
    var mouseY;
    var pieceX;
    var pieceY;
    if ( event.layerX ||  event.layerX == 0) { //Firefox
        mouseX = event.layerX ;
        mouseY = event.layerY;
      } else if (event.offsetX || event.offsetX == 0) { // Opera
        mouseX = event.offsetX;
        mouseY = event.offsetY;
      }

Creating hit test point-style collision detection

Now that we know where the user clicked, we need to test whether that location “hits” any of the puzzle pieces. If so, we set the selected property of that piece to true. What we are going to perform is a simple hit test point-style hit detection. It will tell us whether the x,y position (point) of the mouse is inside (hits) any one of the puzzle pieces when the mouse button is clicked.

First, we create a local variable named selectedList that we will use when we need to swap the pieces in the board array. Next, we will use a set of two nested for:next loops to traverse through all the pieces in the board array. Inside the for:next loops, the first thing we do is find the top-left corner x and y points of the current piece pointed to by board[c][r]. We calculate those values and put them into the placeX and placeY variables:

      var selectedList= new Array();
      for (var c = 0; c < cols; c++) {

         for (var r =0; r < rows; r++) {
            pieceX = c*partWidth+c*xPad+startXOffset;
            pieceY = r*partHeight+r*yPad+startYOffset;

Next, we use those calculated values to test for a hit test point collision. We do this with a semicomplicated if:then statement that tests the following four conditions simultaneously:

mouseY >= pieceY

The mouse pointer lies lower than or equal to the top of the piece.

mouseY <= pieceY+partHeight

The mouse pointer lies above or equal to the bottom of the piece.

mouseX >= pieceX

The mouse pointer lies to the right or equal to the left side of the piece.

mouseX <= pieceX+partWidth

The mouse pointer lies to the left or equal to the right side of the piece.

All of the above conditions must evaluate to true for a hit to be registered on any one piece on the board:

if ( (mouseY >= pieceY) && (mouseY <= pieceY+partHeight) && (mouseX >= pieceX) &&
     (mouseX <= pieceX+partWidth) ) {

If all these conditions are true, we set the selected property of the piece object to true if it was already false, or we set it to false if it was already true. This allows the user to “deselect” the selected piece if he has decided not to move it:

  if ( board[c][r].selected) {
        board[c][r].selected = false;

  } else {
        board[c][r].selected = true;

  }
}

At the end of the nested for:next loop, we make sure to test each piece to see whether its selected property is true. If so, we push it into the selectedList local array so we can perform the swap operation on the pieces:

  if (board[c][r].selected) {
        selectedList.push({col:c,row:r})
  }

  }

}

Swapping two elements in a two-dimensional array

Now we need to test to see whether two pieces have been marked as selected. If so, we swap the positions of those pieces. In this way, it appears that the player is clicking on puzzle pieces and changing their locations to try to solve the puzzle.

To achieve the swap, we use a classic three-way swap programming construct utilizing a temporary variable, tempPiece1, as a placeholder for the values we are going to swap. First, we need to create a couple variables to hold the selected pieces. We will use selected1 and selected2 for that purpose. Next, we move the reference to the piece represented by selected1 into the tempPiece1 variable:

if (selectedList.length == 2) {
        var selected1 = selectedList[0];
        var selected2 = selectedList[1];
        var tempPiece1 = board[selected1.col][selected1.row];
        board[selected1.col][selected1.row] =

Next, we move the piece referenced by selected2 to the location in the board array of the piece represented by selected1 (the first swap). Then we apply the piece referenced in selected1 to the position represented by selected2 (the second swap). Finally, now that they are swapped, we make sure to set the selected properties of both pieces to false:

 board[selected2.col][selected2.row];
         board[selected2.col][selected2.row] = tempPiece1;
         board[selected1.col][selected1.row].selected = false;
         board[selected2.col][selected2.row].selected = false;
      }

   }

Note

This part of the function works because we have limited the number of pieces that can be selected to 2. For a game such as poker, which requires the player to select five cards, you would use a slightly different algorithm that tests for 5 cards instead of 2, and then calculate the value of the hand.

Testing the game

Believe it or not, that is all the code we need to talk about—the rest you have seen many times before. Try running the game (CH6EX10.html). When it loads, you should see the video organized in a 16-piece grid. Each part of the video will be playing, just like one of those magic tricks where a woman appears to be separated into multiple boxes but her legs, arms, and head are still moving. In fact, this game is sort of like one of those magic tricks because, in reality, the video was never “cut” in any way. We simply display the parts of the video to make it appear to be cut into 16 independent, moving pieces that can be swapped to re-form the original video.

Example 6-10 shows the full code listing for the Video Puzzle application.

Example 6-10. Video puzzle

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH6EX10: Video Puzzle</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
var videoElement;
var videoDiv;
function eventWindowLoaded() {

   videoElement = document.createElement("video");
   videoDiv = document.createElement('div');
   document.body.appendChild(videoDiv);
   videoDiv.appendChild(videoElement);
   videoDiv.setAttribute("style", "display:none;");
   var videoType = supportedVideoFormat(videoElement);
   if (videoType == "") {
      alert("no video support");
      return;
   }
   videoElement.setAttribute("src", "muirbeach." + videoType);
   videoElement.addEventListener("canplaythrough",videoLoaded,false);

}

function supportedVideoFormat(video) {
   var returnExtension = "";
   if (video.canPlayType("video/webm") =="probably" || 
       video.canPlayType("video/webm") == "maybe") {
         returnExtension = "webm";
   } else if(video.canPlayType("video/mp4") == "probably" || 
       video.canPlayType("video/mp4") == "maybe") {
         returnExtension = "mp4";
   } else if(video.canPlayType("video/ogg") =="probably" || 
       video.canPlayType("video/ogg") == "maybe") {
         returnExtension = "ogg";
   }

   return returnExtension;

}

function canvasSupport () {
     return Modernizr.canvas;
}


function videoLoaded() {
   canvasApp();

}

function canvasApp() {

  if (!canvasSupport()) {
          return;
        }

  function  drawScreen () {

      //Background
      context.fillStyle = '#303030';
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = '#FFFFFF';
      context.strokeRect(5,  5, theCanvas.width−10, theCanvas.height−10);

      for (var c = 0; c < cols; c++) {
         for (var r = 0; r < rows; r++) {

            var tempPiece = board[c][r];
            var imageX = tempPiece.finalCol*partWidth;
            var imageY = tempPiece.finalRow*partHeight;
            var placeX = c*partWidth+c*xPad+startXOffset;
            var placeY = r*partHeight+r*yPad+startYOffset;
            //context.drawImage(videoElement , imageX, imageY, partWidth, partHeight);
            context.drawImage(videoElement, imageX, imageY, partWidth, partHeight, 
                placeX, placeY, partWidth, partHeight);
            if (tempPiece.selected) {

               context.strokeStyle = '#FFFF00';
               context.strokeRect( placeX,  placeY, partWidth, partHeight);

            }
         }
      }

   }

   function randomizeBoard(board) {
      var newBoard = new Array();
      var cols = board.length;
      var rows = board[0].length
      for (var i = 0; i < cols; i++) {
         newBoard[i] = new Array();
         for (var j =0; j < rows; j++) {
            var found = false;
            var rndCol = 0;
            var rndRow = 0;
            while (!found) {
               var rndCol = Math.floor(Math.random() * cols);
               var rndRow = Math.floor(Math.random() * rows);
               if (board[rndCol][rndRow] != false) {
                  found = true;
               }
            }

            newBoard[i][j] = board[rndCol][rndRow];
            board[rndCol][rndRow] = false;
         }

      }

      return newBoard;

   }

   function eventMouseUp(event) {

      var mouseX;
      var mouseY;
      var pieceX;
      var pieceY;
      if ( event.layerX ||  event.layerX == 0) { // Firefox
            mouseX = event.layerX ;
          mouseY = event.layerY;
        } else if (event.offsetX || event.offsetX == 0) { // Opera
          mouseX = event.offsetX;
          mouseY = event.offsetY;
        }
      var selectedList= new Array();
      for (var c = 0; c < cols; c++) {

         for (var r =0; r < rows; r++) {
            pieceX = c*partWidth+c*xPad+startXOffset;
            pieceY = r*partHeight+r*yPad+startYOffset;
            if ( (mouseY >= pieceY) && (mouseY <= pieceY+partHeight) && 
                 (mouseX >= pieceX) && (mouseX <= pieceX+partWidth) ) {

               if ( board[c][r].selected) {
                     board[c][r].selected = false;

               } else {
                     board[c][r].selected = true;

               }
            }
            if (board[c][r].selected) {
                  selectedList.push({col:c,row:r})
            }

         }

      }
      if (selectedList.length == 2) {
         var selected1 = selectedList[0];
         var selected2 = selectedList[1];
         var tempPiece1 = board[selected1.col][selected1.row];
         board[selected1.col][selected1.row] =  board[selected2.col][selected2.row];
         board[selected2.col][selected2.row] = tempPiece1;
         board[selected1.col][selected1.row].selected = false;
         board[selected2.col][selected2.row].selected = false;
      }

   }

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");
   videoElement.play();

   //Puzzle Settings

   var rows = 4;
   var cols = 4;
   var xPad = 10;
   var yPad = 10;
   var startXOffset = 10;
   var startYOffset = 10;
   var partWidth = videoElement.width/cols;
   var partHeight = videoElement.height/rows;
   //320×240
   partWidth = 80;
   partHeight = 60;
   var board = new Array();

   //Initialize Board

   for (var i = 0; i < cols; i++) {
         board[i] = new Array();
         for (var j =0; j < rows; j++) {
            board[i][j] = { finalCol:i,finalRow:j,selected:false };
         }
   }

   board = randomizeBoard(board);

   theCanvas.addEventListener("mouseup",eventMouseUp, false);

   setInterval(drawScreen, 33);

}

</script>

</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">

<canvas id="canvasOne" width="370" height="300">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

Creating Video Controls on the Canvas

One obvious use of the HTML5 Canvas video display functionality is to create custom video controls to play, pause, stop, etc. You may have already noticed that when a video is rendered on the canvas, it does not retain any of the HTML5 video controls. If you want to create controls on the canvas, you need to make them yourself. Thankfully, we have already learned most everything we need to do this—now we just have to put it all together.

Creating video buttons

We are going to use some video control buttons that were created specifically for this example. Figure 6-11 shows a tile sheet that consists of off and on states for play, pause, and stop. The top row images are the on state; the bottom row images are the off state.

Video control button tile sheet

Figure 6-11. Video control button tile sheet

Note

We don’t use the off state of the stop button in this application, but we included it in case you—the amazing reader and programmer that you are—want to use it later.

We will load this image dynamically onto the canvas, and then place each 32×32 button onto the canvas individually. We use the width and height to calculate which part of the image to display as a control.

Preloading the buttons

The first thing we need to do is preload the button tile sheet. Since we are already testing for the video to preload before we display the canvas, we need a slightly new strategy to preload multiple objects. For this example, we will use a counter variable named loadCount that we will increment each time we detect that an item has loaded. In conjunction with that variable, we will create another named itemsToLoad, which will hold the number of things we are preloading. For this app that number is two: the video and the tile sheet. These two variables are created outside of all functions at the top of our JavaScript:

var loadCount = 0;
var itemsToLoad = 2;

Along with videoElement and videoDiv, we also create another new variable, buttonSheet. This is a reference to the image we load that holds the graphical buttons we will use for the video player interface:

var videoElement;
var videoDiv;
var buttonSheet

We now must make some updates to our standard eventWindowLoaded() function that we have used for most of this chapter. First, we are going to change the canplay event handler for the video to a new function, itemLoaded:

videoElement.addEventListener("canplay",itemLoaded,false);

We used the canplay event instead of canplaythrough because, most of the time, a user wants to start watching a video as soon as enough data has been buffered to play, and not after the entire video has loaded.

Next, we need to load our tile sheet. We create a new Image object and set the src property to videobuttons.png, which is the file shown in Figure 6-11. We also set its onload event handler to itemLoaded, just like the video:

   buttonSheet = new Image();
   buttonSheet.src = "videobuttons.png";
   buttonSheet.onload = itemLoaded;
}

Finally, we create the itemLoaded() event handler function. When this function is called, we increment the loadCount variable and test it against the itemsToLoad variable.

Note

loadCount should never be greater than itemsToLoad if your application is running correctly. However, we find it safer to limit the use of the strict == test if possible. Why? Because if somehow, somewhere, something gets counted twice, the app will never load properly.

If it is equal to or greater than itemsToLoad, we call canvasApp() to start the application:

function itemLoaded() {
   loadCount++;
   if (loadCount >= itemsToLoad) {
      canvasApp();
   }
}

Placing the buttons

We need to set some variables in canvasApp() that will represent the locations of the three buttons we will display: play, pause, and stop. We start by specifying the standard button width and height as the variables bW and bH. All the images in the videobuttons.png tile sheet are 32×32 pixels, so we will set bW and bH accordingly. Then, we proceed to create variables that represent the x and y locations of each button: playX, playY, pauseX, pauseY, stopX, and stopY. We could use literal values; however, these variables will make a couple of the more complicated calculations easier to swallow:

var bW = 32;
var bH = 32;
var playX = 190;
var playY = 300;
var pauseX = 230;
var pauseY = 300;
var stopX = 270
var stopY = 300;

In the drawImage() function, we need to test for the current state of the playing video and render the buttons accordingly. For this application, we will use the paused state of the video object’s attribute to render the buttons properly in their “up” or “down” states.

When a video first loads on the page and is not yet playing, its paused attribute is set to true. When a video is playing, its paused attribute is set to false. Knowing this, we can create the actions for these simple buttons.

First, if we know that the video is not in a paused state, it must be playing, so we display the “down” version of the play button. The “down” position is in the second row on the tile sheet in Figure 6-11. The third parameter of the call to the drawImage() function is 32 because that is where the y position of the image we want to display starts on the tile sheet. If paused is true, it means the video is not playing, so we display the “up” version of the play button. It starts at y position 0:

if (!videoElement.paused) {
    context.drawImage(buttonSheet, 0,32,bW,bH,playX,playY,bW,bH); //Play Down

} else {
    context.drawImage(buttonSheet, 0,0,bW,bH,playX,playY,bW,bH); //Play up
}

Displaying the pause button is simply the opposite of play. If the video paused property is true, we display the “down” version of the pause button. If the video is playing, it means the pause property is false, so we display the “up” version. Notice that the second parameter is 32 because to display the pause buttons in the tile sheet, we need to skip over the play button and start at the x position of the pause button:

if (videoElement.paused) {
    context.drawImage(buttonSheet,  32,32,bW,bH,pauseX,pauseY,bW,bH); //down
} else {
    context.drawImage(buttonSheet,  32,0,bW,bH,pauseX,pauseY,bW,bH); // up
}

context.drawImage(buttonSheet,  64,0,bW,bH,stopX,stopY,bW,bH); // Stop up

Listening for the button presses

We also need to listen for the mouse button click. This process is very similar to how we accomplished much the same thing in the Video Puzzle application. First, back in the canvasApp() function, we set an event handler, eventMouseUp(), for the mouseup event:

theCanvas.addEventListener("mouseup",eventMouseUp, false);

The eventMouseUp() function works very similar to the same function we created earlier for Video Puzzle. First, we find the mouse pointer’s x and y positions based on the way the browser tracks those values, and we put those values into local mouseX and mouseY variables:

function eventMouseUp(event) {

      var mouseX;
      var mouseY;
   if ( event.layerX ||  event.layerX == 0) { // Firefox
          mouseX = event.layerX ;
          mouseY = event.layerY;
        } else if (event.offsetX || event.offsetX == 0) { // Opera
          mouseX = event.offsetX;
          mouseY = event.offsetY;
        }
      //Hit Play

Next, we test for a hit test point inside each button by checking the bounds (right, left, top, bottom) on the canvas to see whether the mouse pointer was over any of our buttons when it was clicked. If so, we detect a hit.

First, we test the play button. Notice that those variables we created to represent the upper-left x and y locations of the button (playX and playY) help us make this calculation. They also help us because the names of the buttons self-document what we are trying to accomplish in each test of this function.

If the play button has been clicked, and the video paused property is true, we call the play() function of the video to start playing:

//Hit Play
      if ( (mouseY >= playY) && (mouseY <= playY+bH) && (mouseX >= playX) &&
           (mouseX <= playX+bW) ) {
         if (videoElement.paused) {
            videoElement.play();

         }

If the stop button was clicked, we set the paused property of the video to true, and set the currentTime property to 0 so that the video will return to the first frame:

//Hit Stop

      if ( (mouseY >= stopY) && (mouseY <= stopY+bH) && (mouseX >= stopX) && 
           (mouseX <= stopX+bW) ) {

         videoElement.pause();
         videoElement.currentTime = 0;
      }

If the pause button is clicked and the paused property of the video is false, we call the pause() function of the video to—you guessed it—pause the video on the current frame. If the paused property is true, we call the play() function of the video so it will resume playing:

//Hit Pause
      if ( (mouseY >= pauseY) && (mouseY <= pauseY+bH) && (mouseX >= pauseX) && 
           (mouseX <= pauseX+bW) ) {

         if (videoElement.paused == false) {
            videoElement.pause();
         } else {
            videoElement.play();
         }

      }

Figure 6-12 shows what the canvas looks like when the video is displayed with controls.

Note

You will notice an odd relationship between the play and pause buttons. When one is “on,” the other is “off.” This is because we have only one property to look at: paused. There is a property named playing that exists in the HTML5 specification, but it did not work in all browsers, so we only used paused. In reality, you could have only one button and swap out the play or paused graphic depending on the paused state. That would make these controls work more like the default HTML video controls.

Canvas video player buttons

Figure 6-12. Canvas video player buttons

Example 6-11 shows the full source code for this application.

Example 6-11. Canvas video with controls

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH6EX11: Canvas Video With Controls</title>
<script src="modernizr-1.6.min.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
var loadCount= 0;
var itemsToLoad = 2;
var videoElement;
var videoDiv;
var buttonSheet

function eventWindowLoaded() {
   videoElement = document.createElement("video");
   videoDiv = document.createElement('div');
   document.body.appendChild(videoDiv);
   videoDiv.appendChild(videoElement);
   videoDiv.setAttribute("style", "display:none;");
   var videoType = supportedVideoFormat(videoElement);
   if (videoType == "") {
      alert("no video support");
      return;
   }
   videoElement.setAttribute("src", "muirbeach." + videoType);
   videoElement.addEventListener("canplay",itemLoaded,false);
   buttonSheet = new Image();
   buttonSheet.onload = itemLoaded;
   buttonSheet.src = "videobuttons.png";
}

function supportedVideoFormat(video) {
   var returnExtension = "";
   if (video.canPlayType("video/webm") =="probably" || 
       video.canPlayType("video/webm") == "maybe") {
         returnExtension = "webm";
   } else if(video.canPlayType("video/mp4") == "probably" || 
       video.canPlayType("video/mp4") == "maybe") {
         returnExtension = "mp4";
   } else if(video.canPlayType("video/ogg") =="probably" || 
       video.canPlayType("video/ogg") == "maybe") {
         returnExtension = "ogg";
   }

   return returnExtension;

}

function canvasSupport () {
     return Modernizr.canvas;
}
function itemLoaded() {
   loadCount++;
   if (loadCount >= itemsToLoad) {
      canvasApp();
   }

}
function canvasApp() {

   if (!canvasSupport()) {
          return;
        }

  function  drawScreen () {

      //Background
      context.fillStyle = '#ffffaa';
      context.fillRect(0, 0, theCanvas.width, theCanvas.height);
      //Box
      context.strokeStyle = '#000000';
      context.strokeRect(5,  5, theCanvas.width−10, theCanvas.height−10);
      //video
      context.drawImage(videoElement , 85, 30);
      //Draw Buttons
      //Play

      if (!videoElement.paused) {
         context.drawImage(buttonSheet, 0,32,bW,bH,playX,playY,bW,bH); //Play Down

      } else {
         context.drawImage(buttonSheet, 0,0,bW,bH,playX,playY,bW,bH); //Play up

      }

      if (videoElement.paused) {
         context.drawImage(buttonSheet,  32,32,bW,bH,pauseX,pauseY,bW,bH); // Pause down
      } else {
         context.drawImage(buttonSheet,  32,0,bW,bH,pauseX,pauseY,bW,bH); // Pause up
      }

      context.drawImage(buttonSheet,  64,0,bW,bH,stopX,stopY,bW,bH); // Stop up

   }

      function eventMouseUp(event) {

      var mouseX;
      var mouseY;

      if ( event.layerX ||  event.layerX == 0) { // Firefox
            mouseX = event.layerX ;
          mouseY = event.layerY;
        } else if (event.offsetX || event.offsetX == 0) { // Opera
          mouseX = event.offsetX;
          mouseY = event.offsetY;
        }

      //Hit Play
      if ( (mouseY >= playY) && (mouseY <= playY+bH) && (mouseX >= playX) &&
           (mouseX <= playX+bW) ) {
         if (videoElement.paused) {
            videoElement.play();

         }

      }

      //Hit Stop

      if ( (mouseY >= stopY) && (mouseY <= stopY+bH) && (mouseX >= stopX) &&
            (mouseX <= stopX+bW) ) {

         videoElement.pause();
         videoElement.currentTime = 0;
      }

      //Hit Pause
      if ( (mouseY >= pauseY) && (mouseY <= pauseY+bH) && (mouseX >= pauseX) && 
           (mouseX <= pauseX+bW) ) {

         if (videoElement.paused == false) {
            videoElement.pause();
         } else {
            videoElement.play();
         }

      }

   }

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");

   var bW = 32;
   var bH = 32;
   var playX = 190;
   var playY = 300;
   var pauseX = 230;
   var pauseY = 300;
   var stopX = 270
   var stopY = 300;


   theCanvas.addEventListener("mouseup",eventMouseUp, false);

   setInterval(drawScreen, 33);
}

</script>

</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">

<canvas id="canvasOne" width="500" height="350">
 Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

The best content for your career. Discover unlimited learning on demand for around $1/day.