Introduction
The majority of modern mobile devices are equipped with accelerometers, gyroscopes, and compasses. In my previous article about the Geolocation API, I described how developers can use the data offered by the Geolocation API to improve the user experience. Another interesting API is the Device Orientation API, which is the focus on this tutorial.
Detecting the orientation of a device is useful for a wide range of applications, from navigation application to games. Have you ever played a racing game on a mobile device that lets you control the car by tilting the device?
Another application of the API is updating the user interface of an application when the orientation of the device changes to offer the user the best possible experience by taking advantage of the entire screen. If you're a fan of YouTube, then you most certainly have taken advantage of this feature.
In this article, I'll introduce you to the Device Orientation API, explaining what type of data it can offer us and how to leverage it in your applications.
1. What is it?
To quote the W3C specification of the Device Orientation API the API "[...] defines several new DOM events that provide information about the physical orientation and motion of a hosting device." The data provided by the API is obtained from various sources, such as the device's gyroscope, the accelerometer, and the compass. This differs from device to device, depending on which sensors are available.
This API is a W3C Working Draft, which means the specification isn't stable and we may expect some changes in the future. It's also worth noting that this API has some known inconsistencies in several browsers and on a number of operating systems. For example, the implementation on Chrome and Opera, based on the Blink rendering engine, have a compatibility issue with Windows 8 for the deviceorientation
event. Another example is that the interval
property is not constant in Opera Mobile.
2. Implementation
The API exposes three events that provide information about the orientation of the device:
deviceorientation
devicemotion
compassneedscalibration
These events are fired on the window
object, which means that we need to attach a handler to the window
object. Let's take a look at each of these events.
deviceorientation
The deviceorientation
event is fired when the accelerometer detects a change of the device orientation. As I mentioned earlier, we can listen for this event and respond to any changes by attaching an event handler to the window
object. When the event handler is invoked, it will receive one argument of type DeviceOrientationEvent
, which contains four properties:
alpha
is the angle around the z-axis. Its value ranges from0
to360
degrees. When the top of the device points to the True North, the value of this property is0
.beta
is the angle around the x-axis. Its value range from-180
to180
degrees. When the device is parallel to surface of the Earth, the value of this property is0
.gamma
is the angle around the y-axis. Its values ranges from-90
to90
degrees. When the device is parallel to the surface of the Earth, the value of this property is0
.absolute
specifies whether the device is providing orientation data that's relative to the Earth's coordinate system, in which case its value istrue
, or to an arbitrary coordinate system.
The following illustration, taken from the official specification, shows the x, y, and z axes mentioned relative to the device.
devicemotion
The devicemotion
event is fired every time the device accelerates or decelerates. You can listen for this event just as we did for the deviceorientation
event. When the event handler is invoked, it receives one argument of type DeviceMotionEvent
, which has four properties:
acceleration
specifies the acceleration of the device relative to the Earth frame on the x, y, and z axes, accessible through itsx
,y
, andz
properties. The values are expressed in m/s2.accelerationIncludingGravity
holds the same values as theacceleration
property, but it takes Earth's gravity into account. The values of this property should be used in situations where the device's hardware doesn't know how to remove gravity from the acceleration data. In fact, in such cases theacceleration
property should not be provided by the user agent.rotationRate
specifies the rate at which the device is rotating around each of its axes in degrees per second. We can access the individual values ofrotationRate
through itsalpha
,beta
, andgamma
properties.interval
provides the interval at which data is obtained. This value must not change once it's set. It is expressed in milliseconds.
compassneedscalibration
This event is fired when the user agent determines the compass requires calibration. The specification also states that "user agents should only fire the event if calibrating the compass will increase the accuracy of the data provided by the deviceorientation
event." This event should be used to inform the user that the compass needs calibration and it should also instruct the user how to do this.
3. Detecting Support
To detect whether the browser or user agent supports one of the first two events,deviceorientation
and devicemotion
, is as simple as including a trivial conditional statement. Take a look at the following code snippet in which we detect support for the deviceorientation
event:if (window.DeviceOrientationEvent) { // We can listen for change in the device's orientation... } else { // Not supported }
To test for the compassneedscalibration
event, we use the following code snippet:
if (!('oncompassneedscalibration' in window)) { // Event supported } else { // Event not supported }
4. Browser Support
Even though support for the Device Orientation API is good, we need to keep a few things in mind when working with the API. Apart from the caveats mentioned in the introduction, the absolute
property is undefined
in Mobile Safari.
However, the real problem is that every browser that supports the Device Orientation API only supports it partially. In fact, at the time of writing, very few browsers support the compassneedscalibration
event. Execute the above code snippet in Chrome or Firefox to illustrate the problem.
With this in mind, the browsers that support the Device Orientation API are Chrome 7+, Firefox 6+, Opera 15+, and Internet Explorer 11. Support by mobile browsers is even better. In addition to the ones I've already mentioned, the API is also supported by the browser of BlackBerry 10, Opera Mobile 12+, Mobile Safari 4.2+, and Chrome 3+ on Android.
For an up to date and accurate picture of support for the Device Orientation API, I recommend visiting Can I use....
5. Demo
We now know what we need to create a demo application that leverages the Device Orientation API. The purpose of this demo is to create a cube, using plain HTML and CSS, and rotate it as the device's orientation changes.
We'll also display the information we retrieve from the API, which shows the type of data we get back from the Device Orientation API. We also show the information in raw text as some browsers may support the Device Orientation API but not the CSS properties to render the cube. This is the case for Opera Mobile, for example.
Because we know that not every browser supports the API, we also test for support of every feature of the API and display this to the user.
The source code for the demo application is shown below, but you can also see it in action.
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"/><meta name="author" content="Aurelio De Rosa"><title>Device Orientation API Demo by Aurelio De Rosa</title><style> * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } body { max-width: 500px; margin: 2em auto; padding: 0 0.5em; font-size: 20px; } h1 { text-align: center; } .hidden { display: none; } .cube { width: 150px; height: 150px; position: relative; margin: 30px auto; -webkit-transform-style: preserve-3d; transform-style: preserve-3d; } .face { width: 150px; height: 150px; position: absolute; font-size: 80px; text-align: center; line-height: 150px; background-color: #999999; box-shadow: inset 0 0 20px #333333; opacity: 0.6; } .cube .one { -webkit-transform: translateZ(75px); transform: translateZ(75px); } .cube .two { -webkit-transform: rotateY(90deg) translateZ(75px); transform: rotateY(90deg) translateZ(75px); } .cube .three { -webkit-transform: rotateY(180deg) translateZ(75px); transform: rotateY(180deg) translateZ(75px); } .cube .four { -webkit-transform: rotateY(-90deg) translateZ(75px); transform: rotateY(-90deg) translateZ(75px); } .cube .five { -webkit-transform: rotateX(90deg) translateZ(75px); transform: rotateX(90deg) translateZ(75px); } .cube .six { -webkit-transform: rotateX(-90deg) translateZ(75px) rotate(0deg); transform: rotateX(-90deg) translateZ(75px) rotate(0deg); } .value { font-weight: bold; } .author { display: block; margin-top: 1em; } </style></head><body><h1>Device Orientation API</h1><span id="do-unsupported" class="hidden">deviceorientation event not supported</span><span id="dm-unsupported" class="hidden">devicemotion event not supported</span><span id="cnc-unsupported" class="hidden">compassneedscalibration event not supported</span><div id="do-results"><div id="cube" class="cube"><div class="face one">1</div><div class="face two">2</div><div class="face three">3</div><div class="face four">4</div><div class="face five">5</div><div class="face six">6</div></div><div id="do-info" class="hidden"><p> Coordinates: (<span id="beta" class="value">null</span>,<span id="gamma" class="value">null</span>,<span id="alpha" class="value">null</span>)<br /> Position absolute? <span id="is-absolute" class="value">unavailable</span></p></div><div id="dm-info" class="hidden"><p> Acceleration: (<span id="acceleration-x" class="value">null</span>,<span id="acceleration-y" class="value">null</span>,<span id="acceleration-z" class="value">null</span>) m/s<sup>2</sup></p><p> Acceleration including gravity: (<span id="acceleration-including-gravity-x" class="value">null</span>,<span id="acceleration-including-gravity-y" class="value">null</span>,<span id="acceleration-including-gravity-z" class="value">null</span>) m/s<sup>2</sup></p><p> Rotation rate: (<span id="rotation-rate-beta" class="value">null</span>,<span id="rotation-rate-gamma" class="value">null</span>,<span id="rotation-rate-alpha" class="value">null</span>)</p><p> Interval: <span id="interval" class="value">0</span> milliseconds</p></div></div><small class="author"> Demo created by <a href="http://www.audero.it">Aurelio De Rosa</a> (<a href="https://twitter.com/AurelioDeRosa">@AurelioDeRosa</a>)</small><script> if (!window.DeviceOrientationEvent) { document.getElementById('do-unsupported').classList.remove('hidden'); } else { document.getElementById('do-info').classList.remove('hidden'); window.addEventListener('deviceorientation', function(event) { document.getElementById('cube').style.webkitTransform = document.getElementById('cube').style.transform = 'rotateX(' + event.beta + 'deg) ' + 'rotateY(' + event.gamma + 'deg) ' + 'rotateZ(' + event.alpha + 'deg)'; document.getElementById('beta').innerHTML = Math.round(event.beta); document.getElementById('gamma').innerHTML = Math.round(event.gamma); document.getElementById('alpha').innerHTML = Math.round(event.alpha); document.getElementById('is-absolute').innerHTML = event.absolute ? "true" : "false"; }); } if (!window.DeviceMotionEvent) { document.getElementById('dm-unsupported').classList.remove('hidden'); } else { document.getElementById('dm-info').classList.remove('hidden'); window.addEventListener('devicemotion', function(event) { document.getElementById('acceleration-x').innerHTML = Math.round(event.acceleration.x); document.getElementById('acceleration-y').innerHTML = Math.round(event.acceleration.y); document.getElementById('acceleration-z').innerHTML = Math.round(event.acceleration.z); document.getElementById('acceleration-including-gravity-x').innerHTML = Math.round(event.accelerationIncludingGravity.x); document.getElementById('acceleration-including-gravity-y').innerHTML = Math.round(event.accelerationIncludingGravity.y); document.getElementById('acceleration-including-gravity-z').innerHTML = Math.round(event.accelerationIncludingGravity.z); document.getElementById('rotation-rate-beta').innerHTML = Math.round(event.rotationRate.beta); document.getElementById('rotation-rate-gamma').innerHTML = Math.round(event.rotationRate.gamma); document.getElementById('rotation-rate-alpha').innerHTML = Math.round(event.rotationRate.alpha); document.getElementById('interval').innerHTML = event.interval; }); } if (!('oncompassneedscalibration' in window)) { document.getElementById('cnc-unsupported').classList.remove('hidden'); } else { window.addEventListener('compassneedscalibration', function(event) { alert('Compass needs calibrating! Wave your device in a figure-eight motion'); }); } </script></body></html>
Conclusion
In this article we've explored the Device Orientation API by taking a look at its features and potential use cases for it. Support for the API isn't great at the time of writing, but I'm sure you agree it opens up a lot of possibilities for mobile developers, game developers in particular. Don't forget to play with the demo to see the API in action.