| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| | <title>GBA.js - Touch Controls</title> |
| | <link rel="stylesheet" href="resources/main.css"> |
| |
|
| | <style> |
| | body { |
| | margin: 0; |
| | padding: 0; |
| | background: #000; |
| | overflow: hidden; |
| | height: 100vh; |
| | touch-action: none; |
| | } |
| | |
| | #screen { |
| | image-rendering: pixelated; |
| | image-rendering: crisp-edges; |
| | width: 100%; |
| | height: auto; |
| | max-height: 100vh; |
| | display: block; |
| | margin: 0 auto; |
| | } |
| | |
| | .touch-controls { |
| | position: absolute; |
| | inset: 0; |
| | pointer-events: none; |
| | z-index: 10; |
| | touch-action: none; |
| | user-select: none; |
| | display: none; |
| | } |
| | |
| | .dpad, .face { |
| | position: absolute; |
| | bottom: 8%; |
| | pointer-events: auto; |
| | } |
| | |
| | .dpad { |
| | left: 5%; |
| | width: 160px; |
| | height: 160px; |
| | } |
| | |
| | .face { |
| | right: 5%; |
| | width: 200px; |
| | display: grid; |
| | grid-template-columns: repeat(3, 1fr); |
| | gap: 12px; |
| | } |
| | |
| | .ctrl-btn { |
| | background: rgba(140,140,140,0.4); |
| | border: 3px solid rgba(240,240,240,0.65); |
| | border-radius: 50%; |
| | color: white; |
| | font-weight: bold; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | font-size: 1.2rem; |
| | box-shadow: 0 3px 12px rgba(0,0,0,0.5); |
| | transition: all 0.1s ease; |
| | } |
| | |
| | .ctrl-btn:active, |
| | .ctrl-btn.pressed { |
| | transform: scale(0.88); |
| | opacity: 0.7; |
| | background: rgba(220,220,220,0.65); |
| | } |
| | |
| | |
| | #up { width:70px; height:70px; position:absolute; left:45px; top:0; border-radius:14px 14px 50% 50%; } |
| | #down { width:70px; height:70px; position:absolute; left:45px; bottom:0; border-radius:50% 50% 14px 14px; } |
| | #left { width:70px; height:70px; position:absolute; top:45px; left:0; border-radius:14px 50% 50% 14px; } |
| | #right { width:70px; height:70px; position:absolute; top:45px; right:0; border-radius:50% 14px 14px 50%; } |
| | #center{ width:70px; height:70px; position:absolute; left:45px; top:45px; background:transparent; border:none; pointer-events:none; } |
| | |
| | |
| | #a { background:rgba(0,200,0,0.55); grid-column:3; grid-row:2; border-radius:50%; width:80px; height:80px; font-size:1.6rem; } |
| | #b { background:rgba(220,0,0,0.55); grid-column:2; grid-row:3; border-radius:50%; width:80px; height:80px; font-size:1.6rem; } |
| | #l { background:rgba(80,80,255,0.55); grid-column:1; grid-row:1; width:70px; height:55px; border-radius:12px; font-size:1.1rem; } |
| | #r { background:rgba(80,80,255,0.55); grid-column:3; grid-row:1; width:70px; height:55px; border-radius:12px; font-size:1.1rem; } |
| | #start { background:rgba(220,220,60,0.65); grid-column:2/4; grid-row:1; width:auto; height:50px; border-radius:12px; font-size:1rem; } |
| | #select{ background:rgba(220,220,60,0.65); grid-column:1/3; grid-row:1; width:auto; height:50px; border-radius:12px; font-size:1rem; } |
| | |
| | @media (orientation: landscape) and (min-width: 900px) { |
| | .touch-controls { display: none !important; } |
| | } |
| | </style> |
| | </head> |
| | <body> |
| |
|
| | <canvas id="screen" width="480" height="320"></canvas> |
| |
|
| | <div id="touch-controls" class="touch-controls"> |
| | |
| | <div class="dpad"> |
| | <div id="up" class="ctrl-btn" data-bit="6"></div> |
| | <div id="down" class="ctrl-btn" data-bit="7"></div> |
| | <div id="left" class="ctrl-btn" data-bit="5"></div> |
| | <div id="right" class="ctrl-btn" data-bit="4"></div> |
| | <div id="center"></div> |
| | </div> |
| |
|
| | |
| | <div class="face"> |
| | <div id="l" class="ctrl-btn" data-bit="9">L</div> |
| | <div id="select" class="ctrl-btn" data-bit="2">Select</div> |
| | <div id="start" class="ctrl-btn" data-bit="3">Start</div> |
| | <div id="r" class="ctrl-btn" data-bit="8">R</div> |
| | <div id="b" class="ctrl-btn" data-bit="1">B</div> |
| | <div id="a" class="ctrl-btn" data-bit="0">A</div> |
| | </div> |
| | </div> |
| |
|
| | <section id="controls"> |
| | <div id="preload"> |
| | <button class="bigbutton" id="select" onclick="document.getElementById('loader').click()">SELECT</button> |
| | <input id="loader" type="file" accept=".gba" onchange="run(this.files[0]);"> |
| | <button onclick="document.getElementById('saveloader').click()">Upload Savegame</button> |
| | <input id="saveloader" type="file" onchange="uploadSavedataPending(this.files[0]);"> |
| | </div> |
| | <div id="ingame" class="hidden"> |
| | <button id="pause" class="bigbutton" onclick="togglePause()">PAUSE</button> |
| | <button class="bigbutton" onclick="reset()">RESET</button> |
| | <button onclick="gba.downloadSavedata()">Download Savegame</button> |
| | <button onclick="screenshot()">Screenshot</button> |
| | <label id="pixelated"> |
| | <input type="checkbox" onchange="setPixelated(this.checked)"> |
| | <p>Pixelated</p> |
| | </label> |
| | <div id="sound"> |
| | <input type="checkbox" checked onchange="gba.audio.masterEnable = this.checked"> |
| | <p>Sound</p> |
| | <input type="range" min="0" max="1" value="1" step="any" onchange="setVolume(this.value)" oninput="setVolume(this.value)"> |
| | </div> |
| | <p id="openDebug" onclick="enableDebug()">Debugger</p> |
| | </div> |
| | </section> |
| |
|
| | <script src="js/util.js"></script> |
| | <script src="js/core.js"></script> |
| | <script src="js/arm.js"></script> |
| | <script src="js/thumb.js"></script> |
| | <script src="js/mmu.js"></script> |
| | <script src="js/io.js"></script> |
| | <script src="js/audio.js"></script> |
| | <script src="js/video.js"></script> |
| | <script src="js/video/proxy.js"></script> |
| | <script src="js/video/software.js"></script> |
| | <script src="js/irq.js"></script> |
| | <script src="js/keypad.js"></script> |
| | <script src="js/sio.js"></script> |
| | <script src="js/savedata.js"></script> |
| | <script src="js/gpio.js"></script> |
| | <script src="js/gba.js"></script> |
| | <script src="resources/xhr.js"></script> |
| |
|
| | <script> |
| | |
| | |
| | var gba; |
| | var runCommands = []; |
| | var debug = null; |
| | |
| | try { |
| | gba = new GameBoyAdvance(); |
| | gba.keypad.eatInput = true; |
| | gba.setLogger(function(level, error) { |
| | console.log(error); |
| | gba.pause(); |
| | var screen = document.getElementById('screen'); |
| | if (screen.getAttribute('class') == 'dead') return; |
| | var crash = document.createElement('img'); |
| | crash.id = 'crash'; |
| | crash.src = 'resources/crash.png'; |
| | screen.parentElement.insertBefore(crash, screen); |
| | screen.className = 'dead'; |
| | }); |
| | } catch (e) { |
| | gba = null; |
| | } |
| | |
| | window.onload = function() { |
| | if (gba && FileReader) { |
| | var canvas = document.getElementById('screen'); |
| | gba.setCanvas(canvas); |
| | gba.logLevel = gba.LOG_ERROR; |
| | |
| | |
| | |
| | |
| | if (!gba.audio.context) { |
| | var soundbox = document.getElementById('sound'); |
| | if (soundbox && soundbox.parentElement) soundbox.parentElement.removeChild(soundbox); |
| | } |
| | } else { |
| | var dead = document.getElementById('controls'); |
| | if (dead && dead.parentElement) dead.parentElement.removeChild(dead); |
| | } |
| | }; |
| | |
| | |
| | |
| | |
| | |
| | function fadeOut(id, nextId, kill) { |
| | var e = document.getElementById(id); |
| | var e2 = document.getElementById(nextId); |
| | if (!e) return; |
| | |
| | var removeSelf = function() { |
| | if (kill) e.parentElement?.removeChild(e); |
| | else { |
| | e.className = 'dead'; |
| | e.removeEventListener('transitionend', removeSelf); |
| | } |
| | if (e2) { |
| | e2.className = 'hidden'; |
| | setTimeout(() => e2.removeAttribute('class'), 0); |
| | } |
| | }; |
| | |
| | e.addEventListener('transitionend', removeSelf); |
| | e.className = 'hidden'; |
| | } |
| | |
| | function run(file) { |
| | document.getElementById('loader').value = ''; |
| | var load = document.getElementById('select'); |
| | load.textContent = 'Loading...'; |
| | load.removeAttribute('onclick'); |
| | var pause = document.getElementById('pause'); |
| | pause.textContent = "PAUSE"; |
| | |
| | gba.loadRomFromFile(file, function(result) { |
| | if (result) { |
| | runCommands.forEach(cmd => cmd()); |
| | runCommands = []; |
| | fadeOut('preload', 'ingame'); |
| | fadeOut('instructions', null, true); |
| | gba.runStable(); |
| | |
| | |
| | document.getElementById('touch-controls').style.display = 'block'; |
| | } else { |
| | load.textContent = 'FAILED'; |
| | setTimeout(() => { |
| | load.textContent = 'SELECT'; |
| | load.onclick = () => document.getElementById('loader').click(); |
| | }, 3000); |
| | } |
| | }); |
| | } |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| | if (gba?.keypad) { |
| | const keypad = gba.keypad; |
| | const pressed = new Set(); |
| | |
| | document.querySelectorAll('.ctrl-btn[data-bit]').forEach(el => { |
| | const bit = parseInt(el.dataset.bit, 10); |
| | const toggle = 1 << bit; |
| | |
| | const press = () => { |
| | if (!pressed.has(el.id)) { |
| | pressed.add(el.id); |
| | keypad.currentDown &= ~toggle; |
| | } |
| | el.classList.add('pressed'); |
| | }; |
| | |
| | const release = () => { |
| | if (pressed.has(el.id)) { |
| | pressed.delete(el.id); |
| | keypad.currentDown |= toggle; |
| | } |
| | el.classList.remove('pressed'); |
| | }; |
| | |
| | el.addEventListener('touchstart', e => { e.preventDefault(); press(); }, { passive: false }); |
| | el.addEventListener('touchend', e => { e.preventDefault(); release(); }, { passive: false }); |
| | el.addEventListener('touchcancel',e => { e.preventDefault(); release(); }, { passive: false }); |
| | }); |
| | |
| | console.log("Touch controls activated (direct keypad mode)"); |
| | } else { |
| | console.warn("gba.keypad not available β touch controls disabled"); |
| | } |
| | } |
| | </script> |
| | </body> |
| | </html> |