Keep it level: responding to device orientation changes

Nowadays sensors are everywhere; mobile devices contain hardware designed to retrieve many different pieces of information about the device and it's surroundings, such as location, ambient light conditions, and motion and orientation data. The web platform provides a standard way to access most of this data. This article in particular explores motion and orientation sensors (detecting when the device is tilted in different directions, for example) and associated W3C specifications, and explains how to makes use of such data in an HTML5 game written for the open web and Firefox OS devices.

Note: gyronorm.js is a polyfill for normalizing the accelerometer and gyroscope data on mobile devices. This is useful for overcoming some of the differences in device support for device orientation.

Situations in which orientation data is useful

Game development is doubtless the most interesting area in which motion and orientation data are useful: for example you might think of using this data to control the direction of characters, vehicles or balls, and make them jump.

Beyond games, motion and orientation data can be used in open web apps to implement gestures, such as the shaking gesture, or combined with other data, such as geolocation data, to improve mapping web applications. You might for example use device orientation to rotate the view of a map as the device rotates.

This article presents a game demo as a use case: before diving into the code, it might be useful to have a quick look at the sensors that provide information about device motion, and give an overview of the specs for the web platform.

Sensors involved in retrieving orientation data

Motion and orientation data are provided by 3 different sensors, which measure different things:

  • Accelerometer
  • Gyroscope
  • Compass

Accelerometer

An accelerometer is a sensor that measures physical acceleration, usually used for measuring small movements. For this article's purpose it is important to remember that gravity acts like a continuous acceleration towards the Earth (Einstein's equivalency principle), so the gravity defines the Z vector (up/down) and, when combined with the X vector (east/west) and the Y vector (north/south), it provides a 3D orientation coordinate system.

Gyroscope

A gyroscope is a sensor for measuring orientation, based on the principles of angular momentum. A gyroscope measures changes in orientation and changes in rotational velocity (rotation rate).

Compass

A compass makes use of the Earth’s magnetic field to determine absolute orientation in the NESW plane.

Why 3 sensors?

The accelerometer, gyroscope and compass have different peculiarities. For example compasses have poor accuracy for fast movements, but are very precise over time; on the other hand gyros and accelerometers react quickly and accurately to changes. Moreover gyros only react to changes, so require a known orientation to start from. Accurate orientation and motion measures can be obtained only by combining the outputs of these 3 sensors.

Specifications

The Web Platform makes use of the outputs provided by the sensors illustrated in the section above and provides 2 different APIs to get motion and orientation data:

Orientation data can be retrieved also through CSS media queries. The orientation media feature can return portrait or landscape.

The purpose of this section is to provide an overview of the specifications involved in retrieving orientation and motion data. For an insight please follow the links to W3C and MDN provided in the paragraphs below.

Device Orientation API

The Device Orientation API defines two events on the window object:

The deviceorientation event

The deviceorientation event fires whenever a significant change in orientation occurs. The event provides the following properties, which are also illustrated further in the diagram below (source: dev.opera.com: The W3C device orientation API):

  • alpha: the rotation of the device around the Z axis.
  • beta: the rotation of the device around the X axis.
  • gamma: the rotation of the device around the Y axis.
  • absolute: indicates whether or not the device is providing orientation data in reference to the Earth's coordinate frame.

an explanation of the x, y and z axes of a device; Z axis rotation is alpha, X axis rotation is beta, Y axis rotation is gamma.

Note: This diagram has been borrowed from The W3C Device Orientation API: Detecting Orientation and Acceleration, written by Shwetank Dixit and published on dev.opera.com.

More information about how to process orientation events can be found in Processing orientation events.

The devicemotion event

The devicemotion event fires whenever a significant change in motion occurs. The event provides the following properties:

  • acceleration. Provides acceleration data expressed in m/s^2 and relative to the Earth's coordinate frame (x,y,z).
  • accelerationIncludingGravity. Provides acceleration data without the effect of gravity, due for example to the lack of a gyroscope in the hosting device.
  • rotationRate. Provides the rate of rotation (expressed in deg/s) of the device in space.
  • interval. Provides the interval (expressed in ms) at which data is obtained from the underlying hardware.

More information about how to process motion events can be found in Processing motion events.

The compassneedscalibration Event

The compassneedscalibration event is fired when the compass used to obtain orientation data is in need of calibration. This can be used to prevent unexpected behaviours or to notify the user about the compass status.

Screen Orientation API

The Screen Orientation API provides the ability to read the screen orientation state, to be informed when this state changes, and to be able to lock the screen orientation to a specific state.

Reading the orientation state

The screen.orientation attribute returns the value representing the orientation of the screen, as specified in this list.

Being informed when the orientation state changes

When screen.orientation changes, the orientationchange event is fired on the screen object.

Locking the screen orientation

Locking the screen orientation is made possible by invoking the screen.lockOrientation() method, while the screen.unlockOrientation() method removes all the previous screen locks that have been set.

More information about the Screen Orientation API can be found in Managing screen orientation.

Demo: Rolling Ball

Rolling Ball is a simple game in which you move a ball through a labyrinth in order to make it fall into a hole and get access to the next level. It makes use of canvas to draw graphics and the Device Orientation API to move the ball. As a Special feature, you can also make the ball jump over obstacles!

Try it live:

We have provided a couple of control alternatives for you to use, depending on what device you are accessing the game through:

  • keyboard control: use arrows to make the ball roll, and space to make it jump
  • device motion control: tilt your mobile device to make the ball roll, and move it up to make the ball jump

You can also grab the source code on Github to play with.

Important: This demo is built using Object Oriented Programming and simplicity was preferred over gameability in order to demonstrate usage of the Device Orientation API. Please check this framework-based version of the Rolling Ball game if you are interested in how to make an HTML5 game rapidly using a framework.

Let's start!

The Javascript layer of Rolling Ball consists of the following modules:

<script src="scripts/Boundaries.js"></script>
<script src="scripts/Obstacles.js"></script>
<script src="scripts/Ball.js"></script>
<script src="scripts/Target.js"></script>
<script src="scripts/CollisionManager.js"></script>
<script src="scripts/ScreenOrientationManager.js"></script>
<script src="scripts/KeyboardControl.js"></script>
<script src="scripts/DeviceMotionControl.js"></script>
<script src="scripts/Game.js"></script>
<script src="scripts/app.js"></script>
<script src="scripts/install.js"></script>
<script src="scripts/start.js"></script>

When the window is loaded, a bunch on new Javascript objects are initialized:

/* Initialize the game */
Game.init();
/* Create new Boundaries and Obstacles */
Boundaries.init({
    margin: 10
});
Obstacles.init();
/* Create a new Target */
Target.init({
    size: 50,
    xPos: 100,
    yPos: 50
});
/* Create the ball */
Ball.init({
    size: 20,
    xPos: Game.playground.width - 30,
    yPos: Game.playground.height - 30
});
/* Lock screen orientation to portrait */
ScreenOrientationManager.init();
ScreenOrientationManager.lockOrientation('portrait-primary');
/* Add devicemotion control */
DeviceMotionControl.init();
/* Add keyboard control */
KeyboardControl.init();
/* Start the game */
Game.start();

Now that we have an overview of all JavaScript objects, let’s have a look at how the game features are implemented.

The playground

The playground is a canvas HTML element:

<canvas id="playground"></canvas>

The context of this canvas context can be referred by the web app as Game.playgroundContext. Here is how it is initialized by Game:

this.playground = document.getElementById("playground");
this.playground.setAttribute('width', window.innerWidth);
this.playground.setAttribute('height', window.innerHeight);
this.playgroundContext = this.playground.getContext("2d");

Obstacles are generated randomly as a set of red canvas rects

Game.playgroundContext.fillStyle = '#721B1B';
Game.playgroundContext.beginPath();
for (var i = 0; i < this.items.length; i++) {
    Game.playgroundContext.rect(this.items[i].left, this.items[i].top, this.items[i].width, this.items[i].height);
}
Game.playgroundContext.closePath();
Game.playgroundContext.fill();

The Ball is a green canvas arc

Game.playgroundContext.fillStyle = '#66CC00';
Game.playgroundContext.beginPath();
Game.playgroundContext.arc(this.position.x, this.position.y, this.size / 2, 0, 2 * Math.PI);
Game.playgroundContext.closePath();
Game.playgroundContext.fill();

and the Target is a bigger black canvas arc:

Game.playgroundContext.fillStyle = '#000000';
Game.playgroundContext.beginPath();
Game.playgroundContext.arc(this.position.x, this.position.y, this.size / 2, 0, 2 * Math.PI);
Game.playgroundContext.closePath();
Game.playgroundContext.fill();

Locking orientation

In order to avoid screen rotation while users play Rolling Ball, the game itself locks the orientation to portrait mode. This is achieved by two complementary solutions:

Locking orientation via Screen Orientation API

ScreenOrientationManager makes use of the Screen Orientation API to lock the orientation. Here is how is it inizialized by the game:

    /* Lock screen orientation to portrait */
    ScreenOrientationManager = new ScreenOrientationManager();
    ScreenOrientationManager.lockOrientation('portrait-primary');

Due to browser compatibility, the lockOrientation function is initialized taking into account vendor prefixes:

    this.lockOrientationFunction = screen.lockOrientation || screen.mozLockOrientation || screen.msLockOrientation;

and used in the ScreenOrientationManager.lockOrientation method:

    /*
     * lockOrientation
     * lock the orientation to the value specified as parameter
     * @param {String} orientation
     */
    lockOrientation: function(orientation) {
        if (this.lockOrientationFunction) {
            this.lockOrientationFunction(orientation);
        }
    }

This code is fairly simple, however, at the time of writing the screen.lockOrientation() worked only for installed Web apps or for Web pages in full-screen mode, so locking the orientation via the App Manifest is the best way to add this feature.

Locking orientation via App Manifest

Rolling Ball can be installed as a Firefox OS Hosted App by clicking on the playground: to know more about installing a Hosted Apps on Firefox OS devices, have a look at the install.js source file and at our Quickstart app tutorial.

The screen can be locked to portrait-primary by adding an orientation field to the manifest.webapp file:

{
  "name": "Rolling Ball",
   …
  "orientation": "portrait-primary"
}

Managing screen orientation

When the orientation can't be locked, we need a fallback solution to manage screen orientation change when the user plays the game. Rolling Ball pauses the game when, after a screen orientation change, the orientation is not portrait-primary; otherwise it resumes the game.

        /* Handle the screen orientation change */
        ScreenOrientationManager.handleOrientationChange({
            portraitPrimaryCallback: Game.resume,
            landscapePrimaryCallback: Game.pause,
            portraitSecondaryCallback: Game.pause,
            landscapeSecondaryCallback: Game.pause
        });

The handleOrientationChange method of ScreenOrientationManager is defined as follows:

    /*
     * handleOrientationChange
     * handle orientation change
     * @param {Object} callbacks
     */
    handleOrientationChange: function(callbacks) {
        var self = this;
        /* if the OrientationChangeEvent is supported in screen */
        if ('onorientationchange' in screen || 'onmozorientationchange' in window) {
            /* Invoke handleOrientation on orientation change */
            screen.onorientationchange = function() {
                setTimeout(function() {
                    self.handleOrientation(callbacks);
                }, 500);
            };
        }
        /* if the OrientationChangeEvent is supported in window */
        else if ('onorientationchange' in window) {
            /* Invoke handleOrientation on orientation change */
            window.onorientationchange = function() {
                setTimeout(function() {
                    self.handleOrientation(callbacks);
                }, 500);
            };
        }
        else { // fallback
            /* Invoke handleOrientation on window resize */
            window.onresize = function() {
                setTimeout(function() {
                    self.handleOrientation(callbacks);
                }, 500);
            };
        }
    }

So the orientationchange event of the Screen Orientation API is used if supported, otherwise the resize event is used.

The handleOrientation method is defined as follows:

    /*
     * handleOrientation
     * handle screen orientation
     * @param {Object} callbacks
     */
    handleOrientation: function(callbacks) {
        var self = this;
        /* Check the orientation and invoke the callback functions */
        var screenOrientation = self.getOrientation();
        if (screenOrientation || window.orientation) {
            if (screenOrientation === 'portrait-primary' || screenOrientation === 0) {
                if (callbacks.portraitPrimaryCallback) {
                    callbacks.portraitPrimaryCallback();
                }
            }
            else if (screenOrientation === 'landscape-primary' || screenOrientation === 90) {
                if (callbacks.landscapePrimaryCallback) {
                    callbacks.landscapePrimaryCallback();
                }
            }
            else if (screenOrientation === 'landscape-secondary' || screenOrientation === -90) {
                if (callbacks.portraitSecondaryCallback) {
                    callbacks.portraitSecondaryCallback();
                }
            }
            else if (screenOrientation === 'portrait-secondary' || screenOrientation === 180) {
                if (callbacks.landscapeSecondaryCallback) {
                    callbacks.landscapeSecondaryCallback();
                }
            }
        }
        else { // portrait-primary
            if (callbacks.portraitPrimaryCallback) {
                callbacks.portraitPrimaryCallback();
            }
        }
    },

while the getOrientation method is defined as follows:

    /*
     * getOrientation
     * get device orientation
     * @returns {String} orientation
     */
    getOrientation: function() {
        return screen.orientation || window.orientation;
    },

The screen.orientation property of the Screen orientation API is used if supported, otherwise the window.orientation property is used.

Making the ball roll and jump

The ball can be controlled by a keyboard or by device motion. Every time the ball rolls or jumps the playground is cleared and the ball is drawn at an updated position.

Keyboard Control

KeyboardControl listens to the keydown event to move the ball.

    var self = this;
    window.onkeydown = function(e) {
        if (Ball.status !== 'jumping') {
            if (e.keyCode === self.leftArrow.keycode) { // left
                self.goLeft();
            }
            else if (e.keyCode === self.upArrow.keycode) { // up
                self.goUp();
            }
            else if (e.keyCode === self.rightArrow.keycode) { // right
                self.goRight();
            }
            else if (e.keyCode === self.downArrow.keycode) { // down
                self.goDown();
            }
            if (e.keyCode === self.jumpButton.keycode) { // space
                self.jump();
            }
        }
    };

Device Motion Control

DeviceMotionControl makes use of the Device Orientation API to listen to the devicemotion event and invoke a callback function every time the event is fired.

When the game start, the device motion control gets activated:

/* Activate the device motion control */
DeviceMotionControl.handleMotionEvent(Game.step);

The handleMotionEvent method is defined as follows:

        /* Check whether the DeviceMotionEvent is supported */
        if (this.isDeviceMotionEventSupported()) {
            /* Add a listener for the devicemotion event */
            window.ondevicemotion = function(deviceMotionEvent) {
                /* Get acceleration on x, y and z axis */
                var x = deviceMotionEvent.accelerationIncludingGravity.x * implementationFix;
                var y = deviceMotionEvent.accelerationIncludingGravity.y * implementationFix;
                var z = deviceMotionEvent.accelerationIncludingGravity.z;
                /* Get the interval (ms) at which data is obtained from the underlying hardware */
                var interval = deviceMotionEvent.interval;
                /* Handle the screen orientation */
                ScreenOrientationManager.handleOrientation({
                    portraitPrimaryCallback: function() {
                        callback(-x, y, z, interval);
                    },
                    landscapePrimaryCallback: function() {
                        callback(y, x, z, interval);
                    },
                    portraitSecondaryCallback: function() {
                        callback(-y, -x, z, interval);
                    },
                    landscapeSecondaryCallback: function() {
                        callback(x, -y, z, interval);
                    }
                });
            };
        }

So the callback (the Game.step method) gets invoked every time the device moves, that is every milliseconds specified by the deviceMotionEvent.interval, and takes the following parameters:

  • x. Acceleration on the x axis, without the effect of gravity, that is deviceMotionEvent.accelerationIncludingGravity.x
  • y. Acceleration on the y axis, without the effect of gravity, that is deviceMotionEvent.accelerationIncludingGravity.y
  • z. Acceleration on the z axis, without the effect of gravity, that is deviceMotionEvent.accelerationIncludingGravity.z
  • interval. The interval (expressed in ms) at which the callback is invoked, that is deviceMotionEvent.interval

Since x and y values can be positive or negative depending on the screen orientation, the sign is tuned by invoking the ScreenOrientationManager's handleOrientation method.

The implementationFix variable defined in the code above is due to the fact that in Safari for iOS the direction are reversed on axes x and y. Here is how implementationFix is calculated:

        var implementationFix = 1;
        if (window.navigator.userAgent.match(/^.*(iPhone|iPad).*(OS\s[0-9]).*(CriOS|Version)\/[.0-9]*\sMobile.*$/i)) { // is Mobile Safari
            implementationFix = -1;
        }

To check whether the devicemotion event is supported:

    isDeviceMotionEventSupported: function() {
        if (window.DeviceMotionEvent) {
            return true;
        }
        return false;
    }

When in the Game.step method, if the parameter z is bigger than a certain value the Ball.jump is called. Have a look at the Ball.js source code for more information about how the ball jumps, lands on the ground, or crashes against boundaries.

Note that the devicemotion event might not be the best way to move a ball: this MDN article explains how to implement a Rolling Ball game based on the Phaser Framework, and how to use the deviceorientation event to make the ball roll.

Managing collisions

The CollisionManager is the JavaScript module that detect if the ball has collided with obstacles, boundaries or the target. For more information about how Rolling Ball manages collisions, have a look at the source code.

Falling into the hole

When the CollisionManager detects that the ball has collided with the target, it means that the ball has reached the hole and can fall into it.

The following code shows how the fall animation is implemented inside the Ball.fall method:

    /*
     * fall
     * Make the ball fall into the hole
     */
    fall: function(x, y) {
        /* Update ball status */
        this.status = 'falling';
        /* Update ball position */
        this.position.x = x;
        this.position.y = y;
        /* Decrease ball size */
        this.size -= 1;
        /* Repaint */
        Game.clearPlayground();
        Target.draw();
        this.draw();
        /* Animate until the ball is visible */
        if (this.size > 0) {
            window.requestAnimationFrame(function() {
                Ball.fall(x, y);
            });
        }
        else {
            this.status = 'rolling';
        }
    }

The window.requestAnimationFrame() method is called recursively until the ball is visible. It is initialized by the Game module in order to take into account vendor prefixes:

    window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

The window.requestAnimationFrame() method is very important for implementing fast animations in games: Rolling Ball also makes use of window.requestAnimationFrame() to make the ball roll and jump.

Have fun !

Document Tags and Contributors

 Contributors to this page: chrisdavidmills, franciov
 Last updated by: chrisdavidmills,