ExplorerCanvas: Interactive Web Apps
May 10, 2006
In my Supertrain article, I
showed how to use the HTML canvas
element to draw dynamically changing
server-side information using AJAX. In that example, the user was a passive recipient
of
information. In this article, I will demonstrate how to handle user input to allow
your
canvas applications to reach the next level of interactivity.
But first, we need to catch up on some of the happenings in the canvas
space.
In my Supertrain article, I
highlighted some of the shortcomings of the canvas
element: 1) supported only
in Firefox and Safari, and 2) no support for rendering text. In the last few months
these
limitations have been blown away thanks to the growing canvas
developer
community. There are now at least two different ways to render text. One was provided
by Benjamin
Joffe and another by Mihai
Parparita. I incorporated Benjamin's drawString
method into http://awordlike.com/ and brought it one step closer to
being an actual clone of The Visual Thesaurus.
The other shortcoming was a killer: Internet Explorer does not (and will not) support
the
canvas
element. Several canvas
developers (including Emil Eklund) attacked this problem early on, and some
progress was made, but the recent emergence of the ExplorerCanvas project, backed by
Google, has made a ubiquitous canvas
a reality.
In this article, I won't be using text, but the example will be supported on Internet Explorer (along with Safari and Firefox). Now, on with the show ...
Cosley Petting Zoo in Wheaton, Illinois, has asked me to create an interactive front end to allow their zoologists to record the movements of their new baby red squirrel as it wanders around the zoo. The zoologists use tablet PCs running Internet Explorer 6, and the petting zoo is equipped with a WiFi network. All of the zoologists share the reponsibility for monitoring their new baby red squirrel and want to stay up-to-date on its whereabouts. They need to be able to easily update its location when it is spotted.
With my canvas
-AJAX hammer in hand, my solution should come as no surprise. By
the end of this article I will have incrementally built up a first release to deploy
onto
the Cosley web server to get some quick feedback from the zoologists (here's a working example). I'll
start with an HTML page with a canvas
that corresponds roughly with the area
the baby red squirrel will be roaming.
InteractiveCanvas.html
<html> <head> <script type="text/javascript" src="excanvas.js"></script> </head> <body> <canvas id="zoo" width="500" height="300" style="border: 1px solid black"></canvas> </body> </html>
The only interesting thing going on here is the inclusion of ExplorerCanvas, which is all I need to do to get canvas to show up in Internet Explorer. Next I'll draw the token that represents the squirrel, which, because I'm so lazy, is a circle.
InteractiveCanvas.html
<html> <head> <script type="text/javascript" src="excanvas.js"></script> <script type="text/javascript" src="prototype-1.4.0.js"></script> <script type="text/javascript"> window.onload = function() { var context = $("zoo").getContext("2d"); context.beginPath(); context.arc(50, 50, 10, 0, 2*Math.PI, false); context.closePath(); context.fill(); }; </script> </head> <body> <canvas id="zoo" width="500" height="300" style="border: 1px solid black"></canvas> </body> </html>
Again, nothing fancy. I have included the Prototype JavaScript library because I'm lazy and I'd rather type $()
than document.getElementById()
. Plus, I'll use Prototype for AJAX later on.
Other than that, I drew a circle and filled it. Now, I want to move the squirrel.
InteractiveCanvas.html
<html> <head> <script type="text/javascript" src="excanvas.js"></script> <script type="text/javascript" src="prototype-1.4.0.js"></script> <script type="text/javascript"> window.onload = function() { if ( document.addEventListener ) { document.addEventListener("click", onClick, false); } else if ( document.attachEvent ) { document.attachEvent("onclick", onClick); } else { alert("Your browser will not work for this example."); } }; function onClick(e) { var context = $("zoo").getContext("2d"); var position = getRelativePosition(e); context.clearRect(0, 0, $("zoo").width, $("zoo").height); context.beginPath(); context.arc(position.x, position.y, 10, 0, 2*Math.PI, false); context.closePath(); context.fill(); } function getRelativePosition(e) { var t = $("zoo"); var x = e.clientX+(window.pageXOffset||0); var y = e.clientY+(window.pageYOffset||0); do x-=t.offsetLeft+parseInt(t.style.borderLeftWidth||0), y-=t.offsetTop+parseInt(t.style.borderTopWidth||0); while (t=t.offsetParent); return {x:x,y:y}; } </script> </head> <body> <canvas id="zoo" width="500" height="300" style="border: 1px solid black"></canvas> </body> </html>
Enter the bad old days of JavaScripting ... Internet Explorer handles event listeners
differently than the other browsers, so I had to put some conditional logic in the
onload
method. The different browsers also pass in canvas
coordinates differently, so I needed to create the getRelativePosition
(found
via the canvas-developers
group) function to give me the coordinates I need. I extracted the drawing
functionality to the onClick
function and added the clearRect
call, which clears the screen before I redraw the squirrel.
Thus far, I developed all of this without a server, and no AJAX. The zoologists could
use
the app right now, but they wouldn't be able to see each other's updates: each zoologist
would have only his or her own isolated tracking information. It's time to introduce
a
server to allow the zoologists to cooperatively track the squirrel. Just like my last
article, I'll use Ruby's WEBrick
server to keep things simple. I'll start by
polling the server for the location of the squirrel and refreshing the canvas
with its coordinates.
cosley-server.rb
require 'webrick' include WEBrick server = HTTPServer.new( :Port => 8053 ) server.mount("/", HTTPServlet::FileHandler, ".") server.mount_proc("/squirrel/location") do |request, response| response['Content-Type'] = "text/plain" response.body = '({"x":50,"y":50})' end trap("INT") { server.shutdown } server.start
This server will use its current directory as the docroot. I also mounted a closure that will respond to http://localhost:8053/squirrel/location with hard-coded JSON coordinates.
InteractiveCanvas.html
<html> <head> <script type="text/javascript" src="excanvas.js"></script> <script type="text/javascript" src="prototype-1.4.0.js"></script> <script type="text/javascript"> window.onload = function() { startPolling(); setupClick(); }; function startPolling() { new PeriodicalExecuter(function() { new Ajax.Request('/squirrel/location', { onComplete: function(request) { var jsonData = eval(request.responseText); if (jsonData == undefined) { return; } draw(jsonData); }}); }, 1); } function setupClick() { if ( document.addEventListener ) { document.addEventListener("click", onClick, false); } else if ( document.attachEvent ) { document.attachEvent("onclick", onClick); } else { alert("Your browser will not work for this example."); } } function onClick(e) { draw(getRelativePosition(e)); } function draw(position) { var context = $("zoo").getContext("2d"); context.clearRect(0, 0, $("zoo").width, $("zoo").height); context.beginPath(); context.arc(position.x, position.y, 10, 0, 2*Math.PI, false); context.closePath(); context.fill(); } function getRelativePosition(e) { var t = $("zoo"); var x = e.clientX+(window.pageXOffset||0); var y = e.clientY+(window.pageYOffset||0); do x-=t.offsetLeft+parseInt(t.style.borderLeftWidth||0), y-=t.offsetTop+parseInt(t.style.borderTopWidth||0); while (t=t.offsetParent); return {x:x,y:y}; } </script> </head> <body> <canvas id="zoo" width="500" height="300" style="border: 1px solid black"></canvas> </body> </html>
I needed to make a few changes to the client code. I refactored the onload
method to a more declarative style because things were getting messy in there. The
startPolling
function was the major addition. It combines two Protoype
classes to poll the server via AJAX for the location of the squirrel once every second.
It
eval
s the asynchronous JSON response and redraws the squirrel in its latest
location. Point your browser at http://localhost:8053/InteractiveCanvas.html and
you'll see the squirrel sitting in the upper left corner.
The problem is that the zoologists can see where the squirrel was, but they can't tell the server where the squirrel is now (the cute little thing keeps moving back up to the corner). One more AJAX call should finish the job. First I'll update the server by mounting another closure that can handle coordinate updates.
cosley-server.rb
require 'webrick' include WEBrick server = HTTPServer.new( :Port => 8053 ) server.mount("/", HTTPServlet::FileHandler, ".") $location = [50, 50] def location_json "({\"x\":#{$location[0]},\"y\"<WBR>:#{$location[1]}})" end server.mount_proc("/squirrel/location") do |request, response| response['Content-Type'] = "text/plain" response.body = location_json end server.mount_proc("/squirrel/update") do |request, response| $location = [ request.query["x"].to_i, request.query["y"].to_i ] response['Content-Type'] = "text/plain" response.body = location_json end trap("INT") { server.shutdown } server.start
The updated location is extracted from the "/squirrel/update"
request
parameters, stored in a global variable, and converted into JSON on the way back to
the
client.
InteractiveCanvas.html
<html> <head> <script type="text/javascript" src="excanvas.js"></script> <script type="text/javascript" src="prototype-1.4.0.js"></script> <script type="text/javascript"> window.onload = function() { startPolling(); setupClick(); }; function startPolling() { new PeriodicalExecuter(function() { new Ajax.Request('/squirrel/location', { onComplete: draw }); }, 1); } function setupClick() { if ( document.addEventListener ) { document.addEventListener("click", onClick, false); } else if ( document.attachEvent ) { document.attachEvent("onclick", onClick); } else { alert("Your browser will not work for this example."); } } function onClick(e) { var position = getRelativePosition(e); new Ajax.Request('/squirrel/update', { parameters: "x=" + position.x + "&y=" + position.y, onComplete: draw }); } function draw(request) { var position = eval(request.responseText); if (position == undefined) { return; } var context = $("zoo").getContext("2d"); context.clearRect(0, 0, $("zoo").width, $("zoo").height); context.beginPath(); context.arc(position.x, position.y, 10, 0, 2*Math.PI, false); context.closePath(); context.fill(); } function getRelativePosition(e) { var t = $("zoo"); var x = e.clientX+(window.pageXOffset||0); var y = e.clientY+(window.pageYOffset||0); do x-=t.offsetLeft+parseInt(t.style.borderLeftWidth||0), y-=t.offsetTop+parseInt(t.style.borderTopWidth||0); while (t=t.offsetParent); return {x:x,y:y}; } </script> </head> <body> <canvas id="zoo" width="500" height="300" style="border: 1px solid black"></canvas> </body> </html>
I added an Ajax.Request
to the onClick
function which sends the
updated coordinates to the server. Then I refactored the JSON processing into the
draw
function so I could pass the draw
function to both of the
Ajax.Request
s' onComplete
callbacks. And that's it! The
zoologists can now monitor and update the location of their baby red squirrel. It
would not
be difficult to extend this example to insert the location updates into a database
in order
to chart the movements and tendencies of the squirrel over time.