| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>GBA Remote</title> |
| <style> |
| |
| body, html { |
| margin: 0; padding: 0; width: 100%; height: 100%; |
| background-color: #000; overflow: hidden; |
| display: flex; flex-direction: column; |
| font-family: sans-serif; -webkit-tap-highlight-color: transparent; |
| } |
| |
| #screen-container { |
| width: 100%; flex: 0 0 auto; |
| display: flex; flex-direction: column; align-items: center; |
| padding-top: 10px; background: #000; |
| } |
| |
| #screen { |
| width: 100vw; height: auto; aspect-ratio: 3 / 2; |
| image-rendering: pixelated; background: #111; |
| } |
| |
| .status-text { |
| color: #4CAF50; font-family: monospace; |
| margin-bottom: 8px; font-size: 14px; |
| } |
| |
| |
| #touchControls { |
| flex: 1; position: relative; width: 100%; |
| user-select: none; -webkit-user-select: none; |
| } |
| |
| |
| .ctrl-btn { |
| position: absolute; background: #A6A6A6; |
| border: none; color: rgba(255,255,255,0.3); |
| display: flex; align-items: center; justify-content: center; |
| font-weight: bold; font-size: 18px; |
| } |
| |
| |
| #dpad { |
| position: absolute; bottom: 100px; left: 20px; |
| width: 160px; height: 160px; |
| } |
| #up { top: 0; left: 55px; width: 50px; height: 60px; border-radius: 5px 5px 0 0; } |
| #down { bottom: 0; left: 55px; width: 50px; height: 60px; border-radius: 0 0 5px 5px; } |
| #left { top: 55px; left: 0; width: 60px; height: 50px; border-radius: 5px 0 0 5px; } |
| #right { top: 55px; right: 0; width: 60px; height: 50px; border-radius: 0 5px 5px 0; } |
| #center-fill { position: absolute; top: 55px; left: 55px; width: 50px; height: 50px; background: #A6A6A6; } |
| |
| |
| #action-cluster { |
| position: absolute; bottom: 100px; right: 20px; |
| width: 160px; height: 160px; |
| display: grid; grid-template-columns: 1fr 1fr; grid-gap: 15px; |
| } |
| .round-btn { |
| position: relative; width: 70px; height: 70px; border-radius: 50%; |
| } |
| |
| |
| .pill-btn { |
| bottom: 30px; width: 85px; height: 35px; border-radius: 20px; |
| font-size: 12px; color: #fff; text-transform: uppercase; |
| } |
| #select { left: 25%; } |
| #start { right: 25%; } |
| |
| |
| #loader-ui { |
| position: absolute; top: 50%; left: 50%; |
| transform: translate(-50%, -50%); z-index: 10; |
| } |
| .load-btn { |
| padding: 15px 30px; background: #4CAF50; color: white; |
| border: none; border-radius: 5px; font-size: 18px; cursor: pointer; |
| } |
| .hidden { display: none !important; } |
| </style> |
|
|
| <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; |
| try { |
| gba = new GameBoyAdvance(); |
| gba.keypad.eatInput = true; |
| } catch (e) { console.error("GBA failed to load:", e); } |
| |
| window.onload = function() { |
| if (gba) { |
| var canvas = document.getElementById('screen'); |
| gba.setCanvas(canvas); |
| |
| loadRom('resources/bios.bin', function(bios) { gba.setBios(bios); }); |
| bindTouch(); |
| } |
| }; |
| |
| function handleFile(file) { |
| if (!file) return; |
| gba.loadRomFromFile(file, function(result) { |
| if (result) { |
| document.getElementById('loader-ui').classList.add('hidden'); |
| gba.runStable(); |
| } else { |
| alert("Failed to load ROM."); |
| } |
| }); |
| } |
| |
| function bindTouch() { |
| const keyMap = { |
| 'up': 38, 'down': 40, 'left': 37, 'right': 39, |
| 'a': 88, 'b': 90, 'x': 83, 'y': 65, |
| 'start': 13, 'select': 16 |
| }; |
| |
| function triggerKey(code, isPressed) { |
| const event = new KeyboardEvent(isPressed ? 'keydown' : 'keyup', { keyCode: code }); |
| window.dispatchEvent(event); |
| } |
| |
| |
| Object.keys(keyMap).forEach(id => { |
| const btn = document.getElementById(id); |
| if (btn) { |
| btn.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| triggerKey(keyMap[id], true); |
| btn.style.opacity = "0.7"; |
| }); |
| btn.addEventListener('touchend', (e) => { |
| e.preventDefault(); |
| triggerKey(keyMap[id], false); |
| btn.style.opacity = "1.0"; |
| }); |
| } |
| }); |
| } |
| </script> |
| </head> |
| <body> |
|
|
| <div id="screen-container"> |
| <div class="status-text">Connecting to Screen...</div> |
| <canvas id="screen" width="480" height="320"></canvas> |
| </div> |
|
|
| <div id="loader-ui"> |
| <button class="load-btn" onclick="document.getElementById('file-in').click()">OPEN GBA ROM</button> |
| <input id="file-in" type="file" accept=".gba" style="display:none" onchange="handleFile(this.files[0])"> |
| </div> |
|
|
| <div id="touchControls"> |
| <div id="dpad"> |
| <button id="up" class="ctrl-btn"></button> |
| <button id="left" class="ctrl-btn"></button> |
| <div id="center-fill"></div> |
| <button id="right" class="ctrl-btn"></button> |
| <button id="down" class="ctrl-btn"></button> |
| </div> |
|
|
| <div id="action-cluster"> |
| <button id="x" class="ctrl-btn round-btn">X</button> |
| <button id="y" class="ctrl-btn round-btn">Y</button> |
| <button id="b" class="ctrl-btn round-btn">B</button> |
| <button id="a" class="ctrl-btn round-btn">A</button> |
| </div> |
|
|
| <button id="select" class="ctrl-btn pill-btn">Select</button> |
| <button id="start" class="ctrl-btn pill-btn">Start</button> |
| </div> |
|
|
| </body> |
| </html> |
|
|