| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>GBA.js Mobile - Fixed Loader</title> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <style> |
| body { margin:0; background:#000; font-family:Arial; color:#fff; overflow:hidden; } |
| #screen { display:block; width:100%; height:auto; image-rendering:pixelated; } |
| #info { position:absolute; top:10px; left:10px; background:rgba(0,0,0,0.6); padding:10px; border-radius:8px; z-index:30; pointer-events:auto; } |
| #touchControls { position:absolute; inset:0; pointer-events:none; z-index:20; padding:10px; display:none; box-sizing:border-box; } |
| .control-row { display:flex; justify-content:space-between; align-items:flex-end; height:100%; pointer-events:none; } |
| button { background:rgba(80,80,80,0.7); color:white; font-weight:bold; border:none; border-radius:10px; box-shadow:0 3px 8px #000; touch-action:manipulation; -webkit-tap-highlight-color:transparent; } |
| #dpad { display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(3,1fr); gap:6px; width:35%; max-width:160px; aspect-ratio:1/1; pointer-events:auto; } |
| #dpad button { font-size:clamp(1.6rem,8vw,2.4rem); } |
| .face-btn { width:clamp(60px,17vw,85px); height:clamp(60px,17vw,85px); border-radius:50%; font-size:clamp(1.8rem,7vw,2.4rem); pointer-events:auto; } |
| #a { background:rgba(220,40,40,0.8); } |
| #b { background:rgba(40,100,220,0.8); } |
| .shoulder { width:clamp(50px,14vw,70px); height:clamp(32px,8vw,40px); font-size:clamp(1rem,4vw,1.3rem); background:rgba(100,100,100,0.8); } |
| .action { width:clamp(65px,17vw,85px); height:clamp(36px,9vw,44px); font-size:clamp(0.95rem,3.8vw,1.2rem); background:rgba(40,140,40,0.8); } |
| @media (max-width:360px) { #dpad {max-width:130px;} .face-btn {width:55px;height:55px;} } |
| </style> |
| </head> |
| <body> |
|
|
| <canvas id="screen" width="240" height="160"></canvas> |
|
|
| <div id="info"> |
| <button onclick="document.getElementById('romfile').click()">Load .gba ROM</button> |
| <input id="romfile" type="file" accept=".gba" style="display:none;"> |
| <p id="status">Waiting for ROM...</p> |
| </div> |
|
|
| <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:8px; width:60%; pointer-events:auto;"> |
| <div style="display:flex; gap:10px;"> |
| <button id="l" class="shoulder">L</button> |
| <button id="r" class="shoulder">R</button> |
| </div> |
| <div style="display:flex; gap:10px;"> |
| <button id="select" class="action">Select</button> |
| <button id="start" class="action">Start</button> |
| </div> |
| <div style="display:flex; gap:18px;"> |
| <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/gbajs/gbajs@master/js/util.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/core.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/arm.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/thumb.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/mmu.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/io.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/audio.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/video.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/video/software.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/irq.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/keypad.js"></script> |
| <script src="https://cdn.jsdelivr.net/gh/gbajs/gbajs@master/js/gba.js"></script> |
|
|
| <script> |
| let gba; |
| const canvas = document.getElementById('screen'); |
| const status = document.getElementById('status'); |
| |
| try { |
| gba = new GameBoyAdvance(); |
| gba.keypad.eatInput = true; |
| gba.setCanvas(canvas); |
| gba.logLevel = 0; |
| console.log("GBA core initialized"); |
| } catch(e) { |
| status.textContent = "Emulator init failed: " + e.message; |
| alert("Cannot start emulator: " + 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 keys = { |
| up:38, down:40, left:37, right:39, |
| a:88, b:90, l:65, r:83, |
| start:13, select:16 |
| }; |
| |
| Object.entries(keys).forEach(([id, code]) => { |
| const btn = document.getElementById(id); |
| if (btn) { |
| btn.addEventListener('touchstart', e => { e.preventDefault(); press(code); }); |
| btn.addEventListener('touchend', e => { e.preventDefault(); release(code); }); |
| btn.addEventListener('touchcancel', e => { e.preventDefault(); release(code); }); |
| } |
| }); |
| |
| |
| document.getElementById('romfile').addEventListener('change', function(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| status.textContent = "Reading file... (" + (file.size / 1024 / 1024).toFixed(1) + " MB)"; |
| |
| const reader = new FileReader(); |
| reader.onload = function() { |
| try { |
| const buffer = reader.result; |
| if (buffer.byteLength < 0x4000) { |
| throw new Error("File too small – probably not a GBA ROM"); |
| } |
| |
| gba.loadRom(buffer, (success) => { |
| if (success) { |
| status.textContent = "ROM loaded! Starting..."; |
| gba.runStable(); |
| setTimeout(() => { document.getElementById('info').style.display = 'none'; }, 1500); |
| } else { |
| throw new Error("loadRom returned false"); |
| } |
| }); |
| } catch (err) { |
| status.textContent = "Load failed: " + err.message; |
| alert("Error loading ROM:\n" + err.message + "\n\nTry a different .gba file (clean dump, not zipped)."); |
| console.error(err); |
| } |
| }; |
| reader.onerror = () => { |
| status.textContent = "File read error"; |
| alert("Cannot read the file – maybe corrupted or browser blocked it."); |
| }; |
| reader.readAsArrayBuffer(file); |
| }); |
| </script> |
| </body> |
| </html> |