Menu

ExplorerCanvas: Interactive Web Apps

May 10, 2006

Dave Hoover

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 evals 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.Requests' 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.