Article
The Art and Science of JavaScript
Creating Perspective
Now that we understand how the floor plan works, and we've seen how to make triangles, we have all the data -- and the building blocks -- we need to create a 3D view.
Take a look at the figure below. What this diagram shows is a breakdown of all of the elements that create the illusion of perspective in our maze. The walls on each side of the long hallway are composed of 16 columns. Each of the columns contains four inner elements which, for the rest of this chapter, we'll refer to as bricks. I've labeled the bricks, and highlighted them in a different color so that they're easier to distinguish. In each column, the top brick is highlighted as a gray rectangle; the upper brick is a rectangle comprising a red and blue triangle, as is the lower brick; and the middle brick is a green rectangle.
![]()
The upper and lower bricks are implementations of the triangles we saw earlier, clipped differently for each of the four orientations we need, thus creating diagonal lines in four directions. The red parts of these bricks will always be visible, whereas the blue parts are only blue for demonstration purposes -- in practice, they'll be transparent. The top bricks will also be transparent, to expose a sky-patterned background. (It isn't strictly necessary to use top bricks -- we could have applied a top margin to the upper bricks -- however, it was easier for me to visualize this way.) The middle bricks will be shaded the same dark red color as the triangles in the upper and lower bricks, so that the bricks merge together and create the appearance of part of a wall.
This Is Not a True Perspective!
What we're dealing with here is not actually a true perspective -- it's skewed slightly so that the vanishing point is a short vertical line, rather than a point.
I originally created this maze using a true perspective with a single vanishing point, but it just didn't look right. The ceiling appeared too low relative to the distance between the walls (or the walls were too far apart, depending on how you looked at it). Changing the aspect ratio (that is, making the viewport square instead of the widescreen ratio that it has) would have made a difference, but I didn't want to do that -- I wanted the game to look more cinematic!
The view is also limited as the columns get smaller, rather than stretching all the way to the vanishing point, because the resolution that we can achieve at such a distance is limited. The view ends at the point where we no longer have enough pixels to draw effectively, which restricts the maximum length of corridor we can represent. We'll talk about this issue again, along with the other limitations of this approach, towards the end of the chapter.
If you look carefully, you'll see in the figure above that each of the triangles has the same angle -- it's just the size of the brick itself that's progressively reducing. This makes the illusion of perspective nice and easy to create, as we don't have any complex math to worry about. Still, it's not something that we'd want to code by hand. Let's use JavaScript to calculate the size of each brick, so that it can be generated on the fly ...
Making a Dynamic View
One of the beautiful things about using a programming language to generate complex visual patterns is that it's not necessary for us to work out every line and angle manually -- we only need to worry about the math that represents the pattern.
There are times when I really wish I'd paid more attention in school math classes. But computer games were in their infancy then, and none of my teachers knew much, if anything, about them. So when I asked in class, "What use is any of this?", they didn't have a good answer!
It's just as well, then, that the math involved here is not complicated -- we don't even need trigonometry, because the angles have already been determined for us. All we need to calculate is the size of the bricks and the clipping regions that are used to create our triangles; the browser's rendering engine will do the rest.
Core Methods
Let's take a look at the scripting now. We'll start with the main script, underground.js, which is located in the scripts folder of the code archive. The entire script would be too large to list in its entirety in this book; instead I've just listed the signature of each method to give you a high-level appreciation for what's going on:
Example 6.1. underground.js (excerpt)
// DungeonView object constructor
function DungeonView(floorplan, start, lang, viewcallback)
{ ... };
// Create the dungeon view.
DungeonView.prototype.createDungeonView = function()
{ ... };
// Reset the dungeon view by applying all of the necessary
// default style properties.
DungeonView.prototype.resetDungeonView = function()
{ ... };
// Apply a floorplan view to the dungeon
// from a given x,y coordinate and view direction.
DungeonView.prototype.applyDungeonView = function(x, y, dir)
{ ... };
// Create the map view.
DungeonView.prototype.createMapView = function()
{ ... };
// Reset the map view.
DungeonView.prototype.resetMapView = function()
{ ... };
// Apply a position to the map view.
DungeonView.prototype.applyMapView = function()
{ ... };
// Clear the view caption.
DungeonView.prototype.clearViewCaption = function()
{ ... };
// Generate the caption for a view.
DungeonView.prototype.generateViewCaption = function(end)
{ ... };
// Shift the characters in a string by n characters to the left,
// carrying over residual characters to the end,
// so shiftCharacters('test', 2) becomes 'stte'
DungeonView.prototype.shiftCharacters = function(str, shift)
{ ... };
// Bind events to the controller form.
DungeonView.prototype.bindControllerEvents = function()
{ ... };
Rather than examine every method here, I'll explain the three core methods that do most of the work for our script, and leave you to fill in the gaps by following the code from the code archive yourself. Throughout this section I'll use the word view to mean "a 3D representation of a position on the floor plan" (that is, the player's point of view, looking north, east, south, or west).
The createDungeonView Method
The createDungeonView method takes an empty container, populates it with all the elements we need (the columns are divs, and the bricks are nested spans), and saves a matrix of references to those elements for later use:
Example 6.2. underground.js (excerpt)
// Create the dungeon view.
DungeonView.prototype.createDungeonView = function()
{
var strip = this.tools.createElement('div',
{ 'class' : 'column C' }
);
this.grid['C'] = this.dungeon.appendChild(strip);
for(var k=0; k<2; k++)
{
// the column classid direction token is "L" or "R"
var classid = k == 0 ? 'L' : 'R';
for(var i=0; i<this.config.gridsize[0]; i++)
{
var div = this.tools.createElement('div',
{ 'class' : 'column ' + classid + ' ' + classid + i }
);
this.grid[classid + i] = {
'column' : this.dungeon.appendChild(div)
};
for(var j=0; j<this.config.gridsize[1]; j++)
{
// create the main span
var span = this.tools.createElement('span',
{ 'class' : 'brick ' + this.bricknames[j] }
);
if (j == 1 || j == 3)
{
var innerspan =
span.appendChild(this.tools.createElement('span'));
}
this.grid[classid + i][this.bricknames[j]] =
div.appendChild(span);
}
}
}
this.resetDungeonView();
};
As you can see if you scroll through the code, there isn't much more to this method: its sole responsibility is to create a group of elements, and assign class names to each of them so that they can be distinguished from one another. The values I've used are reasonably intuitive -- upper identifies an upper brick, for example.
I've made use of CSS floats in order to line the columns up (left floats for a column on the left wall, and right floats for one on the right). To create the columns, we iterate on each side from the edge inwards (in other words, the left-most column is the first of the columns that comprise the left wall, and the right-most column is the first for the right wall).