Reworked touch & drag controls

This commit is contained in:
FlamedDogo99 2024-06-23 17:53:13 -06:00
parent 4fea1b9d7e
commit 03ad0553b4
2 changed files with 155 additions and 64 deletions

View File

@ -18,13 +18,14 @@ try {
unsafeWindow.console.warn("DANGER: This userscript is using unsafeWindow. Unsafe websites could potentially use this to gain access to data and other content that the browser normally wouldn't allow!") unsafeWindow.console.warn("DANGER: This userscript is using unsafeWindow. Unsafe websites could potentially use this to gain access to data and other content that the browser normally wouldn't allow!")
Object.defineProperty(window, "clientWindow", { Object.defineProperty(window, "clientWindow", {
value: unsafeWindow value: unsafeWindow
}); }); // If this is a userscript, use unsafeWindow
} catch { } catch {
Object.defineProperty(window, "clientWindow", { Object.defineProperty(window, "clientWindow", {
value: window value: window
}); }); // If this is plain javascript, use window
} }
// To-do: remove the mobile check is implement the dynamic enabling and disabling of individual features clientWindow.console.log("Eagler Mobile v3.0.4")
// TODO: remove the mobile check is implement the dynamic enabling and disabling of individual features
function isMobile() { function isMobile() {
try { try {
document.createEvent("TouchEvent"); document.createEvent("TouchEvent");
@ -42,12 +43,22 @@ clientWindow.sprintLock = false; // Used for sprint mobile control
clientWindow.keyboardFix = false; // keyboardFix ? "Standard Keyboard" : "Compatibility Mode" clientWindow.keyboardFix = false; // keyboardFix ? "Standard Keyboard" : "Compatibility Mode"
clientWindow.inputFix = false; // If true, Duplicate Mode clientWindow.inputFix = false; // If true, Duplicate Mode
clientWindow.blockNextInput = false; // Used for Duplicate Mode clientWindow.blockNextInput = false; // Used for Duplicate Mode
clientWindow.hiddenInputFocused = false; clientWindow.hiddenInputFocused = false; // Used for keyboard display on mobile
clientWindow.canvasTouchMode = 0; // Used for canvas touch handling
// Used for changing touchmove events to mousemove events /*
var previousTouchX = null; 0 Idle
var previousTouchY = null; 1 Touch initiated
var startTouchX = null; 2 Primary touch
3 Secondary touch
4 Scroll
5 Finished
*/
clientWindow.canvasTouchStartX = null;
clientWindow.canvasTouchStartY = null;
clientWindow.canvasTouchPreviousX = null;
clientWindow.canvasTouchPreviousY = null;
clientWindow.canvasPrimaryID = null;
clientWindow.buttonTouchStartX = null;
// charCodeAt is designed for unicode characters, and doesn't match the behavior of the keyCodes used by KeyboardEvents, thus necessitating this function // charCodeAt is designed for unicode characters, and doesn't match the behavior of the keyCodes used by KeyboardEvents, thus necessitating this function
String.prototype.toKeyCode = function() { String.prototype.toKeyCode = function() {
@ -61,12 +72,12 @@ Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, fn, ...rest) { value: function (type, fn, ...rest) {
if(type == 'keydown') { // Check if a keydown event is being added if(type == 'keydown') { // Check if a keydown event is being added
_addEventListener.call(this, type, function(...args) { _addEventListener.call(this, type, function(...args) {
if(!args[0].isValid && clientWindow.keyboardFix) { // Inject check for isValid flag and Compatibility Mode if(args[0].isTrusted && clientWindow.keyboardFix) { // When we are in compatibility mode, we ignore all trusted keyboard events
return; // When we are in Compatibility Mode, standard key presses will be ignored return;
} }
return fn.apply(this, args); // Appends the rest of the function specified by addEventListener return fn.apply(this, args); // Appends the rest of the function specified by addEventListener
}, ...rest); }, ...rest);
} else { // If it's not a keydown event, behave like normal } else { // If it's not a keydown event, behave like normal (hopefully)
_addEventListener.call(this, type, fn, ...rest); _addEventListener.call(this, type, fn, ...rest);
} }
} }
@ -91,11 +102,10 @@ function keyEvent(name, state) {
keyCode: charCode, keyCode: charCode,
which: charCode which: charCode
}); });
evt.isValid = true; // Disables fix for bad keyboard input
clientWindow.dispatchEvent(evt); clientWindow.dispatchEvent(evt);
} }
function mouseEvent(number, state, canvas) { function mouseEvent(number, state, canvas, clientX, clientY) {
canvas.dispatchEvent(new PointerEvent(state, {"button": number})) canvas.dispatchEvent(new PointerEvent(state, {"button": number, "buttons": number, "clientX": clientX ?? 0, "clientY" : clientY ?? 0, "screenX": clientX ?? 0, "screenY": clientY ?? 0}))
} }
function wheelEvent(canvas, delta) { function wheelEvent(canvas, delta) {
canvas.dispatchEvent(new WheelEvent("wheel", { canvas.dispatchEvent(new WheelEvent("wheel", {
@ -168,8 +178,8 @@ const _createElement = document.createElement;
document.createElement = function(type, ignore) { document.createElement = function(type, ignore) {
this._createElement = _createElement; this._createElement = _createElement;
var element = this._createElement(type); var element = this._createElement(type);
if(type == "input" && !ignore) { if(type == "input" && !ignore) { // We set the ingore flag to true when we create the hiddenInput
document.querySelectorAll('#fileUpload').forEach(e => e.parentNode.removeChild(e)); document.querySelectorAll('#fileUpload').forEach(e => e.parentNode.removeChild(e)); // Get rid of any left over fileUpload inputs
element.id = "fileUpload"; element.id = "fileUpload";
element.addEventListener('change', function(e) { element.addEventListener('change', function(e) {
element.hidden = true; element.hidden = true;
@ -237,30 +247,100 @@ waitForElm('canvas').then(() => {insertCanvasElements()});
function insertCanvasElements() { function insertCanvasElements() {
// Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu // Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu
var canvas = document.querySelector('canvas'); var canvas = document.querySelector('canvas');
canvas.addEventListener("touchmove", function(e) { canvas.addEventListener("touchstart", function(e) {
e.preventDefault(); if(clientWindow.canvasTouchMode < 2) { // If a touch is initiated but not assigned
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements if(clientWindow.canvasPrimaryID == null) {
clientWindow.canvasTouchMode = 1;
const primaryTouch = e.changedTouches[0];
clientWindow.canvasPrimaryID = primaryTouch.identifier
canvasTouchStartX = primaryTouch.clientX;
canvasTouchStartY = primaryTouch.clientY;
canvasTouchPreviousX = canvasTouchStartX
canvasTouchPreviousY = canvasTouchStartY
if (!previousTouchX) { clientWindow.touchTimer = setTimeout(function(e) {
previousTouchX = touch.pageX; // If our touch is still set to initiaited, set it to secondary touch
previousTouchY = touch.pageY; if(clientWindow.canvasTouchMode == 1) {
clientWindow.canvasTouchMode = 3;
mouseEvent(2, "mousedown", canvas, primaryTouch.clientX, primaryTouch.clientY)
if(clientWindow.fakelock) { // We only dispatch mouseup inGame because we want to be able to click + drag items in GUI's
mouseEvent(2, "mouseup", canvas, primaryTouch.clientX, primaryTouch.clientY)
}
}
}, 300);
} else if(clientWindow.canvasTouchMode == 1 && !clientWindow.fakelock) { // If we already have a primary touch, it means we're using two fingers
clientWindow.canvasTouchMode = 4;
clearTimeout(clientWindow.crouchTimer); // TODO: Find out why this isn't redudnant
}
} }
e.movementX = touch.pageX - previousTouchX;
e.movementY = touch.pageY - previousTouchY;
var evt = clientWindow.fakelock ? new MouseEvent("mousemove", {movementX: e.movementX, movementY: e.movementY}) : new WheelEvent("wheel", {"wheelDeltaY": e.movementY});
canvas.dispatchEvent(evt);
previousTouchX = touch.pageX;
previousTouchY = touch.pageY;
}, false); }, false);
canvas.addEventListener("touchend", function(e) { canvas.addEventListener("touchmove", function(e) {
previousTouchX = null; e.preventDefault() // Prevents window zoom when using two fingers
previousTouchY = null; var primaryTouch;
}, false) for (let touchIndex = 0; touchIndex < e.targetTouches.length; touchIndex++) { // Iterate through our touches to find a touch event matching the primary touch ID
//Updates button visibility on load if(e.targetTouches[touchIndex].identifier == clientWindow.canvasPrimaryID) {
setButtonVisibility(clientWindow.fakelock != null); primaryTouch = e.targetTouches[touchIndex];
// Adds all of the touch screen controls break;
// Theres probably a better way to do this but this works for now }
}
if(primaryTouch) {
primaryTouch.distanceX = primaryTouch.clientX - canvasTouchStartX;
primaryTouch.distanceY = primaryTouch.clientY - canvasTouchStartY;
primaryTouch.squaredNorm = (primaryTouch.distanceX * primaryTouch.distanceX) + (primaryTouch.distanceY * primaryTouch.distanceY);
primaryTouch.movementX = primaryTouch.clientX - canvasTouchPreviousX;
primaryTouch.movementY = primaryTouch.clientY - canvasTouchPreviousY;
if(clientWindow.canvasTouchMode == 1) { // If the primary touch is still only initiated
if (primaryTouch.squaredNorm > 25) { // If our touch becomes a touch + drag
clearTimeout(clientWindow.crouchTimer);
clientWindow.canvasTouchMode = 2;
if(!clientWindow.fakelock) { // When we're inGame, we don't want to be placing blocks when we are moving the camera around
mouseEvent(1, "mousedown", canvas, primaryTouch.clientX, primaryTouch.clientY);
}
}
} else { // If our touch is primary, secondary, scroll or finished
if(clientWindow.canvasTouchMode == 4) { // If our touch is scrolling
wheelEvent(canvas, primaryTouch.movementY)
} else {
canvas.dispatchEvent(new MouseEvent("mousemove", {
"clientX": primaryTouch.clientX,
"clientY": primaryTouch.clientY,
"screenX": primaryTouch.screenX,
"screenY": primaryTouch.screenY, // The top four are used for item position when in GUI's, the bottom two are for moving the camera inGame
"movementX": primaryTouch.movementX,
"movementY": primaryTouch.movementY
}));
}
}
canvasTouchPreviousX = primaryTouch.clientX
canvasTouchPreviousY = primaryTouch.clientY
}
}, false);
function canvasTouchEnd(e) {
for(let touchIndex = 0; touchIndex < e.changedTouches.length; touchIndex++) { // Iterate through changed touches to find primary touch
if(e.changedTouches[touchIndex].identifier == clientWindow.canvasPrimaryID) {
let primaryTouch = e.changedTouches[touchIndex]
// When any of the controlling fingers go away, we want to wait until we aren't receiving any other touch events
if(clientWindow.canvasTouchMode == 2) {
mouseEvent(1, "mouseup", canvas, primaryTouch.clientX, primaryTouch.clientY)
} else if (clientWindow.canvasTouchMode == 3) {
e.preventDefault(); // This prevents some mobile devices from dispatching a mousedown + mouseup event after a touch is ended
mouseEvent(2, "mouseup", canvas, primaryTouch.clientX, primaryTouch.clientY)
}
clientWindow.canvasTouchMode = 5;
}
}
if(e.targetTouches.length == 0) { // We want to wait until all fingers are off the canvas before we reset for the next cycle
clientWindow.canvasTouchMode = 0;
clientWindow.canvasPrimaryID = null;
}
}
canvas.addEventListener("touchend", canvasTouchEnd, false);
canvas.addEventListener("touchcancel", canvasTouchEnd, false); // TODO: Find out why this is different than touchend
setButtonVisibility(clientWindow.fakelock != null); //Updates our mobile controls when the canvas finally loads
// All of the touch buttons
let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div"); let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div");
strafeRightButton.style.cssText = "left:20vh;bottom:20vh;" strafeRightButton.style.cssText = "left:20vh;bottom:20vh;"
document.body.appendChild(strafeRightButton); document.body.appendChild(strafeRightButton);
@ -268,7 +348,7 @@ function insertCanvasElements() {
strafeLeftButton.style.cssText = "left:0vh;bottom:20vh;" strafeLeftButton.style.cssText = "left:0vh;bottom:20vh;"
document.body.appendChild(strafeLeftButton); document.body.appendChild(strafeLeftButton);
let forwardButton = createTouchButton("forwardButton", "inGame", "div"); let forwardButton = createTouchButton("forwardButton", "inGame", "div"); // We use a div here so can use the targetTouches property of touchmove events. If we didn't it would require me to make an actual touch handler and I don't want to
forwardButton.style.cssText = "left:10vh;bottom:20vh;" forwardButton.style.cssText = "left:10vh;bottom:20vh;"
forwardButton.addEventListener("touchstart", function(e){ forwardButton.addEventListener("touchstart", function(e){
keyEvent("w", "keydown"); keyEvent("w", "keydown");
@ -278,12 +358,12 @@ function insertCanvasElements() {
}, false); }, false);
forwardButton.addEventListener("touchmove", function(e) { forwardButton.addEventListener("touchmove", function(e) {
e.preventDefault(); e.preventDefault();
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements const touch = e.targetTouches[0]; // We are just hoping that the user will only ever use one finger on the forward button
if (!startTouchX) { if (!buttonTouchStartX) { // TODO: move this to a touchstart event handler
startTouchX = touch.pageX; buttonTouchStartX = touch.pageX;
} }
let movementX = touch.pageX - startTouchX; let movementX = touch.pageX - buttonTouchStartX;
if((movementX * 10) > clientWindow.innerHeight) { if((movementX * 10) > clientWindow.innerHeight) {
strafeRightButton.classList.add("active"); strafeRightButton.classList.add("active");
strafeLeftButton.classList.remove("active"); strafeLeftButton.classList.remove("active");
@ -302,7 +382,7 @@ function insertCanvasElements() {
} }
}, false); }, false);
forwardButton.addEventListener("touchend", function(e) { forwardButton.addEventListener("touchend", function(e) {
keyEvent("w", "keyup"); keyEvent("w", "keyup"); // Luckily, it doesn't seem like eagler cares if we dispatch extra keyup events, so we can get away with just dispatching all of them here
keyEvent("d", "keyup"); keyEvent("d", "keyup");
keyEvent("a", "keyup"); keyEvent("a", "keyup");
strafeRightButton.classList.remove("active"); strafeRightButton.classList.remove("active");
@ -311,7 +391,7 @@ function insertCanvasElements() {
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
forwardButton.classList.remove("active"); forwardButton.classList.remove("active");
startTouchX = null; buttonTouchStartX = null;
}, false); }, false);
strafeRightButton.classList.add("hide"); strafeRightButton.classList.add("hide");
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
@ -344,7 +424,7 @@ function insertCanvasElements() {
crouchButton.addEventListener("touchstart", function(e){ crouchButton.addEventListener("touchstart", function(e){
keyEvent("shift", "keydown") keyEvent("shift", "keydown")
clientWindow.crouchLock = clientWindow.crouchLock ? null : false clientWindow.crouchLock = clientWindow.crouchLock ? null : false
clientWindow.crouchTimer = setTimeout(function(e) { clientWindow.crouchTimer = setTimeout(function(e) { // Allows us to lock the button after a long press
clientWindow.crouchLock = (clientWindow.crouchLock != null); clientWindow.crouchLock = (clientWindow.crouchLock != null);
crouchButton.classList.toggle('active'); crouchButton.classList.toggle('active');
}, 1000); }, 1000);
@ -361,7 +441,10 @@ function insertCanvasElements() {
document.body.appendChild(crouchButton); document.body.appendChild(crouchButton);
let inventoryButton = createTouchButton("inventoryButton", "inGame"); let inventoryButton = createTouchButton("inventoryButton", "inGame");
inventoryButton.style.cssText = "right:0vh;bottom:30vh;" inventoryButton.style.cssText = "right:0vh;bottom:30vh;"
inventoryButton.addEventListener("touchstart", function(e){keyEvent("e", "keydown")}, false); inventoryButton.addEventListener("touchstart", function(e) {
keyEvent("shift", "keyup"); // Sometimes shift gets stuck on, which interferes with item manipulation in GUI's
keyEvent("e", "keydown");
}, false);
inventoryButton.addEventListener("touchend", function(e){keyEvent("e", "keyup")}, false); inventoryButton.addEventListener("touchend", function(e){keyEvent("e", "keyup")}, false);
document.body.appendChild(inventoryButton); document.body.appendChild(inventoryButton);
let exitButton = createTouchButton("exitButton", "inMenu"); let exitButton = createTouchButton("exitButton", "inMenu");
@ -450,7 +533,7 @@ function insertCanvasElements() {
} }
} }
}, false); }, false);
hiddenInput.addEventListener("blur", function(e) { hiddenInput.addEventListener("blur", function(e) { // Updates clientWindow.hiddenInputFocused to reflect the actual state of the focus
clientWindow.hiddenInputFocused = false; clientWindow.hiddenInputFocused = false;
}); });
document.body.appendChild(hiddenInput); document.body.appendChild(hiddenInput);
@ -567,39 +650,47 @@ customStyle.textContent = `
position: absolute; position: absolute;
width: 10vh; width: 10vh;
height: 10vh; height: 10vh;
font-size:4vh; font-size: 4vh;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
line-height: 0px; line-height: 0px;
padding:0px; padding: 0px;
background-color: transparent; background-color: transparent;
box-sizing: content-box; box-sizing: content-box;
image-rendering: pixelated; image-rendering: pixelated;
background-size: cover; background-size: cover;
outline:none;
box-shadow: none; box-shadow: none;
border: none; border: none;
touch-action: pan-x pan-y;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
} }
.mobileControl:active, .mobileControl.active { .mobileControl:active, .mobileControl.active {
position: absolute; position: absolute;
width: 10vh; width: 10vh;
height: 10vh; height: 10vh;
font-size:4vh; font-size: 4vh;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
line-height: 0px; line-height: 0px;
padding:0px; padding: 0px;
background-color: transparent; background-color: transparent;
color: #ffffff;
text-shadow: 0.35vh 0.35vh #000000;
box-sizing: content-box; box-sizing: content-box;
image-rendering: pixelated; image-rendering: pixelated;
background-size: contain, cover; background-size: cover;
outline:none;
box-shadow: none; box-shadow: none;
border: none; border: none;
touch-action: pan-x pan-y;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
} }
html, body, canvas { html, body, canvas {
height: 100svh !important; height: 100svh !important;