Drawing asteroids on an HTML5 canvas

In the last part of this tutorial we started moving shapes around on the canvas, and we've added a new rectangle class (although we haven't demonstrated it yet). This is going to be the part of the tutorial where we really make use of the iShape interface. We are going to create another shape called an Asteroid... which isn't really a shape, but it's going to be the start of what will (eventually) become a game. Our asteroid is going to basically be an irregular polygon with points that will be randomly generated. We are also going to add some additional features like rotation, and velocity that will give our asteroids a little more life.

Polygons are made up of Points

The first thing we need is a Point class. This class will become more useful later, but for the moment we are going to use it as a way to keep track of all the points in our asteroid.

It's a really simple class that looks like this:

class cPoint {
   public x: number = 0;
   public y: number = 0;
   constructor(x: number = 0, y: number = 0) {
      this.x = x;
      this.y = y;
   }
}

The Asteroid Class

The Asteroid class is going to implement the shape interface. At this point, an Asteroid is really more of a game object than a shape, but right now we'll just continue with the interface we've already defined.

class cAsteroid implements iShape {
 public x: number = 0;
 public y: number = 0;
 public velocityX: number = 0;
 public velocityY: number = 0;
 public lineWidth: number = 5;
 public color: string = "white";
 public size: number = 20;
 public rotation: number = 0;
 public rotationSpeed: number = 0;
 public pointList: Array<cPoint> = new Array<cPoint>();

 public draw = (): void => {
  this.x += this.velocityX;
  this.y += this.velocityY;

  if (this.x < -this.size * 2) {
   this.x = 1280 + this.size * 2;
  }
  else if (this.x > 1280 + this.size * 2) {
   this.x = -2 * this.size;
  }

  if (this.y < -this.size * 2) {
   this.y = 720 + this.size * 2;
  }
  else if (this.y > 720 + this.size * 2) {
   this.y = -2 * this.size;
  }

  this.rotation += this.rotationSpeed;
  ctx.save();
  ctx.translate(this.x, this.y);
  ctx.rotate(this.rotation);
  ctx.beginPath();
  ctx.strokeStyle = this.color;
  ctx.lineWidth = this.lineWidth;

  ctx.moveTo(this.pointList[this.pointList.length - 1].x, this.pointList[this.pointList.length - 1].y);

  for (var i: number = 0; i < this.pointList.length; i++) {
   ctx.lineTo(this.pointList[i].x, this.pointList[i].y);
  }

  ctx.closePath();
  ctx.stroke();
  ctx.restore();
 }

 constructor(x: number = undefined, y: number = undefined, size: number = undefined,
        color: string = "white", line_width: number = 2) {
  if (x == undefined) {
   this.x = Math.round(Math.random() * 1280);
  }
  else {
   this.x = x;
  }

  if (y == undefined) {
   this.y = Math.round(Math.random() * 720);
  }
  else {
   this.y = y;
  }

  if (size == undefined) {
   this.size = Math.ceil(Math.random() * 10) + 4;
  }
  else {
   this.size = size;
  }

  this.velocityX = Math.round(Math.random() * 4 - 2);
  this.velocityY = Math.round(Math.random() * 4 - 2);

  this.rotationSpeed = Math.random() * 0.06 - 0.03;

  var xrand: number = 0;
  var yrand: number = 0;

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand, yrand + 3 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand - 1 * this.size, yrand + 2 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand - 2 * this.size, yrand + 2 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand - 3 * this.size, yrand + this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand - 4 * this.size, yrand));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand - 1 * this.size, yrand - 3 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand + 2 * this.size, yrand - 4 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand + 2 * this.size, yrand - 3 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand + 4 * this.size, yrand - 2 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand + 4 * this.size, yrand + this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.pointList.push(new cPoint(xrand + 3 * this.size, yrand + 2 * this.size));

  xrand = Math.round(Math.random() * this.size - this.size / 2);
  yrand = Math.round(Math.random() * this.size - this.size / 2);

  this.color = color;
  this.lineWidth = line_width;
 }

}

So cAsteroid is a fairly large class, but it's not as complicated as you may initially think. A large chunk of the code is in the constructor, and a good chunk of that is about randomizing the shape of the asteroid. I'm using xrand and yrand to modify the detailed shape of the asteroids while preserving the general shape. If no value is passed in for x, y, and size, I'm also randomizing the starting position and size values. I also am creating a random velocity which will determine what direction the asteroid is going to move in. The draw() method is also more complicated that it has been for the circle or the rectangle class we created earlier. I've moved all the code concerning moving the asteroid into the draw method. This will change the position of the asteroid based on the velocity each frame, as well as check to see if the asteroid needs to be looped back to the other side of the canvas. To draw the asteroid we need to loop over the "pointList" and draw lines between each of the points in that list.

Initializing the shape objects

So now that we have an asteroid class defined, we're going to create an array that will hold all our shapes. Most of these shapes will be asteroids, be we'll throw in a few circles and rectangles too.

The code is going to look like this:

var shape_array: Array<iShape> = new Array<iShape>();

window.onload = () => {
   canvas = <HTMLCanvasElement>document.getElementById('cnvs');

   shape_array.push(new cAsteroid());
   shape_array.push(new cAsteroid());
   shape_array.push(new cAsteroid());
   shape_array.push(new cAsteroid());
   shape_array.push(new cAsteroid());

   shape_array.push(new cCircle(20, 50, 30));
   shape_array.push(new cCircle(120, 70, 50));

   shape_array.push(new cRectangle(500, 500, 80, 60));

   ctx = canvas.getContext("2d");
   gameLoop();
}

Before our onload function we are creating an array of shapes called shape_array. This is going to be the array we use to track and render all of our shapes in the game loop. We are then adding within the onload function calls to push different shapes into that array. We are pushing in 5 asteroids, followed by 2 circles, and a single rectangle. After we add these lines in the onload we are going to need to change our game loop to actually render these shapes.

Changing our game loop to render shapes on the canvas

So now that we've defined our new shapes, created an array to track those shapes, and created those shapes in our onload function, the next step is to actuall draw those objects to the canvas.

The way we do this is in the game loop:

function gameLoop() {
   requestAnimationFrame(gameLoop);
   ctx.fillStyle = "black";
   ctx.fillRect(0, 0, 1280, 720);

   var shape: iShape;
   for (var i: number = 0; i < shape_array.length; i++) {
      shape = shape_array[i];
      shape.draw();
   }
}

We are using a for loop to loop over all the shapes in the shape_array and render them to the canvas.

Here is what all this looks like in action:

Check out the full code we wrote to get this working: Draw Shapes Code

If you missed the other parts to this tutorial, you can start at the beginning with part 1, or go back to part 3

Part 3 - Moving TypeScript Canvas Objects

Part 2 - TypeScript classes and interfaces

Part 1 - TypeScript HTML5 Canvas Basics