Wednesday, July 6, 2011

Your Polygons are Hitting Each Other

While working on HTML5 games, I sometimes need something in JavaScript that I'm not able to find. On one such occasion I found myself in need of a JavaScript polygon object that would support collision detection with other polygons. This algorithm, using the Separating Axis Theorem, is well-known and had many implementations in other languages. It wasn't too difficult to convert it to JavaScript. While I was at it, I added methods to support determining if the polygon contains a given point (to detect if I was clicking on it) and rotating the polygon.

You can see what I came up with here, and I have a test page for it here.

As you can see by viewing the source of the test page, it is fairly easy to use. It is designed to combine with the HTML5 canvas element.

To create a polygon centered at a given point and using center-relative coordinates for the vertices, you do something like this:
var poly = new Polygon( { x: 50, y: 50 }, "#00FF00");
poly.addPoint( { x: -20, y: -20 } );
poly.addPoint( { x: -20, y: 20 } );
poly.addPoint( { x: 20, y: 20 } );
If you want to use all absolute coordinates for the vertices, you can do that too:
var poly = new Polygon( { x: 50, y: 50 }, "#00FF00");
poly.addAbsolutePoint( { x: 130, y: 130 } );
poly.addAbsolutePoint( { x: 130, y: 170 } );
poly.addAbsolutePoint( { x: 170, y: 170 } );
To rotate, just call the rotate method with the number of radians you want to rotate. Remember, to convert degrees to radians, multiply by Pi/180.
poly.rotate(0.78539); // 45 degrees
So now you have no excuse for not making a fun HTML5 canvas game. I'd like to see a game about cheese-making. I think that would be awesome.

Saturday, April 16, 2011

Building by the Byte - the HTML5 File API

One of the major features needed in JavaScript to make it truly useful as an application language is file processing. I'm talking about handling the contents of a file totally in your web browser. No server needed. Even non-text format files. Now with HTML5 we have this capability in the File API! I've been thinking about the awesome new possibilities opened up by this development, and put together an example of what it can be used for.

First, let's talk about the browser support. The latest Chrome, Safari, and Firefox browsers support the new JavaScript File API. Internet Explorer? Nope. Get a real browser.

The first thing to understand is the FileReader object. It's a new built-in object, sort of like the XMLHttpRequest object. Like the familiar XHR, FileReader is designed to work asynchronously. That means you'll need to specify your own onload function to the object, which will be called when the browser is done with the file. Think about it - it could take a while to process a file and you don't necessarily need your app tied up waiting for it. Look at this simple example...
var reader = new FileReader();
reader.onload = function(event) {
// file is loaded, contents are in event.target.result
// do something with it!
}
reader.readAsBinaryString(file);
Now you're probably asking a few questions at this point. Where did you get the file object? How does JavaScript handle binary data? What if there's an error? How do they get the peanut butter inside the peanut butter cups? I can answer all but that last one.

First, there are a few ways to get a file object. My favorite is to grab one simply by dragging it into the browser window. This is accomplished via the dataTransfer property of the event object. For example, let's say you have the following div in your page...

<div id="drophere" style="text-align: center; width: 200px; height: 100px;">drop a file here</div>
And then you had some JavaScript like this...
document.getElementById('drophere').ondrop = function (evt) {
evt.preventDefault();
var file = evt.dataTransfer.files[0];
// now you've got a file object, which is the file you dropped
return false; // don't let the browser navigate away
}
Now just drag a file into your browser window and drop it on your div. Awesome! You've got a file. Now just combine this function with the previous one and you're all set to process anything you can drag in. Well, almost. There's still that binary data issue. JavaScript doesn't really have a data structure designed for binary data.

This is where you break out the FileReader and pass in that file object. Add the code from the first example into the second example....

function handleDrop(evt) {
evt.preventDefault();
var file = evt.dataTransfer.files[0];
var reader = new FileReader();
reader.onload = function(event) {
// file is loaded, contents are in event.target.result
// do something with it!
}
reader.readAsBinaryString(file);
return false; // don't let the browser navigate away
}
So when you get the event.target.result object (in the reader's onload function), what will it be? It's actually going to be a String where each character code is between 0 and 255. To read the "bytes" of the file, just loop through all the characters calling charCodeAt on each one. I made an object to help with all the functions you might want to do with the "byte array"...

function DataReader(a) {
 
 this.bytes = a;
 this.index = 0;
 this.byteRead = 0;
 this.bitIndex = 0;
 this.endian = "big";
 
}

DataReader.prototype.readByte = function() {
 if (this.eof()) return;
 var ret = this.bytes.charCodeAt(this.index);
 this.index++;
 return ret;
}

DataReader.prototype.readBytes = function(howMany) {
 if (this.eof()) return;
 var ret = new Array();
 for (var i = 0; i < howMany; i++) {
  ret.push(this.readByte());
 }
 return ret;
}


DataReader.prototype.readInteger = function(numBytes) {
 
 if (this.eof()) return;
 
 var howMany = 4; // default to a 4-byte integer
 if (numBytes) {
  howMany = numBytes;
 }
 
 var ret = 0;
 if (this.endian == "little") {
  var origIndex = this.index;
  for (var n = this.index + howMany - 1; n >= origIndex; n--) {
   ret = ((ret << 8) | this.bytes.charCodeAt(n));
   this.index++;
  }
 } else {
  for (var n = 0; n < howMany; n++) {
   ret = ((ret << 8) | this.bytes.charCodeAt(this.index));
   this.index++;
  }
 }
 return ret;
 
}

DataReader.prototype.readString = function(len) {
 if (!len || this.eof()) return;
 var ret = this.bytes.substring(this.index, this.index + len);
 this.index += len;
 return ret;
}

DataReader.prototype.readNullTerminatedString = function() {
 if (this.eof()) return;
 var slen = 0;
 var n = this.index;
 var finished = false;
 while (!finished && n <= this.bytes.length) {
  var c = this.bytes.charCodeAt(n);
  if (c == 0) {
   finished = true;
  }
  slen++;
  n++;
 }
 var ret = this.bytes.substring(this.index, this.index + (slen - 1));
 this.index += slen;
 return ret;
 
}

DataReader.prototype.skip = function(num) {
 if (this.eof()) return;
 this.index += num;
}

DataReader.prototype.eof = function() {
 return (this.index >= this.bytes.length - 1);
}

I should mention that there are other options for processing the file. If you used readAsText instead of readAsBinaryString, you'd just get a normal string containing the contents of the file. That's only really useful if you know the file will only contain text data. A third option is readAsDataURL, which returns a data: URL instead of a string. You can use this to directly set the src attribute of an img tag with the dropped file. Again, this will have limited usefulness. Getting the binary string is the most powerful.

This is a good time to talk about the onerror function. If you tried the above example in Chrome using a local HTML file, it won't work. You'll get an error. You'll only know that if you specify an onerror function as well as an onload. Don't expect a whole lot of details in the error, however.
reader.onerror = function (event) {
console.log(this.error.code);
}
You'll see a "4" in the console. That's helpful... It actually means that Chrome, by default, does not allow local files (your test HTML file) access other local files (the file you drop in). Firefox does. It's not a real big deal, you can either test using a local server instead of just loading the file or add the "--allow-file-access-from-files" flag to Chrome when you start it. Security thing.

Okay, okay, okay...now what can you build with this? Well, some really amazing things. I put together this nifty example that will read PNG files and display them in the browser not as images, but as a bunch of DIVs (one for each pixel). To accomplish this, I just needed two things. One, the PNG specification which can be found here. And two, a way to inflate compressed data blocks inside the files. For that part, I used my pure JavaScript Inflater that I talked about in my last post.


If you don't have your own PNG file handy, use this one: http://www.amphibian.com/blogstuff/small_dr_frog.png

Make sure you check out the page source to see how it all works. It turns out that PNG files are fairly easy to work with once you have the data inflated.

I know my example is not particularly practical, but I hope it can at least inspire you to make something of your own that uses these splendid new HTML5 features. Use your imagination and let me know what you come up with!

Saturday, January 1, 2011

Inflate in JavaScript

Here's a little something I've been working with for the last few weeks...the Inflate algorithm implemented in JavaScript.

Just what is Inflate? Well, it's the opposite of Deflate. Obviously. In addition, it's also the algorithm used by gzip, WinZip, zlib, etc. to uncompress data. You can read all about it here or read the full RFC. It's been around a long time and has been implemented in lots of different languages, but I really wanted a pure JavaScript version. I'm that crazy.

I needed this because I've been working with the new HTML5 File API. With it, you can process file data in the client browser before uploading it to the server. This is great, until you try to work with a type of file (PNG, for example) that uses the Deflate algorithm to compress its data. I basically converted the simplest possible implementation of the process, Mark Adler's puff.c, to JavaScript and it works pretty well.

I'll have more to say on the HTML5 File API later, but now I release the JavaScript Inflate to the world! Typically, the algorithm works by processing streams of bytes. JavaScript, however, does not have such a data structure. Instead we just use arrays of numbers between 0 and 255 as input and output.

And a word of caution, I wouldn't try running this on a 3-years-out-of-date web browser or some old version of Netscape Navigator you've got running somewhere. It works great in the latest version of Chrome.


UPDATE! Here's a link to a page that you can use to see this thing in action. It uses a little HTML5 File API, which I'll discuss in a later post. I didn't explain much how to use the inflater in my original post so I hope this helps. It is really simple. Once you have your array of "bytes" representing deflated data, just pass it to the puff function along with an empty array you want to get filled up with the inflated "bytes". It will look like this:
var deflated = new Array();
// fill deflated with "bytes"
var inflated = new Array();
puff(inflated, deflated);
Your inflated array will contain the inflated data. It's that easy.