Reimagining xeyes for HTML5

Many of you will know of xeyes, a cute little app that has been around for decades. The eyes follow you as you move your mouse around the screen.

. I don’t know why I thought about it ( I have been setting up unix servers lately ), but I was inspired to do some HTML5 work with the recent release of Firefox 4 and their Web O’ Wonder.

So what’s my favorite HTML5 feature? SVG? Oh sure, everyone loves SVG – can’t over-estimate the effect the resurgence of SVG in HTML5 is going to have. See my TopCoder Map for an example of SVG awesomeness.

But no that’s not my favorite, because it’s not neat enough, the spec is huge, and there are interactions that mean it won’t safe to use in a browser for many years to come. That’s not the reason for this post, so I won’t go into detail.

Canvas is great, we can draw. But we could do that before, albeit server side. Also, I think I dislike the 2d context API.

Truly, I don’t have a favorite feature – the ones I like the best are the smaller APIs that slot neatly into the spec and are well defined. Which brings me to the point of this post:

The Device Orientation API (link to spec)

This magical API works only on mobile devices, I’m using an iPod Touch. It’s incredibly easy to use. Call this from onLoad, or just inline it:

function rotate(oe) {
// your code here
}

function init()
{
window.addEventListener("deviceorientation", rotate,false);
}

To understand the oe event parameter, read the spec. It is tiny, so don’t be afraid! There are 3 orientation attributes, alpha, beta and gamma. Each is a rotation around an axis referenced to a frame where the device is lying flat. Alpha goes between 0 and 360, beta goes between -180 and 180 (degrees), and gamma goes between -90 and 90 degrees.
Zero’s across the board imply the device matches the reference frame, which means it is lying flat on a table in front of you.

There is no guaranteed rate at which the rotate event is called, so you’ll see I use a separate render function called at regular intervals.

Using this API, I decided to implement a mobile version of xeyes – only the eyes follow you, reacting to the angle of the screen so that they are always looking at you!

If you’ve read this far, feel free to see the live version here: Eyes for HTML5.

Here are the steps I took:

1) Draw a face on a Canvas on page load
2) On each render:
a) Clear the eye sockets
b) Add pupils at a set distance based on beta and gamma angles.

Drawing a face


function preload() {
var context = viewer.getContext("2d");
drawFace(context);
context.restore();
}
function drawFace(context) {

context.clearRect(0,0,300,300);

context.save();

context.beginPath();
context.strokeStyle='black';
context.arc(75,75,25,0,Math.PI*2);
context.stroke();
context.closePath();

context.beginPath();
context.arc(175,75,25,0,Math.PI*2);
context.stroke();
context.closePath();

context.beginPath();
context.moveTo(125,100);
context.lineTo(125,150);
context.stroke();
context.closePath();

context.beginPath();
context.arc(125,150,75,Math.PI/4,Math.PI/2 + Math.PI/4);
context.stroke();
context.closePath();
}

Clearing and Rendering the eyes

The trick to this part is simpy that I use the eye drawing code from above to set a clipping region, which I then clear, and then draw the eyes in. This clipping means my eyes stay in their sockets!

function newEyes(context, gamma, beta)
{
context.save();
context.beginPath();
context.arc(175,75,23,0,Math.PI*2);
context.clip();
context.fillStyle = 'white';
context.fill();
context.closePath();

context.beginPath();
context.arc(175-gamma/5,75-beta/5,15,0,Math.PI*2);
context.fillStyle = 'black';
context.fill();
context.closePath();
context.restore();

context.save();
context.beginPath();
context.arc(75,75,23,0,Math.PI*2);
context.clip();
context.fillStyle = 'white';
context.fill();
context.closePath();

context.beginPath();
context.arc(75-gamma/5,75-beta/5,15,0,Math.PI*2);
context.fillStyle = 'black';
context.fill();
context.closePath();
context.restore();

}

The orientation and render handers

Since we don’t know when or if we will get an orientation event, we keep it separate to our render routine. This is just good practice in general. The render routine runs often to keep the animation smooth. The orientation handler can do processing only when needed. Of course, it’s not doing anything other than storing a variable here.


var lastOrientation = null;
var nextOrientation = null;

function renderer() {
if(nextOrientation == null) return;

if(lastOrientation != null &&(nextOrientation.gamma == lastOrientation.gamma)) return;

var context = viewer.getContext("2d");
newEyes(context,nextOrientation.gamma,nextOrientation.beta);
context.restore();
}

// event handler
function rotate(oe) {
lastOrientation = nextOrientation;
nextOrientation = oe;
}

setInterval(renderer,100);

That’s it, feel free to download and use the code. You’ll find it here: eyes.

Comments and ideas for uses of the orientation and motion APIs are welcome. I have some, but perhaps for another post.

Happy coding!

Peter