Working with HTML5 Canvas

Summary

I’m going to change it up a bit by rendering some graphics.  In this post I’m going to use the Canvas object in HTML5.  In the past I’ve written programs to render graphics in SilverLight and when SVG became part of HTML5, I’ve built software that used SVG.  I’ve also written software to use XNA and Mono as well as DirectX.

A Game Map Board

If you’ve ever played one of Avalon Hill’s board games before, you’re probably familiar with the hex map.  Hex maps are more difficult to deal with than the typical square grid (like a chess board).  The advantage of a hex map is that there are no diagonals.  Each playing piece can traverse one of six directions and you don’t have to worry about the difference in the physical distance between straight and diagonal moves.  The best resource for the mathematics of a hex board can be found here.  Amit Patel’s site is one of the best interactive websites for learning subjects such as this and I would recommend bookmarking it.

One of the first things I did was attempt to render a hex using the cosine and sine functions.  The results were as expected, slow.  I’m old enough to remember the “good ole’ days” of graphics programming where you had to render to a buffer using tricks like fixed point arithmetic and cosine/sine lookup tables.  I’ll be using a different trick in a moment.  Here’s the basic javascript function that I used:

var hexCorner = function(center, size, i) {
    var angleDeg = 60.0 * i;
    var angleRad = Math.PI / 180.0 * angleDeg;

    return { X: center.X + size * Math.cos(angleRad), Y: center.Y + size * Math.sin(angleRad) };
};

To make this work faster, I ran through all six sides of a hex cell and then recorded the x and y coordinates, to use as offsets.  As you might guess, this will be a fixed size map.  Here is the array of six coordinates:

var offsets = [
    [40, 0],
    [20, 35],
    [-20, 35],
    [-40, 0],
    [-20, -35],
    [20, -35],
    [20, -35]
];

Next, I did something silly, like this:

for (var row = 0; row < 10; row++) {
    for (var col = 0; col < 15; col++) {
        HexMapObject.DrawHex(col, row);
    }
}

The results were stunning… Well, not quite.  I’m a bit picky about appearances and I didn’t like the way this looked:Notice what is wrong with this?  It’s very subtle, but the inner lines are darker than the outer lines.  The problem is that I’m rendering all six sides of each hex cell even though they sometimes share a side with a neighboring cell.  Canvas will darken any line that is rendered repeatedly.  To fix this problem, I decided to setup an array of lines to keep track of all the lines about to be rendered.  Then I created a method that would check to see if the line was already in the list before inserting it.  This creates a distinct list of lines to render:

var addToList = function(line) {
    for (var i = 0; i < lineList.length; i++) {
        if (lineList[i].SX === line.SX &&
            lineList[i].SY === line.SY &&
            lineList[i].EX === line.EX &&
            lineList[i].EY === line.EY) {
            return;
        }
        if (lineList[i].EX === line.SX &&
            lineList[i].EY === line.SY &&
            lineList[i].SX === line.EX &&
            lineList[i].SY === line.EY) {
            return;
        }
    }

    lineList = lineList.concat([line]);
};

As you can see, I had to check both directions (which is why there are two “if” statements).  The bottom-right line of one hex coincides with the top left side of another hex.  If you’re drawing the lines in a clock-wise direction, then the end point of one line will equal the start point of the other line (and vice-versa).  The result is much improved:I also adjusted the color to keep the lines from appearing too harsh.  You can adjust to whatever looks attractive for your game.  This is just a first attempt.  Next, I wanted to show a red area surrounding a selected cell.  Amit’s site gives an easy to implement algorithm for computing which cells belong to a designated cell.  I created a HighlightRange() JavaScript function that includes a parameter for the color and alpha setting (transparency).  It looks like this:

var highlightRange = function(col, row, range, fillStyle) {
    var cube = offsetToCube(col, row);

    for (var dx = -range; dx <= range; dx++) {
        for (var dy = Math.max(-range, -dx - range); dy <= Math.min(range, -dx + range); dy++) {
            var dz = -dx - dy;
            fillHexCell(dx + cube.X, dz + cube.Z, fillStyle);
        }
    }
};

Now I can feed in a column, row, a range and a fill style.  After trial and error, I came up with a light red fill style like this:

"rgba(255, 0, 0, 0.2)"

And the result of the map looks like this:

Now it’s time to build a real map for a board game.  For this I’m going to build a game that involves battleships on an ocean background.  The object of the game will be to take over all the islands on the map.  To take an island, you occupy a hex cell next to the island when an enemy ship is not in a hex cell next to the same island.  You can attack and sink enemy ships and they can sink your ships to win/lose the game.  For this article, I’m only going to focus on the background cells, the ships and some highlighted regions.

First, I used PhotoShop to generate some background cells.  You can use any graphic editor you like.  To make sure the cells would fit correctly, I knew that I had to make the png files 70 pixels tall by 80 pixels wide:


I did this by taking a screenshot of my finished grid and copying out one cell.  Then I erased around the triangle edges to make that portion “see-through” (in PhotoShop the see-through parts are checkered).  These images are going to overlap each other, and we don’t want a rectangle drawing over top the previous hex cell.  Therefore, the triangle sections must go.

Next, I needed to figure out how I’m going to handle my “assets”.  Assets are images and other static objects that are going to be used in my game.  In this case, there will be images or icons for the background cells and the units.  One technique is to create a directory to contain your images, but I chose to base-64 encode my images and just include the text inside my javascript.  Right now you’re wondering what in the world I’m talking about (or you know exactly what I’m talking about).  Go to Base 64 Image and upload a jpg file.  You’ll get something like this:

 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABGCAY...

It’s a very long text string (I trimmed it for this blog, but the string goes on for a mile after the “…”).  That string is the same data as this image:The syntax for rendering the image at a specific top, left coordinate is:

img.src = " data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...";

Followed by:

ctx.drawImage(img, center.X - 40, center.Y - 35);

The -40 and -35 will adjust for the center point of the hex cell because it is 80 wide by 70 tall.

When I first generated a map, I used one ocean cell image and it looked really bad.  In order to make it look like a real ocean, I had to generate four images that were slightly different offsets and mix them up randomly.  I also had to adjust the hex lines to be white.  Gray blended with the blue and it was difficult to see where the lines were.  Graphical output requires a bit of finesse and an eye for what looks good and what looks bad.  Here’s a sample of what four random images look like on a map:If you use the same pattern for every square, it will look like this:It’s not bad, but if you look close, you’ll notice that the pattern is exactly the same for each hex square.  This technique is used by people who lay tile for bathroom or kitchen floors.  Most of the time, tiles will be exactly the same print for every tile.  If you lay them out the same direction in your kitchen, they will look repetitive and your floor will look like it is made up of a bunch of small identical squares.  If you randomly rotate tiles as you place them, then the kitchen or bathroom floor will appear as though it was made from one large stone that was broken into small squares.  Study the two images above closely, with this concept in mind.  You can also make your background look nicer if you introduce more variety.  I used four tiles, but it would look better if I used eight tiles.  At some point, you’ll make a cut-off, but keep this technique in mind when generating background tiles.

Now it’s time to plot a few islands.  I made up a function to plot an island to a specific coordinate.  Each time the map is re-rendered, we’ll need to plot the islands at the same location.  In the final game, the location of islands can be somewhat randomized (with a minimum inter-island spacing value) when the map is first generated.  An array of the background squares to plot will need to be devised.  For now, I’m just going to hard-code the map and run with it.

Here’s my first stab at an island:It looks like an uncooked pizza and I’ll end up spending hours trying to make it look right before I make it into a game, but I’ll just leave it for now and move on to the next step.

My next step is to create a token representing a battleship.  Here’s a first try at a token.  This will also do for now:I decided to use a yellow square to represent the currently selected battleship token.  The green is the movement distance.  I used the same method mentioned earlier while using green for the color (rgba(0, 255, 0, 0.2)).  For the enemy ships, I’ll paint the background as gray:

To make the tokens different colors, I created a JavaScript function called HighlightShip.  That Function can be fed different colors to overlay the square area:

var highlightShip = function (col, row, fillStyle) {
    var cube = offsetToCube(col, row);
    var center = { X: 60 * cube.X + 100, Y: 35 * cube.X + cube.Z * 70 + 100 };

    var c = document.getElementById("battlefieldCanvas");
    var ctx = c.getContext("2d");

    ctx.fillStyle = fillStyle;
    ctx.lineWidth = 0;
    ctx.beginPath();
    ctx.moveTo(center.X - 25, center.Y - 25);
    ctx.lineTo(center.X + 25, center.Y - 25);
    ctx.lineTo(center.X + 25, center.Y + 25);
    ctx.lineTo(center.X - 25, center.Y + 25);
    ctx.lineTo(center.X - 25, center.Y - 25);
    ctx.closePath();
    ctx.fill();
};

Where to Get the Code

As always, you can go to my GitHub account and download the sample code (click here).  This project consists of nothing more than JQuery code.  There are four different maps that you can experiment with.  At the top of the WebSite/wwwroot/js/site.js file you’ll see this:

$(document).ready(function () {

    //drawSimpleMapSmallCells();
    //drawSimpleMap();
    //drawBlankMap();
    drawGameMap();

});

Just un-comment the line you want to test and run the program in Visual Studio.

Leave a Reply