# Basic isometric projection in Javascript

I've been working on porting an old closed source game to Javascript with Canvas and I've come to a slight problem.

Right now, to display the land, I just have a pretty basic loop that just draws the 44px by 44px diamond-shaped tiles using ctx.drawImage().

It essentially looks like the left side of the image, while I need them to "stretch" to fill the entire tile (like on the right). The tiles have a Z axis which is just in the +j direction on the screen and it's supposed to look like a hill or incline.

Anyway, this leads to my question: Is there a way to correctly do this without using webgl? If not: would it be better to use webgl directly to do this, or use a library to handle it for me?

EDIT 2: Well, after thinking for a while, I remembered that affine transformations are only linear (duh), so I won't be able to use certain transformations like transforming corner tiles... so I guess I can use the transformation matrices for the "light green" tiles, but not the pink tile in this example.

EDIT: With Phillips answer, I was able to create a few matrices to apply an affine transformation to the tile. However, I pretty much did "guess and check" to calculate the values for only one Z-value, but I can't seem to create a general form of it.

I'm guessing for different angles (like when there is an incline going from east->west, west->east, north->south, south->north), I will have to use a ton of if-statements for each scenario, but that's not much of a big deal.

I have the JSFiddle here: http://jsfiddle.net/awwPP/1/

<script>
multiply = function(a, b) {
return [a[0]*b[0] + a[1]*b[2],
a[0]*b[1] + a[1]*b[3],
a[2]*b[0]+a[3]*b[2],
a[2]*b[1]+a[3]*b[3]];
};
multiplyAll = function() {
var result = arguments[0];
for(var i = 1; i < arguments.length; i++) {
result = multiply(result, arguments[i]);
}
return result;
};
drawDiamond = function(ctx, x, y) {
ctx.beginPath();

ctx.moveTo(x, y);
ctx.lineTo(x+22, y+22);
ctx.lineTo(x, y+44);
ctx.lineTo(x-22, y+22);
ctx.lineTo(x, y);
ctx.stroke();
ctx.fill();
};

var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var z = 2 * 3; // each z is like 2px up
ctx.mozImageSmoothingEnabled = false;

ctx.fillStyle = 'rgba(50, 50, 50, 0.5)';
ctx.strokeStyle = 'rgba(255, 50, 50, 0.5)';

//left:
drawDiamond(ctx, 44, 22);
drawDiamond(ctx, 66, 44);
drawDiamond(ctx, 88, 66);

var cos = Math.cos(Math.PI/4);
var sin = Math.sin(Math.PI/4);

// just negate
var nCos = Math.cos(-Math.PI/4);
var nSin = Math.sin(-Math.PI/4);

var result = multiplyAll(
//rotate it so the tile is a square:
[cos, sin,
-sin, cos],

// scale the x axis (although it's actually the Y)
// to make it look longer
[50/44, 0,
0, 1],
// scew the opposite axis by factor K
[1, -0.12,
0, 1],

//negate the rotation
[nCos, nSin,
-nSin, nCos]);

ctx.setTransform(result[0], result[1],
result[2], result[3],
//dunno why there is an x/y offset
0, 10);

//middle:
drawDiamond(ctx, 66, 0 - z);
drawDiamond(ctx, 88, 22 - z);
drawDiamond(ctx, 110, 44 - z);

//right:
ctx.setTransform(1, 0,
0, 1,
0, 0);
drawDiamond(ctx, 88, -22 - z);
drawDiamond(ctx, 110, 0 - z);
drawDiamond(ctx, 132, 22 - z);
}
</script>

<canvas id="c" width="200" height="200"></canvas>

• I am not understanding the problem. Is there a reason you don't simply draw the tiles several pixels down and to the left so that the images overlap? – jhocking Jan 31 '13 at 3:28
• Well, the "higher" tiles are supposed to represent a higher Z value, so it's supposed to be a incline/hill of sorts. – Kevin Evans Jan 31 '13 at 5:27
• @jhocking I added a better example image. – Kevin Evans Jan 31 '13 at 5:32
• ah got it now, thanks for adding in the z axis explanation – jhocking Jan 31 '13 at 16:04

It is not possible to apply an affine transformation to the tile and get the result you want, because affine transformations preserve coplanarity, whereas the shape you need is not flat in the 3D world. Even if you got the outline to match (which can be done), the texturing would be all wrong and cause seams at the junctions.

I suggest splitting the tile in two triangles first. There are two ways to do so, but the first one (vertical split) will probably be easier later on:

The good thing is that it can be done without any additional calculations. You just need to render twice and put transparency in the unused part of the images. You can then reuse exactly the same transformation as for your light green tiles:

• I suggest using split on the left for concave corners, and a split on the right for convex corners. Looks more natural, I suppose. – Helbreder Feb 2 '13 at 11:11
• @Helbreder Can you explain what you mean by "left" and "right" splits? – sam hocevar Feb 2 '13 at 11:18
• As shown on pictures. The left split - I mean a split of a non-planar tile that goes from a upper tile (in means of a height, z) to lower. The right split - this is the one that divides a non-planar tile with a line that goes from left tile to the right tile (on a same plane; picture on the right). – Helbreder Feb 2 '13 at 11:44
• @Helbreder oh I understand now. I’ll go with “horizontal” and “vertical” then :-) But I think the problem with the horizontal split is that it requires a whole new transformation to be computed. – sam hocevar Feb 2 '13 at 12:04
• Yay! It works! You have no idea how much I love you right now. (PS: I gotta wait a few hours before rewarding the bounty) – Kevin Evans Feb 2 '13 at 19:25

This is how I'd do it without matrices, sines and divisions. Just integer math. Optimized for clarity, but it can obviously be optimized for speed with few changes:

// (0, 0) is at the top of the diamond. This is where we'll start drawing
sourceX = 200;
sourceY = 20;

// How much we move on the X axis for each tile
tileWidth = 30;
// How much we move on the Y axis for each tile
tileHeight = 15;
// How much we move on the Y axis for each unit of depth
tileDepth = 10;

// The size of the field, and the field itself
width = 5;
height = 5;
depths = [
[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 2, 2, 2],
[0, 0, 2, 3, 1],
[0, 0, 0, 1, 0]
];

function getDepth(x, y)
{
// Clamp to the borders, but can be modified for wrapping
if (x < 0)
x = 0;
else if (x >= width)
x = width - 1;
if (y < 0)
y = 0;
else if (y >= height)
y = height - 1;
return depths[y][x];
}

function getDrawingPos(x, y)
{
// Get the drawing position for a given point
var destX = sourceX + ((x - y) * tileWidth);
var destY = sourceY + ((x + y) * tileHeight) - (tileDepth * getDepth(x, y));
return {"x": destX, "y": destY};
}

function drawDiamond(ctx, x, y)
{
// Just find the points for all the corners..
var point0 = getDrawingPos(x, y);
var point1 = getDrawingPos(x + 1, y);
var point2 = getDrawingPos(x + 1, y + 1);
var point3 = getDrawingPos(x, y + 1);

ctx.beginPath();

// And draw them
ctx.moveTo(point0.x, point0.y);
ctx.lineTo(point1.x, point1.y);
ctx.lineTo(point2.x, point2.y);
ctx.lineTo(point3.x, point3.y);
ctx.lineTo(point0.x, point0.y);
ctx.stroke();
ctx.fill();
};

function drawField(ctx)
{
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
drawDiamond(ctx, x, y);
}
}
}

var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
ctx.mozImageSmoothingEnabled = false;

ctx.fillStyle = 'rgba(50, 50, 50, 0.5)';
ctx.strokeStyle = 'rgba(255, 50, 50, 0.5)';

drawField(ctx);


http://jsfiddle.net/a4ZbG/

• Looks nice, but it will be hard to stretch textures along that tiles. – Tarion May 10 '14 at 16:50

You could solve this in a pure 2d way by using a projection matrix. I wrote a quite extensive answer about how to use projection matrices in Canvas to turn orthogonal tiles into isometric tiles for a different question.

When your map tiles are all stretched in different ways, you will have to change your projection matrix before drawing each tile.

The 3x3 projection matrices which are offered by the 2d context allow you to do all kinds of perspective transformations to map tiles except for perspective shortening (making close objects larger than small objects).