| | <!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 Controls</title> |
| |
|
| | <style> |
| | body { |
| | margin: 0; |
| | padding: 0; |
| | background: #000; |
| | font-family: Arial, Helvetica, sans-serif; |
| | overflow: hidden; |
| | height: 100vh; |
| | touch-action: none; |
| | } |
| | |
| | #screen { |
| | image-rendering: pixelated; |
| | image-rendering: -moz-crisp-edges; |
| | 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.45); |
| | 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.12s ease; |
| | } |
| | |
| | .dpad div:active, .face-buttons div:active, |
| | .dpad div.pressed, .face-buttons div.pressed { |
| | transform: scale(0.88); |
| | opacity: 0.65; |
| | background: rgba(220, 220, 220, 0.75) !important; |
| | } |
| | |
| | .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-keycode="38"></div> |
| | <div class="dpad-down" data-keycode="40"></div> |
| | <div class="dpad-left" data-keycode="37"></div> |
| | <div class="dpad-right" data-keycode="39"></div> |
| | <div class="dpad-center"></div> |
| | </div> |
| |
|
| | |
| | <div class="face-buttons"> |
| | <div class="btn btn-l" data-keycode="65">L</div> |
| | <div class="btn btn-r" data-keycode="83">R</div> |
| | <div class="btn btn-select"data-keycode="220">Select</div> |
| | <div class="btn btn-start" data-keycode="13">Start</div> |
| | <div class="btn btn-b" data-keycode="88">B</div> |
| | <div class="btn btn-a" data-keycode="90">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 style="color:white; margin:0 12px;"> |
| | <input type="checkbox" onchange="setPixelated(this.checked)"> Pixelated |
| | </label> |
| |
|
| | <div id="sound" style="display:inline-block; color:white;"> |
| | <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()" style="cursor:pointer; color:#88f; margin:8px 0;">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 = null; |
| | var runCommands = []; |
| | var debug = null; |
| | |
| | try { |
| | gba = new GameBoyAdvance(); |
| | gba.keypad.eatInput = true; |
| | |
| | gba.setLogger(function(level, error) { |
| | console.error("Emulator error:", error); |
| | gba.pause(); |
| | const screen = document.getElementById('screen'); |
| | if (screen.className === 'dead') return; |
| | const crash = document.createElement('img'); |
| | crash.id = 'crash'; |
| | crash.src = 'resources/crash.png'; |
| | screen.parentElement.insertBefore(crash, screen); |
| | screen.className = 'dead'; |
| | }); |
| | } catch (e) { |
| | console.error("Failed to initialize GBA emulator:", e); |
| | gba = null; |
| | } |
| | |
| | window.onload = function() { |
| | if (gba && FileReader) { |
| | gba.setCanvas(document.getElementById('screen')); |
| | gba.logLevel = gba.LOG_ERROR || 1; |
| | |
| | |
| | |
| | |
| | if (!gba.audio?.context) { |
| | const soundbox = document.getElementById('sound'); |
| | if (soundbox) soundbox.style.display = 'none'; |
| | } |
| | } else { |
| | const controls = document.getElementById('controls'); |
| | if (controls) controls.remove(); |
| | } |
| | }; |
| | |
| | |
| | |
| | |
| | |
| | function fadeOut(id, nextId, kill) { |
| | const e = document.getElementById(id); |
| | if (!e) return; |
| | const e2 = document.getElementById(nextId); |
| | |
| | const removeSelf = () => { |
| | if (kill) e.remove(); |
| | else { |
| | e.className = 'dead'; |
| | e.removeEventListener('transitionend', removeSelf); |
| | } |
| | if (e2) { |
| | e2.classList.add('hidden'); |
| | setTimeout(() => e2.classList.remove('hidden'), 20); |
| | } |
| | }; |
| | |
| | e.addEventListener('transitionend', removeSelf); |
| | e.classList.add('hidden'); |
| | } |
| | |
| | function run(file) { |
| | const selectBtn = document.getElementById('select'); |
| | selectBtn.textContent = 'Loading...'; |
| | selectBtn.removeAttribute('onclick'); |
| | |
| | const pauseBtn = document.getElementById('pause'); |
| | if (pauseBtn) pauseBtn.textContent = "PAUSE"; |
| | |
| | gba.loadRomFromFile(file, result => { |
| | if (result) { |
| | runCommands.forEach(cmd => cmd()); |
| | runCommands = []; |
| | fadeOut('preload', 'ingame'); |
| | gba.runStable(); |
| | |
| | |
| | document.getElementById('touch-controls').style.display = 'block'; |
| | } else { |
| | selectBtn.textContent = 'FAILED'; |
| | setTimeout(() => { |
| | selectBtn.textContent = 'SELECT ROM'; |
| | selectBtn.onclick = () => document.getElementById('loader').click(); |
| | }, 2000); |
| | } |
| | }); |
| | } |
| | |
| | function reset() { |
| | if (!gba) return; |
| | gba.pause(); |
| | gba.reset(); |
| | document.getElementById('select').textContent = 'SELECT ROM'; |
| | const crash = document.getElementById('crash'); |
| | if (crash) { |
| | const ctx = gba.targetCanvas?.getContext('2d'); |
| | if (ctx) ctx.clearRect(0,0,480,320); |
| | gba.video?.drawCallback?.(); |
| | crash.remove(); |
| | document.getElementById('screen').className = ''; |
| | } |
| | document.getElementById('select').onclick = () => document.getElementById('loader').click(); |
| | fadeOut('ingame', 'preload'); |
| | } |
| | |
| | function uploadSavedataPending(file) { |
| | runCommands.push(() => gba?.loadSavedataFromFile?.(file)); |
| | } |
| | |
| | function togglePause() { |
| | if (!gba) return; |
| | const e = document.getElementById('pause'); |
| | if (gba.paused) { |
| | gba.runStable(); |
| | e.textContent = "PAUSE"; |
| | } else { |
| | gba.pause(); |
| | e.textContent = "UNPAUSE"; |
| | } |
| | } |
| | |
| | function screenshot() { |
| | if (!gba?.indirectCanvas) return; |
| | window.open(gba.indirectCanvas.toDataURL('image/png'), 'gba-screenshot'); |
| | } |
| | |
| | function setVolume(value) { |
| | if (gba?.audio) gba.audio.masterVolume = Math.pow(2, value) - 1; |
| | } |
| | |
| | function setPixelated(pixelated) { |
| | const screen = document.getElementById('screen'); |
| | const ctx = screen.getContext('2d'); |
| | if (ctx) ctx.imageSmoothingEnabled = !pixelated; |
| | } |
| | |
| | |
| | |
| | |
| | |
| | if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| | const keypad = gba?.keypad; |
| | |
| | if (!keypad) { |
| | console.warn("GBA keypad not available β touch controls disabled"); |
| | } else { |
| | const keyMap = { |
| | "38": keypad.KEYCODE_UP || 6, |
| | "40": keypad.KEYCODE_DOWN || 7, |
| | "37": keypad.KEYCODE_LEFT || 5, |
| | "39": keypad.KEYCODE_RIGHT || 4, |
| | "13": keypad.KEYCODE_START || 3, |
| | "220": keypad.KEYCODE_SELECT || 2, |
| | "90": keypad.KEYCODE_A || 0, |
| | "88": keypad.KEYCODE_B || 1, |
| | "65": keypad.KEYCODE_L || 9, |
| | "83": keypad.KEYCODE_R || 8 |
| | }; |
| | |
| | const pressed = new Set(); |
| | |
| | document.querySelectorAll('[data-keycode]').forEach(el => { |
| | const dataCode = el.dataset.keycode; |
| | const bitIndex = keyMap[dataCode]; |
| | |
| | if (bitIndex === undefined) return; |
| | |
| | const toggle = 1 << bitIndex; |
| | |
| | el.addEventListener('touchstart', e => { |
| | e.preventDefault(); |
| | if (!pressed.has(dataCode)) { |
| | pressed.add(dataCode); |
| | keypad.currentDown &= ~toggle; |
| | } |
| | el.classList.add('pressed'); |
| | }, { passive: false }); |
| | |
| | el.addEventListener('touchend', e => { |
| | e.preventDefault(); |
| | if (pressed.has(dataCode)) { |
| | pressed.delete(dataCode); |
| | keypad.currentDown |= toggle; |
| | } |
| | el.classList.remove('pressed'); |
| | }, { passive: false }); |
| | |
| | el.addEventListener('touchcancel', e => { |
| | e.preventDefault(); |
| | if (pressed.has(dataCode)) { |
| | pressed.delete(dataCode); |
| | keypad.currentDown |= toggle; |
| | } |
| | el.classList.remove('pressed'); |
| | }, { passive: false }); |
| | }); |
| | |
| | console.log("Touch controls enabled via direct keypad manipulation"); |
| | } |
| | } |
| | |
| | |
| | function enableDebug() { |
| | console.log("Debugger requested (implement if needed)"); |
| | } |
| | </script> |
| | </body> |
| | </html> |