Monday, July 2, 2012

Simple Spaceship in HTML 5

Today's post extends last week's orbit by allowing you to control the spaceship.

Demo


Mult:    Refresh: ms   


JavaScript
This code uses the same 3 classes as the previous demo. To keep the math simpler, I assume that the spaceship starts out 1 unit from the center of the solar system and has a speed of 1. Unlike the previous demo, the acceleration follows the inverse square law to improve realism.

Ship
The ship is still a position, velocity, and acceleration, but now it also has a direction and a concept of a thruster.
  1 var Ship = function() {
  2     this.pos = new Vector();
  3     this.vel = new Vector();
  4     this.acc = new Vector();
  5     this.color = "255,255,255";
  6     this.dir = 0;
  7     this.max_dir = 12;
  8     this.thrusterOn = false;
  9     this.thrusterForce = 0.1;
 10     this.size = 0.01;
 11 }
 12 
 13 Ship.prototype.turnRight = function() {
 14     this.dir = (this.dir + 1) % this.max_dir;
 15 }
 16 
 17 Ship.prototype.turnLeft = function() {
 18     this.dir = (this.dir + this.max_dir - 1) % this.max_dir;
 19 }
 20 
 21 Ship.prototype.angle = function() {
 22     return this.dir * 2 * Math.PI / this.max_dir
 23 }
 24 
 25 Ship.prototype.adjust = function(time) {
 26     if (this.thrusterOn) {
 27         this.acc.x += this.thrusterForce * Math.sin(this.angle());
 28         this.acc.y -= this.thrusterForce * Math.cos(this.angle());
 29     }
 30     this.vel.adjust(this.acc, time);
 31     this.pos.adjust(this.vel, time);
 32 }
 33 
 34 Ship.prototype.draw = function(ctx) {
 35     ctx.save();
 36     ctx.translate(this.pos.x, this.pos.y);
 37     ctx.rotate(this.angle());
 38     ctx.strokeStyle = "rgb(" + this.color + ")";
 39     ctx.scale(this.size, this.size);
 40     ctx.beginPath();
 41     ctx.moveTo(0, -10);
 42     ctx.lineTo(5, 10);
 43     ctx.lineTo(-5, 10);
 44     ctx.lineTo(0, -10);
 45     ctx.stroke();
 46 
 47     if (this.thrusterOn) {
 48         ctx.fillStyle = "rgb(" + this.color + ")";
 49         circle(ctx, 0, 11, 1);
 50         circle(ctx, -2, 13, 1);
 51         circle(ctx, 2, 13, 1);
 52         circle(ctx, -4, 15, 1);
 53         circle(ctx, 4, 15, 1);
 54         circle(ctx, -1, 15, 1);
 55         circle(ctx, 1, 15, 1);
 56     }
 57     
 58     ctx.restore();
 59 }
Simulation
The simulation has the same responsibilities as before, but also scales the display so that a radius of 1 in ship space corresponds to halfway out on the canvas.
  1 var Simulation = function(ctx, size) {
  2     this.ctx = ctx;
  3     this.size = size;
  4     this.reset();
  5 }
  6 
  7 Simulation.prototype.drawStar = function() {
  8     this.ctx.fillStyle= "#FF0";
  9     circle(this.ctx, 0, 0, 20);
 10 }
 11 
 12 Simulation.prototype.draw = function() {
 13     this.ctx.fillStyle = "rgba(0,0,0,1.0)";
 14     this.ctx.fillRect(0, 0, this.size, this.size);
 15     
 16     var dim = this.size/2;
 17     
 18     this.ctx.save();
 19     this.ctx.translate(dim, dim);
 20     this.drawStar();
 21     this.ctx.scale(dim/2.0, dim/2.0);
 22     this.ship.draw(this.ctx);
 23     this.ctx.restore();
 24 }
 25 
 26 Simulation.prototype.step = function() {
 27     var ship = this.ship;
 28     var acc = ship.acc;
 29     var angle = Math.atan2(ship.pos.x, ship.pos.y);
 30     var d2 = ship.pos.x*ship.pos.x + ship.pos.y*ship.pos.y;
 31     acc.x = -Math.sin(angle) / d2;
 32     acc.y = -Math.cos(angle) / d2;
 33     ship.adjust(this.mult);
 34     if (d2 > 16) {
 35         this.stop();
 36         alert("Ship Left Orbit!");
 37     }
 38 }
 39 
 40 
 41 Simulation.prototype.reset = function() {
 42     this.stop();
 43     var ship = new Ship();
 44     ship.pos.x = 1.0;
 45     ship.pos.y = 0.0;
 46     ship.vel.x = 0.0;
 47     ship.vel.y = -1.0;
 48     this.ship = ship;
 49 }
 50 
 51 Simulation.prototype.run = function(mult, refresh) {
 52     this.stop();
 53     this.mult = mult;
 54     var self = this;
 55     this.timer = setInterval(function() {
 56         self.step();
 57         self.draw();
 58     }, refresh);
 59 }
 60 
 61 Simulation.prototype.stop = function() {
 62     clearInterval(this.timer);
 63 }
Helper Function
Both the above classes call the helper function "circle" for drawing filled in circles.
  1 var circle = function(ctx, x, y, rad) {
  2     ctx.beginPath();
  3     ctx.arc(x, y, rad, 0, 2*Math.PI, false);
  4     ctx.fill();
  5 }
Onload
We need a few more controls, so the onload portion has a little extra functionality.
  1 $(function() {
  2     var canvas = document.getElementById("canvas");
  3     var size = 500;
  4     canvas.width = size;
  5     canvas.height = size;
  6     var ctx = canvas.getContext('2d');
  7     var sim = new Simulation(ctx, size);
  8     sim.draw();
  9 
 10     $("#run").click(function() {sim.run($("#mult").val(), $("#refresh").val())});
 11     $("#stop").click(function() {sim.stop()});
 12     $("#reset").click(function() {sim.reset(); sim.draw();});
 13     $("#ccw").click(function() {sim.ship.turnLeft(); sim.draw();});
 14     $("#cw").click(function() {sim.ship.turnRight(); sim.draw();});
 15     $("#thruster").mousedown(function() {sim.ship.thrusterOn = true; sim.draw();});
 16     $("#thruster").mouseup(function() {sim.ship.thrusterOn = false; sim.draw();});
 17 });
HTML
Again, the HTML is just a canvas along with some controls.
  1 <canvas id="canvas"></canvas>
  2 <br />
  3 Mult: <input id="mult" type="text" value="0.01" size="4" />
  4   
  5 Refresh: <input id="refresh" type="text" value="15" size="3" />ms
  6   
  7 <button id="run">Run</button>
  8 <button id="stop">Stop</button>
  9 <button id="reset">Reset</button>
 10 <br />
 11 <button id="ccw">CCW</button>
 12 <button id="cw">CW</button>
 13 <button id="thruster">Thruster</button>