Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Learn JavaScript: Part 1 - Create a Starfield

4.96/5 (96 votes)
31 Jan 2014CPOL10 min read 176.1K   1.9K  
In this article, we're going to create a starfield in JavaScript. We'll see how the core langauge features work, how to create classes, and how to use the HTML5 Canvas.

Introduction

See an example of what we'll make: dwmkerr.com/experiments/starfield

Image 1

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:

HTML
<!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:

HTML
<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:

HTML
<body>
   <!-- snip -->
   <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:

JavaScript
//  Define the starfield class.
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:

JavaScript
//  Create a starfield
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 functions, for example:

JavaScript
//  Define the starfield class
function Starfield() {
    /* snip */
    this.start = function() { /* do something*/ };
} 

We could call this function like this:

JavaScript
//  Create a starfield.  
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:

JavaScript
//  Define the starfield class.
function Starfield() {
    /* snip */
}

//  Add a function to the starfield class
Starfield.prototype.start = function() {
    /* here's the 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:

JavaScript
var starfield = new Starfield();
starfield.stop = function() { /* do something */  }; 

We've created a stop function on an instance of starfield - so we can only call stop on that starfield. What about this?

JavaScript
var starfield = new Starfield();
Starfield.prototype.pause = function() { /* do something */  }; 

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 Starfields 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 prototypes 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.

JavaScript
//  The initialise function initialises a starfield object so that
//  it's ready to be started. We must provide a container div, that's
//  what the starfield will live in.
Starfield.prototype.initialise = function(div) {
    
    var self = this;
 
    //  Store the div
    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();
    });
 
    //  Create the canvas
    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.

JavaScript
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).

JavaScript
var self = this;

//  Store the div.
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.

JavaScript
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.

JavaScript
    //  create the canvas
    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:

  1. Store the div.
  2. Store useful properties, the width and height.
  3. Listen for the window resize, when it does, update the width and height and redraw.
  4. 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.

JavaScript
Starfield.prototype.start = function() {
 
    //  Create the stars
    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!

JavaScript
function Star(x, y, size, velocity) {
    this.x = x;
    this.y = y; 
    this.size = size;
    this.velocity = velocity;
} 

Functions 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!

JavaScript
    var self = this;
    //	Start the timer.
    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.

JavaScript
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 the star has moved from the bottom of the screen, spawn it at the top.
        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 stars - 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:

JavaScript
Starfield.prototype.draw = function() {
    
    //  Get the drawing context
    var ctx = this.canvas.getContext("2d");
 
    // Draw the background
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, this.width, this.height);
 
    //  Draw stars
    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:

JavaScript
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)