Introduction
See an example of what we'll make: dwmkerr.com/experiments/starfield
JavaScript is exploding in popularity. In this series of articles, we're going to learn JavaScript by using it. I'm not going to go into deep theoretical discussions on syntax and design patterns - I'm going to make projects and we'll learn the theory along the way.
The idea behind this series is that you can drop in at any point. Take a look at the table of contents below and find an article that discusses something you're interested in. In each article, we'll actually create something, and learn some tricks along the way.
Step 1 - Create a Web Page
We're going to need a super simple web page to run the Starfield. Let's create the leanest boiler plate HTML we can:
<!DOCTYPE html>
<html>
<head>
<title>Starfield</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
We've created a lightweight HTML page that contains a div
, this div
will hold our starfield. Let's first make sure that it fills the screen, we can do this by adding some CSS styling to the head
:
<head>
<title>Starfield</title>
<style>
#container {
width: 100%;
height: 100%;
position: absolute;
left: 0px;
top: 0px;
}
</style>
</head>
Now let's create a new JavaScript file, we'll call it starfield.js. Put it in the same directory as the HTML file (which we can call index.html). Let's make sure that we include the starfield
in the HTML:
<body>
<!--
<script src="starfield.js"></script>
</body>
Step 2 - Create the Starfield Class
If you're unfamiliar with JavaScript, this is where things are going to get interesting. We're going to create a class to represent the Starfield
. In the starfield.js file, add the code below:
function Starfield() {
this.fps = 30;
this.canvas = null;
this.width = 0;
this.height = 0;
this.minVelocity = 15;
this.maxVelocity = 30;
this.stars = 100;
this.intervalId = 0;
}
If you've created classes in JavaScript before, you can skip this section of explanation, otherwise...
JavaScript Classes
What we're just written doesn't look much like a class to a C++ or C# developer. It looks like a function. Well, it is. In JavaScript, there are no classes (although in ECMAScript 6, we'll get them). That doesn't actually really stop us from creating classes, or at least class like objects. Functions are objects - we can create instances of them and we can set properties on them. We'll create a Starfield
like this:
var starfield = new Starfield();
This is really important. We create a new instance of the function and we call it. By calling it, we've set some properties on the function
object. We've only set them on the function
object we've created - not every instance.
Now just like creating properties, we can also create function
s, for example:
function Starfield() {
this.start = function() { };
}
We could call this function
like this:
var starfield = new Starfield();
starfield.start();
But we can actually do a little better. If our function is complicated, then re-creating it for every instance is actually not very efficient, what we want to do is actually create the function
once and have every instance automatically get it. That's where the prototype comes in.
When you put the dot after an instance of the type created by the function
, the engine will try to find a property on that type. If it can't find it, it'll look on the 'prototype
' of the type. When we create an instance of type using the function, it inherits the same prototype each time. Here's what I mean:
function Starfield() {
}
Starfield.prototype.start = function() {
};
We've modified the prototype
of the Starfield
function, that means every time that the Starfield
is created, it will get the start
function, even though we've only declared it once.
Still not clear? Think about this:
var starfield = new Starfield();
starfield.stop = function() { };
We've created a stop
function on an instance of starfield
- so we can only call stop
on that starfield
. What about this?
var starfield = new Starfield();
Starfield.prototype.pause = function() { };
Now we've created a function
called 'pause
' on the prototype
of Starfield
. This means we can call it from any starfield
instance. Why? Because first the engine looks for 'pause
' on the instance, and doesn't find it. Then it looks on the prototype
of the instance. The prototype
is shared for all Starfield
s so it finds it!
This is actually really difficult to get used to at first, the most useful thing I can recommend is play with it. Create some classes, read up on prototype
s and give it a try.
Step 3 - Initialising the Starfield
We got a little side-tracked there, but that's the purpose of the article. We created the Starfield
class with some properties, now let's actually do something with it.
We're going to aim for two functions - the first will initialize the starfield
, setting it up to be ready to use, the second will start it, actually running the animation. Let's write initialise
first.
Remember the prototype
? That's where we'll put the initialise
function - because every starfield
will need it.
Starfield.prototype.initialise = function(div) {
var self = this;
this.containerDiv = div;
self.width = window.innerWidth;
self.height = window.innerHeight;
window.addEventListener('resize', function resize(event) {
self.width = window.innerWidth;
self.height = window.innerHeight;
self.canvas.width = self.width;
self.canvas.height = self.height;
self.draw();
});
var canvas = document.createElement('canvas');
div.appendChild(canvas);
this.canvas = canvas;
this.canvas.width = this.width;
this.canvas.height = this.height;
};
This is a learn JavaScript article, so I'm going to go through this blow by blow. Skip it if it looks comprehensible to you.
Starfield.prototype.initialise = function(div) {
We're adding a function
to the starfield prototype
, this means each starfield
will be able to use it. The function
takes one parameter, it's a div
. It's not typed, because JavaScript has no typing (although if you want it, check out TypeScript).
var self = this;
this.containerDiv = div;
self.width = window.innerWidth;
self.height = window.innerHeight;
We're storing a copy of the 'this
' variable in a local variable. The reason will become apparent shortly... Next, we store a reference to the div
we've been provided (notice that we didn't create 'containerDiv
' in the constructor? Doesn't matter, you can create properties as you need them. I normally create them in the constructor so I can quickly look to see what should be there.)
We also store the client area of the browser window. The 'window
' object is provided by the browser, it lets you do lots of things with the browser.
window.addEventListener('resize', function resize(event) {
self.width = window.innerWidth;
self.height = window.innerHeight;
self.canvas.width = self.width;
self.canvas.height = self.height;
self.draw();
});
Now we're going to handle the 'resize
' event of the window. There are two ways to do this. The first is to set the 'onresize
' function, the second is to use 'addEventListener
'.
Typically, we should use the 'addEventListener
' function, because this will not stop any other events that have already been added from working. If we set 'onresize
' directly, we replace whatever might have been there before. So by using 'addEventListener
', we're making sure we won't interfere with other libraries. When the function is called, we're going to update our width
and height
, update the canvas width
and height
(we'll create the canvas
in just a little bit) and call the 'draw
' function, which we'll create shortly.
Why are we using 'self
' and not this
?
OK, we'll we're writing this code in the 'initialise
' function, in the context of the initialise
function, 'this
' is the Starfield
. But when the window calls the 'resize
' function for us, by the time we're in that function
, 'this
' is actually the window. So to edit the starfield
instance, we use the 'self
' variable we declared earlier, which is a reference to the starfield
.
This is actually quite advanced - the function
is called and somehow we're using a variable that was created outside of the function. This is called a closure, and it makes our lives a lot easier! Closures allow us to access state from another context. When writing callback functions and so on, this is a very helpful thing to be able to do.
var canvas = document.createElement('canvas');
div.appendChild(canvas);
this.canvas = canvas;
this.canvas.width = this.width;
this.canvas.height = this.height;
};
Here's the last part of the initialise
function. We use the 'document
' object to create a new HTML element.
The document
object is insanely important in web based JavaScript development - why? It represents the 'DOM' (the document object model). This is actually the tree structure of the HTML page - the nodes, elements, attributes, and so on. A huge amount of what we do in client side JavaScript works with the DOM, we change styles of elements, add new items and so on. In this case, we use the document to create a new HTML Canvas
, then add it to our container div
- then we set its width
and height
.
This is fundamental - we've just created HTML programmatically, and this is one of the things we'll do a lot of in JavaScript.
That's initialise!
Let's recap:
- Store the
div
. - Store useful properties, the
width
and height
. - Listen for the window resize, when it does, update the
width
and height
and redraw. - Create a
canvas
to draw on, and make it a child of the container div
.
Step 4 - Starting the Starfield
This is the fun part, we can now actually create the main starfield
logic. Let's create the starfield
start
function, it'll start running the starfield
.
Starfield.prototype.start = function() {
var stars = [];
for(var i=0; i<this.stars; i++) {
stars[i] = new Star(Math.random()*this.width, Math.random()*this.height,
Math.random()*3+1,
(Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity);
}
this.stars = stars;
We're adding a function
, just like before by using the prototype
. The first thing we do is create an array - that's the line that starts with 'var stars
'. By setting it equal to '[]
' we've made it an array. An array is quite functional in JavaScript, we can use it like a queue or list as well. Now we loop over the number of stars (which we set in the constructor) and create a Star
in the array each time - hold on, what's a star
? Put this code at the end of the file - not in the function we're still writing!
function Star(x, y, size, velocity) {
this.x = x;
this.y = y;
this.size = size;
this.velocity = velocity;
}
Function
s as classes are weird aren't they? But they're also pretty easy to work with! I want to represent star
objects, so I have a function that sets some properties. Calling 'new
' on the function instantiates a type from it, with the properties I've provided. This is how I add 'star
' objects to my array. I'm using Math.random
, which is a standard JavaScript function that returns a value between 0
and 1
to randomise the initial position, size and velocity of the star.
Now back to the function - we created the array, now we store it in 'this
' - the Starfield
object. Next, we'll use the JavaScript setInterval
function. This function sets a callback that will be called every time an interval elapses. Our interval is specified by the fps (frames per second). Each time we hit the function, we'll call 'update
' and 'draw
'. We use the 'self
' closure to make sure we're calling them on the starfield
object!
var self = this;
this.intervalId = setInterval(function() {
self.update();
self.draw();
}, 1000 / this.fps);
};
We're also storing the id returned by 'setInterval
' - we can use that to stop it later.
Now we'll create the update
function, this'll update
the state of the starfield
.
Starfield.prototype.update = function() {
var dt = 1 / this.fps;
for(var i=0; i<this.stars.length; i++) {
var star = this.stars[i];
star.y += dt * star.velocity;
if(star.y > this.height) {
this.stars[i] = new Star(Math.random()*this.width, 0, Math.random()*3+1,
(Math.random()*(this.maxVelocity - this.minVelocity))+this.minVelocity);
}
}
};
This is the core logic of moving the star
s - we work out how much time has passed (dt
is delta t
). Then we go through each star
, and update its position based on its velocity and the time that has passed.
Finally, if the star
has moved past the bottom of the screen, we create a new star
at the top!
Next is the draw
function:
Starfield.prototype.draw = function() {
var ctx = this.canvas.getContext("2d");
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, this.width, this.height);
ctx.fillStyle = '#ffffff';
for(var i=0; i<this.stars.length;i++) {
var star = this.stars[i];
ctx.fillRect(star.x, star.y, star.size, star.size);
}
};
Believe it or not, this is a new HTML5 feature - the Canvas
. The Canvas
is an object that you can use to do bitmap-based drawing in JavaScript. You can draw lines, polygons and so on. In fact, from this you can draw just about anything.
All we need to do is fill the background with black, set the fill color to white and draw a little rectangle for each star
.
Step 5 - Try It Out!
We've done it! Add the code to your HTML page:
<body>
<!-- snip -->
<script>
var container = document.getElementById('container');
var starfield = new Starfield();
starfield.initialise(container);
starfield.start();
</script>
</body>
Run the page and there you go.
What Have We Learnt?
Here's a roundup of what we've learnt:
- In JavaScript, classes are created using a '
constructor
' function. - The '
constructor
' function is called with 'new
' to create a new instance of the type. - The '
constructor
' function has a property called 'prototype
' and is shared between all instances of the type. - Normally, class member functions are defined on the
prototype
. - The JavaScript '
window
' object is provided by the browser and represents the environment the code is running in. - The JavaScript '
document
' object is provided by the engine and represents the HTML document. - Timers can be created in JavaScript with '
setInterval
'. - The '
this
' keyword in JavaScript should be used with caution - in callback functions, 'this
' might not be what you expect. - You can create an array in JavaScript using
var array = [];
- You can use a variable defined outside of a callback function in the callback function, this is a closure.
Tell Me What You Think
If you have found this article useful, let me know - if there's anything I can do to explain things more clearly, let me know too.
The next article will be 'Creating Space Invaders with JavaScript'.
History
- 31st January, 2014: Initial version