Source: vr/Gyro.js

/**
 *	@class Gyro
 *	@memberof SQR
 *
 *	@description A gyrospcope data handler, based on Gyro.js by Tom Gallacher <tom.gallacher23@gmail.com>
 */
SQR.Gyro = (function() {

	var quaternion = { x:0, y:0, z:0, w:1 };
	var gyro = {};
	var gotReading = false;
	var initialized = false;

	var lastTime, deltaTime = 0, numReadings = 0, sumDelta = 0, maxFreq = 60; 
	// if event fires less often than this, consider it beign to slow (60ms = 16.66Hz / fps)

	gyro.getOrientation = function(dontinit, permissionFunc) {

		if(!initialized && !dontinit) {
			if(permissionFunc) {
				permissionFunc(init);
			} else {
				init();
			}
		}

		return quaternion;
	};

	gyro.hasGyro = function() {
		return gotReading;
	};

	gyro.isSlow = function() {
		return initialized && (numReadings < 3 || gyro.delta() > maxFreq);
	}

	gyro.delta = function() {
		return numReadings == 0 ? 0 : parseInt(sumDelta / numReadings);
	}

	gyro.externalProcess = function(alpha, beta, gamma, orientation) {
		processGyroData(alpha, beta, gamma, orientation);
	}	
	
	// -SHA
	var offset = null;

	gyro.getOffset = function() {
		return offset ? offset : 0;
	}

	gyro.resetOffset = function() {
		offset = null;
	}

	var calculateOffset = function() {
		var q = quaternion;
		var x = 2 * (q.x * q.z + q.w * q.y);
		// var y = 2 * (q.y * q.z - q.w * q.x);
		var z = 1 - 2 * (q.x * q.x + q.y * q.y);
		return Math.atan2(z, x);
	}

	var logOffset = function(o, prefix) {
		prefix = prefix || "";
		var d = Math.round(o / Math.PI * 180);
		console.log(prefix + 'gyro.offset: ' + d + 'deg (' + o + 'rad)');  
	} 

	var eulerToQuaternion = function(alpha, beta, gamma) {
		var x = -beta, y = -alpha; z = gamma;
		var cX = Math.cos(x / 2);
		var cY = Math.cos(y / 2);
		var cZ = Math.cos(z / 2);
		var sX = Math.sin(x / 2);
		var sY = Math.sin(y / 2);
		var sZ = Math.sin(z / 2);
		var w = cX * cY * cZ - sX * sY * sZ;
		x = sX * cY * cZ - cX * sY * sZ;
		y = cX * sY * cZ + sX * cY * sZ;
		z = cX * cY * sZ + sX * sY * cZ;
		return { x:x, y:y, z:z, w:w };
	}

	var quaternionMultiply = function(a, b) {
		return {
			w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
			x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
			y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
			z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w
		};
	}

	var wo = Math.PI * 0.5;
	var worldFix = {
		x: Math.sin(wo * 0.5),
		y: 0,
		z: 0,
		w: Math.cos(wo * 0.5),
	};

	var deviceOrientationListener = function(e) {
		processGyroData(e.alpha, e.beta, e.gamma, window.orientation);		
	}

	var processGyroData = function(alpha, beta, gamma, orientation) {

		if(lastTime) {
			deltaTime = new Date().getTime() - lastTime;
			numReadings++;
			sumDelta += deltaTime;
		}

		lastTime = new Date().getTime();

		if(alpha != null && window.orientation != null) {
			gotReading = true;
		} else {
			return;
		}

		var raw = eulerToQuaternion(
			alpha / 180 * Math.PI, 
			beta / 180 * Math.PI, 
			gamma / 180 * Math.PI
		);

		var wo = window.orientation / 180 * Math.PI;
		var orientFix = {
			x: 0,
			y: 0,
			z: Math.sin(wo * 0.5),
			w: Math.cos(wo * 0.5)
		};

		quaternion = raw;
		quaternion = quaternionMultiply(worldFix, quaternion);
		quaternion = quaternionMultiply(orientFix, quaternion);

		if(offset == null && numReadings > 2) {
			offset = calculateOffset();
			// logOffset(offset);
		} else if(numReadings % 100 == 0) {
			// logOffset(calculateOffset(), "----- ");
		}
	};

	var init = function() {
		window.addEventListener('deviceorientation', deviceOrientationListener, true);
		initialized = true;
		if(gyro.onInit) gyro.onInit();
	}

	return gyro;

})();