LESSON 8 - April 11, 2013

HTML5 GAME DEVELOPMENT

Operation C.A.N.T (Canvas Ain't No Thang) LESSON # 8

Ben W. Savage

And here we are fresh and eager to jump into our second week of HTML5 game dev.!

Ok so last yesterday I mentioned that we were either going to be talking about event listeners (in more detail) or we were going to discuss transformations. Well, I think from this point it's a good idea to talk about transformations, though it's a bit of a complex subject, so I'll be dividing it into two parts.

By doing so I'll give you a chance to open the doors even wider on your gaming techniques and bring us one step closer to making an actual game. We're really almost to that point, by the way! We just need to tackle a couple more issues. Today will be a VERY important step ahead because we're going to be learning some very powerful stuff!

So let's get going!

Ahhh, transformations! First of all what do I mean by transformations? Well, they're techniques for moving, scaling, rotating and setting the alpha to the objects that we put onto our canvas. Alpha? What the heck's that? Well, alpha is the amount of transparency an object can have. If the alpha of something is high, then it's opaque. If it's low, then it's extremely transparent.

APPLYING ALPHA

Well, this one is so simple that I've already given you most of the information necessary to plug it into your code.

Alpha values are set to your stuff by a property called globalAlpha. The values range from 0 (completely transparent) to 1 (completely opaque). You must assign your global alpha BEFORE drawing the shapes/text/image whatever you want to make transparent. Of course, if you don't want to apply any transparency to an object, you don't even need to bother writing globalAlpha. The default is 100% opacity.

Here's an example:

<!doctype html>
<title> TRANSPARENT TOOTH</title>
<canvas id = "canvas" width = "550" height = "400"></canvas>
<style>
canvas
{
background-color: #dddddd;
}
</style>

<script>

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

context.shadowColor = "#000000";
context.shadowOffsetX = 2;
context.shadowOffsetY = 2;
context.globalAlpha = .2;
context.strokeStyle = "#0000ff";
context.fillStyle = "#00ff00";
context.moveTo(200,100);
context.lineTo(250,150);
context.lineTo(200,200);
context.lineTo(200,300);
context.quadraticCurveTo(175,125,150,300);
context.lineTo(150,200);
context.lineTo(100,150);
context.lineTo(150,100);
context.quadraticCurveTo(175,125,200,100);
context.fill();

</script>

As you can see, we've set the background to a light grey and have given our old pal, the tooth, a fillStyle of green and an alpha assignment of .2 (nearly completely transparent). Note again that the alpha is assigned BEFORE you draw the lines, otherwise it won't work. Also note that for now, that once you assign globalAlpha to an object, ALL the objects you code in after that will have the alpha effect applied to them. Go ahead and draw a second tooth to the right of this one (you should be fine doing this on your own) and see for yourself. We'll be discussing ways to counteract your alpha leaking all over the place sometime this week, so hold tight for now and try to understand the version of alpha we have right now.

And that's bascially IT for apha for the time being! You can now make your artwork transparent! Give it a shot, then let's after you're ready, let's move on to the second sub-category:

BOUNDING BOXES AND ORIGIN POINTS

Now:

A bounding box is an invisible rectangle around ALL of your objects, whether they're circles, trapezoids or whatever. You could try to imagine the most non-rectangular shape out there and there will ALWAYS be a rectangular bounding box outside your object. Never will it be a strange 88-sided polygon but ALWAYS a rectangle.

If you've worked with Photoshop, Illustrator or other similar graphics programs, it's the border that's automatically generated around the objects you select. Every object you make on the canvas has an invisible bounding box around it. Keep this in mind.

The bounding box rectangle works like this:

If you have a square, the box is nothing more than the sides of the square itself. Why? Because a bounding box is the SMALLEST possible rectangluar container you can fit around an object.

A bounding box for a circle is a box that touches the the circle on all of its four sides. Just imagine a circle that fits perfectly inside a square. Again, that's the smallest possible rectangle you can fit around a circle.

Now take a star. Where's the bounding box? It fits perfectly around the star and only touches the points that extend the furthesr outwards in all 4 directions.

In other words, a bounding box asks itself this: What are the outermost top,down,left,and right values of this object? Then it asks: how can I make a rectangle fit around this thing with a minimum of wasted space inside it? Bounding boxes are super efficient, you see!

And that, my friends, is a bounding box.

Got it?

Soooo, if you understand the bounding box concept, you can understand the coordinate system at how it works for rotating objects!

Now, here's the CRUCIAL part of this. I'll need your complete, undivided attention for this one.

If you get these next few concepts, the rest of translation will be a breeze:

ROTATION AND TRANSLATION

Canvas elements are, by default, rotated in relation to the top-left (0,0) value of the canvas.

Got it?

Let me repeat that again so it sinks in:

Canvas elements are, by default, rotated in relation to the top-left (0,0) value of the canvas.

Just as the planets revolve around the Sun, all our rotations must rotate around the (0,0) coordinates. We can't change the fact that everything revolves around 0,0... BUT we CAN MOVE (through translation, that is) THE LOCATION OF THESE (0,0) COORDINATES TO THE CENTER OF THE OBJECT WE WANT TO ROTATE!

What I just said should be CRYSTAL CLEAR before you proceed.

Let's see this in action and try to rotate a rectangle. You might not get it the frist couple times, but keep trying until you do. Also keep in mind that the value for the rotate() method is in RADIANS, not DEGREES. For a refresher on raidans, go back and look at my first few tutorials. Either way, I won't ask for a lot of complicated math here.

This is everything in our script tag BEFORE rotation:

<script>

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

context.fillStyle = "#ff0000";
context.fillRect(150,150,100,100);


</script>

See how the rectangle is nice and straight? Now, let's try rotating it .3 radians (clockwise, the default value)

Go ahead and add the context.rotate(.3) line you see below.

<script>

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


context.rotate(.3);

context.fillStyle = "#ff0000";
context.fillRect(150,150,100,100);


</script>

See what I mean? If you go back and forth between these two pieces of code, you'll notice that the rectangle appears as if it's pinned to a board by the upper-left (0,0) corner of the canvas. All the rotation is happening in relation to that point. Try a slightly larger or smaller rotation value and you might not see the rectangle at all. That's because the recatangle has been rotated off of the canvas. Now, at a rotation of .3 radians, for example, our rectangle is situated at about 5 o'clock. If we add a higher rotation value, the rectangle may end up at 8 or 9 o'clock or even at 12 or 1 o'clock.

This, of course, is problematic because that means that only a quarter of the rotations we do according to this point will be visible on the canvas!

Definitely not a good way to go about rotating things, am I right?

In Flash it was so much easier.... SIGH.. but there IS an HTML5 remedy!

Wouldn't it be nice if we could rotate our objects from their center instead of the upper left point of the canvas? I mean, that's what comes to mind when you think rotation, right? When you watch the tires of a car spin around, you don't think of them rotating around any other point but the axle and certainly not the rearview mirror or something wacky like that!

Well, you're in luck - there's a method for doing this! But, as with most things in life, there's good news and there's bad news.

The good news is: for rectangles and images it's pretty simple. The bad news is: for our dear tooth and everything else made with plotted points, it's sort of a pain in the a...

Here's how it works:

There's a little method called translate(), see?

Now the formula you're about to see is a little weird, but we'll explain it step by step:

context.translate(x value + .5 * width, y value + .5 * height);

This little sucker requires only two variables which are, if you've been following along, the new point where we're going to place our (0,0) axis.

Now the formula may be an odd one, but not as perilous as it seems. All we need are the X,Y,WIDTH and HEIGHT values of our object.

So if we look at the code for our rectangle again:

<script> var theCanvas = document.getElementById("canvas");
var context = theCanvas.getContext("2d");

context.fillStyle = "#ff0000";
context.fillRect(150,150,100,100);


</script>

What are our X,Y,WIDTH and HEIGHT values?

Well, this is simple because we plugged all 4 of them into our rectangle!

Our 4 values are: 150,150,100,and 100!

Now what was that translate thing again??

context.translate(x value + .5 * box width, y value + .5 * box height);

By plugging our values into the formula we get:

context.translate(150 + .5 * 100, 150 + .5 * 100);

Now, as you'll recall from math class, we do multiplication first, then addition. So go ahead and multiply these two things:

  • .5 * 100
  • .5 * 100
  • 50
  • 50
  • Now our formula becomes:

    context.translate(150 + 50, 150 + 50);

    Which, in turn, becomes:

    context.translate(200,200);

    And those coordinates are where our new (0,0) goes! Now comes the frustrating part.

    Translating the origin point itself isn't gonna cut it. Why not?? Because we still need to bring the rectangle back to what would be its original X and Y points on the stage before we translated the (0,0). As you'll notice, the rectangle is quite a bit further down and to the right compared to where it was before. So let's bring it back up to where it was, but the rotation being around its center point.

    So let's change the code again by adding the following features:

    context.fillRect(-.5 * 100,-.5 * 100,100,100);

    Which turns out to be: -50,-50,100,100. So change your new fillRect values to those.


    <script>

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

    context.translate(200,200);
    context.rotate(1.3);

    context.fillStyle = "#ff0000";
    context.fillRect(-50,-50,100,100);


    </script>

    As you can see here in the last line, we've taken the width and heght values, then projected them BACK by half the width and height to put our rectangle exactly where it was before we translated anything. Satisfying? Note that for a series of points plotted on the canvas this could get PRETTY difficult! Fortunately, in the games we'll be making, rotation will often be applied directly to an image which, fortunately, has all four values we need for rotation. WHEW!

    One more important thing you need to burn into your memory:

    ALWAYS apply the translation BEFORE to rotation! This took me a while to get at first. And I thought I was going insane trying to figure out why it didn't work. Learn from this silly man's mistakes - TRANSLATE BEFORE ROTATE.

    So, as a last exercise, let's add an image to the canvas, then rotate it. Sound good? I know this is tough, but once you get it, it's not so bad. Here's our image, just to show you how easy it really can be. I'll break it down into bite-size steps:

    1. Set up your canvas skeleton like so:

    <!doctype html>
    <title> ROTATING AN IMAGE</title>
    <canvas id = "canvas" width = "550" height = "400"></canvas>
    <style>
    canvas
    {
    background-color: #dddddd;
    }
    </style>

    <script>

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

    </script>

    2. Assign the new image a name and make it a new instance of the Image() class.

    var myImage = new Image();

    3. Tell the computer where to look for your image and specify its exact name + extension. Have it saved in the SAME directory as this HTML file.

    myImage.src= "Hipster.png"; <--- in my case, it's a file called Hipster.png

    4. Declare drawImage and assign some X,Y,WIDTH and HEIGHT values to it.

    context.drawImage(myImage,200,200,100,100);

    At this point you should see a scaled-down version of your image if it was larger than the dimensions 100X100 . Now to translate the (0,0) coordinates!

    5. Calculate the translation values using the formula (x value + .5 * width, y value + .5 * height)

    In this case we have: (200 + 50, 200 + 250)

    which becomes (250,250);

    6. Assign the new translation.

    context.translate(250,250);

    7. write the following line just BELOW the translate line:

    context.rotate(.3);

    8. Finally, change the variables in the drawImage to return it to its starting point on the canvas.

    The formula for this is: (-.5 * width, -.5 * height, width, height)

    So... context.drawImage(myImage,-.5 * 100,-.5 * 100,100,100);

    Then finally....

    context.drawImage(myImage, -50,-50,100,100);

    9. Run it and adjust your rotation accordingly!

    I know it seems like hard work, and it IS, but unfortunately, this is how it's done in the canvas. On the up side, this is one of the most difficult things to grasp. A lot of the other new stuff we'll be dealing with will seem like a breeze after you've mastered this.

    As usual, I advise you to test this stuff out and try many, many examples. This technique will come in very useful in your games. Don't skip over it and deprive yourself of a powerful tool! Just think of how easy it would be to use rotate to set up a windmill that turns around and around, a clock or the wheels on an enemy vehicle! Just throw some timers into the code and go crazy!

    Also, luckily for you, after all this hard work, you've got your foot in the door for tomorrow's lesson on scaling.

    So stay tuned! As always, thanks for following along! Code didn't work for you? Hate me? Are you my illegitimate child? Send input anytime!

    Until tomorrow!

    -Ben
    @benwhi
    Onward to Lesson Nine!
    Back to Lesson Seven
    Back to Index

    P.S. I've had several requests in the forums to put my tutorials in a better format - and you're absolutely right! Problem is, I've got to have the time to do my money-earning work too! These tutorials ain't paying the bills! Once I DO get some free time, you have my word I'll reformat them. Please bear with the ugly .txt for the time being!

    Also, thanks so much for your very kind words about these turorials! Very encouraging!!!