HTML5 and Flash development may have a lot in common, but as a Flash developer I've still found it to be a monumental task to re-learn my old skills in HTML5. In this tutorial, I'll show you how I created an animated menu and screen transition for an HTML5 shoot-'em-up game.
Final Result Preview
Take a look at the result we'll be working towards:
Note the scrolling background, the ships that appear and rotate either side of each menu item, and the way the screen fades to black when you select an option.
Introduction
HTML5 and JavaScript are similar to ActionScript in many ways; there's a lot of overlap in syntax, event listeners and methods. However there are some very distinct differences that I will cover in this tutorial:
- Drawing Shapes
- Drawing Images
- Using Intervals
- Animating
- Mouse Events
- Adding Support For Multiple Browsers
Something to note is that this tutorial mainly uses images which can be downloaded with the source or you may use your own images if you please (you will need to know the widths and heights).
Step 1: Setting Up
The first thing we will need to do is add the <canvas> element inside the body of an HTML file, so create one called ShootEmUp.html
and insert the following:
<!DOCTYPE html> <html> <head> <title>Shoot 'Em Up</title> </head> <body> <canvas id="myCanvas" width="480" height="320"> <p>Your browser does not support HTML5!</p> </canvas> </body> </html>
The highlighted lines insert the canvas element, which will render our actual menu. See this tutorial for a guide to canvas from scratch.
It's already almost time to begin coding JavaScript! We have two options as to where the code can go; It can be written inside the HTML within <script> tags or it can be written in an external file. To keep things simple I will write all the code below the <canvas> element. But feel free to use an external file if you prefer; just remember to source it in.
<script type="text/javascript"> // Javascript Goes Here </script>
Our next step will be to create four variables to reference the canvas element easily.
var canvas = document.getElementById("myCanvas"); var context = canvas.getContext("2d"); var width = canvas.getAttribute('width'); var height = canvas.getAttribute('height');
We first referenced the myCanvas
variable and set it to point to the HTML canvas element. Another variable named context
was created to get the canvas dimensionality (2D). Similar to Flash we are creating the final two variables, width
and height
, to simplify the process of accessing the canvas's width and height properties.
Step 2: Loading and Drawing Images
Like in ActionScript we are going to create instances of our images.
var bgImage = new Image(); var logoImage = new Image(); var playImage = new Image(); var instructImage = new Image(); var settingsImage = new Image(); var creditsImage = new Image(); var shipImage = new Image();
We are missing a crucial piece of code for each instance - the source path! I saved all the images in the folder 'Images' within the same directory as the HTML file, so:
shipImage.src = "Images/ship.png"; bgImage.src = "Images/Background.png"; logoImage.src = "Images/logo.png"; playImage.src = "Images/play.png"; instructImage.src = "Images/instructions.png"; settingsImage.src = "Images/settings.png"; creditsImage.src = "Images/credits.png";
Before we draw the images to the canvas let's create four arrays to hold the positions and sizes of the buttons (playImage
, instructImage
, settingsImage
, creditsImage
). These arrays will be used later to create a mouse over function.
var buttonX = [192,110,149,160]; var buttonY = [100,140,180,220]; var buttonWidth = [96,260,182,160]; var buttonHeight = [40,40,40,40];
Now we can draw the images to the canvas; this can be done within an onload
function for each image, but an onload function does not need to be included - we can simply use drawImage()
.
bgImage.onload = function(){ context.drawImage(bgImage, 0, 0); }; logoImage.onload = function(){ context.drawImage(logoImage, 50, -10); } playImage.onload = function(){ context.drawImage(playImage, buttonX[0], buttonY[0]); } instructImage.onload = function(){ context.drawImage(instructImage, buttonX[1], buttonY[1]); } settingsImage.onload = function(){ context.drawImage(settingsImage, buttonX[2], buttonY[2]); } creditsImage.onload = function(){ context.drawImage(creditsImage, buttonX[3], buttonY[3]); }
If tested now, you should see a static image of a menu which we will soon breathe life into. The ship was not drawn in with the rest of the images because we are going to draw it later in a mouse event. By the way, if you haven't been doing this so far, keep your variables grouped together at the top and do the same with functions.
Step 3: Animating Through Intervals
JavaScript lacks an onEnterFrame()
equivalent, but we can easily create our own through the use of an interval (timer).
var frames = 30; var timerId = 0; timerId = setInterval(update, 1000/frames);
You may be confused as to how the interval is working so I will explain in brief. The interval is calling the function update()
every (1000/frames
) milliseconds to create a smooth refresh rate. The value of frames
controls the fps; if frames
is 25 then the browser will attempt to call update()
every (1000/25=) 40 milliseconds.
Our next obvious step is to create the function update()
function update() { clear(); move(); draw(); }
Three more functions were just called. clear()
is used to clear the canvas because unlike flash the canvas works like putting stickers on a board; the images can't be moved after they have been placed. The next function, move()
, is used for changing the variable values that are used with the images. Finally draw()
is called to place those "stickers".
funcion clear(){ context.clearRect(0, 0, width, height); }
Put simply, this code clears everything within the rectangle that is the size of the canvas and is drawn from (0,0), the top-left corner. That means it clears the entire visible canvas.
Before we move onto the next function, two variables should be introduced. backgroundY
will be the variable for the background image's y-position and speed
will be used to subtract from backgroundY
each update cycle.
var backgroundY = 0; var speed = 1;
The effect that we are going to produce is a continuously scrolling background. The image is made up of two identical starfield images, one above the other, in a larger image (the image being twice the height of the canvas). We will slowly move the image up until the second half is completely in view and then we will reset the position of the image back to the first half.
function move(){ backgroundY -= speed; if(backgroundY == -1 * height){ backgroundY = 0; } }
Finally we have the draw()
function. All of the images will be redrawn, but one change to notice is that the bgImage
's y value has been replaced with the variable backgroundY
.
context.drawImage(bgImage, 0, backgroundY); context.drawImage(logoImage, 50,-10); context.drawImage(playImage, buttonX[0], buttonY[0]); context.drawImage(instructImage, buttonX[1], buttonY[1]); context.drawImage(settingsImage, buttonX[2], buttonY[2]); context.drawImage(creditsImage, buttonX[3], buttonY[3]);
Test now and admire the smooth scrolling background.
Step 4: Checking the Mouse Position
One thing the HTML5 <canvas> is missing is support for image event listeners, meaning we can't simply write playImage.mouseover = function(){}
. Instead, we have to get the postion of the mouse relative to the canvas and figure out whether it is over an object.
var mouseX; var mouseY; canvas.addEventListener("mousemove", checkPos);
The two variables introduced are going to be used to get the current postion of the mouse. We added an event listener, like in ActionScript, that calls the function checkPos()
every time the mouse moves.
function checkPos(mouseEvent){ mouseX = mouseEvent.pageX - this.offsetLeft; mouseY = mouseEvent.pageY - this.offsetTop; }
If you alerted the values of mouseX
and mouseY
every time you moved the mouse you would get the correct position. But there is one problem: not all of the modern desktop browsers support this method. To overcome this problem we can use the little hack instead:
if(mouseEvent.pageX || mouseEvent.pageY == 0){ mouseX = mouseEvent.pageX - this.offsetLeft; mouseY = mouseEvent.pageY - this.offsetTop; }else if(mouseEvent.offsetX || mouseEvent.offsetY == 0){ mouseX = mouseEvent.offsetX; mouseY = mouseEvent.offsetY; }
This checks whether the browser uses "page" or "offset" properties to return the position of the mouse, and adjusts the values (if necessary) to get the mouse position relative to the canvas.
Now remember that ship we instanced, but didn't draw? We are going to take that static image, spin it and make it appear whenever we mouse over the buttons!
var shipX = [0,0]; var shipY = [0,0]; var shipWidth = 35; var shipHeight = 40; var shipVisible = false; var shipSize = shipWidth; var shipRotate = 0;
The first four variables are the same as before (we have two positions because there will be two ships). The shipVisible
variable will be set true when the mouse is over a button. As for shipSize
and shipRotate
, they will be used to scale the ship vertically and reposition it to give the illusion that it is spinning. Keep in mind that images scale right to left.
for(i = 0; i < buttonX.length; i++){ if(mouseX > buttonX[i] && mouseX < buttonX[i] + buttonWidth[i]){ if(mouseY > buttonY[i] && mouseY < buttonY[i] + buttonHeight[i]){ } }else{ } }
Add the code in the checkPos()
function. First we cycle through the buttons there are (I figured out the value using buttonX.length
). Next we compare the mouseX
to see whether it is greater than the current button's buttonX
and less than its buttonX + buttonWidth
- i.e. within the horizontal bounds of the button. We then repeat the process in another if statement for the Y values. If this is all true, then the mouse must be over a button, so set shipVisible
to true
:
shipVisible = true;
And within the empty else
statement set it to false
; it will then be called whenever you mouse out of a button:
shipVisible = false;
Under shipVisible = true
we will set the initial values for the shipX
and shipY
, and perform all of the scaling within the move and draw functions.
shipX[0] = buttonX[i] - (shipWidth/2) - 2; shipY[0] = buttonY[i] + 2; shipX[1] = buttonX[i] + buttonWidth[i] + (shipWidth/2); shipY[1] = buttonY[i] + 2;
For the first shipX
, which we want just to the left of the button, we set the value to (the current button's X - half of the ship width), and I moved it over 2 pixels to the left to make it look better. A similar process is repeated for the first shipY
. For the second shipX
we position at the (current button's X + that button's width + half the ship width), and then we set the Y like before.
The tricky part comes now. We have to scale the ship and move it over to compensate for the scaling. Within the move()
function write this if
statement.
if(shipSize == shipWidth){ shipRotate = -1; } if(shipSize == 0){ shipRotate = 1; } shipSize += shipRotate;
The code begins subtracting the value of shipSize
, which will be used to scale the image when we draw it; once it reaches zero the process reverses until it is full scale again.
Now we can move into the draw()
function. Below all of the other draw methods add the following if statement.
if(shipVisible == true){ context.drawImage(shipImage, shipX[0] - (shipSize/2), shipY[0], shipSize, shipHeight); context.drawImage(shipImage, shipX[1] - (shipSize/2), shipY[1], shipSize, shipHeight); }
The ships are being drawn normally with the exception of the X positions being compensated by subtracting half of the current scale.
Step 5: Checking for Mouse Clicks
Add another event listener for mouseup and create a new variable for a second interval we will create.
var fadeId = 0; canvas.addEventListener("mouseup", checkClick);
Create the function checkClick().
function checkClick(mouseEvent){ for(i = 0; i < buttonX.length; i++){ if(mouseX > buttonX[i] && mouseX < buttonX[i] + buttonWidth[i]){ if(mouseY > buttonY[i] && mouseY < buttonY[i] + buttonHeight[i]){ } } } }
Like before, we check whether the position of the mouse is correct. Now we need to create the new interval, and stop the other interval and event listeners.
fadeId = setInterval("fadeOut()", 1000/frames); clearInterval(timerId); canvas.removeEventListener("mousemove", checkPos); canvas.removeEventListener("mouseup", checkClick);
Nothing new here except that we need to create a function called fadeOut()
. We also need to create another variable called time
.
var time = 0.0;
function fadeOut(){ context.fillStyle = "rgba(0,0,0, 0.2)"; context.fillRect (0, 0, width, height); time += 0.1; if(time >= 2){ clearInterval(fadeId); time = 0; timerId = setInterval("update()", 1000/frames); canvas.addEventListener("mousemove", checkPos); canvas.addEventListener("mouseup", checkClick); } }
It has some new methods, but it's quite simple. Since we stopped all of the event listeners and the other interval our menu is completely static. So we are repeatively drawing a transparent black rectangle on top of the menu - without clearing it - to give the illusion of fading out.
The variable time
is increased every time the function is called and once it reaches a certain value (once 20 "frames" have passed, in this case) we clear the current interval. Here I reset the menu, but this is where you would draw a new section of the menu.
One last thing to note is that when you draw shapes on the canvas the fillStyle
is set with a rgb (red, green, blue) value. When you want to draw transparent shapes, you use rgba (red,green,blue,alpha).
I hope that's demystified a bit of the learning process for switching from simple AS3 programming to simple canvas programming. Post a comment if you have any questions!
Comments