Up-in-the-Air – commitdiff

You can use Git to clone the repository via the web URL. Download snapshot (zip)
First version of touchpad control
authorJulian Fietkau <git@fietkau.software>
Thu, 26 Sep 2024 21:59:40 +0000 (23:59 +0200)
committerJulian Fietkau <git@fietkau.software>
Thu, 26 Sep 2024 22:05:48 +0000 (00:05 +0200)
index.html
main.js

index a62466a86bae715f0a8396e4103e0fa95d9573f3..557e0cd2613a398697b2ce36cc72e641c9df6023 100644 (file)
     font-display: block;
   }
   body {
-    font-family: sans-serif;
     box-sizing: border-box;
     min-height: 100svh;
     margin: 0;
-    padding: 2rem 0;
+    padding: 0;
     background: #fff;
     display: flex;
     justify-content: center;
     align-items: center;
   }
   .game-upintheair {
+    container-type: inline-size;
+    box-sizing: border-box;
+    width: calc(100% - 2 * var(--game-margin));
+    height: calc(100svh - 2 * var(--game-margin));
+    font-family: sans-serif;
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+    --game-margin: 2rem;
+  }
+  .game-upintheair .ui-container {
     position: relative;
-    width: min(calc(100vh - 4rem), calc(100vw));
-    height: min(calc(100vh - 4rem), calc(100vw));
+    width: min(calc(100cqh - 2 * var(--game-margin)), calc(100cqw));
+    aspect-ratio: 1 / 1;
     font-size-adjust: 0.4;
     image-rendering: pixelated;
+    box-shadow: 1px 1px 3px #000a;
   }
-  .game-upintheair.font-atkinson * {
+  .virtual-input-widget {
+    box-sizing: border-box;
+    display: none;
+    width: min(4cm, 100cqw, calc(100cqh - 2 * var(--game-margin)));
+    aspect-ratio: 1 / 1;
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    z-index: 25;
+  }
+  @media (min-width: 15cm) and (min-height: 15cm) {
+    @container (min-width: calc(100cqh - 4cm - 1rem - 4rem)) {
+      .game-upintheair .ui-container.control-touchpad {
+        margin: 0 calc(1rem + min(4cm, 100cqw, calc(100cqh - 2 * var(--game-margin))));
+        align-self: flex-end;
+      }
+    }
+    @container (min-width: calc(100cqh - 4cm - 1rem - 4rem)) and (max-width: calc(100cqh + 2 * 4cm + 2rem)) {
+      .game-upintheair .ui-container.control-touchpad {
+        margin: 0 calc(1rem + min(4cm, 100cqw, calc(100cqh - 2 * var(--game-margin)))) 0 0;
+        align-self: flex-end;
+      }
+    }
+  }
+  @media (max-width: 15cm) or (max-height: 15cm) {
+    .game-upintheair {
+      --game-margin: 0px;
+    }
+    .virtual-input-widget {
+      bottom: 1rem;
+      right: 1rem;
+    }
+    @container (min-width: calc(100cqh - 4cm - 2rem)) {
+      .game-upintheair .ui-container.control-touchpad {
+        margin: 0 calc(2rem + min(4cm, 100cqw, calc(100cqh - 2 * var(--game-margin))));
+        align-self: flex-end;
+      }
+    }
+    @container (min-width: calc(100cqh - 4cm - 2rem)) and (max-width: calc(100cqh + 2 * 4cm + 4rem)) {
+      .game-upintheair .ui-container.control-touchpad {
+        margin: 0 calc(2rem + min(4cm, 100cqw, calc(100cqh - 2 * var(--game-margin)))) 0 0;
+        align-self: flex-end;
+      }
+    }
+  }
+  .game-upintheair .ui-container.font-atkinson * {
     font-family: 'Atkinson Hyperlegible' !important;
     font-size-adjust: 0.45;
   }
-  .game-upintheair.font-opendyslexic * {
+  .game-upintheair .ui-container.font-opendyslexic * {
     font-family: 'OpenDyslexic' !important;
     font-size-adjust: 0.4;
     line-height: 1.0;
   }
+  .virtual-input-widget.touchpad {
+    display: block;
+    background-color: #585858;
+    background-image: url('data:image/svg+xml,%3Csvg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="m33.5 224a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-96-32a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-96-32a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-96-32a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-96-32a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-96-32a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-96-32a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm160 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-128 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm-32 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5zm64 0a1.5 1.5 0 0 1-1.5 1.5 1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5 1.5 1.5 0 0 1 1.5 1.5z" fill="none" stroke="%23aaa" opacity=".25"/%3E%3C/svg%3E'), radial-gradient(circle at 25% 25%, #686868, #484848);
+    border-radius: 3%;
+    border: 2px inset #808080;
+  }
   .ui-page {
     position: absolute;
     left: 0;
     display: block;
     width: 100% !important;
     height: 100% !important;
-    border: 1px solid #000;
     margin: 0 auto;
   }
   .options {
 </head>
 <body>
 <div class="game-upintheair">
+<div class="ui-container">
 <div class="ui-page loading">
 <img src="textures/pinwheel.png" alt="Spinning red pinwheel loading animation">
 <progress value="0" max="100"></progress>
 </div>
 <div class="ui-page gameplay">
 <canvas width="800" height="800"></canvas>
+</div>
+</div>
+<div class="virtual-input-widget">
+
 </div>
 </div>
 <script type="module" src="main.js"></script>
diff --git a/main.js b/main.js
index 0df42844d2080f66e39b49c47704ebe0ed9e0718..6745c2de2fe93b0fd14428b1046b6a7fd3978b10 100644 (file)
--- a/main.js
+++ b/main.js
@@ -172,6 +172,7 @@ function init(game, canvas) {
   game.objects = {};
   game.view = {};
   game.view.canvas = canvas;
+  game.view.virtualInput = canvas.closest('.game-upintheair').querySelector('.virtual-input-widget');
 
   const scene = new THREE.Scene();
   game.view.camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
@@ -252,15 +253,27 @@ function init(game, canvas) {
     game.controls.positionY = - viewportHeight * viewportY;
   }
 
-  function cursorMoveEvent(game, viewportLocalX, viewportLocalY) {
-    if(game.settings['controls'] == 'mouse') {
-      const canvasBb = game.view.canvas.getBoundingClientRect();
+  function cursorMoveEvent(game, viewportLocalX, viewportLocalY, pressed) {
+    if(game.settings['controls'] == 'mouse' || game.settings['controls'] == 'touchpad') {
+      let sensorElem = game.view.canvas;
+      if(game.settings['controls'] == 'touchpad') {
+        sensorElem = game.view.virtualInput;
+      }
+      let bbox = sensorElem.getBoundingClientRect();
       // Intentional division by height instead of width in the following line, since
       // three.js controls the vertical FOV. So if we ever change the aspect ratio from 1:1,
       // y will still be in range (-0.5, 0.5), but the range for x will be smaller or larger.
-      const x = (viewportLocalX - canvasBb.x - (canvasBb.width / 2)) / canvasBb.height;
-      const y = (viewportLocalY - canvasBb.y - (canvasBb.height / 2)) / canvasBb.height;
-      pinwheelPositionUpdate(game, x, y);
+      let x = (viewportLocalX - bbox.x - (bbox.width / 2)) / bbox.height;
+      let y = (viewportLocalY - bbox.y - (bbox.height / 2)) / bbox.height;
+      if(game.settings['controls'] == 'touchpad') {
+        x *= 1.05;
+        y *= 1.05;
+      }
+      // The pinwheel gets to go a little bit past the edge of the playing field.
+      const maxDist = 0.55;
+      if(game.settings['controls'] == 'mouse' || (pressed && Math.abs(x) <= maxDist && Math.abs(y) <= maxDist)) {
+        pinwheelPositionUpdate(game, x, y);
+      }
     }
   }
 
@@ -330,8 +343,10 @@ function init(game, canvas) {
     }
   }
 
-  document.body.addEventListener('mousemove', e => cursorMoveEvent(game, e.clientX, e.clientY));
-  document.body.addEventListener('touchmove', e => cursorMoveEvent(game, e.touches[0].clientX, e.touches[0].clientY));
+  document.body.addEventListener('mousemove', e => cursorMoveEvent(game, e.clientX, e.clientY, (e.buttons % 2 == 1)));
+  document.body.addEventListener('mousedown', e => cursorMoveEvent(game, e.clientX, e.clientY, (e.buttons % 2 == 1)));
+  document.body.addEventListener('mouseup', e => cursorMoveEvent(game, e.clientX, e.clientY, (e.buttons % 2 == 1)));
+  document.body.addEventListener('touchmove', e => cursorMoveEvent(game, e.touches[0].clientX, e.touches[0].clientY, true));
   document.body.addEventListener('keydown', e => keyboardEvent(game, e.key, 'down'));
   document.body.addEventListener('keyup', e => keyboardEvent(game, e.key, 'up'));
 
@@ -885,6 +900,28 @@ function loadSettings(game) {
 function applySettings(game) {
   const ui = game.ui.root.querySelector('.ui-page.options');
   game.settings['controls'] = ui.querySelector('input[name="upInTheAirGame-controls"]:checked').value;
+  const virtualInput = game.ui.root.parentNode.querySelector('.virtual-input-widget');
+  if(game.settings['controls'] == 'touchpad') {
+    virtualInput.classList.remove('thumbstick');
+    virtualInput.classList.add('touchpad');
+    game.ui.root.classList.remove('control-mouse', 'control-thumbstick');
+    game.ui.root.classList.add('control-touchpad');
+  } else if(game.settings['controls'] == 'thumbstick') {
+    virtualInput.classList.remove('touchpad');
+    virtualInput.classList.add('thumbstick');
+    game.ui.root.classList.remove('control-mouse', 'control-touchpad');
+    game.ui.root.classList.add('control-thumbstick');
+  } else if(game.settings['controls'] == 'mouse') {
+    virtualInput.classList.remove('touchpad', 'thumbstick');
+    game.ui.root.classList.remove('control-touchpad', 'control-thumbstick');
+    game.ui.root.classList.add('control-mouse');
+  } else {
+    virtualInput.classList.remove('touchpad', 'thumbstick');
+    game.ui.root.classList.remove('control-mouse', 'control-touchpad', 'control-thumbstick');
+  }
+  for(let timeout of [10, 100, 1000]) {
+    setTimeout(() => { game.ui.root.style.fontSize = (game.ui.root.clientWidth / 50) + 'px'; }, timeout);
+  }
   game.settings['graphics'] = parseInt(ui.querySelector('input[name="upInTheAirGame-graphics"]:checked').value, 10);
   for(let audioCategory of ['music', 'sounds']) {
     game.settings['audio'][audioCategory] = parseInt(ui.querySelector('.audio input[type=range].' + audioCategory).value, 10) / 100;
@@ -1205,7 +1242,7 @@ function createMeshes(game) {
 window['game'] = {
   state: 'loadingAssets',
   ui: {
-    root: document.querySelector('.game-upintheair'),
+    root: document.querySelector('.game-upintheair .ui-container'),
     gamepads: [],
   },
   settings: {},