| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>GBA.js - Mobile Friendly</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: #111; |
| font-family: Arial, sans-serif; |
| overflow: hidden; |
| 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: 10px; |
| box-sizing: border-box; |
| display: none; |
| } |
| .control-row { |
| display: flex; |
| justify-content: space-between; |
| align-items: flex-end; |
| height: 100%; |
| pointer-events: none; |
| } |
| .touch-btn, .face-btn, .shoulder-btn, .action-btn { |
| background: rgba(70,70,70,0.7); |
| color: white; |
| font-weight: bold; |
| border: none; |
| border-radius: 12px; |
| touch-action: manipulation; |
| -webkit-tap-highlight-color: transparent; |
| box-shadow: 0 3px 10px rgba(0,0,0,0.5); |
| user-select: none; |
| } |
| |
| #dpad { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| grid-template-rows: repeat(3, 1fr); |
| gap: 6px; |
| width: 38%; |
| max-width: 170px; |
| aspect-ratio: 1 / 1; |
| pointer-events: auto; |
| } |
| #dpad button { |
| font-size: clamp(1.8rem, 9vw, 2.6rem); |
| } |
| |
| .face-btn { |
| width: clamp(64px, 18vw, 90px); |
| height: clamp(64px, 18vw, 90px); |
| border-radius: 50%; |
| font-size: clamp(1.9rem, 8vw, 2.6rem); |
| } |
| #a { background: rgba(220, 40, 40, 0.75); } |
| #b { background: rgba(40, 100, 220, 0.75); } |
| |
| .shoulder-btn { |
| width: clamp(54px, 15vw, 80px); |
| height: clamp(34px, 9vw, 44px); |
| font-size: clamp(1.1rem, 4.5vw, 1.4rem); |
| border-radius: 8px; |
| background: rgba(110,110,110,0.75); |
| } |
| |
| .action-btn { |
| width: clamp(68px, 18vw, 90px); |
| height: clamp(38px, 10vw, 48px); |
| font-size: clamp(1rem, 4vw, 1.3rem); |
| border-radius: 10px; |
| background: rgba(50, 140, 50, 0.75); |
| } |
| |
| @media (max-width: 380px) { |
| #dpad { max-width: 140px; gap: 5px; } |
| .face-btn { width: 62px; height: 62px; font-size: 1.8rem; } |
| .shoulder-btn { width: 50px; height: 32px; } |
| .action-btn { width: 64px; height: 36px; font-size: 0.95rem; } |
| } |
| @media (min-width: 700px) { |
| #dpad { max-width: 210px; } |
| .face-btn { width: 100px; height: 100px; font-size: 2.8rem; } |
| } |
| #controls { |
| position: absolute; |
| top: 8px; |
| left: 8px; |
| z-index: 30; |
| color: white; |
| background: rgba(0,0,0,0.4); |
| padding: 8px 12px; |
| border-radius: 8px; |
| font-size: 0.9rem; |
| pointer-events: auto; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <canvas id="screen" width="480" height="320"></canvas> |
|
|
| <div id="touchControls"> |
| <div class="control-row"> |
|
|
| |
| <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: 10px; width: 58%; 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: 20px; justify-content: flex-end;"> |
| <button id="b" class="face-btn">B</button> |
| <button id="a" class="face-btn">A</button> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| <div id="controls"> |
| <button onclick="document.getElementById('loader').click()">Load ROM</button> |
| <input id="loader" type="file" accept=".gba" style="display:none;"> |
| </div> |
|
|
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/util.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/core.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/arm.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/thumb.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/mmu.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/io.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/audio.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/video.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/video/proxy.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/video/software.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/irq.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/keypad.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/sio.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/savedata.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/gpio.js"></script> |
| <script src="https://raw.githack.com/gbajs/gbajs/master/js/gba.js"></script> |
|
|
| <script> |
| |
| var gba; |
| var canvas = document.getElementById('screen'); |
| |
| try { |
| gba = new GameBoyAdvance(); |
| gba.keypad.eatInput = true; |
| gba.setCanvas(canvas); |
| gba.logLevel = gba.LOG_ERROR; |
| |
| |
| |
| |
| } catch(e) { |
| console.error("GBA init failed", e); |
| } |
| |
| |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| document.getElementById('touchControls').style.display = 'block'; |
| canvas.style.width = '100%'; |
| canvas.style.height = 'auto'; |
| } |
| |
| |
| function press(code) { |
| window.dispatchEvent(new KeyboardEvent('keydown', {keyCode: code, bubbles: true})); |
| } |
| function release(code) { |
| window.dispatchEvent(new KeyboardEvent('keyup', {keyCode: code, bubbles: true})); |
| } |
| |
| const map = { |
| up: 38, |
| down: 40, |
| left: 37, |
| right: 39, |
| a: 88, |
| b: 90, |
| l: 65, |
| r: 83, |
| start: 13, |
| select:16 |
| }; |
| |
| Object.keys(map).forEach(key => { |
| const btn = document.getElementById(key); |
| if (!btn) return; |
| btn.addEventListener('touchstart', e => { e.preventDefault(); press(map[key]); }); |
| btn.addEventListener('touchend', e => { e.preventDefault(); release(map[key]); }); |
| |
| btn.addEventListener('touchcancel', e => { e.preventDefault(); release(map[key]); }); |
| }); |
| |
| |
| document.getElementById('loader').addEventListener('change', function(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = function() { |
| try { |
| gba.loadRom(reader.result, function success() { |
| gba.runStable(); |
| document.getElementById('controls').style.display = 'none'; |
| }); |
| } catch(e) { |
| alert("Could not load ROM\n" + e); |
| } |
| }; |
| reader.readAsArrayBuffer(file); |
| }); |
| </script> |
|
|
| </body> |
| </html> |