| <!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, viewport-fit=cover"> |
| <title>GBA.js Emulator - Touch Controls</title> |
| <style> |
| * { margin:0; padding:0; box-sizing:border-box; } |
| html, body { |
| height:100%; |
| overflow:hidden; |
| background:#000; |
| touch-action: manipulation; |
| font-family: Arial, sans-serif; |
| } |
| #screen-container { |
| position:relative; |
| width:100%; |
| height:100%; |
| display:flex; |
| justify-content:center; |
| align-items:center; |
| background:#111; |
| } |
| #screen { |
| width:100%; |
| height:auto; |
| max-width:100%; |
| max-height:80vh; |
| image-rendering:pixelated; |
| touch-action:none; |
| } |
| #ui { |
| position:absolute; |
| top:10px; |
| left:10px; |
| z-index:30; |
| background:rgba(0,0,0,0.7); |
| color:white; |
| padding:10px; |
| border-radius:8px; |
| font-size:14px; |
| } |
| button { padding:8px 16px; margin:4px; font-size:16px; cursor:pointer; } |
| #touchControls { |
| position:absolute; |
| inset:0; |
| pointer-events:none; |
| display:none; |
| z-index:20; |
| padding:4vmin; |
| } |
| .btn { |
| font-weight:bold; |
| color:white; |
| border:none; |
| border-radius:50%; |
| opacity:0.75; |
| touch-action:manipulation; |
| -webkit-tap-highlight-color:transparent; |
| } |
| .btn:active { opacity:1; } |
| #dpad { |
| display:grid; |
| grid-template-columns:repeat(3, 20vmin); |
| grid-template-rows:repeat(3, 20vmin); |
| gap:1.5vmin; |
| } |
| #face { |
| display:flex; |
| flex-direction:column; |
| align-items:flex-end; |
| gap:3vmin; |
| } |
| .shoulder, .sys { |
| width:20vmin; |
| height:10vmin; |
| border-radius:10vmin; |
| background:rgba(180,180,180,0.7); |
| font-size:4vmin; |
| } |
| #a { background:rgba(220,60,60,0.85); width:24vmin; height:24vmin; font-size:6vmin; } |
| #b { background:rgba(60,60,220,0.85); width:24vmin; height:24vmin; font-size:6vmin; } |
| |
| @media (orientation: landscape) { |
| #touchControls { display:flex; flex-direction:row; justify-content:space-between; align-items:flex-end; } |
| #dpad { grid-template-columns:repeat(3, 16vmin); grid-template-rows:repeat(3, 16vmin); } |
| #face { flex-direction:row; gap:5vmin; } |
| } |
| @media (orientation: portrait) { |
| #touchControls { display:flex; flex-direction:column; justify-content:flex-end; gap:4vmin; } |
| #dpad { align-self:flex-start; } |
| #face { align-self:flex-end; } |
| } |
| </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 = null; |
| var runCommands = []; |
| |
| try { |
| gba = new GameBoyAdvance(); |
| gba.keypad.eatInput = true; |
| gba.setLogger((level, msg) => console.log("[GBA]", msg)); |
| } catch (e) { |
| console.error("Failed to init GBA:", e); |
| } |
| |
| window.onload = () => { |
| if (!gba) { |
| alert("Emulator failed to initialize. Check console for errors."); |
| return; |
| } |
| |
| const canvas = document.getElementById('screen'); |
| gba.setCanvas(canvas); |
| gba.logLevel = 1; |
| |
| |
| const xhr = new XMLHttpRequest(); |
| xhr.open('GET', 'resources/bios.bin', true); |
| xhr.responseType = 'arraybuffer'; |
| xhr.onload = () => { |
| if (xhr.status === 200) { |
| gba.setBios(new Uint8Array(xhr.response)); |
| console.log("BIOS loaded"); |
| } else { |
| console.warn("BIOS not found – some games may not boot correctly"); |
| } |
| }; |
| xhr.send(); |
| |
| |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| document.getElementById('touchControls').style.display = 'flex'; |
| canvas.style.maxHeight = '70vh'; |
| } |
| |
| |
| canvas.addEventListener('touchstart', () => canvas.focus()); |
| |
| setupTouch(); |
| }; |
| |
| function run(file) { |
| if (!gba) return; |
| document.getElementById('select').textContent = 'Loading...'; |
| gba.loadRomFromFile(file, success => { |
| if (success) { |
| gba.runStable(); |
| document.getElementById('ui').innerHTML = '<button onclick="gba.pause(); this.innerText=gba.paused?\'Run\':\'Pause\'">Pause</button> <button onclick="gba.reset()">Reset</button>'; |
| } else { |
| alert("Failed to load ROM. Try another file."); |
| document.getElementById('select').textContent = 'SELECT ROM'; |
| } |
| }); |
| } |
| |
| function setupTouch() { |
| const press = code => window.dispatchEvent(new KeyboardEvent('keydown', {keyCode: code, bubbles: true})); |
| const release = code => window.dispatchEvent(new KeyboardEvent('keyup', {keyCode: code, bubbles: true})); |
| |
| const mapping = { |
| up: 38, down: 40, left: 37, right: 39, |
| a: 88, |
| b: 90, |
| l: 81, |
| r: 69, |
| start: 13, |
| select:16 |
| }; |
| |
| document.querySelectorAll('.btn').forEach(btn => { |
| const action = btn.id; |
| if (!mapping[action]) return; |
| |
| btn.addEventListener('touchstart', e => { e.preventDefault(); press(mapping[action]); }); |
| btn.addEventListener('touchend', e => { e.preventDefault(); release(mapping[action]); }); |
| btn.addEventListener('touchcancel',e => { e.preventDefault(); release(mapping[action]); }); |
| }); |
| } |
| </script> |
| </head> |
| <body> |
|
|
| <div id="screen-container"> |
| <canvas id="screen" width="480" height="320" tabindex="0"></canvas> |
| </div> |
|
|
| <div id="ui"> |
| <button id="select" onclick="document.getElementById('romfile').click()">SELECT ROM</button> |
| <input id="romfile" type="file" accept=".gba" onchange="run(this.files[0])" style="display:none;"> |
| </div> |
|
|
| <div id="touchControls"> |
| <div id="dpad"> |
| <button id="up" class="btn">↑</button> |
| <button id="left" class="btn">←</button> |
| <button id="right" class="btn">→</button> |
| <button id="down" class="btn">↓</button> |
| </div> |
|
|
| <div id="face"> |
| <div> |
| <button id="l" class="shoulder btn">L</button> |
| <button id="r" class="shoulder btn">R</button> |
| </div> |
| <div> |
| <button id="select" class="sys btn">SEL</button> |
| <button id="start" class="sys btn">STR</button> |
| </div> |
| <div> |
| <button id="b" class="btn">B</button> |
| <button id="a" class="btn">A</button> |
| </div> |
| </div> |
| </div> |
|
|
| </body> |
| </html> |