html5 - Get device orientation rotation in relativ

2020-03-20 06:21发布

问题:

I'm trying to get the change in orientation between two deviceorientation events along the left-right axis, and top-bottom axis, those axis being usually defined as the phone x and y axis (https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Orientation_and_motion_data_explained)

ie between instants t1 and t2 where those phone axis move from (x1, y1) to (x2, y2), It'd like to get (angle(x2-x1), angle(y1-y2)).

When the device is in portrait mode (in opposition to landscape mode), those axis seems to correspond to the beta and gamma. However when the phone is vertical (bottom facing the ground), the gamma value becomes extremely instable, and jumps from 90 to -90 degrees (at the same occasion, the alpha jumps by 180 degrees) You can easily see that here on your phone

I'd like to avoid that, and also get values in the 360 range. Here is what I have so far:

// assuming portrait mode
var beta0, gamma0;
window.addEventListener('deviceorientation', function(orientation) {
  if (typeof beta0 === 'undefined') {
    beta0 = beta;
    gamma0 = gamma;
  } 

  console.log('user has moved to the left by', gamma - gamma0, ' and to the top by', beta - beta0);
});

That works ok when the device is mostly horizontal, and not at all when it is vertical

回答1:

All right. First, a simple explanation of the device orientation input:

The absolute coordinate system, (X, Y, Z) is such that X is East, Y is North and Z is up. The device relative coordinate system, (x, y, z) is such that x is right, y is top and z is up. Then the orientation angles, (alpha, beta, gamma) are the angles that describe the succession of three simple rotations that change (X, Y, Z) to (x, y, z) as so:

  • rotate around Z by alpha degrees, which transforms (X, Y, Z) to (X', Y', Z') with Z' = Z
  • rotate around X' by beta degrees, which transforms (X', Y', Z') to (X'', Y'', Z'') with X'' = X'
  • rotate around Y'' by gamma degrees, which transforms (X'', Y'', Z'') to (x, y, z) with y = Y''

(they are called intrinsic Tait-Bryan angles of type Z-X'-Y'')

Now we can get the corresponding rotation matrix by composing simple rotation matrix that each correspond to one of the three rotations.

                                 [   cC   0    sC  ] [  1    0    0   ] [  cA   -sA  0  ]
R(A, B, C) = Ry(C)*Rx(B)*Rz(A) = |   0    1    0   |*|  0    cB  -sB  |*[  sA   cA   0  ]
                                 [  -sC   0    cC  ] [  0    sB   cB  ] [  0    0    1  ]

where A, B, C are short for alpha, beta, gamma and s, c for sin, cos.

Now, we are interested in the angles of the right-left (y axis) and top-down (x axis) rotations deltas between two positions (x, y, z) and (x', y', z') that correspond to the orientations (A, B, C) and (A', B', C')

The coordinates of (x', y', z') in term of (x, y, z) are given by R(A', B', C') * R(A, B, C)^-1 = R(A', B', C') * R(A, B, C)^T since the inverse is the transpose for orthogonal (rotation) matrix. Finally, if z' = p*x + q*y + r*z, the angle of those rotations are p around the right-left axis and q around the top-down one (this is true for small angles, which assume frequent orientation update, else asin(p) and asin(r) are closer from the truth)

So here is some javascript to get the rotation matrix:

/*
 * gl-matrix is a nice library that handles rotation stuff efficiently
 * The 3x3 matrix is a 9 element array
 * such that indexes 0-2 correspond to the first column, 3-5 to the second column and 6-8 to the third
 */
import {mat3} from 'gl-matrix';

let _x, _y, _z;
let cX, cY, cZ, sX, sY, sZ;
/*
 * return the rotation matrix corresponding to the orientation angles
 */
const fromOrientation = function(out, alpha, beta, gamma) {
  _z = alpha;
  _x = beta;
  _y = gamma;

  cX = Math.cos( _x );
  cY = Math.cos( _y );
  cZ = Math.cos( _z );
  sX = Math.sin( _x );
  sY = Math.sin( _y );
  sZ = Math.sin( _z );

  out[0] = cZ * cY + sZ * sX * sY,    // row 1, col 1
  out[1] = cX * sZ,                   // row 2, col 1
  out[2] = - cZ * sY + sZ * sX * cY , // row 3, col 1

  out[3] = - cY * sZ + cZ * sX * sY,  // row 1, col 2
  out[4] = cZ * cX,                   // row 2, col 2
  out[5] = sZ * sY + cZ * cY * sX,    // row 3, col 2

  out[6] = cX * sY,                   // row 1, col 3
  out[7] = - sX,                      // row 2, col 3
  out[8] = cX * cY                    // row 3, col 3
};

and now we get the angular deltas:

const deg2rad = Math.PI / 180; // Degree-to-Radian conversion
let currentRotMat, previousRotMat, inverseMat, relativeRotationDelta,
  totalRightAngularMovement=0, totalTopAngularMovement=0;

window.addEventListener('deviceorientation', ({alpha, beta, gamma}) => {
  // init values if necessary
  if (!previousRotMat) {
    previousRotMat = mat3.create();
    currentRotMat = mat3.create();
    relativeRotationDelta = mat3.create();

    fromOrientation(currentRotMat, alpha * deg2rad, beta * deg2rad, gamma * deg2rad);
  }

  // save last orientation
  mat3.copy(previousRotMat, currentRotMat);

  // get rotation in the previous orientation coordinate
  fromOrientation(currentRotMat, alpha * deg2rad, beta * deg2rad, gamma * deg2rad);
  mat3.transpose(inverseMat, previousRotMat); // for rotation matrix, inverse is transpose
  mat3.multiply(relativeRotationDelta, currentRotMat, inverseMat);

  // add the angular deltas to the cummulative rotation
  totalRightAngularMovement += Math.asin(relativeRotationDelta[6]) / deg2rad;
  totalTopAngularMovement += Math.asin(relativeRotationDelta[7]) / deg2rad;
}

Finally, to account for screen orientation, we have to replace

  _z = alpha;
  _x = beta;
  _y = gamma;

by

const screen = window.screen;
const getScreenOrientation = () => {
  const oriented = screen && (screen.orientation || screen.mozOrientation);
  if (oriented) switch (oriented.type || oriented) {
    case 'landscape-primary':
      return 90;
    case 'landscape-secondary':
      return -90;
    case 'portrait-secondary':
      return 180;
    case 'portrait-primary':
      return 0;
  }
  return window.orientation|0; // defaults to zero if orientation is unsupported
};

const screenOrientation = getScreenOrientation();

_z = alpha;
if (screenOrientation === 90) {
  _x = - gamma;
  _y = beta;
}
else if (screenOrientation === -90) {
  _x = gamma;
  _y = - beta;
}
else if (screenOrientation === 180) {
  _x = - beta;
  _y = - gamma;
}
else if (screenOrientation === 0) {
  _x = beta;
  _y = gamma;
}

Note that the cumulative right-left and top-bottom angles will depend of the path chosen by the user, and cannot be infer directly from the device orientation but have to be tracked through the movement. You can arrive to the same position with different movements:

  • method 1:

    • keep your phone horizontal and rotate by 90 degrees clockwise. (this is neither a left-right nor a top-bottom rotation)
    • keep your phone in landscape mode and rotate by 90 toward you. (this is neither a 90 degrees left-right rotation)
    • keep your phone facing you and rotate by 90 so that it's up. (this is neither a 90 degrees left-right rotation)
  • method 2:

    • rotate the phone by 90 degrees so that it faces you and is vertical (this is a 90 degrees top-bottom rotation)