| <!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"/> |
| <title>GBA.js - Touch Friendly</title> |
| <link rel="stylesheet" href="resources/main.css"> |
| |
| <style> |
| body { |
| margin: 0; |
| padding: 0; |
| background: #000; |
| font-family: Arial, Helvetica, sans-serif; |
| overflow: hidden; |
| touch-action: none; |
| } |
| |
| #screen { |
| image-rendering: pixelated; |
| image-rendering: crisp-edges; |
| width: 100%; |
| height: auto; |
| max-height: 100vh; |
| display: block; |
| margin: 0 auto; |
| } |
| |
| .touch-controls { |
| position: absolute; |
| inset: 0; |
| pointer-events: none; |
| user-select: none; |
| -webkit-user-select: none; |
| touch-action: none; |
| z-index: 100; |
| display: none; |
| } |
| |
| .dpad, .face-buttons { |
| position: absolute; |
| bottom: 5%; |
| pointer-events: auto; |
| } |
| |
| .dpad { |
| left: 4%; |
| width: 140px; |
| height: 140px; |
| } |
| |
| .face-buttons { |
| right: 4%; |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 10px; |
| width: 180px; |
| } |
| |
| .dpad div, .face-buttons div { |
| background: rgba(120, 120, 120, 0.4); |
| border: 3px solid rgba(220, 220, 220, 0.7); |
| border-radius: 50%; |
| color: white; |
| font-weight: bold; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1.1rem; |
| box-shadow: 0 4px 10px rgba(0,0,0,0.5); |
| transition: all 0.08s ease; |
| } |
| |
| .dpad div:active, .face-buttons div:active { |
| transform: scale(0.92); |
| opacity: 0.75; |
| background: rgba(180, 180, 180, 0.6); |
| } |
| |
| .dpad-up { width: 60px; height: 60px; position: absolute; left: 40px; top: 0; border-radius: 12px 12px 50% 50%; } |
| .dpad-down { width: 60px; height: 60px; position: absolute; left: 40px; bottom: 0; border-radius: 50% 50% 12px 12px; } |
| .dpad-left { width: 60px; height: 60px; position: absolute; top: 40px; left: 0; border-radius: 12px 50% 50% 12px; } |
| .dpad-right { width: 60px; height: 60px; position: absolute; top: 40px; right: 0; border-radius: 50% 12px 12px 50%; } |
| .dpad-center{ width: 60px; height: 60px; position: absolute; left: 40px; top: 40px; background: transparent; border: none; pointer-events: none; } |
| |
| .btn-a { background: rgba(0, 180, 0, 0.55); grid-column: 3; grid-row: 2; border-radius: 50%; width: 70px; height: 70px; font-size: 1.4rem; } |
| .btn-b { background: rgba(220, 0, 0, 0.55); grid-column: 2; grid-row: 3; border-radius: 50%; width: 70px; height: 70px; font-size: 1.4rem; } |
| .btn-l { background: rgba(100, 100, 255, 0.5); grid-column: 1; grid-row: 1; width: 60px; height: 50px; border-radius: 10px; font-size: 1rem; } |
| .btn-r { background: rgba(100, 100, 255, 0.5); grid-column: 3; grid-row: 1; width: 60px; height: 50px; border-radius: 10px; font-size: 1rem; } |
| .btn-start { background: rgba(200, 200, 50, 0.6); grid-column: 2 / 4; grid-row: 1; width: auto; height: 44px; border-radius: 10px; font-size: 0.95rem; } |
| .btn-select { background: rgba(200, 200, 50, 0.6); grid-column: 1 / 3; grid-row: 1; width: auto; height: 44px; border-radius: 10px; font-size: 0.95rem; } |
| |
| #controls { |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| right: 0; |
| z-index: 200; |
| padding: 10px; |
| background: rgba(0,0,0,0.4); |
| color: white; |
| text-align: center; |
| } |
| |
| .bigbutton { |
| padding: 12px 24px; |
| font-size: 1.1rem; |
| margin: 6px; |
| background: #444; |
| color: white; |
| border: none; |
| border-radius: 8px; |
| cursor: pointer; |
| } |
| |
| .hidden { display: none !important; } |
| .dead { opacity: 0.3; pointer-events: none; } |
| |
| @media (orientation: landscape) and (min-width: 800px) { |
| .touch-controls { display: none !important; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <canvas id="screen" width="480" height="320"></canvas> |
|
|
| <div id="touch-controls" class="touch-controls"> |
|
|
| |
| <div class="dpad"> |
| <div class="dpad-up" data-key="ArrowUp"></div> |
| <div class="dpad-down" data-key="ArrowDown"></div> |
| <div class="dpad-left" data-key="ArrowLeft"></div> |
| <div class="dpad-right" data-key="ArrowRight"></div> |
| <div class="dpad-center"></div> |
| </div> |
|
|
| |
| <div class="face-buttons"> |
| <div class="btn btn-l" data-key="a">L</div> |
| <div class="btn btn-select"data-key="Shift">Select</div> |
| <div class="btn btn-start" data-key="Enter">Start</div> |
| <div class="btn btn-r" data-key="s">R</div> |
| <div class="btn btn-b" data-key="x">B</div> |
| <div class="btn btn-a" data-key="z">A</div> |
| </div> |
| </div> |
|
|
| <section id="controls"> |
| <div id="preload"> |
| <button class="bigbutton" id="select" onclick="document.getElementById('loader').click()">SELECT ROM</button> |
| <input id="loader" type="file" accept=".gba" onchange="run(this.files[0]);" style="display:none;"> |
| |
| <button class="bigbutton" onclick="document.getElementById('saveloader').click()">Upload Save</button> |
| <input id="saveloader" type="file" onchange="uploadSavedataPending(this.files[0]);" style="display:none;"> |
| </div> |
|
|
| <div id="ingame" class="hidden"> |
| <button id="pause" class="bigbutton" onclick="togglePause()">PAUSE</button> |
| <button class="bigbutton" onclick="reset()">RESET</button> |
| <button class="bigbutton" onclick="gba.downloadSavedata()">Download Save</button> |
| <button class="bigbutton" onclick="screenshot()">Screenshot</button> |
| |
| <label> |
| <input type="checkbox" onchange="setPixelated(this.checked)"> Pixelated |
| </label> |
| |
| <div id="sound"> |
| <label> |
| <input type="checkbox" checked onchange="gba.audio.masterEnable = this.checked"> Sound |
| </label> |
| <input type="range" min="0" max="1" value="1" step="any" onchange="setVolume(this.value)" oninput="setVolume(this.value)"> |
| </div> |
| |
| <p id="openDebug" onclick="enableDebug()">Open Debugger</p> |
| </div> |
| </section> |
|
|
| <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; |
| var runCommands = []; |
| var debug = null; |
| |
| try { |
| gba = new GameBoyAdvance(); |
| gba.keypad.eatInput = true; |
| gba.setLogger(function(level, error) { |
| console.log(error); |
| gba.pause(); |
| var screen = document.getElementById('screen'); |
| if (screen.getAttribute('class') == 'dead') return; |
| var crash = document.createElement('img'); |
| crash.setAttribute('id', 'crash'); |
| crash.setAttribute('src', 'resources/crash.png'); |
| screen.parentElement.insertBefore(crash, screen); |
| screen.setAttribute('class', 'dead'); |
| }); |
| } catch (e) { |
| gba = null; |
| } |
| |
| window.onload = function() { |
| if (gba && FileReader) { |
| var canvas = document.getElementById('screen'); |
| gba.setCanvas(canvas); |
| gba.logLevel = gba.LOG_ERROR; |
| |
| loadRom('resources/bios.bin', function(bios) { |
| gba.setBios(bios); |
| }); |
| |
| if (!gba.audio.context) { |
| var soundbox = document.getElementById('sound'); |
| if (soundbox) soundbox.parentElement.removeChild(soundbox); |
| } |
| } else { |
| var dead = document.getElementById('controls'); |
| if (dead && dead.parentElement) dead.parentElement.removeChild(dead); |
| } |
| }; |
| |
| |
| |
| |
| |
| function fadeOut(id, nextId, kill) { |
| var e = document.getElementById(id); |
| var e2 = document.getElementById(nextId); |
| if (!e) return; |
| |
| var removeSelf = function() { |
| if (kill) { |
| e.parentElement.removeChild(e); |
| } else { |
| e.setAttribute('class', 'dead'); |
| e.removeEventListener('transitionend', removeSelf); |
| } |
| if (e2) { |
| e2.classList.add('hidden'); |
| setTimeout(() => e2.classList.remove('hidden'), 0); |
| } |
| }; |
| |
| e.addEventListener('transitionend', removeSelf); |
| e.classList.add('hidden'); |
| } |
| |
| function run(file) { |
| document.getElementById('select').textContent = 'Loading...'; |
| document.getElementById('select').removeAttribute('onclick'); |
| |
| var pauseBtn = document.getElementById('pause'); |
| if (pauseBtn) pauseBtn.textContent = "PAUSE"; |
| |
| gba.loadRomFromFile(file, function(result) { |
| if (result) { |
| runCommands.forEach(cmd => cmd()); |
| runCommands = []; |
| fadeOut('preload', 'ingame'); |
| fadeOut('instructions', null, true); |
| gba.runStable(); |
| |
| |
| document.getElementById('touch-controls').style.display = 'block'; |
| } else { |
| document.getElementById('select').textContent = 'FAILED'; |
| setTimeout(() => { |
| document.getElementById('select').textContent = 'SELECT ROM'; |
| document.getElementById('select').onclick = () => document.getElementById('loader').click(); |
| }, 2000); |
| } |
| }); |
| } |
| |
| function reset() { |
| gba.pause(); |
| gba.reset(); |
| document.getElementById('select').textContent = 'SELECT ROM'; |
| var crash = document.getElementById('crash'); |
| if (crash) { |
| var ctx = gba.targetCanvas.getContext('2d'); |
| ctx.clearRect(0, 0, 480, 320); |
| gba.video.drawCallback(); |
| crash.remove(); |
| document.getElementById('screen').removeAttribute('class'); |
| } |
| document.getElementById('select').onclick = () => document.getElementById('loader').click(); |
| fadeOut('ingame', 'preload'); |
| } |
| |
| function uploadSavedataPending(file) { |
| runCommands.push(() => gba.loadSavedataFromFile(file)); |
| } |
| |
| function togglePause() { |
| var e = document.getElementById('pause'); |
| if (gba.paused) { |
| gba.runStable(); |
| e.textContent = "PAUSE"; |
| } else { |
| gba.pause(); |
| e.textContent = "UNPAUSE"; |
| } |
| } |
| |
| function screenshot() { |
| var canvas = gba.indirectCanvas; |
| window.open(canvas.toDataURL('image/png'), 'screenshot'); |
| } |
| |
| function setVolume(value) { |
| gba.audio.masterVolume = Math.pow(2, value) - 1; |
| } |
| |
| function setPixelated(pixelated) { |
| var screen = document.getElementById('screen'); |
| var ctx = screen.getContext('2d'); |
| if (ctx.imageSmoothingEnabled !== undefined) { |
| ctx.imageSmoothingEnabled = !pixelated; |
| } |
| } |
| |
| |
| |
| |
| |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| const keyMap = {}; |
| |
| function dispatchKey(type, keyName) { |
| if (!keyName) return; |
| const event = new KeyboardEvent(type, { |
| key: keyName, |
| code: keyName, |
| bubbles: true, |
| cancelable: true |
| }); |
| document.dispatchEvent(event); |
| window.dispatchEvent(event); |
| document.getElementById('screen')?.dispatchEvent(event); |
| } |
| |
| document.querySelectorAll('[data-key]').forEach(el => { |
| const key = el.dataset.key; |
| |
| el.addEventListener('touchstart', e => { |
| e.preventDefault(); |
| if (!keyMap[key]) { |
| keyMap[key] = true; |
| dispatchKey('keydown', key); |
| } |
| }, { passive: false }); |
| |
| el.addEventListener('touchend', e => { |
| e.preventDefault(); |
| if (keyMap[key]) { |
| keyMap[key] = false; |
| dispatchKey('keyup', key); |
| } |
| }, { passive: false }); |
| |
| el.addEventListener('touchcancel', e => { |
| e.preventDefault(); |
| if (keyMap[key]) { |
| keyMap[key] = false; |
| dispatchKey('keyup', key); |
| } |
| }, { passive: false }); |
| }); |
| } |
| |
| |
| function enableDebug() { |
| |
| } |
| </script> |
| </body> |
| </html> |