Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
2 contributors

Users who have contributed to this file

@modulesio @chrisplatorres
2810 lines (2491 sloc) 91.3 KB
<!doctype html>
<html>
<body>
<script src="three.js"></script>
<script src="fakeDisplay.js"></script>
<script src="keycode.js"></script>
<script src="svg-boundings.js"></script>
<script src="sprite3d.js"></script>
<script src="skin.js"></script>
<script src="xrmp.js"></script>
<script src="xrmp-three.js"></script>
<script>
let display, fakeDisplay, tabs = [], tabId = 0, xrmp, lastPresseds = [false, false], lastAxes = [[0.5, 0.5], [0.5, 0.5]], lastPadToucheds = [false, false], scrollFactors = [0, 0], scaleFactors = [1, 1];
const links = [
{
name: 'Kitchen sink',
url: 'hello_ml.html',
},
{
name: 'Tutorial',
url: 'tutorial.html',
},
{
name: 'Exobot',
url: 'exobot.html',
},
{
name: 'Meshing',
url: 'meshing_ml.html',
},
{
name: 'Planes',
url: 'planes_ml.html',
},
{
name: 'Paint',
url: 'paint_ml.html',
},
{
name: 'Graffiti',
url: 'graffiti_ml.html',
},
{
name: 'Hands',
url: 'hands_ml.html',
},
{
name: 'Minimap',
url: 'minimap_ml.html',
},
{
name: 'Avatar',
url: 'avatar_ml.html',
},
{
name: 'Shooter',
url: 'shooter_ml.html',
},
{
name: 'Sword',
url: 'sword_ml.html',
},
{
name: 'Bow',
url: 'bow_ml.html',
},
{
name: 'Pathfinding',
url: 'pathfinding_ml.html',
},
{
name: 'Radar',
url: 'radar_ml.html',
},
{
name: 'Image tracking',
url: 'imagetracking_ml.html',
},
{
name: 'Microphone',
url: 'microphone.html',
},
].map(({name, url}) => ({name, url, iconMesh: null}));
let servers = [];
let serversLoading = false;
let serverConnectedUrl = null;
const DEFAULT_URL = 'https://aframe.io/';
const DEFAULT_REGISTRY_URL = 'http://xrmp.exokit.org:9001';
const DEFAULT_SKIN_URL = 'skin2.png';
const RAY_COLOR = 0x44c2ff;
const RAY_HIGHLIGHT_COLOR = new THREE.Color(RAY_COLOR).multiplyScalar(0.5).getHex();
const rayDistance = 3;
const menuWidth = 1024;
const menuHeight = menuWidth * 0.1;
const menuWorldWidth = 1;
const menuWorldHeight = menuWorldWidth / menuWidth * menuHeight;
const menuPositionHeight = 0.3;
const urlBarWidth = menuWorldWidth;
const urlBarHeight = menuHeight;
const urlBarOffset = urlBarHeight;
const fontFamily = 'Arial';
// const fontFamily = 'monospace';
const fontSize = 60;
const keyboardWidth = 2048;
const keyboardHeight = 716;
const keyboardMatrix = [keyboardWidth / 963.266, keyboardHeight / 337.215];
const cursorWidth = 4;
const armQuaternionOffset = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, -1, 0), new THREE.Vector3(0, 0, -1));
const localVector = new THREE.Vector3();
const localVector2 = new THREE.Vector3();
const localVector3 = new THREE.Vector3();
const localQuaternion = new THREE.Quaternion();
const localQuaternion2 = new THREE.Quaternion();
const localEuler = new THREE.Euler(0, 0, 0, 'YXZ');
const localEuler2 = new THREE.Euler(0, 0, 0, 'YXZ');
const localMatrix = new THREE.Matrix4();
function parseQuery(s) {
var query = {};
var pairs = (s[0] === '?' ? s.substr(1) : s).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
const query = parseQuery(window.location.search);
const registryUrl = query.registryUrl || DEFAULT_REGISTRY_URL;
// load
const keyMap = [];
fetch('keyboard.svg')
.then(res => res.text())
.then(keyboardText => {
const div = document.createElement('div');
div.innerHTML = keyboardText;
const keyEls = div.querySelectorAll('svg > g[key]');
for (let i = 0; i < keyEls.length; i++) {
const keyEl = keyEls[i];
const key = keyEl.getAttribute('key');
const shapeEl = keyEl.children[0];
const boundings = svgBoundings[shapeEl.tagName.toLowerCase()](shapeEl);
const {
left,
right,
top,
bottom,
} = boundings;
const x1 = left * keyboardMatrix[0];
const x2 = right * keyboardMatrix[0];
const y1 = top * keyboardMatrix[1];
const y2 = bottom * keyboardMatrix[1];
keyMap.push([key, x1, y1, x2, y2]);
}
})
.catch(err => {
console.warn(err.stack);
});
let keyboardHighlightCanvasCtx = null;
new Promise((accept, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = 'keyboard-highlight.png';
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
accept(ctx);
};
img.onerror = err => {
reject(err);
};
})
.then(ctx => {
keyboardHighlightCanvasCtx = ctx;
})
.catch(err => {
console.warn(err.stack);
});
const iconMeshes = [];
const _requestSpriteMesh = url => new Promise((accept, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = url;
img.onload = () => {
const spriteMesh = sprite3d.makeSpriteMesh(img);
accept(spriteMesh);
};
img.onerror = err => {
reject(err);
};
});
const _makeBoxGeometry = (x = 1, y = 1, z = 1) => {
const size = 1;
const width = 0.005;
const lineGeometry = new THREE.CylinderBufferGeometry(width, width, size, 3, 1);
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(lineGeometry.attributes.position.array.length * 12);
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
// axis
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, y, 1)
)
.applyMatrix(
localMatrix.makeTranslation(-size/2, 0, -size/2)
)
.attributes.position.array,
lineGeometry.attributes.position.array.length * 0
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, y, 1)
)
.applyMatrix(
localMatrix.makeTranslation(size/2, 0, -size/2)
)
.attributes.position.array,
lineGeometry.attributes.position.array.length * 1
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, y, 1)
)
.applyMatrix(
localMatrix.makeTranslation(-size/2, 0, size/2)
)
.attributes.position.array,
lineGeometry.attributes.position.array.length * 2
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, y, 1)
)
.applyMatrix(
localMatrix.makeTranslation(size/2, 0, size/2)
)
.attributes.position.array,
lineGeometry.attributes.position.array.length * 3
);
// axis
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(x, 1, 1)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(0, -size/2, -size/2)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 4
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(x, 1, 1)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(0, -size/2, size/2)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 5
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(x, 1, 1)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(0, size/2, -size/2)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 6
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(x, 1, 1)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(0, size/2, size/2)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 7
);
// axis
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, 1, z)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(-size/2, -size/2, 0)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 8
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, 1, z)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(-size/2, size/2, 0)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 9
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, 1, z)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(size/2, -size/2, 0)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 10
);
positions.set(
lineGeometry.clone()
.applyMatrix(
localMatrix.makeScale(1, 1, z)
)
.applyMatrix(
localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
)
.applyMatrix(
localMatrix.makeTranslation(size/2, size/2, 0)
).attributes.position.array,
lineGeometry.attributes.position.array.length * 11
);
const numLinePositions = lineGeometry.attributes.position.array.length / 3;
const indices = new Uint16Array(lineGeometry.index.array.length * 12);
for (let i = 0; i < 12; i++) {
indices.set(
lineGeometry.index.array,
lineGeometry.index.array.length * i
);
for (let j = 0; j < lineGeometry.index.array.length; j++) {
lineGeometry.index.array[j] += numLinePositions;
}
}
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
return geometry;
};
const boxMaterial = new THREE.MeshPhongMaterial({
color: 0x333333,
});
const _makeBoxMesh = () => {
const geometry = _makeBoxGeometry();
const material = boxMaterial;
const mesh = new THREE.Mesh(geometry, material);
mesh.visible = false;
mesh.frustumCulled = false;
return mesh;
};
const _mod = (v, d) => {
const n = v % d;
return n < 0 ? (d + n) : n;
};
const _angleDiff = (a, b) => _mod((b - a) + Math.PI, Math.PI * 2) - Math.PI;
const _makeSkinMesh = () => {
const object = {};
let onupdate = null;
const uniforms = THREE.UniformsUtils.clone(skin.SKIN_SHADER.uniforms);
object.setSkinUrl = skinUrl => {
if (skinUrl) {
const mesh = skin({
limbs: true,
});
mesh.frustumCulled = false;
mesh.onBeforeRender = (onBeforeRender => function() {
mesh.material.uniforms.headRotation.value.copy(uniforms.headRotation.value);
mesh.material.uniforms.leftArmRotation.value.copy(uniforms.leftArmRotation.value);
mesh.material.uniforms.rightArmRotation.value.copy(uniforms.rightArmRotation.value);
mesh.material.uniforms.theta.value = uniforms.theta.value;
mesh.material.uniforms.headVisible.value = uniforms.headVisible.value;
mesh.material.uniforms.hit.value = uniforms.hit.value;
onBeforeRender.apply(this, arguments);
})(mesh.onBeforeRender);
return new Promise((accept, reject) => {
const skinImg = new Image();
skinImg.crossOrigin = 'Anonymous';
skinImg.src = skinUrl;
skinImg.onload = () => {
accept(skinImg);
};
skinImg.onerror = err => {
reject(err);
};
})
.then(skinImg => {
mesh.setImage(skinImg);
onupdate = (hmd, gamepads) => {
const hmdEuler = localEuler.setFromQuaternion(hmd.quaternion, localEuler.order);
const playerEuler = localEuler2.setFromQuaternion(mesh.quaternion, localEuler2.order);
const angleDiff = _angleDiff(hmdEuler.y, playerEuler.y);
const angleDiffAbs = Math.abs(angleDiff);
if (angleDiffAbs > Math.PI / 2) {
playerEuler.y += (angleDiffAbs - (Math.PI / 2)) * (angleDiff < 0 ? 1 : -1);
mesh.quaternion.setFromEuler(playerEuler);
}
const oldWorldPosition = mesh.getWorldPosition(localVector);
mesh.position.copy(hmd.position)
.sub(mesh.eye.getWorldPosition(localVector2))
.add(oldWorldPosition);
const playerQuaternionInverse = localQuaternion.copy(mesh.quaternion).inverse();
mesh.head.quaternion.copy(playerQuaternionInverse).multiply(hmd.quaternion);
mesh.updateMatrixWorld();
const headQuaternionInverse = localQuaternion2.copy(mesh.head.quaternion).inverse();
uniforms.headRotation.value.set(headQuaternionInverse.x, headQuaternionInverse.y, headQuaternionInverse.z, headQuaternionInverse.w);
for (let i = 0; i < 2; i++) {
const armRotation = uniforms[i === 0 ? 'leftArmRotation' : 'rightArmRotation'];
const gamepad = gamepads[i];
if (gamepad.visible) {
const armQuaternionInverse = localQuaternion2.setFromRotationMatrix(
localMatrix.lookAt(
mesh.arms[i === 0 ? 'left' : 'right']
.getWorldPosition(localVector),
gamepad.position,
localVector2
.set(0, 1, 0)
.applyQuaternion(gamepad.quaternion)
)
)
.multiply(armQuaternionOffset)
.premultiply(playerQuaternionInverse)
.inverse();
armRotation.value.set(armQuaternionInverse.x, armQuaternionInverse.y, armQuaternionInverse.z, armQuaternionInverse.w);
} else {
armRotation.value.set(0, 0, 0, 1);
}
}
};
if (object.skinMesh) {
object.skinMesh.parent.remove(object.skinMesh);
object.skinMesh = null;
}
scene.add(mesh);
object.skinMesh = mesh;
});
} else {
onupdate = null;
if (object.skinMesh) {
object.skinMesh.parent.remove(object.skinMesh);
object.skinMesh = null;
}
}
};
object.update = (hmd, gamepads) => {
onupdate && onupdate(hmd, gamepads);
};
object.animate = thetaValue => {
uniforms.theta.value = thetaValue;
};
object.destroy = () => {
onupdate = null;
if (object.skinMesh) {
object.skinMesh.parent.remove(object.skinMesh);
object.skinMesh = null;
}
};
object.skinMesh = null;
return object;
};
/* const localSkinMesh = _makeSkinMesh();
localSkinMesh.setSkinUrl(DEFAULT_SKIN_URL) */
const _makePositionSnapshotter = () => {
const numSnapshots = 8;
const positionShapshots = Array(numSnapshots);
for (let i = 0; i < positionShapshots.length; i++) {
positionShapshots[i] = {
position: new THREE.Vector3(),
timestamp: 0,
};
}
let positionShapshotIndex = 0;
let positionShapshotsInitialized = false;
return {
capturePositionSnapshot(position, timestamp) {
const snapshot = positionShapshots[positionShapshotIndex];
snapshot.position.copy(position);
snapshot.timestamp = timestamp;
positionShapshotIndex = (positionShapshotIndex + 1) % positionShapshots.length;
},
getFirstPosition() {
return positionShapshots[positionShapshotIndex];
},
getLastPosition() {
let index = positionShapshotIndex - 1;
if (index < 0) {
index += positionShapshots.length;
}
return positionShapshots[index];
},
};
};
// helpers
const _makeRenderer = () => {
const scene = new THREE.Scene();
// scene.background = new THREE.Color(0x7E57C2);
scene.matrixAutoUpdate = false;
const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.y = 1.6;
// camera.position.z = 1;
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild(renderer.domElement);
return {
scene,
camera,
renderer,
destroy() {
document.body.removeChild(renderer.domElement);
},
};
};
const _getFrontOfCamera = () => {
const vrCamera = renderer.vr.enabled ? renderer.vr.getCamera(camera).cameras[0] : camera;
vrCamera.matrixWorld.decompose(localVector, localQuaternion, localVector2);
localVector.add(
localVector2.set(0, 0, -1.5)
.applyQuaternion(localQuaternion)
);
localVector2.set(1, 1, 1);
return {
position: localVector.clone(),
quaternion: localQuaternion.clone(),
scale: localVector2.clone(),
};
};
const _openUrl = (u, position = new THREE.Vector3(), orientation = new THREE.Quaternion(), scale, d = 3, local = true, id = tabId++) => {
if (scale === undefined) {
scale = new THREE.Vector3(1, d === 3 ? 1 : window.innerHeight/window.innerWidth, 1);
if (d !== 3) {
scale.multiplyScalar(0.4);
}
}
const _drawOk = () => {
console.log('reality tab load ok: ' + u);
};
const _drawFail = () => {
console.log('reality tab load error: ' + u);
};
const iframe = document.createElement('iframe');
iframe.onconsole = (jsString, scriptUrl, startLine) => {
console.log('parent got console', {jsString, scriptUrl, startLine});
};
iframe.onload = function() {
const contentDocument = (() => {
try {
if (this.contentDocument) { // this potentially throws
return this.contentDocument;
} else {
return null;
}
} catch(err) {
console.warn(err.stack);
return null;
}
})();
if (contentDocument) {
_drawOk();
// scene.background = null;
} else {
_drawFail();
_closeTab(tab);
if (focusedTab === tab) {
focusedTab = rig.menuMesh.urlMesh;
}
rig.menuMesh.urlMesh.updateText();
_updateRigLists();
}
};
iframe.d = d;
iframe.src = u;
/* iframe.addEventListener('destroy', () => {
// scene.background = _makeBackground();
}); */
const tab = _addTab(iframe, position, orientation, scale, d, local, id);
};
const _addTab = (iframe, position = new THREE.Vector3(), orientation = new THREE.Quaternion(), scale, d = 3, local = true, id = tabId++) => {
if (scale === undefined) {
scale = new THREE.Vector3(1, d === 3 ? 1 : window.innerHeight/window.innerWidth, 1);
if (d !== 3) {
scale.multiplyScalar(0.4);
}
}
iframe.position = position.toArray();
iframe.orientation = orientation.toArray();
iframe.scale = scale.toArray();
document.body.appendChild(iframe);
const tab = {
url: iframe.src,
id,
iframe,
};
tabs.push(tab);
layers.push(iframe);
focusedTab = tab;
rig.menuMesh.urlMesh.updateText();
rig.menuMesh.listMesh.updateList();
if (serverConnectedUrl && local) {
const objectMesh = xrmp.createObjectMesh(tab.id, {
url: tab.url,
d,
});
const {object} = objectMesh;
position.toArray(object.objectMatrix.position);
orientation.toArray(object.objectMatrix.quaternion);
object.pushUpdate();
_bindObject(object);
}
return tab;
};
const _closeTab = tab => {
const {id, iframe} = tab;
if (iframe.destroy) {
iframe.destroy();
}
document.body.removeChild(iframe);
tabs.splice(tabs.indexOf(tab), 1);
layers.splice(layers.indexOf(iframe), 1);
if (serverConnectedUrl) {
const objectMesh = xrmp.getObjectMeshes().find(objectMesh => objectMesh.object.id === id);
xrmp.removeObjectMesh(objectMesh);
}
};
const _closeAllTabs = () => { // XXX trigger this when switching servers
for (let i = 0; i < tabs.length; i++) {
const {iframe} = tab;
if (iframe.destroy) {
iframe.destroy();
}
document.body.removeChild(iframe);
}
tabs.length = 0;
layers.length = 0;
};
const _openServer = (serverUrl, host) => {
xrmp = new XRMultiplayerTHREE(new XRMultiplayer(serverUrl));
xrmp.localPlayerMesh = null;
xrmp.onopen = () => {
xrmp.localPlayerMesh = xrmp.createLocalPlayerMesh(undefined, {});
if (host) {
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
const {url, iframe: {xrOffset, d}} = tab;
const objectMesh = xrmp.createObjectMesh(tab.id, {
url,
d,
});
const {object} = objectMesh;
object.objectMatrix.position.set(xrOffset.position);
object.objectMatrix.quaternion.set(xrOffset.orientation);
object.pushUpdate();
_bindObject(object);
}
}
};
xrmp.onsync = () => {
console.log('server sync', serverUrl);
serverConnectedUrl = serverUrl;
rig.menuMesh.listMesh.updateList();
};
xrmp.onclose = () => {
console.log('server close', serverUrl);
xrmp = null;
serverConnectedUrl = null;
rig.menuMesh.listMesh.updateList();
};
xrmp.onobjectadd = objectMesh => {
const {object} = objectMesh;
const {id, state} = object;
if (!tabs.some(tab => tab.id === id)) {
const {url, d} = state;
_openUrl(url, undefined, undefined, undefined, d, false, id);
_bindObject(object);
console.log('server object add', id);
} else {
console.warn('ignoring duplicate server tab', id);
}
};
xrmp.onobjectremove = objectMesh => {
const {id} = objectMesh.object;
const index = tabs.findIndex(tab => tab.id === id);
if (index !== -1) {
tabs.splice(index, 1);
console.log('server object remove', id);
} else {
console.warn('failed to remove local server tab', id);;
}
};
xrmp.onplayerenter = playerMesh => {
const skinMesh = _makeSkinMesh();
skinMesh.setSkinUrl(DEFAULT_SKIN_URL);
playerMesh.skinMesh = skinMesh;
const positionSnapshotter = _makePositionSnapshotter();
playerMesh.onupdate = () => {
skinMesh.update(playerMesh.hmd, playerMesh.gamepads);
localVector.copy(playerMesh.hmd.position);
localVector.y = 0;
positionSnapshotter.capturePositionSnapshot(localVector, Date.now());
};
playerMesh.animate = () => {
const walkRate = 1000;
const f = (Date.now() % walkRate) / walkRate;
const firstPosition = positionSnapshotter.getFirstPosition();
const lastPosition = positionSnapshotter.getLastPosition();
const walkSpeed = localVector.copy(firstPosition.position)
.distanceTo(lastPosition.position) / (lastPosition.timestamp - firstPosition.timestamp);
const walkIntensity = Math.min(walkSpeed * 1000, 1);
const thetaValue = Math.sin(f * Math.PI*2) * walkIntensity * 0.7;
skinMesh.animate(thetaValue);
};
scene.add(playerMesh);
};
xrmp.onplayerleave = playerMesh => {
scene.remove(playerMesh);
playerMesh.skinMesh.destroy();
};
xrmp.onerror = e => {
console.warn(e.error);
};
};
const _closeServer = () => {
xrmp.onopen = null;
xrmp.onsync = null;
xrmp.onobjectadd = null;
xrmp.onobjectremove = null;
xrmp.close();
};
const _bindObject = o => {
o.onupdate = e => {
const {object, matrix} = e;
const {id} = object;
const tab = tabs.find(tab => tab.id === id);
const {iframe} = tab;
iframe.position = Array.from(matrix.position);
iframe.orientation = Array.from(matrix.quaternion);
};
};
const _updateRigLists = () => {
const {menuMesh} = rig;
if (menuMesh.listMesh.scrollIndex < 0) {
menuMesh.listMesh.scrollIndex = 0;
} else {
const items = menuMesh.optionsMesh.list === 'tabs' ? tabs : links;
if (menuMesh.listMesh.scrollIndex > items.length - 1) {
menuMesh.listMesh.scrollIndex = items.length - 1;
}
}
menuMesh.listMesh.updateList();
};
// main
const {scene, camera, renderer, destroy} = _makeRenderer();
const layers = [renderer.domElement];
const ambientLight = new THREE.AmbientLight(0x808080);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// controller meshes
const controllerGeometry = new THREE.BoxBufferGeometry(0.05, 0.1, 0.01);
const controllerMaterial = new THREE.MeshPhongMaterial({
color: 0x4caf50,
});
const controllerMeshes = Array(2);
for (let i = 0; i < controllerMeshes.length; i++) {
const controllerMesh = new THREE.Object3D();
controllerMesh.position.set(i === 0 ? -0.1 : 0.1, 0, -0.1);
controllerMesh.quaternion.setFromUnitVectors(
new THREE.Vector3(0, 0, -1),
new THREE.Vector3(0, -1, -1)
);
const rayMesh = (() => {
const geometry = new THREE.CylinderBufferGeometry(0.001, 0.001, 1, 32, 1)
.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI / 2))
.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, -0.5));
const material = new THREE.MeshBasicMaterial({
color: RAY_COLOR,
});
const mesh = new THREE.Mesh(geometry, material);
// mesh.visible = true;
mesh.frustumCulled = false;
return mesh;
})();
controllerMesh.add(rayMesh);
controllerMesh.rayMesh = rayMesh;
/* const dotMesh = (() => {
const geometry = new THREE.SphereBufferGeometry(0.01, 5, 5);
const material = new THREE.MeshBasicMaterial({
color: 0xe91e63,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.z = -1;
mesh.visible = true;
return mesh;
})();
controllerMesh.add(dotMesh);
controllerMesh.dotMesh = dotMesh; */
controllerMesh.ray = new THREE.Ray();
controllerMesh.update = () => {
controllerMesh.ray.origin.copy(controllerMesh.position);
controllerMesh.ray.direction
.set(0, 0, -1)
.applyQuaternion(controllerMesh.quaternion);
};
scene.add(controllerMesh);
controllerMeshes[i] = controllerMesh;
}
const _getControllerIndex = gamepad => gamepad.hand === 'left' ? 0 : 1;
// rig
let focusedTab = null;
const _makeRig = () => {
const rig = new THREE.Object3D();
rig.scale.set(0, 0, 0);
rig.visible = false;
rig.open = false;
rig.animation = null;
const menuMesh = (() => {
const object = new THREE.Object3D();
object.position.set(0, menuPositionHeight, 0);
const urlMesh = (() => {
const canvas = document.createElement('canvas');
canvas.width = menuWidth;
canvas.height = menuHeight;
const ctx = canvas.getContext('2d');
const geometry = new THREE.PlaneBufferGeometry(menuWorldWidth, menuWorldHeight);
const texture = new THREE.Texture(
canvas,
THREE.UVMapping,
THREE.ClampToEdgeWrapping,
THREE.ClampToEdgeWrapping,
THREE.NearestFilter,
THREE.NearestFilter,
THREE.RGBAFormat,
THREE.UnsignedByteType,
1
);
texture.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.9,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.updateMatrixWorld();
mesh.frustumCulled = false;
focusedTab = mesh;
const text = DEFAULT_URL;
const urlState = {
text,
cursor: text.length,
measures: [],
};
mesh.urlState = urlState;
const _updateText = () => {
ctx.fillStyle = '#EEE';
ctx.fillRect(0, 0, menuWidth*0.8 - 5, menuHeight);
if (focusedTab === mesh) {
ctx.fillStyle = '#42a5f5';
ctx.fillRect(0, menuHeight - 5, menuWidth*0.8 - 5, 5);
}
ctx.fillStyle = '#EEE';
ctx.fillRect(menuWidth*0.8, 0, menuWidth*0.2 - 5, menuHeight);
ctx.fillRect(0, menuHeight, menuWidth*0.5, menuHeight);
ctx.fillStyle = '#000';
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillText(urlState.text, 10, urlBarHeight - 10*2 - 5);
ctx.fillText('x', menuWidth*0.88, menuHeight*0.7);
urlState.measures.length = 0;
urlState.measures.push(0);
const {width: barWidth} = ctx.measureText('[');
for (let i = 1; i <= urlState.text.length; i++) {
const {width} = ctx.measureText('[' + urlState.text.slice(0, i) + ']');
urlState.measures.push(width - barWidth*2);
}
ctx.fillStyle = '#000';
ctx.fillRect(10 + urlState.measures[urlState.cursor] - cursorWidth/2, 20, cursorWidth, urlBarHeight - 20*2);
texture.needsUpdate = true;
};
_updateText();
mesh.updateText = _updateText;
const _handleDefaultKey = (code, shiftKey) => {
let c = String.fromCharCode(code);
if (!shiftKey) {
c = c.toLowerCase();
}
_handleSingleKey(c);
};
const _handleSingleKey = c => {
mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor) + c + mesh.urlState.text.slice(mesh.urlState.cursor);
mesh.urlState.cursor++;
mesh.updateText();
};
const _handleKey = (code, shiftKey) => {
if (code === 8) { // backspace
if (mesh.urlState.cursor > 0) {
mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor - 1) + mesh.urlState.text.slice(mesh.urlState.cursor);
mesh.urlState.cursor--;
mesh.updateText();
} else {
if (mesh.urlState.text.length === 0) {
_closeRig();
}
}
} else if (code === 46) { // delete
if (mesh.urlState.cursor < mesh.urlState.text.length) {
mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor) + mesh.urlState.text.slice(mesh.urlState.cursor + 1);
_updateText();
}
} else if (code === 32) { // space
mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor) + ' ' + mesh.urlState.text.slice(mesh.urlState.cursor);
mesh.urlState.cursor++;
_updateText();
} else if (code === 13) { // enter
const u = (optionsMesh.d === 3 && urlMesh.urlState.text === DEFAULT_URL) ? 'realitytab.html?color=29b6f6' : urlMesh.urlState.text;
const {position, quaternion, scale} = _getFrontOfCamera();
_openUrl(u, position, quaternion, scale, optionsMesh.d);
_updateRigLists();
} else if (
code === 9 || // tab
code === 16 || // shift
code === 17 || // ctrl
code === 18 || // alt
code === 20 || // capslock
code === 27 || // esc
code === 91 // win
) {
// nothing
} else if (code === 37) { // left
mesh.urlState.cursor = Math.max(mesh.urlState.cursor - 1, 0);
_updateText();
} else if (code === 39) { // right
mesh.urlState.cursor = Math.min(mesh.urlState.cursor + 1, mesh.urlState.text.length);
_updateText();
} else if (code === 38) { // up
if (focusedTab === urlMesh) {
// nothing
} else {
let tabIndex = tabs.indexOf(focusedTab);
if (tabIndex === 0) {
focusedTab = urlMesh;
} else {
tabIndex--;
focusedTab = tabs[tabIndex];
if (tabIndex < listMesh.scrollIndex) {
listMesh.scrollIndex = tabIndex;
} else if (tabIndex > listMesh.scrollIndex + 4) {
listMesh.scrollIndex = tabIndex - 4;
}
}
}
_updateText();
listMesh.updateList();
} else if (code === 40) { // down
if (focusedTab === urlMesh) {
if (tabs.length > 0) {
focusedTab = tabs[0];
listMesh.scrollIndex = 0;
} else {
// nothing
}
} else {
let tabIndex = tabs.indexOf(focusedTab);
if (tabIndex === tabs.length - 1) {
// nothing
} else {
tabIndex++;
focusedTab = tabs[tabIndex];
if (tabIndex < listMesh.scrollIndex) {
listMesh.scrollIndex = tabIndex;
} else if (tabIndex > listMesh.scrollIndex + 4) {
listMesh.scrollIndex = tabIndex - 4;
}
}
}
_updateText();
listMesh.updateList();
} else if (code === 190) { // .
_handleSingleKey('.');
} else if (code === 188) { // ,
_handleSingleKey(',');
} else if (code === 191) { // /
_handleSingleKey('/');
} else if (code === 186) { // ;
_handleSingleKey(';');
} else if (code === 222) { // ;
_handleSingleKey('\'');
} else if (code === 219) { // [
_handleSingleKey('[');
} else if (code === 221) { // ]
_handleSingleKey(']');
} else if (code === 189) { // -
_handleSingleKey('-');
} else if (code === 187) { // =
_handleSingleKey('=');
} else if (code === 220) { // \
_handleSingleKey('\\');
} else if (code === -1) {
// nothing
} else {
_handleDefaultKey(code, shiftKey);
}
};
mesh.handleKey = _handleKey;
mesh.plane = new THREE.Plane();
mesh.leftLine = new THREE.Line3();
mesh.topLine = new THREE.Line3();
mesh.update = () => {
mesh.leftLine.start
.set(-menuWorldWidth/2, menuWorldHeight/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.leftLine.end
.set(-menuWorldWidth/2, -menuWorldHeight/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.topLine.start
.set(-menuWorldWidth/2, menuWorldHeight/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.topLine.end
.set(menuWorldWidth/2, menuWorldHeight / 2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.plane.setFromCoplanarPoints(
mesh.leftLine.start,
mesh.leftLine.end,
mesh.topLine.end
);
};
return mesh;
})();
object.add(urlMesh);
object.urlMesh = urlMesh;
const optionsMesh = (() => {
const canvas = document.createElement('canvas');
canvas.width = menuWidth;
canvas.height = menuHeight;
const ctx = canvas.getContext('2d');
const geometry = new THREE.PlaneBufferGeometry(menuWorldWidth, menuWorldHeight);
const texture = new THREE.Texture(
canvas,
THREE.UVMapping,
THREE.ClampToEdgeWrapping,
THREE.ClampToEdgeWrapping,
THREE.NearestFilter,
THREE.NearestFilter,
THREE.RGBAFormat,
THREE.UnsignedByteType,
1
);
texture.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.9,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = -menuWorldHeight - (10/menuHeight*menuWorldHeight);
mesh.updateMatrixWorld();
mesh.frustumCulled = false;
mesh.d = 3;
mesh.list = 'tabs';
const _updateOptions = () => {
ctx.font = `${fontSize}px ${fontFamily}`;
// 3D
ctx.fillStyle = mesh.d === 3 ? '#42a5f5' : '#EEE';
ctx.fillRect(0, 0, menuWidth*0.2 - 5, menuHeight);
ctx.fillStyle = mesh.d === 3 ? '#FFF' : '#000';
ctx.fillText('3D', menuWidth*0.2/2 - 30, menuHeight/2 + 20);
// 2D
ctx.fillStyle = mesh.d === 2 ? '#42a5f5' : '#EEE';
ctx.fillRect(menuWidth*0.2, 0, menuWidth*0.2 - 5, menuHeight);
ctx.fillStyle = mesh.d === 2 ? '#FFF' : '#000';
ctx.fillText('2D', menuWidth*0.2 + menuWidth*0.2/2 - 30, menuHeight/2 + 20);
// Tabs
ctx.fillStyle = mesh.list === 'tabs' ? '#42a5f5' : '#EEE';
ctx.fillRect(menuWidth*0.4, 0, menuWidth*0.2 - 5, menuHeight);
ctx.fillStyle = mesh.list === 'tabs' ? '#FFF' : '#000';
ctx.fillText('Tabs', menuWidth*0.4 + menuWidth*0.2/2 - 70, menuHeight/2 + 20);
// Links
ctx.fillStyle = mesh.list === 'links' ? '#42a5f5' : '#EEE';
ctx.fillRect(menuWidth*0.6, 0, menuWidth*0.2 - 5, menuHeight);
ctx.fillStyle = mesh.list === 'links' ? '#FFF' : '#000';
ctx.fillText('Links', menuWidth*0.6 + menuWidth*0.2/2 - 70, menuHeight/2 + 20);
// Party
ctx.fillStyle = mesh.list === 'party' ? '#42a5f5' : '#EEE';
ctx.fillRect(menuWidth*0.8, 0, menuWidth*0.2 - 5, menuHeight);
ctx.fillStyle = mesh.list === 'party' ? '#FFF' : '#000';
ctx.fillText('Party', menuWidth*0.8 + menuWidth*0.2/2 - 70, menuHeight/2 + 20);
texture.needsUpdate = true;
};
_updateOptions();
mesh.updateOptions = _updateOptions;
mesh.plane = new THREE.Plane();
mesh.leftLine = new THREE.Line3();
mesh.topLine = new THREE.Line3();
mesh.update = () => {
mesh.leftLine.start
.set(-menuWorldWidth/2, menuWorldHeight/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.leftLine.end
.set(-menuWorldWidth/2, -menuWorldHeight/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.topLine.start
.set(-menuWorldWidth/2, menuWorldHeight/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.topLine.end
.set(menuWorldWidth/2, menuWorldHeight / 2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.plane.setFromCoplanarPoints(
mesh.leftLine.start,
mesh.leftLine.end,
mesh.topLine.end
);
};
return mesh;
})();
object.add(optionsMesh);
object.optionsMesh = optionsMesh;
const listMesh = (() => {
const canvas = document.createElement('canvas');
canvas.width = menuWidth;
canvas.height = menuHeight*4;
const ctx = canvas.getContext('2d');
const geometry = new THREE.PlaneBufferGeometry(menuWorldWidth, menuWorldHeight*4);
const texture = new THREE.Texture(
canvas,
THREE.UVMapping,
THREE.ClampToEdgeWrapping,
THREE.ClampToEdgeWrapping,
THREE.NearestFilter,
THREE.NearestFilter,
THREE.RGBAFormat,
THREE.UnsignedByteType,
1
);
texture.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.9,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = -menuWorldHeight*3.5 - (10/menuHeight*menuWorldHeight)*2;
mesh.updateMatrixWorld();
mesh.frustumCulled = false;
mesh.scrollIndex = 0;
mesh.scrollIndexFine = 0;
const _updateList = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = `${fontSize}px ${fontFamily}`;
const _renderScrollbar = listLength => {
ctx.fillStyle = '#808080';
ctx.fillRect(menuWidth*0.99, 0, menuWidth*0.01, menuHeight*4);
const fullBarHeight = menuHeight*4;
const barHeight = fullBarHeight/Math.min(listLength > 0 ? listLength : 1, 4);
const barSpace = fullBarHeight - barHeight;
ctx.fillStyle = '#42a5f5';
ctx.fillRect(menuWidth*0.99, mesh.scrollIndex*barSpace/(listLength > 1 ? (listLength-1) : 1), menuWidth*0.01, barHeight);
};
for (let i = 0; i < iconMeshes.length; i++) {
const iconMesh = iconMeshes[i];
if (iconMesh.parent) {
iconMesh.parent.remove(iconMesh);
}
}
if (optionsMesh.list === 'tabs') {
if (tabs.length > 0) {
const localTabs = tabs.slice(mesh.scrollIndex, mesh.scrollIndex + 4);
for (let i = 0; i < localTabs.length; i++) {
const tab = localTabs[i];
const {url} = tab;
const y = menuHeight*i;
ctx.fillStyle = tab === focusedTab ? '#2196f3' : '#FFF';
ctx.fillRect(0, y, menuWidth*0.6 - 5, menuHeight);
ctx.fillStyle = '#FFF';
ctx.fillRect(menuWidth*0.6, y, menuWidth*0.1 - 5, menuHeight);
ctx.fillRect(menuWidth*0.7, y, menuWidth*0.1 - 5, menuHeight);
ctx.fillRect(menuWidth*0.8, y, menuWidth*0.18, menuHeight);
ctx.fillStyle = tab === focusedTab ? '#FFF' : '#000';
ctx.fillText(url, 100, y + menuHeight/2 + 20);
ctx.fillStyle = '#000';
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(menuWidth*0.65 - 5, y + menuHeight*0.5 - 20);
ctx.lineTo(menuWidth*0.65 - 5, y + menuHeight*0.5 + 20);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(menuWidth*0.65 - 5 - 20, y + menuHeight*0.5);
ctx.lineTo(menuWidth*0.65 - 5 + 20, y + menuHeight*0.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(menuWidth*0.75 - 5, y + menuHeight*0.5 - 20);
ctx.lineTo(menuWidth*0.75 - 5 + 20, y + menuHeight*0.5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(menuWidth*0.75 - 5 + 20, y + menuHeight*0.5);
ctx.lineTo(menuWidth*0.75 - 5, y + menuHeight*0.5 + 20);
ctx.stroke();
ctx.fillText('x', menuWidth*0.88, y + menuHeight*0.7);
}
} else {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, menuWidth*0.3 - 5, menuHeight);
ctx.fillStyle = '#FFFFFF';
ctx.fillText('No tabs', 50, menuHeight/2 + 20);
}
_renderScrollbar(tabs.length);
} else if (optionsMesh.list === 'links') {
const localLinks = links.slice(mesh.scrollIndex, mesh.scrollIndex + 4);
for (let i = 0; i < localLinks.length; i++) {
const link = localLinks[i];
const {name} = link;
const y = menuHeight*i;
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, y, menuWidth*0.98, menuHeight);
ctx.fillStyle = '#000000';
ctx.fillText(name, 100, y + menuHeight/2 + 20);
if (!link.iconMesh) {
const iconMesh = (() => {
const iconMesh = new THREE.Object3D();
function pad(n, width) {
return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n;
}
_requestSpriteMesh(`icons/Food_${pad((4+mesh.scrollIndex+i) + '', 2)}.png`)
.then(spriteMesh => {
spriteMesh.position.set(-0.45, 0.15 - i*0.1, 0.05);
spriteMesh.rotation.order = 'YXZ';
spriteMesh.scale.set(0.16, 0.16, 0.16);
spriteMesh.frustumCulled = false;
iconMesh.add(spriteMesh);
});
return iconMesh;
})();
link.iconMesh = iconMesh;
iconMeshes.push(iconMesh);
}
mesh.add(link.iconMesh);
link.iconMesh.children[0] && (link.iconMesh.children[0].position.y = 0.15 - i*0.1);
}
_renderScrollbar(links.length);
} else if (optionsMesh.list === 'party') {
if (!serverConnectedUrl) {
if (!serversLoading) {
if (servers.length > 0) {
const localServers = servers.slice(mesh.scrollIndex, mesh.scrollIndex + 4);
for (let i = 0; i < localServers.length; i++) {
const server = localServers[i];
const {name} = server;
const y = menuHeight*i;
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, y, menuWidth*0.98, menuHeight);
ctx.fillStyle = '#000000';
ctx.fillText(name, 100, y + menuHeight/2 + 20);
if (!server.iconMesh) {
const iconMesh = (() => {
const iconMesh = new THREE.Object3D();
function pad(n, width) {
return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n;
}
_requestSpriteMesh(`icons/Food_${pad((4+mesh.scrollIndex+i) + '', 2)}.png`)
.then(spriteMesh => {
spriteMesh.position.set(-0.45, 0.15 - i*0.1, 0.05);
spriteMesh.rotation.order = 'YXZ';
spriteMesh.scale.set(0.16, 0.16, 0.16);
spriteMesh.frustumCulled = false;
iconMesh.add(spriteMesh);
});
return iconMesh;
})();
server.iconMesh = iconMesh;
iconMeshes.push(iconMesh);
}
mesh.add(server.iconMesh);
server.iconMesh.children[0] && (server.iconMesh.children[0].position.y = 0.15 - i*0.1);
}
} else {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, menuWidth*0.99 - 5, menuHeight);
ctx.fillStyle = '#FFFFFF';
ctx.fillText('No servers', 50, menuHeight/2 + 20);
}
_renderScrollbar(servers.length);
} else {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, menuWidth*0.99 - 5, menuHeight);
ctx.fillStyle = '#FFFFFF';
ctx.fillText('Loading servers...', 50, menuHeight/2 + 20);
_renderScrollbar(0);
}
} else {
ctx.fillStyle = '#2196f3';
ctx.fillRect(0, 0, menuWidth*0.8 - 5, menuHeight);
ctx.fillStyle = '#FFF';
ctx.fillRect(menuWidth*0.8, 0, menuWidth*0.18, menuHeight);
ctx.fillStyle = '#FFF';
ctx.fillText(serverConnectedUrl, 100, menuHeight/2 + 20);
ctx.fillStyle = '#000';
ctx.fillText('x', menuWidth*0.88, menuHeight*0.7);
_renderScrollbar(0);
}
}
texture.needsUpdate = true;
};
_updateList();
mesh.updateList = _updateList;
const _updateWheel = (dx, dy) => {
mesh.scrollIndexFine += dy;
while (mesh.scrollIndexFine >= 0) {
mesh.scrollIndexFine--;
mesh.scrollIndex--;
}
while (rig.menuMesh.listMesh.scrollIndexFine <= -1) {
mesh.scrollIndexFine++;
mesh.scrollIndex++;
}
_updateRigLists();
};
mesh.updateWheel = _updateWheel;
mesh.plane = new THREE.Plane();
mesh.leftLine = new THREE.Line3();
mesh.topLine = new THREE.Line3();
mesh.update = () => {
mesh.leftLine.start
.set(-menuWorldWidth/2, menuWorldHeight*4/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.leftLine.end
.set(-menuWorldWidth/2, -menuWorldHeight*4/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.topLine.start
.set(-menuWorldWidth/2, menuWorldHeight*4/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.topLine.end
.set(menuWorldWidth/2, menuWorldHeight*4/2, 0)
.applyMatrix4(mesh.matrixWorld);
mesh.plane.setFromCoplanarPoints(
mesh.leftLine.start,
mesh.leftLine.end,
mesh.topLine.end
);
};
return mesh;
})();
object.add(listMesh);
object.listMesh = listMesh;
return object;
})();
rig.add(menuMesh);
rig.menuMesh = menuMesh;
const keyboardMesh = (() => {
const object = new THREE.Object3D();
const planeMesh = (() => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = 'keyboard.png';
img.onload = () => {
texture.needsUpdate = true;
};
img.onerror = err => {
console.warn(err.stack);
};
const geometry = new THREE.PlaneBufferGeometry(1, keyboardHeight / keyboardWidth);
const texture = new THREE.Texture(
img,
THREE.UVMapping,
THREE.ClampToEdgeWrapping,
THREE.ClampToEdgeWrapping,
THREE.NearestFilter,
THREE.NearestFilter,
THREE.RGBAFormat,
THREE.UnsignedByteType,
1
);
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.9,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
mesh.position.y = -0.5;
mesh.quaternion.setFromUnitVectors(
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(0, 1, 1).normalize()
);
const keyMeshes = [];
for (let i = 0; i < 2; i++) {
const keyMesh = (() => {
const geometry = new THREE.PlaneBufferGeometry(1, 1);
const texture = new THREE.Texture(
null,
THREE.UVMapping,
THREE.ClampToEdgeWrapping,
THREE.ClampToEdgeWrapping,
THREE.NearestFilter,
THREE.NearestFilter,
THREE.RGBAFormat,
THREE.UnsignedByteType,
1
);
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.5,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.frustumCulled = false;
return mesh;
})();
mesh.add(keyMesh);
keyMeshes.push(keyMesh);
}
mesh.keyMeshes = keyMeshes;
return mesh;
})();
object.add(planeMesh);
object.planeMesh = planeMesh;
object.plane = new THREE.Plane();
object.leftLine = new THREE.Line3();
object.topLine = new THREE.Line3();
object.update = () => {
object.leftLine.start
.set(-1/2, keyboardHeight / keyboardWidth / 2, 0)
.applyMatrix4(planeMesh.matrixWorld);
object.leftLine.end
.set(-1/2, -keyboardHeight / keyboardWidth / 2, 0)
.applyMatrix4(planeMesh.matrixWorld);
object.topLine.start
.set(-1/2, keyboardHeight / keyboardWidth / 2, 0)
.applyMatrix4(planeMesh.matrixWorld);
object.topLine.end
.set(1/2, keyboardHeight / keyboardWidth / 2, 0)
.applyMatrix4(planeMesh.matrixWorld);
object.plane.setFromCoplanarPoints(
object.leftLine.start,
object.leftLine.end,
object.topLine.end
);
};
return object;
})();
rig.add(keyboardMesh);
rig.keyboardMesh = keyboardMesh;
return rig;
};
const rig = _makeRig();
scene.add(rig);
const _openRig = (position, quaternion) => {
if (!rig.open) {
rig.position.copy(position);
rig.quaternion.copy(quaternion);
rig.open = true;
} else {
rig.open = false;
}
_animateRig();
};
const _closeRig = () => {
rig.open = false;
_animateRig();
};
const _animateRig = () => {
const startScale = rig.scale.x;
const endScale = rig.open ? 1 : 0;
const startTime = Date.now();
const endTime = startTime + 150;
rig.animation = {
update() {
const now = Date.now();
const factor = Math.min(Math.max(Math.pow((now - startTime) / (endTime - startTime), 0.2), 0), 1);
const scaleFactor = startScale*(1-factor) + endScale*factor;
rig.scale.set(scaleFactor, scaleFactor, scaleFactor);
rig.visible = rig.scale.x > 0.00001;
if (factor >= 1) {
rig.animation = null;
}
},
};
};
const cacheCanvas = document.createElement('canvas');
cacheCanvas.width = 1024;
cacheCanvas.height = 1024;
const cacheCanvasCtx = cacheCanvas.getContext('2d');
// box meshes
const boxMeshes = [
_makeBoxMesh(),
_makeBoxMesh(),
];
boxMeshes.forEach(boxMesh => {
scene.add(boxMesh);
});
const moves = [null, null];
window.addEventListener('click', () => {
renderer.domElement.requestPointerLock();
});
window.addEventListener('mousedown', e => {
if (window.document.pointerLockElement) {
fakeDisplay.gamepads[1].buttons[1].pressed = true;
}
});
window.addEventListener('mouseup', e => {
if (window.document.pointerLockElement) {
fakeDisplay.gamepads[1].buttons[1].pressed = false;
}
});
window.addEventListener('mousemove', e => {
if (window.document.pointerLockElement) {
const {movementX, movementY} = e;
/* if (keys.ctrl) {
const controllerOffset = controllerOffsets[controllerIndex];
controllerOffset.x += movementX * 0.002;
controllerOffset.y -= movementY * 0.002;
} else if (keys.alt) {
const controllerOffset = controllerOffsets[controllerIndex];
controllerOffset.x += movementX * 0.002;
controllerOffset.z += movementY * 0.002;
} else { */
localEuler.setFromQuaternion(fakeDisplay.quaternion, localEuler.order);
localEuler.y -= movementX * 0.01;
localEuler.x -= movementY * 0.01;
localEuler.x = Math.min(Math.max(localEuler.x, -Math.PI/2), Math.PI/2);
fakeDisplay.quaternion.setFromEuler(localEuler);
fakeDisplay.pushUpdate();
// }
}
});
window.addEventListener('wheel', e => {
if (window.document.pointerLockElement) {
rig.menuMesh.listMesh.updateWheel(e.deltaX/100, -e.deltaY/100);
}
});
const keys = {
up: false,
down: false,
left: false,
right: false,
};
const _makeButtons = () => ({
trigger: false,
bumper: false,
});
const lastButtons = [
_makeButtons(),
_makeButtons(),
];
const intersectionSpecs = [
null,
null,
];
window.addEventListener('keydown', e => {
if (window.document.pointerLockElement) {
switch (e.which) {
case 87: { // W
keys.up = true;
/* if (!window.document.pointerLockElement) {
renderer.domElement.requestPointerLock();
} */
break;
}
case 83: { // S
keys.down = true;
/* if (!window.document.pointerLockElement) {
renderer.domElement.requestPointerLock();
} */
break;
}
case 65: { // A
keys.left = true;
/* if (!window.document.pointerLockElement) {
renderer.domElement.requestPointerLock();
} */
break;
}
case 68: { // D
keys.right = true;
/* if (!window.document.pointerLockElement) {
renderer.domElement.requestPointerLock();
} */
break;
}
case 69: { // E
fakeDisplay.gamepads[1].buttons[2].pressed = true;
break;
}
case 32: { // space
keys.space = true;
break;
}
case 17: { // ctrl
keys.ctrl = true;
break;
}
}
} else {
if (focusedTab) {
if (focusedTab === rig.menuMesh.urlMesh) {
rig.menuMesh.urlMesh.handleKey(e.keyCode, e.shiftKey);
} else if (focusedTab.iframe) {
focusedTab.iframe.sendKeyDown(e.which, {
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
});
}
}
}
});
window.addEventListener('keyup', e => {
if (window.document.pointerLockElement) {
switch (e.which) {
case 87: { // W
keys.up = false;
break;
}
case 83: { // S
keys.down = false;
break;
}
case 65: { // A
keys.left = false;
break;
}
case 68: { // D
keys.right = false;
break;
}
case 69: { // E
fakeDisplay.gamepads[1].buttons[2].pressed = false;
break;
}
case 32: { // space
keys.space = false;
break;
}
case 17: { // ctrl
keys.ctrl = false;
break;
}
}
} else {
if (focusedTab && focusedTab.iframe) {
focusedTab.iframe.sendKeyUp(e.which, {
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
});
}
}
});
window.addEventListener('keypress', e => {
if (!window.document.pointerLockElement) {
if (focusedTab) {
/* if (focusedTab === rig.menuMesh.urlMesh) {
rig.menuMesh.urlMesh.handleKey(e.keyCode, e.shiftKey);
} else */if (focusedTab.iframe) {
focusedTab.iframe.sendKeyPress(e.which, {
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
});
}
}
}
});
// let renderTarget = null;
const startTime = Date.now();
let lastTime = 0;
function animate(time, frame) {
const now = Date.now();
const timeDiff = now - lastTime;
const startTimeDiff = now - startTime;
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length && i < 2; i++) {
const gamepad = gamepads[i];
const moveTab = moves[i];
const boxMesh = boxMeshes[i];
if (moveTab) {
const {iframe} = moveTab;
const scrollFactor = scrollFactors[i];
const scaleFactor = scaleFactors[i];
localVector.fromArray(gamepad.pose.position);
localQuaternion.fromArray(gamepad.pose.orientation);
localVector
.add(
localVector2.set(0, 0, -scrollFactor)
.applyQuaternion(localQuaternion)
);
localVector2.set(scaleFactor, scaleFactor, scaleFactor);
iframe.position = localVector.toArray();
iframe.orientation = localQuaternion.toArray();
iframe.scale = localVector2.toArray();
boxMesh.position.copy(localVector);
boxMesh.quaternion.copy(localQuaternion);
boxMesh.scale.copy(localVector2);
// boxMesh.updateMatrixWorld();
boxMesh.visible = true;
if (serverConnectedUrl) {
const objectMesh = xrmp.getObjectMeshes().find(objectMesh => objectMesh.object.id === moveTab.id);
const {object} = objectMesh;
const {objectMatrix} = object;
localVector.toArray(objectMatrix.position);
localQuaternion.toArray(objectMatrix.quaternion);
object.pushUpdate();
}
} else {
boxMesh.visible = false;
}
}
const _updateControllerMeshes = () => {
if (renderer.vr.enabled) {
for (let i = 0; i < controllerMeshes.length; i++) {
controllerMeshes[i].visible = false;
}
for (let i = 0; i < gamepads.length && i < 2; i++) {
const gamepad = gamepads[i];
if (gamepad.connected) {
const controllerIndex = _getControllerIndex(gamepad);
const controllerMesh = controllerMeshes[controllerIndex];
controllerMesh.position.fromArray(gamepad.pose.position);
controllerMesh.quaternion.fromArray(gamepad.pose.orientation);
controllerMesh.updateMatrixWorld(true);
controllerMesh.visible = true;
}
}
}
};
_updateControllerMeshes();
const _updateIntersections = () => {
for (let i = 0; i < 2; i++) {
const controllerMesh = controllerMeshes[i];
controllerMesh.update();
const _setIntersectionDefault = () => {
controllerMesh.rayMesh.scale.z = rayDistance;
controllerMesh.rayMesh.updateMatrixWorld();
rig.keyboardMesh.planeMesh.keyMeshes[i].visible = false;
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
moves[i] = null;
}
}
intersectionSpecs[i] = null;
};
const intersectionPoints = [];
const intersectionCbs = [];
const {keyboardMesh, menuMesh} = rig;
const {planeMesh} = keyboardMesh;
const keyMesh = planeMesh.keyMeshes[i];
keyboardMesh.update();
menuMesh.urlMesh.update();
menuMesh.optionsMesh.update();
menuMesh.listMesh.update();
// keyboard
{
const intersectionPoint = keyboardMesh.visible ? controllerMesh.ray.intersectPlane(keyboardMesh.plane, localVector) : null;
if (intersectionPoint) {
const leftIntersectionPoint = keyboardMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);
const topIntersectionPoint = keyboardMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);
const xFactor = topIntersectionPoint.distanceTo(keyboardMesh.topLine.start) / (1);
const yFactor = leftIntersectionPoint.distanceTo(keyboardMesh.leftLine.start) / (keyboardHeight / keyboardWidth);
const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);
if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
const x = xFactor * keyboardWidth;
const y = yFactor * keyboardHeight;
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
for (let j = 0; j < keyMap.length; j++) {
const [key, kx1, ky1, kx2, ky2] = keyMap[j];
if (x >= kx1 && x < kx2 && y >= ky1 && y < ky2) {
if (keyboardHighlightCanvasCtx) {
const width = kx2 - kx1;
const height = ky2 - ky1;
let imageData = keyboardHighlightCanvasCtx.getImageData(kx1, ky1, width, height);
if (key === 'enter') { // special case the enter key; it has a non-rectangular shape
cacheCanvasCtx.putImageData(imageData, 0, 0);
cacheCanvasCtx.clearRect(0, 0, 80, 140);
imageData = cacheCanvasCtx.getImageData(0, 0, imageData.width, imageData.height);
}
keyMesh.material.map.image = imageData;
keyMesh.material.map.needsUpdate = true;
keyMesh.position
.set(
-1/2 + ((width/2 + kx1) / keyboardWidth),
(keyboardHeight / keyboardWidth)/2 - ((height/2 + ky1) / keyboardHeight * (keyboardHeight / keyboardWidth)),
0.01 * (1)
);
keyMesh.scale.set(
width / keyboardWidth,
height / keyboardHeight * (keyboardHeight / keyboardWidth),
1
);
keyMesh.updateMatrixWorld();
keyMesh.visible = true;
}
intersectionSpecs[i] = {
type: 'key',
data: {
key,
rig,
},
};
break;
}
}
});
}
}
}
// menu mesh
{
const intersectionPoint = menuMesh.visible ? controllerMesh.ray.intersectPlane(menuMesh.urlMesh.plane, localVector) : null;
if (intersectionPoint) {
const leftIntersectionPoint = menuMesh.urlMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);
const topIntersectionPoint = menuMesh.urlMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);
const xFactor = topIntersectionPoint.distanceTo(menuMesh.urlMesh.topLine.start) / menuWorldWidth;
const yFactor = leftIntersectionPoint.distanceTo(menuMesh.urlMesh.leftLine.start) / menuWorldHeight;
const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);
if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
const x = xFactor * menuWidth;
const y = yFactor * menuHeight;
if (y < urlBarOffset) {
if (x < menuWidth*0.8) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
if (gamepad.buttons[1].pressed) {
const textX = x - cursorWidth;
let closestIndex = -1;
let closestDistance = Infinity;
for (let i = 0; i < menuMesh.urlMesh.urlState.measures.length; i++) {
const urlMeasure = menuMesh.urlMesh.urlState.measures[i];
const distance = Math.abs(urlMeasure - textX);
if (distance < closestDistance) {
closestIndex = i;
closestDistance = distance;
}
}
if (closestIndex !== -1) {
menuMesh.urlMesh.urlState.cursor = closestIndex;
}
focusedTab = menuMesh.urlMesh;
menuMesh.urlMesh.updateText();
menuMesh.listMesh.updateList();
}
intersectionSpecs[i] = null;
});
} else {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
if (gamepad.buttons[1].pressed) {
_closeRig();
}
intersectionSpecs[i] = null;
});
}
}
}
}
}
// options mesh
{
const intersectionPoint = menuMesh.visible ? controllerMesh.ray.intersectPlane(menuMesh.optionsMesh.plane, localVector) : null;
if (intersectionPoint) {
const leftIntersectionPoint = menuMesh.optionsMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);
const topIntersectionPoint = menuMesh.optionsMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);
const xFactor = topIntersectionPoint.distanceTo(menuMesh.optionsMesh.topLine.start) / menuWorldWidth;
const yFactor = leftIntersectionPoint.distanceTo(menuMesh.optionsMesh.leftLine.start) / menuWorldHeight;
const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);
if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
const x = xFactor * menuWidth;
const y = yFactor * menuHeight;
if (y >= 0 && y < urlBarOffset) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
if (gamepad.buttons[1].pressed) {
const optionX = Math.floor(x/(menuWidth*0.2));
if (optionX === 0) {
menuMesh.optionsMesh.d = 3;
menuMesh.optionsMesh.updateOptions();
} else if (optionX === 1) {
menuMesh.optionsMesh.d = 2;
menuMesh.optionsMesh.updateOptions();
} else if (optionX === 2) {
menuMesh.optionsMesh.list = 'tabs';
menuMesh.optionsMesh.updateOptions();
menuMesh.listMesh.scrollIndex = 0;
menuMesh.listMesh.updateList();
} else if (optionX === 3) {
menuMesh.optionsMesh.list = 'links';
menuMesh.optionsMesh.updateOptions();
menuMesh.listMesh.scrollIndex = 0;
menuMesh.listMesh.updateList();
} else if (optionX === 4) {
menuMesh.optionsMesh.list = 'party';
if (!serversLoading) {
XRMultiplayer.requestServers(`${registryUrl}/servers`)
.then(result => {
servers = result.servers;
serversLoading = false;
menuMesh.listMesh.updateList();
})
.catch(err => {
console.warn(err.stack);
serversLoading = false;
menuMesh.listMesh.updateList();
});
serversLoading = true;
}
menuMesh.optionsMesh.updateOptions();
menuMesh.listMesh.scrollIndex = 0;
menuMesh.listMesh.updateList();
}
}
intersectionSpecs[i] = null;
});
}
}
}
}
// list mesh
{
const intersectionPoint = menuMesh.visible ? controllerMesh.ray.intersectPlane(menuMesh.listMesh.plane, localVector) : null;
if (intersectionPoint) {
const leftIntersectionPoint = menuMesh.listMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);
const topIntersectionPoint = menuMesh.listMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);
const xFactor = topIntersectionPoint.distanceTo(menuMesh.listMesh.topLine.start) / menuWorldWidth;
const yFactor = leftIntersectionPoint.distanceTo(menuMesh.listMesh.leftLine.start) / (menuWorldHeight*4);
const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);
if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
const x = xFactor * menuWidth;
const y = yFactor * menuHeight*4;
if (x < menuWidth * 0.98) {
if (menuMesh.optionsMesh.list === 'tabs') {
const localTabs = tabs.slice(menuMesh.listMesh.scrollIndex, menuMesh.listMesh.scrollIndex + 4);
for (let j = 0; j < localTabs.length; j++) {
if (y >= urlBarOffset*j && y < urlBarOffset*(j+1)) {
if (x >= menuWidth*0.8) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
const tab = tabs[menuMesh.listMesh.scrollIndex + j];
_closeTab(tab);
if (focusedTab === tab) {
focusedTab = rig.menuMesh.urlMesh;
}
rig.menuMesh.urlMesh.updateText();
_updateRigLists();
}
intersectionSpecs[i] = null;
});
} else if (x >= menuWidth*0.7) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
const tab = tabs[menuMesh.listMesh.scrollIndex + j];
const {iframe} = tab;
if (iframe.d === 3) {
window.browser.devTools.requestDevTools(iframe.contentWindow, document)
.then(devTools => {
const {position, quaternion, scale} = _getFrontOfCamera();
// position.add(localVector.set(0, -0.3, 0).applyQuaternion(quaternion));
_addTab(devTools.getIframe(), position, quaternion, scale, 2);
})
.catch(err => {
console.warn(err.stack);
});
}
}
intersectionSpecs[i] = null;
});
} else if (x >= menuWidth*0.6) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
const tab = tabs[menuMesh.listMesh.scrollIndex + j];
moves[i] = tab;
localVector.fromArray(gamepad.pose.position);
localQuaternion.fromArray(gamepad.pose.orientation);
const distance = localVector
.distanceTo(
localVector2
.fromArray(tab.iframe.xrOffset.position)
);
scrollFactors[i] = distance;
const scale = tab.iframe.xrOffset.scale[0];
scaleFactors[i] = scale;
}
intersectionSpecs[i] = null;
});
} else {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
focusedTab = tabs[menuMesh.listMesh.scrollIndex + j];
menuMesh.urlMesh.updateText();
menuMesh.listMesh.updateList();
}
intersectionSpecs[i] = null;
});
}
break;
}
}
} else if (menuMesh.optionsMesh.list === 'links') {
const localLinks = links.slice(menuMesh.listMesh.scrollIndex, menuMesh.listMesh.scrollIndex + 4);
for (let j = 0; j < localLinks.length; j++) {
if (y >= urlBarOffset*j && y < urlBarOffset*(j+1)) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
const link = localLinks[j];
const {url} = link;
const {position, quaternion, scale} = _getFrontOfCamera();
_openUrl(url, position, quaternion, scale, menuMesh.optionsMesh.d);
menuMesh.optionsMesh.list = 'tabs';
menuMesh.optionsMesh.updateOptions();
menuMesh.listMesh.scrollIndex = 0;
_updateRigLists();
}
intersectionSpecs[i] = null;
});
break;
}
}
} else if (menuMesh.optionsMesh.list === 'party') {
if (!serverConnectedUrl) {
const localServers = servers.slice(menuMesh.listMesh.scrollIndex, menuMesh.listMesh.scrollIndex + 4);
for (let j = 0; j < localServers.length; j++) {
if (y >= urlBarOffset*j && y < urlBarOffset*(j+1)) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
const server = localServers[j];
const {name} = server;
_openServer(`${registryUrl}/servers/${name}`, true);
}
intersectionSpecs[i] = null;
});
break;
}
}
} else {
if (y >= 0 && y < urlBarOffset) {
if (x >= menuWidth*0.8) {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
const lastPressed = lastPresseds[i];
if (pressed && !lastPressed) {
_closeServer();
}
intersectionSpecs[i] = null;
});
}
}
}
}
} else {
intersectionPoints.push(intersectionPoint.clone());
intersectionCbs.push(() => {
controllerMesh.rayMesh.scale.z = distance;
controllerMesh.updateMatrixWorld();
/* controllerMesh.dotMesh.position.z = -distance;
controllerMesh.updateMatrixWorld();
controllerMesh.dotMesh.visible = true; */
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
const pressed = gamepad.buttons[1].pressed;
if (pressed) {
const list = menuMesh.optionsMesh.list === 'tabs' ? tabs : links;
menuMesh.listMesh.scrollIndex = Math.floor(yFactor*list.length);
if (menuMesh.listMesh.scrollIndex >= list.length) {
menuMesh.listMesh.scrollIndex = list.length;
}
menuMesh.listMesh.updateList();
}
intersectionSpecs[i] = null;
});
}
}
}
}
if (intersectionPoints.length > 0) {
const intersectionCb = intersectionPoints
.map((a, i) => ({
distance: a.distanceTo(controllerMesh.position),
cb: intersectionCbs[i],
}))
.sort((a, b) => a.distance - b.distance)[0].cb;
intersectionCb();
} else {
_setIntersectionDefault();
}
const gamepads = navigator.getGamepads();
const gamepad = gamepads[i];
if (gamepad) {
const pressed = gamepad.buttons[1].pressed;
const padPressed = gamepad.buttons[0].pressed;
const padTouched = gamepad.buttons[0].touched;
const {axes} = gamepad;
const lastPadTouched = lastPadToucheds[i];
const lastAxis = lastAxes[i];
if (padTouched && lastPadTouched && rig.open && !moves[i]) {
const dx = axes[0] - lastAxis[0];
const dy = axes[1] - lastAxis[1];
rig.menuMesh.listMesh.updateWheel(dx*4, dy*4);
}
lastPresseds[i] = pressed;
lastPadToucheds[i] = padTouched;
if (padPressed && moves[i]) {
if (padPressed) {
if (gamepad.axes[1] > 0.5) {
scrollFactors[i] += 1/20;
} else if (gamepad.axes[1] < -0.5) {
scrollFactors[i] -= 1/20;
} else if (gamepad.axes[0] > 0.5) {
scaleFactors[i] = Math.min(scaleFactors[i] *= 1.02, 100);
} else if (gamepad.axes[0] < -0.5) {
scaleFactors[i] = Math.max(scaleFactors[i] /= 1.02, 0.01);
}
}
}
lastAxis[0] = axes[0];
lastAxis[1] = axes[1];
}
}
};
_updateIntersections();
const _updateButtons = () => {
if (display) {
for (let i = 0; i < gamepads.length && i < 2; i++) {
const gamepad = gamepads[i];
{
const trigger = gamepad.buttons[1].pressed;
const lastTrigger = lastButtons[i].trigger;
if (trigger && !lastTrigger) {
const spec = intersectionSpecs[i];
if (spec && spec.type === 'key') {
const {data: {key, rig}} = spec;
const {menuMesh: {urlMesh, optionsMesh}} = rig;
if (focusedTab === rig.menuMesh.urlMesh) {
if (key !== 'enter') {
const code = keyCode(key);
rig.menuMesh.urlMesh.handleKey(code, false);
} else {
const u = (optionsMesh.d === 3 && urlMesh.urlState.text === DEFAULT_URL) ? 'realitytab.html?color=29b6f6' : urlMesh.urlState.text;
const {position, quaternion, scale} = _getFrontOfCamera();
_openUrl(u, position, quaternion, scale, optionsMesh.d);
_updateRigLists();
}
} else if (focusedTab.iframe) {
let code = keyCode(key);
const opts = {
shiftKey: false,
ctrlKey: false,
altKey: false,
};
focusedTab.iframe.sendKeyDown(code, opts);
focusedTab.iframe.sendKeyPress(code + 32, opts);
focusedTab.iframe.sendKeyUp(code, opts);
}
}
}
lastButtons[i].trigger = trigger;
}
{
const bumper = gamepad.buttons[2].pressed;
const lastBumper = lastButtons[i].bumper;
if (bumper && !lastBumper) {
const {position, quaternion} = _getFrontOfCamera();
localEuler.setFromQuaternion(quaternion, localEuler.order);
localEuler.x = 0;
localEuler.z = 0;
quaternion.setFromEuler(localEuler);
_openRig(position, quaternion);
}
lastButtons[i].bumper = bumper;
}
}
}
};
_updateButtons();
const _render = () => {
renderer.render(scene, renderer.vr.enabled ? renderer.vr.getCamera(camera) : camera);
};
_render();
const _updateMovement = () => {
if (fakeDisplay) {
// if (window.document.pointerLockElement) {
// let moving = false;
const speed = 0.004;
const velocity = localVector.set(0, 0, 0);
if (keys.up) {
velocity.z -= speed * timeDiff;
// moving = true;
}
if (keys.down) {
velocity.z += speed * timeDiff;
// moving = true;
}
if (keys.left) {
velocity.x -= speed * timeDiff;
// moving = true;
}
if (keys.right) {
velocity.x += speed * timeDiff;
// moving = true;
}
if (keys.space) {
velocity.y += speed * timeDiff;
// moving = true;
}
if (keys.ctrl) {
velocity.y -= speed * timeDiff;
// moving = true;
}
// if (moving) {
// velocity.multiplyScalar(Math.pow(moving ? 0.98 : 0.91, timePassed / 5));
localEuler.setFromQuaternion(fakeDisplay.quaternion, localEuler.order);
localEuler.x = 0;
fakeDisplay.position.add(
localVector.copy(velocity)
.applyEuler(localEuler)
);
fakeDisplay.pushUpdate();
// }
// }
}
};
_updateMovement();
const _updateRig = () => {
if (rig.animation) {
rig.animation.update();
}
const iconMeshRotation = (Date.now() % 1000) / 1000 * Math.PI * 2;
for (let i = 0; i < iconMeshes.length; i++) {
const iconMesh = iconMeshes[i];
const child = iconMesh.children[0];
if (child) {
child.rotation.y = iconMeshRotation;
}
}
};
_updateRig();
const _updateMp = () => {
/* const vrCamera = renderer.vr.enabled ? renderer.vr.getCamera(camera).cameras[0] : camera;
vrCamera.matrixWorld.decompose(localVector, localQuaternion, localVector2);
localSkinMesh.update(vrCamera, controllerMeshes); */
if (serverConnectedUrl) {
const vrCamera = renderer.vr.enabled ? renderer.vr.getCamera(camera).cameras[0] : camera;
vrCamera.matrixWorld.decompose(xrmp.localPlayerMesh.hmd.position, xrmp.localPlayerMesh.hmd.quaternion, localVector2);
for (let i = 0; i < 2; i++) {
const controllerMesh = controllerMeshes[i];
xrmp.localPlayerMesh.gamepads[i].visible = true;
xrmp.localPlayerMesh.gamepads[i].position.copy(controllerMesh.position);
xrmp.localPlayerMesh.gamepads[i].quaternion.copy(controllerMesh.quaternion);
}
xrmp.pushUpdate();
const playerMeshes = xrmp.getRemotePlayerMeshes();
for (let i = 0; i < playerMeshes.length; i++) {
playerMeshes[i].animate();
}
}
};
_updateMp();
lastTime = now;
}
// bootstrap
const _bootFakeDisplay = async () => {
fakeDisplay = _makeFakeDisplay();
fakeDisplay.position.set(0, 1.6, 0);
fakeDisplay.pushUpdate();
await fakeDisplay.enter({
renderer,
animate,
layers,
});
display = fakeDisplay;
const tabUrl = (() => {
const {t = 'examples/tutorial.html'} = query;
if (!/^[a-z]+:\/\//.test(t)) {
return `../${t}`;
} else {
return t;
}
})();
_openUrl(tabUrl);
focusedTab = rig.menuMesh.urlMesh;
rig.menuMesh.urlMesh.updateText();
rig.menuMesh.listMesh.updateList();
console.log('loaded root in 2D');
};
(async () => {
if (!query.fake) {
if (navigator.xr) { // WebXR
display = await navigator.xr.requestDevice();
if (display) {
const session = await display.requestSession({
exclusive: true,
});
display.session = session;
session.layers = layers;
// console.log('request first frame');
session.requestAnimationFrame((timestamp, frame) => {
renderer.vr.setSession(session, {
frameOfReferenceType: 'stage',
});
const viewport = session.baseLayer.getViewport(frame.views[0]);
// const width = viewport.width;
const height = viewport.height;
const fullWidth = (() => {
let result = 0;
for (let i = 0; i < frame.views.length; i++) {
result += session.baseLayer.getViewport(frame.views[i]).width;
}
return result;
})();
renderer.setSize(fullWidth, height);
renderer.setAnimationLoop(null);
renderer.vr.enabled = true;
renderer.vr.setDevice(display);
renderer.vr.setAnimationLoop(animate);
if (window.browser && window.browser.magicleap) {
window.browser.magicleap.RequestDepthPopulation(true);
// renderer.autoClearDepth = false;
}
_openUrl('tutorial.html');
_updateRigLists();
console.log('loaded root in XR');
});
} else {
console.log('no xr displays');
await _bootFakeDisplay();
}
} else { // WebVR
console.log('request device');
const displays = await navigator.getVRDisplays();
display = displays[0];
if (display) {
console.log('request present');
await display.requestPresent([
{
source: renderer.domElement,
}
]);
console.log('entered vr');
display.layers = layers;
const {renderWidth: width, renderHeight: height} = display.getEyeParameters('left');
renderer.setSize(width * 2, height);
renderer.setAnimationLoop(null);
renderer.vr.enabled = true;
renderer.vr.setDevice(display);
renderer.vr.setAnimationLoop(animate);
if (window.browser && window.browser.magicleap) {
window.browser.magicleap.RequestDepthPopulation(true);
// renderer.autoClearDepth = false;
}
_openUrl('tutorial.html');
_updateRigLists();
console.log('loaded root in XR');
} else {
console.log('no vr displays');
await _bootFakeDisplay();
}
}
} else {
await _bootFakeDisplay();
}
})()
.catch(err => {
console.warn(err.stack);
});
// renderer.setAnimationLoop(animate);
// _openRig(new THREE.Vector3(0, 1.5, -1), new THREE.Quaternion());
</script>
</body>
</html>
You can’t perform that action at this time.