Device orientation using Quaternion

Viewed 2218

I've written a JS SDK that listens to mobile device rotation, providing 3 inputs:

α : An angle can range between 0 and 360 degrees
β : An Angle between -180 and 180 degrees
γ : An Angle between -90 to 90 degrees

Documentation for device rotation

I have tried using Euler Angles to determine the device orientation but encountered the gimbal lock effect, that made calculation explode when the device was pointing up. That lead me to use Quaternion, that does not suffer from the gimbal lock effect.

I've found this js library that converts α,β and γ to a Quaternion, so for the following values:

α : 81.7324
β : 74.8036
γ : -84.3221

I get this Quaternion for ZXY order:

w: 0.7120695154301472
x: 0.6893688637611577
y: -0.10864439143062626
z: 0.07696733776346154

Code:

var rad = Math.PI / 180;
window.addEventListener("deviceorientation", function(ev) {

  // Update the rotation object
  var q = Quaternion.fromEuler(ev.alpha * rad, ev.beta * rad, ev.gamma * rad, 'ZXY');

  // Set the CSS style to the element you want to rotate
  elm.style.transform = "matrix3d(" + q.conjugate().toMatrix4() + ")";

}, true);

Visualizing the device orientation using 4d CSS matrix derived from the Quaternion Reflected the right device orientation (DEMO, use mobile):
enter image description here


Wrong visualizing with Euler Angels and the developer tools (DEMO, use mobile):
enter image description here


I would like to write a method that gets α,β and γ and outputs if the device is in one of the following orientations:

  • portrait
  • portrait upside down
  • landscape left
  • landscape right
  • display up
  • display down

Defining each orientation as a range of +- 45° around the relevant axes.

What approach should I take?

1 Answers

Given that you've already managed to convert the Euler angles into a unit quaternion, here's a simple way to determine the orientation of the device:

  1. Take a world-space vector pointing straight up (i.e. along the +z axis) and use the quaternion (or its conjugate) to rotate it into device coordinates. (Note that you could also do this using the Euler angles directly, or using a rotation matrix, or using any other representation of the device rotation that you can apply to transform a vector.)

  2. Take the transformed vector, and find the component with the largest absolute value. This will tell you which axis of your device is pointing closest to vertical, and the sign of the component value tells you whether it's pointing up or down.

In particular:

  • if the device x axis is the most vertical, the device is in a landscape orientation;
  • if the device y axis is the most vertical, the device is in a portrait orientation;
  • if the device z axis is the most vertical, the device has the screen pointing up or down.

Here's a simple JS demo that should work at least on Chrome — or it would, except that the device orientation API doesn't seem to work in Stack Snippets at all. :( For a live demo, try this CodePen instead.

const orientations = [
  ['landscape left', 'landscape right'],  // device x axis points up/down
  ['portrait', 'portrait upside down'],   // device y axis points up/down
  ['display up', 'display down'],         // device z axis points up/down
];

const rad = Math.PI / 180;

function onOrientationChange (ev) {
  const q = Quaternion.fromEuler(ev.alpha * rad, ev.beta * rad, ev.gamma * rad, 'ZXY');

  // transform an upward-pointing vector to device coordinates
  const vec = q.conjugate().rotateVector([0, 0, 1]);

  // find the axis with the largest absolute value
  const [value, axis] = vec.reduce((acc, cur, idx) => (Math.abs(cur) < Math.abs(acc[0]) ? acc : [cur, idx]), [0, 0]);

  const orientation = orientations[axis][1 * (value < 0)];

  document.querySelector('#angles').textContent = `alpha = ${ev.alpha.toFixed(1)}°, beta = ${ev.beta.toFixed(1)}°, gamma = ${ev.gamma.toFixed(1)}°`;
  document.querySelector('#vec').textContent = `vec = ${vec.map(a => a.toFixed(3))}, dominant axis = ${axis}, value = ${value.toFixed(3)}`;
  document.querySelector('#orientation').textContent = `orientation = ${orientation}`;
}

onOrientationChange({ alpha: 0, beta: 0, gamma: 0 });
window.addEventListener("deviceorientation", onOrientationChange, true);
<script src="https://cdn.jsdelivr.net/npm/quaternion@1.1.0/quaternion.min.js"></script>
<div id="angles"></div>
<div id="vec"></div>
<div id="orientation"></div>

Note that there's apparently some inconsistency between browsers in the signs and ranges of the Euler angles supplied by the device orientation API, potentially causing the wrong signs to be computed on other browsers. You might need to do some browser sniffing to fix this, or use a wrapper library like gyronorm.js.

Related