Tuesday, 15 December 2009

Cosy Orbs – Web App Games in HTML5 for the iPhone

image [Concrete/Interesting] In this article I’m going to talk about HTML5 Canvas and graphics programming for web apps. I’ll be focusing on my experience and as way of example, I will be talking about ‘Cosy Orbs’ – a very simple, interactive graphics web application.

http://www.lowrieweb.com/iphone/cosyorbs

Installing Cosy Orbs

You can install Cosy Orbs onto your iPhone by following the link below in Safari:

http://www.lowrieweb.com/iphone/cosyorbs

Once the page has loaded, ‘Add to Home Screen’ by selecting the ‘+’ icon on the navigation bar at the bottom of the Safari page. Cosy Orbs will now be placed on your home screen and can be run by clicking the application icon. 

About Cosy Orbs

In 1999, the moon was knocked out of its orbit by a nuclear waste explosion, sending all 311 inhabitants of Moonbase Alpha into outer space. Also, like a lot of other people, I was looking at Java and Java Applets. Working my way through the SDK and samples, I came across a sample called ‘Graph Layout’. I was quite impressed with Java but I was blown over by Graph Layout. It was so cool. Basically, a string parameter to the applet defined a number of nodes on a network graph. So the string ‘Nigel-Aiesha, Aiesha-Sophia, Sophia-Nigel, Sophia-Melina, Melina-Aiesha’ would define a graph with four nodes (Nigel, Aiesha, Sophia and Melina). In addition, it defines the relationship between these nodes.

The applet would graphically represent these nodes as boxes connected by lines. The lines represent the relationship between the nodes. So, with the example above, the graph would look like:

image

But that wasn’t the cool bit.

The cool bit was the applet applies an algorithm to the graph that worked to minimize the stress between the nodes. The stress is defined as the length of the connecting lines, measured against a desired value. So if two nodes have a desired distance of 100 and the actual distance is 150, the stress is 50.

Every cycle, the stresses are calculated and the nodes are moved relative to the other nodes so as to reduce the stress.The result is the graph gracefully reorganizes itself into the lowest stress state by moving the nodes about each other.

Because of the nature of the algorithm and the fact that it acts across the entire system of nodes, the process of reducing the stress or ‘relaxing the graph’ is indeed very graceful.

This applet has sat in a dusty corner of my mind waiting to be re-applied or re-engineered. Then came iPhone, HTML5 and Canvas and Cosy Orbs was born.

Cosy Orbs uses a similar algorithm to relax the stress across a network of nodes, but:

  • It is written in HTML5, JavaScript, jQuery
  • It is an iPhone Offline web app.
  • It is more graphical with ‘Orbs’ or shiny spheres rather than boxes with text
  • It uses a Touch enabled UI rather than mouse.

So how’s it all done?

Offline Web App

This is a must have feature for any web app, especially a game or fun app. No problem on the iPhone, with HTML5 all you need to do is set up a manifest file. Well, not that simple but I’ve blogged about it before so won’t repeat myself here. See iPhone Offline Web Apps – the RESTful Way – Part 1 of 2.

Initializing the Canvas

The Canvas is an HTML5 element that represents a drawing surface. In essence it is a bitmap. HTML5 currently support ‘2D’ drawing operations and some other useful functions over the canvas like streaming the canvas to a string.

I use the following code to initialize the canvas:

myCanvas = document.createElement('canvas');
myCanvas.id = 'myCanvas';
myCanvas.width = 300;
myCanvas.height = 300;
$('#myDrawing').append(myCanvas);

After creating the canvas, the element is appended to a DIV on my page. I also remove a compatibility statement. This statement is shown if you run the app in an incompatible browser, otherwise it is removed:

$("#notice").hide();
<div id="notice" style="text-align:center"><strong>This application is designed for the iPhone. Link to this page in Safari on your iPhone.</strong></div>

Preloading the Orbs

Unlike the Java Applet, I am representing nodes with lovely coloured spheres. I have five different images for my Orbs; blue, red, green, purple and yellow. So when drawing a node, I will assign it one of the colours.

When drawing I want to work with Image elements, so I need to preload the images before any drawing starts. This was a bit tricky because loading an image requires going to the server to fetch the image data, so the Image.onload() is asynchronous. Because I wanted to wait for the images to finish loading before starting any drawing, I decided to set up a simple timer:

var imageString = 'images/orb-blue.png,images/orb-green.png,images/orb-purple.png,images/orb-red.png,images/orb-yellow.png';
var imagesToLoad = 0;
 
function initImages()
{
  $.each(imageString.split(','), 
    function(idx, val) {
      imagesToLoad++;
      var img = new Image();
      img.onload = function() {imagesToLoad--};
      img.src = val;
      images.push(img);
    });
    setTimeout('completeImageLoad()',500);
}
 
function completeImageLoad()
{
  if (imagesToLoad > 0)
    setTimeout('completeImageLoad()',500);
  else
  {
    // Ready to go
  }
}

The images are all named in the string ‘imageString’. When loading, I simply split this string, and for each image named, I create an Image element and load from the source. I also increment a load count, counting each image as it’s loaded. Finally I add the image to the list of preloaded images - this will be used when drawing the images.

During the Image.onLoad(), which is fired asynchronously once the image has loaded, I decrement the load count, signalling that this images has loaded completely.

Once all the images have been processed (but not necessarily completed loading), I call a complete method. This is called asynchronously using the setTimeout() method.

Now all I need to do is poll for the load count to reach zero, which will happen when all the images have completed loading. If the images are still loading, the complete method is called again under timer.

Once all the images have been loaded, I need to set up the nodes…

Initialize the Nodes and off we go

Nodes and their relationships are defined by a string called ‘edgeString’. The process of initializing the nodes involves reading this string, and building a list of Node() and Edge() objects. Each Node() object holds the canvas co-ordinates of the node and the image assigned to the node – these are used when drawing the network. Each Edge() object defines the stress between two Nodes(). As a node is created, an image from the list of images is assigned to the node in a round robin fashion.

Once the Nodes() and Edges() are initialized, the network is rendered to the canvas.

Finally, a timer is set to refresh the display:

function drawCanvas()
{
  setTimeout('drawCanvas()', refresh);
  relax();
 
  if (myCanvas.getContext) {
    var ctx = myCanvas.getContext('2d');
    ctx.fillStyle = 'rgb(0,0,0)';
    ctx.lineWidth = 2;
    ctx.fillRect(0,0,myCanvas.width,myCanvas.height);
 
    for(var j=0; j<nodes.length; j++)
      ctx.drawImage(nodes[j].img, nodes[j].x-21, nodes[j].y-21);
  }
}

When fired, a function is called to Relax() the graph – that is, to process each node’s stresses,  incrementally reducing the stress across the network. Once the stress has been reduced and the network is a little more relaxed, a call is made to render the network to the canvas. This happens every 150ms, which provides a smooth animation with the network slowly relaxing into its lowest stress state.

Rendering is simple, first, a ‘2d’ context is obtained on the canvas. With this context, the canvas is cleared with a black background through a call to the fillRect() function. Then for each node in our array of Nodes(),  the image on the node is drawn to the canvas using the drawImage() method.

What about Touch Events

Not much fun if you can’t interact with the Orbs, so I implemented a simple touch interface:

$('#myCanvas').bind('touchstart touchmove touchend', function(evt) {
  var touch = evt.originalEvent.targetTouches[0];
  switch (evt.type) {
    case 'touchstart':dragNode = findNodeByPos(touch.pageX-this.offsetLeft, touch.pageY-this.offsetTop);break;
    case 'touchend':dragNode = null;break;
    case 'touchmove':
      if (dragNode) {
        dragNode.x = touch.pageX-this.offsetLeft;
        dragNode.y = touch.pageY-this.offsetTop;
      }
      break;
  }
  evt.preventDefault();
});

To do this I have to bind the touch events, touchstart, touchmove and touchend, to a callback function for the named element. jQuery make life very simple through the use of the bind() method. 

In the event handler, the event type is determined and for touchstart events, the nodes are searched for a match to the touch position. That is to say, the nodes are searched for any node under the user’s finger.

Touchmove tracks the movement of a finger on the iPhone surface. I use this to update the screen position of the selected node (if one was found during touchstart). Finally,  touchend stops the dragging operation. Job done.

Summary

CosyOrbs, not exactly a game for the iPhone, but I am impressed as to how easy it is to create interactive graphics under HTML5.

No comments:

Post a Comment