/**
* Class to handle device orientation.
* IMPORTANT - this code is a modified version the former official three.js
* DeviceOrientationControls class, which was formerly provided with the
* three.js repo
*
* Changes:
*
* - use "deviceorientationabsolute" rather than "deviceorientation"
* where available
*
* @author richt / http://richt.me
* @author WestLangley / http://github.com/WestLangley
*
* W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html)
*/
import { Euler, EventDispatcher, MathUtils, Quaternion, Vector3 } from "three";
const isIOS = navigator.userAgent.match(/iPhone|iPad|iPod/i) ||
(/Macintosh/i.test(navigator.userAgent) &&
navigator.maxTouchPoints != null &&
navigator.maxTouchPoints > 1); // for iPad Safari - see #660 in main repo
const _zee = new Vector3(0, 0, 1);
const _euler = new Euler();
const _q0 = new Quaternion();
const _q1 = new Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)); // - PI/2 around the x-axis
const _changeEvent = { type: "change" };
class DeviceOrientationControls extends EventDispatcher {
/**
* Create an instance of DeviceOrientationControls.
* @param {Object} object - the object to attach the controls to
* (usually your Three.js camera)
* @param {Object} options - options for DeviceOrientationControls: currently accepts smoothingFactor
*/
constructor(object, options = { }) {
super();
if (window.isSecureContext === false) {
console.error(
"THREE.DeviceOrientationControls: DeviceOrientationEvent is only available in secure contexts (https)",
);
}
const scope = this;
const EPS = 0.000001;
const lastQuaternion = new Quaternion();
this.object = object;
this.object.rotation.reorder("YXZ");
this.enabled = true;
this.deviceOrientation = null;
this.screenOrientation = 0;
this.alphaOffset = 0; // radians
this.initialOffset = null; // used in fix provided in issue #466 on main AR.js repo, iOS related
this.TWO_PI = 2 * Math.PI;
this.HALF_PI = 0.5 * Math.PI;
this.orientationChangeEventName =
"ondeviceorientationabsolute" in window
? "deviceorientationabsolute"
: "deviceorientation";
this.smoothingFactor = options.smoothingFactor || 1;
const onDeviceOrientationChangeEvent = function ({
alpha,
beta,
gamma,
webkitCompassHeading,
}) {
if (isIOS) {
const ccwNorthHeading = 360 - webkitCompassHeading;
scope.alphaOffset = MathUtils.degToRad(ccwNorthHeading - alpha);
scope.deviceOrientation = { alpha, beta, gamma, webkitCompassHeading };
} else {
if (alpha < 0) alpha += 360;
scope.deviceOrientation = { alpha, beta, gamma };
}
window.dispatchEvent(
new CustomEvent("camera-rotation-change", {
detail: { cameraRotation: object.rotation },
}),
);
};
const onScreenOrientationChangeEvent = function () {
scope.screenOrientation = window.orientation || 0;
};
// The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y''
const setObjectQuaternion = function (
quaternion,
alpha,
beta,
gamma,
orient,
) {
_euler.set(beta, alpha, -gamma, "YXZ"); // 'ZXY' for the device, but 'YXZ' for us
quaternion.setFromEuler(_euler); // orient the device
quaternion.multiply(_q1); // camera looks out the back of the device, not the top
quaternion.multiply(_q0.setFromAxisAngle(_zee, -orient)); // adjust for screen orientation
};
/**
* Update the device orientation controls.
* Should be called from your three.js rendering/animation function.
*/
this.connect = function () {
onScreenOrientationChangeEvent(); // run once on load
// iOS 13+
if (
window.DeviceOrientationEvent !== undefined &&
typeof window.DeviceOrientationEvent.requestPermission === "function"
) {
window.DeviceOrientationEvent.requestPermission()
.then((response) => {
if (response === "granted") {
window.addEventListener(
"orientationchange",
onScreenOrientationChangeEvent,
);
window.addEventListener(
scope.orientationChangeEventName,
onDeviceOrientationChangeEvent,
);
}
})
.catch(function (error) {
console.error(
"THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:",
error,
);
});
} else {
window.addEventListener(
"orientationchange",
onScreenOrientationChangeEvent,
);
window.addEventListener(
scope.orientationChangeEventName,
onDeviceOrientationChangeEvent,
);
}
scope.enabled = true;
};
this.disconnect = function () {
window.removeEventListener(
"orientationchange",
onScreenOrientationChangeEvent,
);
window.removeEventListener(
scope.orientationChangeEventName,
onDeviceOrientationChangeEvent,
);
scope.enabled = false;
scope.initialOffset = false;
scope.deviceOrientation = null;
};
this.update = function ({ theta = 0 } = { theta: 0 }) {
if (scope.enabled === false) return;
const device = scope.deviceOrientation;
if (device) {
let alpha = device.alpha
? MathUtils.degToRad(device.alpha) + scope.alphaOffset
: 0; // Z
let beta = device.beta ? MathUtils.degToRad(device.beta) : 0; // X'
let gamma = device.gamma ? MathUtils.degToRad(device.gamma) : 0; // Y''
const orient = scope.screenOrientation
? MathUtils.degToRad(scope.screenOrientation)
: 0; // O
if (isIOS) {
const currentQuaternion = new Quaternion();
setObjectQuaternion(currentQuaternion, alpha, beta, gamma, orient);
// Extract the Euler angles from the quaternion and add the heading angle to the Y-axis rotation of the Euler angles
// (If we replace only the alpha value of the quaternion without using Euler angles, the camera will rotate unexpectedly. This is because a quaternion does not represent rotation values individually but rather through a combination of rotation axes and weights.)
const currentEuler = new Euler().setFromQuaternion(
currentQuaternion,
"YXZ",
);
console.log(currentEuler.x, currentEuler.y, currentEuler.z);
// Replace the current alpha value of the Euler angles and reset the quaternion
currentEuler.y = MathUtils.degToRad(
360 - device.webkitCompassHeading,
);
currentQuaternion.setFromEuler(currentEuler);
scope.object.quaternion.copy(currentQuaternion);
} else {
if (this.smoothingFactor < 1) {
if (this.lastOrientation) {
const k = this.smoothingFactor;
alpha = this._getSmoothedAngle(
alpha,
this.lastOrientation.alpha,
k,
);
beta = this._getSmoothedAngle(
beta + Math.PI,
this.lastOrientation.beta,
k,
);
gamma = this._getSmoothedAngle(
gamma + this.HALF_PI,
this.lastOrientation.gamma,
k,
Math.PI,
);
} else {
beta += Math.PI;
gamma += this.HALF_PI;
}
this.lastOrientation = {
alpha,
beta,
gamma,
};
}
setObjectQuaternion(
scope.object.quaternion,
alpha + theta,
this.smoothingFactor < 1 ? beta - Math.PI : beta,
this.smoothingFactor < 1 ? gamma - this.HALF_PI : gamma,
orient,
);
}
// NB - NOT present in IOS fixed version issue #466
// Is it needed?
if (8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) {
lastQuaternion.copy(scope.object.quaternion);
scope.dispatchEvent(_changeEvent);
}
}
};
// NW Added
this._orderAngle = function (a, b, range = this.TWO_PI) {
if (
(b > a && Math.abs(b - a) < range / 2) ||
(a > b && Math.abs(b - a) > range / 2)
) {
return { left: a, right: b };
} else {
return { left: b, right: a };
}
};
// NW Added
this._getSmoothedAngle = function (a, b, k, range = this.TWO_PI) {
const angles = this._orderAngle(a, b, range);
const angleshift = angles.left;
const origAnglesRight = angles.right;
angles.left = 0;
angles.right -= angleshift;
if (angles.right < 0) angles.right += range;
let newangle =
origAnglesRight == b
? (1 - k) * angles.right + k * angles.left
: k * angles.right + (1 - k) * angles.left;
newangle += angleshift;
if (newangle >= range) newangle -= range;
return newangle;
};
// Provided in fix on issue #466 - iOS related
this.updateAlphaOffset = function () {
scope.initialOffset = false;
};
this.dispose = function () {
scope.disconnect();
};
// provided with fix on issue #466
this.getAlpha = function () {
const { deviceOrientation: device } = scope;
return device && device.alpha
? MathUtils.degToRad(device.alpha) + scope.alphaOffset
: 0;
};
// provided with fix on issue #466
this.getBeta = function () {
const { deviceOrientation: device } = scope;
return device && device.beta ? MathUtils.degToRad(device.beta) : 0;
};
// Provide gesture before initialising device orientation controls
// From PR #659 on the main AR.js repo
// Thanks to @ma2yama
if(window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function') {
this.initPermissionDialog();
} else {
this.connect();
}
}
// Provide gesture before initialising device orientation controls
// From PR #659 on the main AR.js repo
// Thanks to @ma2yama
initPermissionDialog() {
const startModal = document.createElement("div");
const innerDiv = document.createElement("div");
const msgDiv = document.createElement("div");
const btnDiv = document.createElement("div");
document.body.appendChild(startModal);
const startModalStyles = {
display: 'flex',
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1,
backgroundColor: 'rgba(0,0,0,0.6)',
justifyContent: 'center',
alignItems: 'center'
};
const innerDivStyles = {
backgroundColor: 'white',
padding: '6px',
borderRadius: '3px',
width: '36rem',
height: '24rem'
};
const msgDivStyles = {
width: '100%',
height: '70%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
};
const btnDivStyles = {
display: 'inline-flex',
width: '100%',
height: '30%',
justifyContent: 'center',
alignItems: 'center'
};
for(let key in startModalStyles) {
startModal.style[key] = startModalStyles[key];
}
for(let key in innerDivStyles) {
innerDiv.style[key] = innerDivStyles[key];
}
for(let key in msgDivStyles) {
msgDiv.style[key] = msgDivStyles[key];
}
for(let key in btnDivStyles) {
btnDiv.style[key] = btnDivStyles[key];
}
startModal.appendChild(innerDiv);
innerDiv.appendChild(msgDiv);
innerDiv.appendChild(btnDiv);
msgDiv.innerHTML = '<div style="font-size: 24pt; margin: 1rem;">This immersive website requires access to your device motion sensors.</div>';
const onStartClick = () => {
this.connect();
startModal.style.display = 'none';
}
const btn = document.createElement("button");
btn.addEventListener("click", onStartClick);
btn.style.width = '50%';
btn.style.height = '80%';
btn.style.fontSize = '20pt';
btn.appendChild(document.createTextNode("OK"));
btnDiv.appendChild(btn);
document.body.appendChild(startModal);
}
}
export default DeviceOrientationControls;