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>
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);
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);
+ }
}
}
}
}
- 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'));
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;
window['game'] = {
state: 'loadingAssets',
ui: {
- root: document.querySelector('.game-upintheair'),
+ root: document.querySelector('.game-upintheair .ui-container'),
gamepads: [],
},
settings: {},