| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>GBA.js - Mobile Touch Controls</title> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <style> |
| body { |
| margin: 0; |
| padding: 0; |
| background: #000; |
| overflow: hidden; |
| font-family: Arial, sans-serif; |
| color: white; |
| touch-action: none; |
| } |
| #screen { |
| display: block; |
| width: 100%; |
| height: auto; |
| image-rendering: pixelated; |
| } |
| #touchControls { |
| position: absolute; |
| inset: 0; |
| pointer-events: none; |
| z-index: 20; |
| padding: 12px; |
| box-sizing: border-box; |
| display: none; |
| } |
| .controls-container { |
| display: flex; |
| justify-content: space-between; |
| align-items: flex-end; |
| height: 100%; |
| pointer-events: none; |
| } |
| button.touch-btn { |
| background: rgba(80,80,80,0.75); |
| color: white; |
| font-weight: bold; |
| border: none; |
| border-radius: 12px; |
| box-shadow: 0 4px 12px rgba(0,0,0,0.6); |
| touch-action: manipulation; |
| -webkit-tap-highlight-color: transparent; |
| user-select: none; |
| } |
| #dpad { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| grid-template-rows: repeat(3, 1fr); |
| gap: 8px; |
| width: 40%; |
| max-width: 180px; |
| aspect-ratio: 1 / 1; |
| pointer-events: auto; |
| } |
| #dpad button { font-size: clamp(2rem, 10vw, 3rem); } |
| .face-btn { |
| width: clamp(70px, 20vw, 100px); |
| height: clamp(70px, 20vw, 100px); |
| border-radius: 50%; |
| font-size: clamp(2.2rem, 9vw, 3rem); |
| pointer-events: auto; |
| } |
| #a { background: rgba(220, 40, 40, 0.8); } |
| #b { background: rgba(40, 120, 220, 0.8); } |
| .shoulder-btn { |
| width: clamp(60px, 16vw, 90px); |
| height: clamp(36px, 10vw, 48px); |
| font-size: clamp(1.2rem, 5vw, 1.6rem); |
| border-radius: 10px; |
| background: rgba(110,110,110,0.8); |
| } |
| .action-btn { |
| width: clamp(70px, 18vw, 100px); |
| height: clamp(40px, 11vw, 52px); |
| font-size: clamp(1.1rem, 4.5vw, 1.4rem); |
| border-radius: 12px; |
| background: rgba(50, 150, 50, 0.8); |
| } |
| #loader-area { |
| position: absolute; |
| top: 12px; |
| left: 12px; |
| z-index: 30; |
| background: rgba(0,0,0,0.6); |
| padding: 12px; |
| border-radius: 10px; |
| pointer-events: auto; |
| } |
| #status { |
| margin: 8px 0 0; |
| font-size: 0.95rem; |
| } |
| @media (max-width: 400px) { |
| #dpad { max-width: 150px; gap: 6px; } |
| .face-btn { width: 65px; height: 65px; font-size: 2rem; } |
| .shoulder-btn, .action-btn { width: 55px; height: 35px; font-size: 1rem; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <canvas id="screen" width="480" height="320"></canvas> |
|
|
| <div id="loader-area"> |
| <button onclick="document.getElementById('romInput').click()">Load GBA ROM</button> |
| <input id="romInput" type="file" accept=".gba" style="display:none;"> |
| <div id="status">Select a .gba file to start</div> |
| </div> |
|
|
| <div id="touchControls"> |
| <div class="controls-container"> |
|
|
| |
| <div id="dpad"> |
| <div></div> |
| <button id="up">β</button> |
| <div></div> |
| <button id="left">β</button> |
| <div></div> |
| <button id="right">β</button> |
| <div></div> |
| <button id="down">β</button> |
| <div></div> |
| </div> |
|
|
| |
| <div style="display: flex; flex-direction: column; align-items: flex-end; gap: 12px; width: 55%; max-width: 240px; pointer-events: auto;"> |
| <div style="display: flex; gap: 12px; width: 100%; justify-content: flex-end;"> |
| <button id="l" class="shoulder-btn">L</button> |
| <button id="r" class="shoulder-btn">R</button> |
| </div> |
| <div style="display: flex; gap: 12px; width: 100%; justify-content: flex-end;"> |
| <button id="select" class="action-btn">Select</button> |
| <button id="start" class="action-btn">Start</button> |
| </div> |
| <div style="display: flex; gap: 24px; justify-content: flex-end;"> |
| <button id="b" class="face-btn">B</button> |
| <button id="a" class="face-btn">A</button> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/util.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/core.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/arm.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/thumb.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/mmu.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/io.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/audio.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/video.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/video/software.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/irq.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/keypad.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/endrift/gbajs@master/js/gba.js"></script> |
|
|
| <script> |
| |
| let gba = null; |
| const canvas = document.getElementById('screen'); |
| const statusEl = document.getElementById('status'); |
| |
| |
| try { |
| gba = new GameBoyAdvance(); |
| gba.keypad.eatInput = true; |
| gba.setCanvas(canvas); |
| gba.logLevel = gba.LOG_ERROR; |
| console.log("GBA emulator core ready"); |
| } catch (err) { |
| statusEl.textContent = "Emulator failed to start: " + err.message; |
| alert("Emulator init error: " + err.message); |
| } |
| |
| |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| document.getElementById('touchControls').style.display = 'block'; |
| canvas.style.width = '100%'; |
| canvas.style.height = 'auto'; |
| } |
| |
| |
| function pressKey(code) { |
| window.dispatchEvent(new KeyboardEvent('keydown', {keyCode: code, bubbles: true})); |
| } |
| function releaseKey(code) { |
| window.dispatchEvent(new KeyboardEvent('keyup', {keyCode: code, bubbles: true})); |
| } |
| |
| const keyMap = { |
| up: 38, |
| down: 40, |
| left: 37, |
| right: 39, |
| a: 88, |
| b: 90, |
| l: 65, |
| r: 83, |
| start: 13, |
| select: 16 |
| }; |
| |
| Object.keys(keyMap).forEach(id => { |
| const btn = document.getElementById(id); |
| if (btn) { |
| btn.addEventListener('touchstart', e => { e.preventDefault(); pressKey(keyMap[id]); }); |
| btn.addEventListener('touchend', e => { e.preventDefault(); releaseKey(keyMap[id]); }); |
| btn.addEventListener('touchcancel', e => { e.preventDefault(); releaseKey(keyMap[id]); }); |
| } |
| }); |
| |
| |
| document.getElementById('romInput').addEventListener('change', function(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| statusEl.textContent = `Loading ${file.name} (${(file.size / 1024 / 1024).toFixed(1)} MB)...`; |
| |
| const reader = new FileReader(); |
| reader.onload = function() { |
| try { |
| const buffer = reader.result; |
| if (!buffer || buffer.byteLength < 16384) { |
| throw new Error("File too small - not a valid GBA ROM"); |
| } |
| |
| gba.loadRom(buffer, success => { |
| if (success) { |
| statusEl.textContent = "ROM loaded β starting game..."; |
| gba.runStable(); |
| setTimeout(() => { |
| document.getElementById('loader-area').style.display = 'none'; |
| }, 1200); |
| } else { |
| throw new Error("ROM load failed (invalid format?)"); |
| } |
| }); |
| } catch (err) { |
| statusEl.textContent = "Error: " + err.message; |
| alert("Failed to load ROM:\n" + err.message + "\n\nTry a clean .gba file (not zipped)."); |
| console.error(err); |
| } |
| }; |
| reader.onerror = () => { |
| statusEl.textContent = "Cannot read file"; |
| alert("File read error - file may be damaged or browser blocked access."); |
| }; |
| reader.readAsArrayBuffer(file); |
| }); |
| </script> |
|
|
| </body> |
| </html> |