Advertisement

An Introduction to the Device Orientation API

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

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 from 0 to 360 degrees. When the top of the device points to the True North, the value of this property is 0.
  • beta is the angle around the x-axis. Its value range from -180 to 180 degrees. When the device is parallel to surface of the Earth, the value of this property is 0.
  • gamma is the angle around the y-axis. Its values ranges from -90 to 90 degrees. When the device is parallel to the surface of the Earth, the value of this property is 0.
  • absolute specifies whether the device is providing orientation data that's relative to the Earth's coordinate system, in which case its value is true, 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.

An image showing a device and the x y and z axis

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 its x, y, and z properties. The values are expressed in m/s2.
  • accelerationIncludingGravity holds the same values as the acceleration 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 the acceleration 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 of rotationRate through its alpha, beta, and gamma 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.

Advertisement