I’m designing a webapp that is supposed to be an AR environment on your phone, to be viewed with something like Google Cardboard, but I am having an issue that the segmentPointer object that is meant to appear when clicking on an object is not.
I’ve checked the geometry displays correctly in a sandbox, and when I change it to a 3d object rather than shapeGeometry it does display, but I cannot figure out why it is not displaying how I want it to.
The project is at https://voxelverse.jackgreenearth.org/, and the code is quite long, but it is here to read in its totality below as it might need the whole context to discover the error. I’ve tried myself looking through the code, and I’ve tried searching the web and asking LLMs, but I couldn’t figure it out, so please help me, fellow humans.
Tap for code
"use strict";
import \* as THREE from 'three';
import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
\
const loader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
const manager = THREE.DefaultLoadingManager;
\
// Basic functions
\
function ls(id) {
return(localStorage.getItem(id));
};
\
function setLs(id, val) {
localStorage.setItem(id, val);
};
\
function byId(id) {
return(document.getElementById(id));
};
\
function bySel(sel) {
return(document.querySelector(sel));
};
\
function byClass(id) {
return(document.getElementsByClassName(id));
};
\
function toTitleCase(str) {
return str.replace(
/\w\S\*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
};
\
function randInt(max) {
return Math.floor(Math.random() \* (max));
};
\
function getRandomFloat(min, max, decimals) {
return(parseFloat((Math.random() \* (max - min) + min).toFixed(decimals)));
};
\
function confine(value, min, max) {
if(value < min) {
return(min);
} else if(value > max) {
return(max);
} else {
return(value);
};
};
\
function wrap(value, min, max) {
const range = max - min;
\
if(value < min) {
return(wrap(value + range, min, max));
} else if(value > max) {
return(wrap(value - range, min, max));
} else {
return(value);
};
};
\
function removeFromArray(array, forDeletion) {
return(array.filter(item => !forDeletion.includes(item)));
};
\
function radToDeg(radians) {
return radians \* (180 / PI);
}
\
function range(start, stop, step = 1) {
if (stop === undefined) {
stop = start;
start = 0
}
return Array.from({ length: (stop - start) / step }, (\_, i) => start + (i \* step));
}
\
function between(variable, min, max, inclusive='min') {
switch(inclusive) {
case 'none':
return((variable > min) && (variable < max));
break;
case 'both':
return((variable >= min) && (variable <= max));
break;
case 'min':
return((variable >= min) && (variable < max));
break;
case 'max':
return((variable > min) && (variable <= max));
break;
}
}
\
function download(data, filename, type) {
var file = new Blob(\[data], {type: type});
if (window\.navigator.msSaveOrOpenBlob) // IE10+
window\.navigator.msSaveOrOpenBlob(file, filename);
else { // Others
var a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window\.URL.revokeObjectURL(url);
}, 0);
};
};
\
function log(text) {
console.log(text);
};
\
function distance2d(x1, y1, x2, y2) {
return(Math.sqrt(
(Math.abs(x1 - x2) \*\* 2) +
(Math.abs(y1 - y2) \*\* 2)
));
};
\
function distance3d(p1 = new THREE.Vector3(0, 0, 0), p2 = new THREE.Vector3(0, 0, 0)) {
return(Math.sqrt((distance2d(p1.x, p1.y, p2.x, p2.y) \*\* 2) + (Math.abs(p1.z - p2.z) \*\* 2)));
};
\
let totalElementsToLoad = 0;
let numberOfElementsLoaded = 0;
\
function onAllElementsLoaded() {
\
}
\
function load(path, type, functionOnLoad) {
totalElementsToLoad += 1;
\
if(type == 'html') {
fetch(path)
.then(response => response.text())
.then(html => {
let doc = new DOMParser().parseFromString(html, "text/html");
\
functionOnLoad(doc);
\
// If all elements to load have been loaded, execute the relevant function
numberOfElementsLoaded += 1;
if(numberOfElementsLoaded == totalElementsToLoad) {
onAllElementsLoaded();
}
})
.catch(error => {
console.error(error);
});
} else if(type == 'json') {
fetch(path)
.then(response => response.json()) // parse the response as JSON
.then(json => {
functionOnLoad(json);
\
// If all elements to load have been loaded, execute the relevant function
numberOfElementsLoaded += 1;
if(numberOfElementsLoaded == totalElementsToLoad) {
onAllElementsLoaded();
}
})
.catch(error => {
console.error(error);
});
}
}
\
// Setup
\
const PI = 3.1415926535897932384626433832795028841971;
\
// Objects
\
let orientation = {
'absolute': false,
'alpha': 0,
'beta': 0,
'gamma': 0
}
\
// vars
const fps = 60;
\
let keysDown = \[];
let pointerPosition = {'x': 0, 'y': 0, 'positions': \[{'clientX': 0, 'clientY': 0}], 'type': 'mouse'};
\
// Camera
let cameraRotation = new THREE.Euler(0, 0, 0, 'YXZ');
let cameraTargetRotation = {'x': 0, 'y': 0, 'z': 0};
const cameraRotationSensitivity = 0.002;
\
// Other variables
let logicInterval;
\
// Load default settings
let defaultSettings;
\
load("/assets/json/default-settings.json", 'json', function(defset) {
defaultSettings = defset;
\
// Create custom settings
if(!Object.keys(localStorage).includes('settings')) {
setLs('settings', JSON.stringify({}));
};
\
onSettingsLoad();
});
\
function settingURL(url, addValue=true) {
return('children/' + url.split('/').join('/children/') + (addValue ? '/value' : ''));
}
\
function customiseSetting(url, value) {
url = settingURL(url).split('/');
\
let newSettings;
\
function recursiveSet(object, list, index, setTo) {
// If the current component is the last one, assign the value
if(index == list.length - 1) {
object\[list\[index]] = setTo;
return(object);
} else {
// Check if it already contains the value
if(object.hasOwnProperty(list\[index])) {
object\[list\[index]] = recursiveSet(object\[list\[index]], list, index + 1, setTo);
} else {
object\[list\[index]] = recursiveSet({}, list, index + 1, setTo);
}
return(object);
}
};
\
newSettings = recursiveSet(JSON.parse(ls('settings')), url, 0, value);
\
setLs('settings', JSON.stringify(newSettings));
}
\
function getSetting(url, addValue) {
url = settingURL(url, addValue).split('/');
\
function recursiveGet(object, list, index) {
// If the current component is the last one, return the value
if (index == list.length - 1) {
return object\[list\[index]];
} else {
// Check if it contains the value
if (object.hasOwnProperty(list\[index])) {
return recursiveGet(object\[list\[index]], list, index + 1);
} else {
return null; // No such setting
}
}
}
\
// Try to find it in local settings first, otherwise get it from defaultSettings
const localGet = recursiveGet(JSON.parse(ls('settings')), url, 0);
if(localGet == null) {
return(recursiveGet(defaultSettings, url, 0));
} else {
return(localGet);
}
}
\
// First, lets define some functions
// Rendering functions
\
// Thanks, https\://discourse.threejs.org/t/roundedrectangle-squircle/28645!
function roundRectangleGeometry(w, h, r, s) { // width, height, radius corner, smoothness
// helper const's
const wi = w / 2 - r; // inner width
const hi = h / 2 - r; // inner height
const w2 = w / 2; // half width
const h2 = h / 2; // half height
const ul = r / w; // u left
const ur = ( w - r ) / w; // u right
const vl = r / h; // v low
const vh = ( h - r ) / h; // v high
let positions = \[
-wi, -h2, 0, wi, -h2, 0, wi, h2, 0,
-wi, -h2, 0, wi, h2, 0, -wi, h2, 0,
-w2, -hi, 0, -wi, -hi, 0, -wi, hi, 0,
-w2, -hi, 0, -wi, hi, 0, -w2, hi, 0,
wi, -hi, 0, w2, -hi, 0, w2, hi, 0,
wi, -hi, 0, w2, hi, 0, wi, hi, 0
];
let uvs = \[
ul, 0, ur, 0, ur, 1,
ul, 0, ur, 1, ul, 1,
0, vl, ul, vl, ul, vh,
0, vl, ul, vh, 0, vh,
ur, vl, 1, vl, 1, vh,
ur, vl, 1, vh, ur, vh
];
let phia = 0;
let phib, xc, yc, uc, vc, cosa, sina, cosb, sinb;
for (let i = 0; i < s \* 4; i ++) {
phib = Math.PI \* 2 \* ( i + 1 ) / ( 4 \* s );
cosa = Math.cos( phia );
sina = Math.sin( phia );
cosb = Math.cos( phib );
sinb = Math.sin( phib );
xc = i < s || i >= 3 \* s ? wi : - wi;
yc = i < 2 \* s ? hi : -hi;
positions.push( xc, yc, 0, xc + r \* cosa, yc + r \* sina, 0, xc + r \* cosb, yc + r \* sinb, 0 );
uc = i < s || i >= 3 \* s ? ur : ul;
vc = i < 2 \* s ? vh : vl;
uvs.push( uc, vc, uc + ul \* cosa, vc + vl \* sina, uc + ul \* cosb, vc + vl \* sinb );
phia = phib;
}
const geometry = new THREE.BufferGeometry( );
geometry.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array( positions ), 3 ) );
geometry.setAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( uvs ), 2 ) );
return geometry;
}
\
// Render
function render() {
requestAnimationFrame(render);
leftRenderer.render(scene, leftCamera);
rightRenderer.render(scene, rightCamera);
\
framesSoFar++;
};
\
// Functions
function setCameraRotation() {
// Calculate drag
cameraRotation.x = Number((cameraRotation.x + ((cameraTargetRotation.x - cameraRotation.x) / getSetting('Input/Mouse/Camera Rotation Drag'))).toFixed(5));
cameraRotation.y = Number((cameraRotation.y + ((cameraTargetRotation.y - cameraRotation.y) / getSetting('Input/Mouse/Camera Rotation Drag'))).toFixed(5));
cameraRotation.z = Number((cameraRotation.z + ((cameraTargetRotation.z - cameraRotation.z) / getSetting('Input/Mouse/Camera Rotation Drag'))).toFixed(5));
// Update cameras
for(let camera of \[leftCamera, rightCamera]) {
camera.rotation.set(cameraRotation.x, cameraRotation.y, cameraRotation.z, 'YXZ');
}
\
const eyeGap = getSetting('Quick Settings/Eye Gap');
\
// Set camera positions
leftCamera.position.x = -1 \* eyeGap \* Math.sin(cameraRotation.y);
leftCamera.position.z = -1 \* eyeGap \* Math.cos(cameraRotation.y);
rightCamera.position.x = eyeGap \* Math.sin(cameraRotation.y);
rightCamera.position.z = eyeGap \* Math.cos(cameraRotation.y);
\
byId('camera-target-rot-x').innerHTML = cameraTargetRotation.x.toFixed(2);
byId('camera-target-rot-y').innerHTML = cameraTargetRotation.y.toFixed(2);
byId('camera-target-rot-z').innerHTML = cameraTargetRotation.z.toFixed(2);
byId('camera-rot-x').innerHTML = cameraRotation.x.toFixed(2);
byId('camera-rot-y').innerHTML = cameraRotation.y.toFixed(2);
byId('camera-rot-z').innerHTML = cameraRotation.z.toFixed(2);
\
byId('camera-left-rot-x').innerHTML = leftCamera.rotation.x.toFixed(2);
byId('camera-left-rot-y').innerHTML = leftCamera.rotation.y.toFixed(2);
byId('camera-left-rot-z').innerHTML = leftCamera.rotation.z.toFixed(2);
}
\
function takeScreenshot() {
downloadCanvasImage(document.getElementById('game-canvas'), gameName + ' screenshot');
sendAlert('Screenshot Taken!', 'tick');
};
\
function takePanorama() {
const canvas = document.getElementById('game-canvas');
const height = canvas.height;
const width = canvas.width \* (360 / (camera.fov \* camera.aspect));
let newCanvas = document.createElement('canvas');
newCanvas.height = height;
newCanvas.width = width;
newCanvas.style.display = 'none';
let context = newCanvas.getContext("2d");
document.body.appendChild(newCanvas);
for(let x = 0; x < width; x++) {
// Rotate
cameraRotation.y += ((2 \* PI) / width);
let calculatedRotation = rotationToAbsolute(playerPosition, cameraRotation);
camera.rotation.set(calculatedRotation.x, calculatedRotation.y, calculatedRotation.z, 'YXZ');
renderer.render(scene, camera);
const gl = renderer.getContext();
// Get canvas data
const pixelData = new Uint8ClampedArray(1 \* height \* 4);
const reversedPixelData = new Uint8ClampedArray(1 \* height \* 4);
gl.readPixels((canvas.width / 2), 0, 1, height, gl.RGBA, gl.UNSIGNED\_BYTE, pixelData);
for (let i = 0; i < height; i++) {
for (let j = 0; j < 4; j++) {
reversedPixelData\[i\*4 + j] = pixelData\[(height - i - 1)\*4 + j];
};
};
const imageData = new ImageData(reversedPixelData, 1, height);
context.putImageData(imageData, x, 0);
};
downloadCanvasImage(newCanvas, gameName + ' panorama');
newCanvas.remove();
sendAlert('Panoramic screenshot taken!', 'tick');
};
\
function setRotation(object, rotation) {
object.rotation.set(rotation.x, rotation.y, rotation.z);
};
\
function downloadCanvasImage(canvas, name) {
let canvasImage = canvas.toDataURL('image/png');
// this can be used to download any image from webpage to local disk
let xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.onload = function () {
let a = document.createElement('a');
a.href = window\.URL.createObjectURL(xhr.response);
a.download = name;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
};
xhr.open('GET', canvasImage); // This is to download the canvas image
xhr.send();
};
\
function xyToRealPosRot(x, y, distance) {
let realX, realY, realZ, rotX, rotY, rotZ;
\
// Position is an object {x: x, y: y} x determines which face it will be on horizontally, and y determines if it will be on the top or the bottom
// Beyond 400, x position wraps
x = wrap(x, 0, 400);
log('x before: ' + x)
const horizontalFace = (x / 100) % 4;
//rotY = (x / 400) \* (1) // horizontalFace);
\
// The top of the screen is y 100, the bottom is y -100, and the horizontals are between -50 and 50
realY = confine(y, -100, 100);
\
// Calculate real position
const unit = getSetting('Display/UI/Distance') / 50;
\
let forward = getSetting('Display/UI/Distance');
\
const bevel = getSetting('Display/UI/Bevel');
\
rotX = 0;
\
// If it is horizontal...
if(between(y, -50 + bevel, 50 - bevel)) {
realY = y;
rotX = 0;
} else if(y < -50 - bevel) {
// If it is on the lower face
realY = -50;
forward = (y + 100) \* unit;
rotX = -(PI / 2);
} else if(y >= 50 + bevel) {
// If it is on the upper face
realY = 50;
forward = (y - 100) \* unit;
//side = unit \* (((x - 50) % 100) + 50);
rotX = (PI / 2);
} else if(between(y, -50 - bevel, -50 + bevel)) {
// If it is on the lower bevel
realY = -50 - ((y + 50) / 2);
rotX = (PI / 4);
} else if(between(y, 50 - bevel, 50 + bevel)) {
// If it is on the upper bevel
realY = 50 + ((y - 50) / 2) ;
rotX = -(PI / 4);
}
\
realY = realY \* unit;
\
let flip = false;
\
/\*if(
(horizontalFace >= 0 && horizontalFace < 0.5) ||
(horizontalFace >= 1.5 && horizontalFace < 2.5) ||
(horizontalFace >= 3.5 && horizontalFace < 4)
) {
flip = true;
}\*/
\
let angle = (x / 400) \* (PI \* 2);
realX = Math.sin(angle) \* forward;
realZ = Math.cos(angle) \* forward;
rotY = angle;
log('rot y: ' + rotY)
\
log({
'x': realX,
'y': realY,
'forward': forward,
})
\
// Take distance into account
realX \*= distance;
realY \*= distance;
realZ \*= distance;
\
return({
'position': new THREE.Vector3(realX, realY, realZ),
'rotation': new THREE.Euler(rotX, rotY, rotZ, 'YXZ'),
'flip': flip
});
}
\
function addWidget({
name = '',
position = {'x': 0, 'y': 0},
rotation = {'x': 0, 'y': 0, 'z': 0},
distance = 1,
size = {'x': 10, 'y': 10},
radius = 3,
shape = 'rRect',
background = '#000000',
opacity,
textStyle = {
'align': 'center',
'weight': 0, // Range is 0 to 10
'font': 'DINRoundPro,arial,sans-serif',
'color': '#b0b0b0',
'vertical-align': 'center',
'font-size': 1 // Uses the same sizing system as the rest of the UI, so one unit of text is also one unit of object
},
textContent = '',
onclick = function() {},
onlongpress = function() {},
onhover = function() {},
onhoverexit = function() {},
ontruehover = function() {}
}) {
const realPosRot = xyToRealPosRot(position.x, position.y, distance);
log(realPosRot)
const realPos = realPosRot.position;
let realRot = realPosRot.rotation;
\
realRot.x += rotation.x;
realRot.y += rotation.y;
realRot.z = rotation.z;
\
// Calculate real size
const unit = getSetting('Display/UI/Distance') / 100;
\
let width = unit \* size.x;
let height = unit \* size.y;
radius \*= unit;
const scale = getSetting('Display/UI/Scale/General');
width \*= scale;
height \*= scale;
radius \*= scale;
\
// Set mesh geometry
let geometry;
switch(shape) {
case 'rRect':
geometry = roundRectangleGeometry(width, height, radius, 10);
break;
case 'rect':
geometry = new THREE.PlaneGeometry(width, height);
break;
case 'circle':
geometry = new THREE.CircleGeometry((width + height) / 2, 32);
break;
}
let material;
\
if(opacity == undefined) {
opacity = 1;
}
\
if(textContent == '') {
if(background\[0] == '/') {
loadTexture(background, function(texture) {
material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
opacity: opacity,
transparent: true
});
onTextureLoad(material);
})
} else {
material = new THREE.MeshBasicMaterial({
color: background,
side: THREE.DoubleSide,
opacity: opacity,
transparent: true
});
onTextureLoad(material);
}
} else {
function prepareText(canvas) {
// Proceed to prepare the canvas with the text
ctx.font = \`${textStyle\["font-size"]}em ${textStyle\["font"]}\`;
ctx.textAlign = textStyle\["align"];
ctx.fillStyle = textStyle\["color"];
ctx.fillText(textContent, 0, 0);
// Compose the text onto the background
const composedTexture = new THREE.CanvasTexture(canvas);
\
// Generate the material
material = new THREE.MeshBasicMaterial({
map: composedTexture,
side: THREE.DoubleSide,
transparent: true,
alphaTest: 0.5
});
\
onTextureLoad(material);
}
\
// Initialize tmpcanvas only when needed
const tmpcanvas = document.createElement('canvas');
tmpcanvas.width = width;
tmpcanvas.height = height;
const ctx = tmpcanvas.getContext('2d');
\
\
// Fill the background first
if (background\[0] == '/') {
loadTexture(background, function(texture) {
ctx.fillStyle = texture;
ctx.fillRect(0, 0, width, height);
\
prepareText(tmpcanvas);
})
} else {
ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);
\
prepareText(tmpcanvas);
}
}
function onTextureLoad(material) {
// Create a mesh with the geometry and the material
let mesh = new THREE.Mesh(geometry, material);
\
mesh.name = name;
\
mesh.position.set(realPos.x, realPos.y, realPos.z );
mesh.rotation.set(realRot.x, realRot.y, realRot.z);
\
if(realPosRot.flip) {
mesh.scale.x = -1;
}
\
mesh.onclick = onclick;
mesh.onlongpress = onlongpress;
mesh.onhoverexit = onhoverexit;
mesh.ontruehover = ontruehover;
mesh.onchover = onhover;
\
scene.add(mesh);
};
}
\
function transitionWidget(name, property, newProperty, time, condition) {
if(condition != null) {
}
}
\
// three.js Scene setup
const scene = new THREE.Scene();
\
// three functions
\
function loadTexture(path, onload) {
textureLoader.load(path, function (texture) {
onload(texture);
}, undefined, function (error) {
console.error(error);
});
};
\
// Define objects to make them global, they will mostly be only added to the scene when settings are loaded
let sun;
let wallpaper;
let cameraStream;
let pointer;
\
let pointerMaterial = new THREE.MeshBasicMaterial({
color: "hsl(0, 100%, 50%)",
side: THREE.DoubleSide
});
\
let segmentShape = new THREE.Shape();
let segmentGeometry = new THREE.ShapeGeometry(segmentShape);
let segmentPointer = new THREE.Mesh(segmentGeometry, pointerMaterial);
segmentPointer.name = 'segmentPointer';
\
function setSegmentPointer(angle = 0, radius = 0.1, rotation = new THREE.Euler(0, 0, 0), clockwise=true) {
let oldGeometry = segmentPointer.geometry;
\
let segmentShape = new THREE.Shape();
segmentShape.moveTo(0, 0);
segmentShape.arc(0, 0, radius, 0, angle, clockwise);
segmentShape.lineTo(0, 0);
\
let extrudeSettings = {
steps: 1,
depth: 0.1,
bevelEnabled: false
};
\
let segmentGeometry = new THREE.ExtrudeGeometry(segmentShape, extrudeSettings);
segmentPointer.geometry = segmentGeometry;
\
oldGeometry.dispose();
\
segmentPointer.rotation.set(rotation);
}
\
// Camera stuff
let cameraViewDistance;
\
// Setup cameras
let leftCamera;
let rightCamera;
let leftRenderer;
let rightRenderer;
\
function setRendererSize() {
for(let renderer of \[leftRenderer, rightRenderer]) {
let canvas = renderer.domElement;
renderer.setSize(
canvas.offsetWidth \* getSetting('Display/Anti-alias'),
canvas.offsetHeight \* getSetting('Display/Anti-alias'),
false
);
}
}
\
function updateCameraAspectRatio() {-0.2
for(let camera of \[leftCamera, rightCamera]) {
let canvas = leftRenderer.domElement;
camera.aspect = canvas.offsetWidth / canvas.offsetHeight;
camera.updateProjectionMatrix();
}
}
\
// temp
function startAssistant() {
log('assisstant')
}
\
// When settings are loaded, start settings stuff up
function onSettingsLoad() {
// Add sun
sun = new THREE.PointLight(0xffffff, getSetting('Quick Settings/Brightness'));
scene.add(sun);
\
// Pointers
pointer = new THREE.Mesh(new THREE.IcosahedronGeometry(getSetting('Display/UI/Pointer/Size'), 1), pointerMaterial);
pointer.name = 'pointer';
scene.add(pointer);
\
pointerMaterial = new THREE.MeshBasicMaterial(
{color: "hsl(" + (getSetting('Quick Settings/Theme Hue') + getSetting('Display/UI/Pointer/Hue Shift')) + ", 100%, 50%)"}
);
pointer.material = pointerMaterial;
segmentPointer.material = pointerMaterial;
\
// Add wallpaper
let wallpaperURL;
if(getSetting('Display/UI/Wallpaper/Use Direct Link') == true) {
wallpaperURL = getSetting('Display/UI/Wallpaper/Direct Link');
} else {
wallpaperURL = getSetting('Display/UI/Wallpaper/Wallpapers Folder') + '/' + getSetting('Display/UI/Wallpaper/Wallpaper');
}
\
loadTexture(wallpaperURL, function(texture) {
let material = new THREE.MeshStandardMaterial({
map: texture,
side: THREE.BackSide,
transparent: true,
opacity: getSetting('Display/UI/Wallpaper/Opacity')
});
\
wallpaper = new THREE.Mesh(
new THREE.IcosahedronGeometry(
getSetting('Display/UI/Distance') \* getSetting('Display/UI/Wallpaper/Distance') \* getSetting('Display/UI/Testing Size Multiplier'), 4),
material
);
wallpaper.scale.x = -1;
wallpaper.name = "wallpaper";
scene.add(wallpaper);
});
\
// Setup cameras
cameraViewDistance = getSetting('Display/UI/Distance') \* getSetting('Display/UI/Testing Size Multiplier') \* 2; // Keep this down to destroy lag
leftCamera = new THREE.PerspectiveCamera(80, 1, 0.001, cameraViewDistance);
rightCamera = new THREE.PerspectiveCamera(80, 1, 0.001, cameraViewDistance);
\
// Setup renderers
leftRenderer = new THREE.WebGLRenderer({canvas: byId('left-canvas'), antialias: true, logarithmicDepthBuffer: true, preserveDrawingBuffer: true, alpha: true});
rightRenderer = new THREE.WebGLRenderer({canvas: byId('right-canvas'), antialias: true, logarithmicDepthBuffer: true, preserveDrawingBuffer: true, alpha: true});
\
updateCameraAspectRatio();
setRendererSize();
\
window\.addEventListener('resize', function() {
updateCameraAspectRatio();
setRendererSize();
});
\
// Setup control center
const baseY = getSetting('Display/Control Center/Switch To Bottom') ? -100 : 100;
const backgroundFolder = getSetting('Display/Control Center/Icon Folder');
function createTileGrid(scale, x, y, farness, tiles, name) {
let counter = 0;
log(tiles)
addWidget({
position: {'x': x, 'y': baseY + y},
size: {'x': 3 \* scale, 'y': 3 \* scale},
background: "hsl(" + getSetting('Quick Settings/Theme Hue') + ", 50%, 80%)",
name: name + ' background',
radius: 0.5 \* scale,
onhover: openTileGroup(name)
});
for(let widgetY = 1; widgetY >= -1; widgetY--) {
for(let widgetX = -1; widgetX <=1; widgetX++) {
if(counter < tiles.length) {
if(tiles\[counter].folder) {
createTileGrid(scale / 3, (widgetX \* scale), (widgetY \* scale), farness \* 0.99, tiles\[counter].children);
} else {
log('scale' + scale)
addWidget({
position: {'x': x + (widgetX \* scale), 'y': baseY + y + (widgetY \* scale)},
size: {'x': scale, 'y': scale},
background: backgroundFolder + '/' + tiles\[counter].icon + '.svg',
name: tiles\[counter].name,
group: name,
radius: scale / 3,
distance: farness \* 0.99
});
log('added widget control center')
};
} else {
break;
};
counter++;
};
if(counter >= tiles.length) {
break;
};
};
};
\
createTileGrid(
getSetting('Display/UI/Scale/Control Center') \* 1.5,
0,
baseY,
1,
getSetting('Display/Control Center/Tiles'),
getSetting('Display/Control Center/Tiles', false)\['name']
);
// Quick function
let quickFunction = getSetting('Display/Control Center/Quick Function', false);
\
addWidget({
position: {'x': 0, 'y': getSetting('Display/Control Center/Switch To Bottom') ? 100 : -100},
background: '/assets/images/icons/control\_center/' + quickFunction.icon + '.svg',
name: quickFunction.name,
onclick: quickFunction.onclick
});
\
// testing
addWidget({
position: {'x': 0, 'y': -50},
background: '/assets/images/icons/control\_center/torch.svg',
name: "torch"
});
addWidget({
position: {'x': 200, 'y': 10},
background: '/assets/images/icons/control\_center/screencast.svg',
name: "screencast"
});
for(let i of range(16)) {
addWidget({
position: {'x': i \* 25, 'y': 0},
background: 'hsl(' + getSetting('Quick Settings/Theme Hue') + ', 100%, ' + ((i / 16) \* 100) + '%)',
name: "test" + i,
textContent: '',//i.toString()
'onclick': function() {
log('click' + i);
},
'onhover': function() {
log('hover' + i);
}
});
}
};
\
function updateSetting(url, value) {
customiseSetting(url, value);
\
switch(url) {
case 'Display/UI/Wallpaper/Opacity':
wallpaper.material.opacity = value;
break;
};
};
\
// Start
\
// Setup the camera stream
function setupCameraStream() {
function handleSuccess(stream) {
cameraStream = document.createElement('video');
cameraStream.style.transform = 'rotate(270deg)';
cameraStream.srcObject = stream;
cameraStream.play();
\
let texture = new THREE.VideoTexture(cameraStream);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
scene.background = texture;
\
customiseSetting('Display/UI/Wallpaper/Opacity', 0); // Temporary until GUI settings are introduced
}
\
function handleError(error) {
// Set wallpaper opacity to 1
updateSetting('Display/UI/Wallpaper/Opacity', 1);
\
log('Unable to access the camera/webcam.');
}
\
navigator.mediaDevices.getUserMedia({video: {facingMode: "environment"}})
.then(handleSuccess)
.catch(function(error) {
if (error.name === 'OverconstrainedError') {
// Fallback to default video settings
navigator.mediaDevices.getUserMedia({video: true})
.then(handleSuccess)
.catch(handleError);
} else {
// Handle other errors
handleError(error);
}
});
};
\
// Fullscreen and pointer lock, request fullscreen mode for the element
function openFullscreen(elem, then) {
if (elem.requestFullscreen) {
elem.requestFullscreen().then(then);
} else if (elem.webkitRequestFullscreen) { /\* Safari \*/
elem.webkitRequestFullscreen().then(then);
} else if (elem.msRequestFullscreen) { /\* IE11 \*/
elem.msRequestFullscreen().then(then);
}
}
// Request pointer lock
function requestPointerLock(myTargetElement) {
const promise = myTargetElement.requestPointerLock({
unadjustedMovement: true,
});
\
if (!promise) {
log("disabling mouse acceleration is not supported");
return;
}
\
return promise
.then(() => log("pointer is locked"))
.catch((error) => {
if (error.name === "NotSupportedError") {
// Some platforms may not support unadjusted movement.
// You can request again a regular pointer lock.
return myTargetElement.requestPointerLock();
}
});
}
\
function lockPointer() {
requestPointerLock(byId('body'));
document.addEventListener("pointerlockchange", lockChangeAlert, false);
};
\
function lockChangeAlert() {
if (document.pointerLockElement === byId('body')) {
document.addEventListener("mousemove", updatePosition, false);
} else {
document.removeEventListener("mousemove", updatePosition, false);
}
}
\
function updatePosition(e) {
onLockedMouseMove(e.movementX, e.movementY);
};
\
function fullscreenAndPointerLock() {
openFullscreen(byId('body'), function() {
lockPointer();
});
}
\
function permission() {
// Check if the device supports deviceorientation and requestPermission
if (typeof(DeviceMotionEvent) !== "undefined" && typeof(DeviceMotionEvent.requestPermission) === "function") {
// Request permission
DeviceMotionEvent.requestPermission()
.then(response => {
// Check the response
if (response == "granted") {};
})
.catch(console.error); // Handle errors
} else {
// Device does not support deviceorientation
log("DeviceOrientationEvent is not defined");
}
}
\
async function keepScreenAwake() {
// Create a reference for the Wake Lock.
let wakeLock = null;
\
// create an async function to request a wake lock
try {
wakeLock = await navigator.wakeLock.request("screen");
log("Wake Lock is active!");
} catch (err) {
// The Wake Lock request has failed - usually system related, such as battery.
log(\`${err.name}, ${err.message}\`);
}
}
\
let framesSoFar = 0;
\
function startFPSCount() {
byId('logic-fps').innerHTML = fps.toString();
\
let renderFPSInterval = setInterval(function() {
byId('render-fps').innerHTML = framesSoFar.toString();
framesSoFar = 0;
}, 1000);
}
\
function start() {
byId('loading-screen').style.display = 'none';
setupCameraStream();
startLogic();
startFPSCount();
render();
permission();
fullscreenAndPointerLock();
keepScreenAwake();
\
byId('left-canvas').addEventListener('click', fullscreenAndPointerLock);
byId('right-canvas').addEventListener('click', fullscreenAndPointerLock);
};
\
// Loading
byId('loading-bar').style.display = 'block';
\
manager.onProgress = function (url, itemsLoaded, itemsTotal) {
//log('Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.');
\
byId('loading-bar-fg').style.setProperty('--size', ((itemsLoaded / itemsTotal) \* 100) + '%');
};
\
manager.onError = function (url) {
log('There was an error loading ' + url);
};
\
manager.onLoad = function ( ) {
setTimeout(function() {
byId('loading-bar').style.display = 'none';
byId('play').style.display = 'block';
}, 500);
byId('play').addEventListener('click', start);
};
\
function openTileGroup(group) {
}
\
// Logic
let pointerRaycast = new THREE.Raycaster();
let previousIntersection = pointerRaycast.intersectObjects(scene.children);
let clicking = false;
\
function logic() {
// Set camera rotation
if(cameraTargetRotation.x != cameraRotation.x || cameraTargetRotation.y != cameraRotation.y) {
setCameraRotation();
};
\
// Update pointer
pointerRaycast.set(
new THREE.Vector3(0, 0, 0),
leftCamera.getWorldDirection(new THREE.Vector3())
);
\
// Check if the pointer is itersecting with any object
\
const intersections = pointerRaycast.intersectObjects(scene.children);
if (intersections.length > 0) {
for(let intersection of intersections) {
// If it's intersecting with itself, don't do anything
if(intersection.object.name == 'pointer' || intersection.object.name == 'segmentPointer') {
return;
} else {
// If it's intersecting with an object, copy that intersection's position, and start to click
const point = intersections\[0].point;
pointer.position.copy(point);
\
// Truehover
if(Object.keys(intersection.object).includes('ontruehover')) {
// Prevent hover being continuously trigggered
if(previousIntersection.uuid != intersections\[0].uuid) {
intersection.object.ontruehover();
}
}
log('truehover')
\
// Start click after grace period, if object is clickable
if(
Object.keys(intersection.object).includes('onclick') ||
Object.keys(intersection.object).includes('onhover') ||
Object.keys(intersection.object).includes('onhoverexit') ||
Object.keys(intersection.object).includes('onlongpress')
) {
let gracePeriod = setTimeout(function() {
// onhover
if(Object.keys(intersection.object).includes('onhover')) {
intersection.object.onhover();
}
log('hover')
\
// Start click
if(Object.keys(intersection.object).includes('onclick') && (!clicking)) {
clicking = true;
\
let fullness = 0;
\
// Manage pointers
scene.add(segmentPointer);
segmentPointer.position.copy(pointer);
scene.remove(pointer);
let startClick = setInterval(function() {
fullness += (PI \* 2) / (fps \* getSetting('Input/Eye Click/Duration/Pre-click duration'));
setSegmentPointer(
fullness,
getSetting('Display/UI/Pointer/Size') \* getSetting('Display/UI/Pointer/Clicking Size'),
intersection.object.rotation
);
\
byId('pointer-angle').innerHTML = fullness.toFixed(4);
\
// On forfeit
let forfeitDistance = distance3d(point, pointerRaycast.intersectObjects(scene.children)\[0].point);
if(forfeitDistance > getSetting('Input/Eye Click/Movement limit')) {
log('forfeit ' + forfeitDistance)
clearInterval(startClick);
afterClick();
}
\
if(fullness >= PI \* 2) {
log('click')
intersection.object.onclick();
clearInterval(startClick);
\
if(Object.keys(intersection.object).includes('onlongpress')) {
// Start longpress
fullness = 0;
let startLongPress = setInterval(function() {
fullness += (PI \* 2) / (fps \* getSetting('Input/Eye Click/Duration/Long-press duration'));
setSegmentPointer(
fullness,
getSetting('Display/UI/Pointer/Size') \* getSetting('Display/UI/Pointer/Clicking Size'),
intersection.object.rotation,
false
);
byId('pointer-angle').innerHTML = fullness.toFixed(4);
\
let forfeitDistance = distance3d(point, pointerRaycast.intersectObjects(scene.children)\[0].point);
if(forfeitDistance > getSetting('Input/Eye Click/Movement limit')) {
log('forfeitlongpress')
clearInterval(startLongPress);
afterClick();
};
\
// On click
if(fullness >= PI \* 2) {
intersection.object.onlongpress();
log('longpress')
clearInterval(startLongPress);
afterClick();
}
}, 1000 / fps);
} else {
afterClick();
}
}
}, 1000 / fps);
};
}, getSetting('Input/Eye Click/Delayed hover duration') \* 1000)
return;
} else {
afterClick();
}
\
function afterClick() {
// Update previous intersection
previousIntersection = intersections;
previousIntersection.intersection = intersection;
\
// Onhoverexit
if(intersection.object.uuid != previousIntersection.intersection.object.uuid) {
previousIntersection.object.onhoverexit();
}
\
clicking = false;
log('afterclick')
\
// Change back pointers
scene.remove(segmentPointer);
scene.add(pointer);
\
return;
}
}
}
};
};
\
function startLogic() {
logicInterval = setInterval(logic, 1000 / fps);
};
\
function stopLogic() {
clearInterval(logicInterval);
cameraStream.pause();
};
\
// Input
function onLockedMouseMove(xMotion, yMotion) {
cameraTargetRotation.x = confine(cameraTargetRotation.x - (yMotion \* cameraRotationSensitivity), -0.5 \* PI, 0.6 \* PI);
if(wrap(cameraTargetRotation.y - (xMotion \* cameraRotationSensitivity), -PI, PI) != (cameraTargetRotation.y - (xMotion \* cameraRotationSensitivity))) {
cameraRotation.y = wrap(cameraTargetRotation.y - (xMotion \* cameraRotationSensitivity), -PI, PI);
};
cameraTargetRotation.y = wrap(cameraTargetRotation.y - (xMotion \* cameraRotationSensitivity), -PI, PI);
setCameraRotation();
};
\
// Setup buttons
byId('toggle-debug').addEventListener('click', function(event) {
if(byId('debug-menu').style.display == 'block') {
byId('debug-menu').style.display = 'none';
} else {
byId('debug-menu').style.display = 'block';
}
});
\
// Keypress manager
const keysToPreventDefault = \['alt', '/', 'f1', 'f2', 'f3'];
\
function putKeyDown(key) {
if(!keysDown.includes(key.toLowerCase())) {
keysDown.push(key.toLowerCase());
};
};
\
function putKeyUp(key) {
keysDown = removeFromArray(keysDown, \[key.toLowerCase()]);
};
\
document.addEventListener('keydown', function(e) {
if(keysToPreventDefault.includes(e.key.toLowerCase())) {
e.preventDefault();
};
putKeyDown(e.key);
});
\
document.addEventListener('keyup', function(e) {
putKeyUp(e.key);
});
\
// Pointer position
document.addEventListener('mousemove', function(e) {
pointerPosition = {'x': e.clientX, 'y': e.clientY, 'positions': \[{'clientX': e.clientX, 'clientY': e.clientY}], 'type': 'mouse'};
});
\
document.addEventListener('touchmove', function(e) {
pointerPosition = {'x': e.touches\[0].clientX, 'y': e.touches\[0].clientY, 'positions': e.touches, 'type': 'touch'};
});
\
// Gyrometer
window\.addEventListener("deviceorientation", function(event) {
orientation = {
'absolute': event.absolute,
'alpha': event.alpha,
'beta': event.beta,
'gamma': event.gamma
};
\
byId('gyro-absolute').innerHTML = orientation.absolute;
byId('gyro-alpha').innerHTML = orientation.alpha.toFixed(2);
byId('gyro-beta').innerHTML = orientation.beta.toFixed(2);
byId('gyro-gamma').innerHTML = orientation.gamma.toFixed(2);
const theOrientation = (window\.offsetWidth > window\.offsetHeight) ? 'landscape' : 'portrait';
\
// If orientation is logged correctly
if(!Object.values(orientation).includes(null)) {
// Subtract 90deg if in portrait mode
if(theOrientation == 'portrait') {
orientation.alpha = wrap(orientation.alpha + 90, 0, 360);
}
\
// Offset y
const offsetY = 89.5; // I have no idea why this works
\
if(Math.abs(orientation.beta) < 100) {
cameraTargetRotation.x = (-orientation.gamma \* (PI / 180) % 180) - offsetY;
} else {
cameraTargetRotation.x = (orientation.gamma \* (PI / 180) % 180) + offsetY;
}
cameraTargetRotation.y = orientation.alpha \* (PI / 180);
cameraTargetRotation.z = (-orientation.beta \* (PI / 180)) + offsetY;
\
cameraRotation.x = cameraTargetRotation.x;
cameraRotation.y = cameraTargetRotation.y;
cameraRotation.z = cameraTargetRotation.z;
\
if(theOrientation == 'landscape') {
}
\
setCameraRotation();
};
}, true);