So I wanted to do a compass from a while just to push it to some apps, mostly as a nice way of showing directions (e.g. "this way to the museum"). I thought that the hardest part would be to draw the needle, and translate North azimuth to object azimuth. Turned out I was wrong...
Update: There is now a more stable version of below code with additional library for mocking compass and PhoneGap support. See: WebCompass project.
But let's start from the beginning. How is this even possible to have a compass in a browser? Well almost all smartphones and tablets have it. This works like a real compass and - in case you are wondering - you can have fun and make it go nuts with any magnet (like the magnets you probably have on your refrigerator).
But from a browser? Yes. You just need to observe a DeviceOrientation
event. Like this:
window.ondeviceorientation = function(eventData) { drawCompass(eventData.alpha); };
That's easy... Well, no. But for now let's assume it will work and draw the needle on canvas.
Quick background - canvas in real life is something artists use to paint on. Canvas in HTML is an element which programmers can use to paint on :-). This is how you declare it:
<canvas height="400" id="canvasId" width="400"></canvas>
To keep it simple we can initialize our canvas for drawing like this:
// this is what we'll use for accessing drawing functions var context; // we need to wait on load of the page window.onload = function() { var canvas = document.getElementById("canvasId"); context = canvas.getContext("2d"); }
So we have the angle and something to paint on. We now need to draw a line that rotates when an angle changes. Rotation is a bit tricky but this is not that bad.
An example implementation:
/** * alpha is a normalized angle where 0°/360° is North, 90° is East and so on. */ function drawCompassArrow(alpha) { // line paramaters var cx, cy, radius; cx = cy = 70; // this is a rotation center r = 50; // this is a radius // clear canvas context.clearRect(0, 0, canvas.width, canvas.height); // save context matrix/settings for later restoration context.save(); // transform deg. to radians var aRad = alpha * Math.PI / 180; // rotate on (cx, cy) point context.translate(cx,cy); context.rotate(-aRad); context.translate(-cx,-cy); // stroke styling context.lineWidth = 1; context.strokeStyle = 'black'; // draw context.beginPath(); context.moveTo(x, y); context.lineTo(x, y + radius); context.stroke(); // restore matrix context.restore(); // info about current angle context.font = "25px sans-serif"; context.fillText(alpha.toString(), cx - 5, cy + radius + 25); }
So we test e.g. on HTC Desire HD on Firefox. It works! But wait, let's test on other browsers/devices...
Well I had 2 device at hand 3 browsers at each.
DeviceOrientation
event.DeviceOrientation
event.DeviceOrientation
event (WTF?!).So you might say - it's not that bad. Only two cases, right? We just need to ask the user to rotate the device and if we get a negative value that we just do:
normalizedAlpha = Math.abs(eventData.alpha - 180);
OK, so real normalizing is not a one-liner (as the one above), but few state variables, if-s and one function later you get a working example. North becomes North. Great!
But wait... Rotating works funny... It turns out we have 3 browsers and 3 cases. It's like the good old times ;-). What happens? Firefox gives you 90° for East, but Opera gives you 270° for East. Yupi!
You now need to:
So now this little example code:
window.ondeviceorientation = function(eventData) { drawCompass(eventData.alpha); };
// function for translating what is read from the browser to 0-360 var directionTranslation = function(browserDirection) { return browserDirection; }; // if still calibrating var inCalibration = true; // initial calibration? (not yet know the range) var inInitCalibration = true; // max alpha var maxAlphaDirection = 0; /** * Basic device orientation handling which includes heading relative to magnetic North. */ var initCalibrationAngle = 0; window.ondeviceorientation = function(e) { if (inCalibration) { if (inInitCalibration) { // Nokia browser style (N9 tested): -180 to 180 where 180/-180 is North if (e.alpha < 0) { directionTranslation = function(browserDirection) { return Math.abs(browserDirection - 180); }; inCalibration = inInitCalibration = false; } // Android HTC HD, Fox+Opera; 0 to 360 where 0/360 is North else if (e.alpha > 180) { // Some rotate to clockwise some oposite - still calibrating... inInitCalibration = false; initCalibrationAngle = Math.round(directionTranslation(e.alpha)); } } else { // a is normalized to 0-360 integer var a = Math.round(directionTranslation(e.alpha)); // normalize to 0-360 as if initCalibrationAngle where at 0 a = (360 + a - initCalibrationAngle) % 360; // if user is between 60 and 120 deg. in the rotated space // this means we already have clock-wise compass (yupi!) if (60 < a && a < 120) { inCalibration = false; } // if user is between 300 and 240 deg. in the rotated space // this means we need make it a clock-wise compass... else if (240 < a && a < 300) { inCalibration = false; var prevDirectionTranslation = directionTranslation; directionTranslation = function(browserDirection) { return Math.abs(360 - prevDirectionTranslation(browserDirection)); }; } } } drawCompass(directionTranslation(e.alpha)); };
I've also made a working, simple compass available here: http://m.enux.pl/_tests/compass/
OK. So that should work everywhere it can work. Still it's hard to calibrate, so users might have problems using it. Some extra calculations might make it easier, but still many browsers (also in modern devices) don't support device motion events. So it's still not a prime time for compass in browsers.
However if you want to add compass to your web-view based app you can use PhoneGap (or other Cordova based framework). See compass API documentation on Apache Cordova page.