2013-04-29

Canvas Trains

Intro

This is the fourth post in the "canvas drawing series". This one is about a small railway system and the trains directed by the control tower. I won't cover the basics of drawing etc. because it's done in the previous posts on the canvas:

  1. Canvas Bus
  2. Canvas Crossroad
  3. Canvas Balloons
Most of the techniques and patterns are also described in the previous posts, so I'll kind of concentrate only on the specifics of this example.

Rotating cars depending on the current rail line

Calculating the car angle depending on the current rail line:

  Rail.prototype.toAngle = function (x) {

   for (var i = 0; i < this.railLines.length; i++) {
    // find the rail line
    if (this.railLines[i].x1 <= x && x <= this.railLines[i].x2) {
     // calculate the angle
     var dy = (this.railLines[i].y2 - this.railLines[i].y1);
     var dx = (this.railLines[i].x2 - this.railLines[i].x1);
     
     return Math.atan2(dy, dx);
    }
   }

   return 0;
  };

Canvas text output

There are a couple of catches when working with canvas text. The first one is using the textBaseline property. The other is calculating the actual text width on a canvas used to center the text about train queue in the middle:

  ctx.fillStyle = textColor;
  ctx.font = '20px arial';
  var text = 'Sample text';
  var metrics = ctx.measureText(text);
  console.log(metrics.width);

The rail system

The rail system is made relative to the available canvas space:

  var RailSystem = function () {
   this.rails = [

   new Rail(0, [
    new RailLine(0, height/2, width/9, height/2)
   ]),
   new Rail(1, [
    new RailLine(width/9, height/2, width * 2/9, height/3),
    new RailLine(width * 2/9, height/3, width * 3/9, height/3),
    new RailLine(width * 3/9, height/3, width * 4/9, height/2)
   ]),
   new Rail(2, [
    new RailLine(width/9, height/2, width * 4/9, height/2)
   ]),
   new Rail(3, [
    new RailLine(width/9, height/2, width * 2/9, height * 2/3),
    new RailLine(width * 2/9, height * 2/3, width * 3/9, height * 2/3),
    new RailLine(width * 3/9, height * 2/3, width * 4/9, height/2)
   ]),
   new Rail(4, [
    new RailLine(width * 4/9, height/2, width * 5/9, height/2)
   ]),
   new Rail(5, [
    new RailLine(width * 5/9, height/2, width * 6/9, height/3),
    new RailLine(width * 6/9, height/3, width * 7/9, height/3),
    new RailLine(width * 7/9, height/3, width * 8/9, height/2)
   ]),
   new Rail(6, [
    new RailLine(width * 5/9, height/2, width * 8/9, height/2)
   ]),
   new Rail(7, [
    new RailLine(width * 5/9, height/2, width * 6/9, height * 2/3),
    new RailLine(width * 6/9, height * 2/3, width * 7/9, height * 2/3),
    new RailLine(width * 7/9, height * 2/3, width * 8/9, height/2)
   ]),
   new Rail(8, [
    new RailLine(width * 8/9, height/2, width, height/2)
   ])

   ];
  };

Train states

Every train goes trough the following states:

  • Creation - dat.gui controlled (counter) on every drop to zero and free entry rail
  • firstTurn - takes random free rail and goes into queue for the middle rail
  • waitingForMiddle - remains in this state until the middle rail is free for it to pass
  • goIntoMiddle - goes to the middle rail and passes trough
  • pickWayOut - waits on the trains going out and picks a rail going out of the rail system
  • goToExit - performs the exit, cleans the data describing the train

The states are self explanatory but the transitioning logic is a bit complex and it'll not be listed. But feel free to look the states up in the source code provided.


And here's my example on:
Codepen
GitHub

2013-04-13

Canvas Bus

Intro

This is the third post in the "canvas drawing series". This one is about bus picking up passengers and distributing them to their destinations. I won't cover basics of drawing etc. because it's done in the previous posts on the canvas:

  1. Canvas Crossroad
  2. Canvas Balloons
The main difference to the previous examples is that this one makes a complete redraw every time because there would be a lot of sprite redrawing and recalculating to remove all of the last states of sprites. Also, keep in mind that the browser does a very good job of repainting and keeps an internal off-screen canvas so you don't have to. The animation loop and sprite drawing of this example is in fact pretty simple:
  (function animloop(){
   requestAnimFrame(animloop);

   ctx.clearRect(0, 0, canvas.width, canvas.height);

   sim.draw();
  })();
There are four main entities involved in this simulation:
  • Passengers
  • Bus
  • Stations
  • Simulation master

Passengers appear on the stations in regular intervals and wait in the queue for the bus to pick them up. Their destination is denoted by their color, so the passenger appearing on the station always have a color different then the color of the station. The algorithm for color picking is listed at the end of this post.

Bus is drawn with simple geometric shapes and has an aisle and seats for the passengers. There is a separate space for the driver, the door and blinkers for signalization to the rest of the traffic. The bus has fixed loading time, but if it becomes full it's going to leave the station.

Stations are represented by their waiting queues plus the loading and unloading spot. As mentioned before the new passenger arrival interval is linear, but can be very easily configured to take some kind of arrival event time distribution, since the main focus is on the canvas drawing it's just plain simple linear time. Every station has a capacity, so no new passengers arrive when the station is full.

Simulation master is the god object of this simulation and directs the bus around and tells it when to load, unload passengers and syncs it with the stations. It's the usual way of doing this type of simulations.

Passengers loading

The standard bus has an aisle and seats. In every draw cycle while loading the passengers the simulation checks if the station has passengers to load. Before the passenger enters, the bus assigns a seat to the passenger by using this function:

  function toXY(seat) {
   var x = this.rightLine;

   if (seat % 2 === 0) {
    x = this.leftLine;
   }
   var y = this.startingRow + Math.floor(seat / 2) * passengerSize;

   return {
    x : x,
    y : y
   };
  }

The loading passenger then goes trough the following states:

  • outside - ends by going to the beginning of the aisle
  • upTheLane - ends by reaching the destination row
  • takeSeat - the passenger takes the left or the right seat

It's all better described with the following code listing:

  if (state == 'outside') {
   if (loadingPassenger.x + passengerSpeed <= middleLine) {
    loadingPassenger.x = middleLine;
    loadingPassenger.state = 'upTheLane';
   } else {
    loadingPassenger.x -= passengerSpeed;
   }

  } else if (state == 'upTheLane') {
   if (loadingPassenger.y + passengerSpeed >= 
     toXY(loadingPassenger.destinationSeat).y) {
    loadingPassenger.y = toXY(loadingPassenger.destinationSeat).y;
    loadingPassenger.state = 'takeSeat';
   } else {
    loadingPassenger.y += passengerSpeed;
   }

  } else if (state == 'takeSeat') {
   var dx = passengerSpeed;
   var destinationSeat = toXY(loadingPassenger.destinationSeat);
   if (destinationSeat.x == leftLine) {
    dx = -dx;
   }
   if (loadingPassenger.x + dx <= leftLine 
     || loadingPassenger.x + dx >= rightLine) {
    loadingPassenger.x = destinationSeat.x;
    passengers[loadingPassenger.destinationSeat] = loadingPassenger;
    loadingPassenger = null;
   } else {
    loadingPassenger.x += dx;
   }
  }

Unloading passengers

In every draw cycle while the bus is in the station there is a check for unloading passengers, basically a check if there are passengers in the bus with the color of the current station. Every unloading passenger goes trough the following states:

  • stepToLineL or stepToLineR - depending on the taken seat
  • goToDoor
  • getOut
The code is similar to the unloading code so theres no need to list it here.

Bus and Station

Drawing a bus and a station is pretty straight forward. To make the object drawing as simple as possible and to keep everything relative to the front left corner of a bus and to the loading unloading spot of the station we rotate and translate the whole canvas context. After that all of the drawing is done relative regardless of the current object orientation and position. The alternative would be complex 2d computation but thanks to the canvas it's pretty simple and one can concentrate on the drawing logic. The pattern enabling relative drawing with rotation is listed bellow:

  ctx.save();

  ctx.translate(this.x, this.y);
  ctx.rotate(this.angle);

  // relative drawing code

  ctx.restore();

Simulation master

The simulation master has the following states:

  • busInStation
  • busLeaving
  • busTraveling
  • unloadPassangers
  • waitForUnload

The states are self explanatory but the transitioning logic is a bit complex and it'll not be listed. But feel free to look the states up in the source code provided.

The one important thing that should be mentioned is the way the initial passenger color is chosen. Basically it depends on the current station color and all other station colors are all right. Then we are picking a random color from a set until we pick the color that isn't the color of the station. Note that a couple of cycles might pass since we have a new color but writing a complex pseudo random function would just complicate things so the simulation uses the following function:

  function randomColor(differentThan) {
   if (differentThan) {
    do {
     var color = stationColors[
      Math.floor(Math.random() * stationColors.length)
      ];
     if (color !== differentThan) {
      return color;
     }
    } while (true);
   }

   return stationColors[Math.floor(Math.random() * stationColors.length)];
  }
 

dat.gui

When experimenting with graphics and simulations it's best practice to provide a user with some kind of interface for changing variables. There is a small library for creating variable modifying interfaces called "dat.gui". I've found a very interesting and short tutorial about it under dat.gui.


And here's my example on:
Codepen
GitHub