| <html> |
| <head> |
| <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
| <title>GBA.js - Mobile Optimized</title> |
| <link rel="stylesheet" href="resources/main.css"> |
| <style> |
| |
| body { |
| margin: 0; |
| padding: 0; |
| background-color: #000; |
| overflow: hidden; |
| touch-action: none; |
| } |
| |
| #screen { |
| display: block; |
| margin: 0 auto; |
| max-width: 100vw; |
| max-height: 100vh; |
| image-rendering: pixelated; |
| object-fit: contain; |
| } |
| |
| |
| #touchControls { |
| position: fixed; |
| bottom: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| pointer-events: none; |
| display: none; |
| z-index: 100; |
| flex-direction: row; |
| justify-content: space-between; |
| align-items: flex-end; |
| padding: 20px; |
| box-sizing: border-box; |
| user-select: none; |
| -webkit-user-select: none; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| #touchControls button { |
| pointer-events: auto; |
| border: none; |
| font-weight: bold; |
| font-family: sans-serif; |
| text-transform: uppercase; |
| transition: opacity 0.1s; |
| } |
| |
| #touchControls button:active { |
| opacity: 0.5; |
| filter: brightness(1.2); |
| } |
| |
| .dpad-btn { |
| background: rgba(255, 255, 255, 0.15); |
| color: white; |
| border-radius: 10px; |
| font-size: 24px; |
| } |
| |
| .action-btn { |
| width: 75px; |
| height: 75px; |
| border-radius: 50%; |
| background: rgba(255, 255, 255, 0.25); |
| color: white; |
| font-size: 20px; |
| } |
| |
| .shoulder-btn { |
| width: 70px; |
| height: 35px; |
| background: rgba(255, 255, 255, 0.15); |
| color: white; |
| border-radius: 5px; |
| font-size: 14px; |
| } |
| |
| .meta-btn { |
| width: 60px; |
| height: 25px; |
| background: rgba(255, 255, 255, 0.15); |
| color: white; |
| border-radius: 15px; |
| font-size: 10px; |
| } |
| </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; |
| 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') { |
| console.log('We appear to have crashed multiple times without reseting.'); |
| 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 (exception) { |
| 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); |
| } |
| |
| if (window.navigator.appName == 'Microsoft Internet Explorer') { |
| var pixelatedBox = document.getElementById('pixelated'); |
| if(pixelatedBox) pixelatedBox.parentElement.removeChild(pixelatedBox); |
| } |
| |
| if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| document.getElementById('touchControls').style.display = 'flex'; |
| } |
| |
| setupTouchControls(); |
| } else { |
| var dead = document.getElementById('controls'); |
| if(dead) 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.setAttribute('class', 'hidden'); |
| setTimeout(function() { |
| e2.removeAttribute('class'); |
| }, 0); |
| } |
| } |
| e.addEventListener('transitionend', removeSelf, false); |
| e.setAttribute('class', 'hidden'); |
| } |
| |
| function run(file) { |
| var dead = document.getElementById('loader'); |
| dead.value = ''; |
| var load = document.getElementById('select'); |
| load.textContent = 'Loading...'; |
| load.removeAttribute('onclick'); |
| var pause = document.getElementById('pause'); |
| pause.textContent = "PAUSE"; |
| gba.loadRomFromFile(file, function(result) { |
| if (result) { |
| for (var i = 0; i < runCommands.length; ++i) { |
| runCommands[i](); |
| } |
| runCommands = []; |
| fadeOut('preload', 'ingame'); |
| fadeOut('instructions', null, true); |
| gba.runStable(); |
| } else { |
| load.textContent = 'FAILED'; |
| setTimeout(function() { |
| load.textContent = 'SELECT'; |
| load.onclick = function() { document.getElementById('loader').click(); } |
| }, 3000); |
| } |
| }); |
| } |
| |
| function reset() { |
| gba.pause(); |
| gba.reset(); |
| var load = document.getElementById('select'); |
| load.textContent = 'SELECT'; |
| var crash = document.getElementById('crash'); |
| if (crash) { |
| var context = gba.targetCanvas.getContext('2d'); |
| context.clearRect(0, 0, 480, 320); |
| gba.video.drawCallback(); |
| crash.parentElement.removeChild(crash); |
| var canvas = document.getElementById('screen'); |
| canvas.removeAttribute('class'); |
| } else { |
| lcdFade(gba.context, gba.targetCanvas.getContext('2d'), gba.video.drawCallback); |
| } |
| load.onclick = function() { document.getElementById('loader').click(); } |
| fadeOut('ingame', 'preload'); |
| } |
| |
| function uploadSavedataPending(file) { |
| runCommands.push(function() { 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 lcdFade(context, target, callback) { |
| var i = 0; |
| var drawInterval = setInterval(function() { |
| i++; |
| var pixelData = context.getImageData(0, 0, 240, 160); |
| for (var y = 0; y < 160; ++y) { |
| for (var x = 0; x < 240; ++x) { |
| var xDiff = Math.abs(x - 120); |
| var yDiff = Math.abs(y - 80) * 0.8; |
| var xFactor = (120 - i - xDiff) / 120; |
| var yFactor = (80 - i - ((y & 1) * 10) - yDiff + Math.pow(xDiff, 1 / 2)) / 80; |
| pixelData.data[(x + y * 240) * 4 + 3] *= Math.pow(xFactor, 1 / 3) * Math.pow(yFactor, 1 / 2); |
| } |
| } |
| context.putImageData(pixelData, 0, 0); |
| target.clearRect(0, 0, 480, 320); |
| if (i > 40) clearInterval(drawInterval); |
| else callback(); |
| }, 50); |
| } |
| |
| function setVolume(value) { |
| gba.audio.masterVolume = Math.pow(2, value) - 1; |
| } |
| |
| function setPixelated(pixelated) { |
| var screen = document.getElementById('screen'); |
| var context = screen.getContext('2d'); |
| context.imageSmoothingEnabled = !pixelated; |
| } |
| |
| function setupTouchControls() { |
| function pressKey(code) { window.dispatchEvent(new KeyboardEvent('keydown', {keyCode: code})); } |
| function releaseKey(code) { window.dispatchEvent(new KeyboardEvent('keyup', {keyCode: code})); } |
| |
| const mapping = { |
| 'up': 38, 'down': 40, 'left': 37, 'right': 39, |
| 'a': 88, 'b': 90, 'l': 65, 'r': 83, |
| 'start': 13, 'select': 16 |
| }; |
| |
| Object.keys(mapping).forEach(id => { |
| const el = document.getElementById(id); |
| if(el) { |
| el.addEventListener('touchstart', (e) => { e.preventDefault(); pressKey(mapping[id]); }); |
| el.addEventListener('touchend', (e) => { e.preventDefault(); releaseKey(mapping[id]); }); |
| } |
| }); |
| } |
| </script> |
| </head> |
| <body> |
| <canvas id="screen" width="480" height="320"></canvas> |
|
|
| <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"> |
| </div> |
| <div id="ingame" class="hidden"> |
| <button id="pause" onclick="togglePause()">PAUSE</button> |
| <button onclick="reset()">RESET</button> |
| </div> |
| </section> |
|
|
| <div id="touchControls"> |
| <div id="dpad-container" style="display: grid; grid-template-columns: repeat(3, 65px); grid-template-rows: repeat(3, 65px); gap: 5px;"> |
| <button id="up" class="dpad-btn" style="grid-column: 2; grid-row: 1;">↑</button> |
| <button id="left" class="dpad-btn" style="grid-column: 1; grid-row: 2;">←</button> |
| <button id="right" class="dpad-btn" style="grid-column: 3; grid-row: 2;">→</button> |
| <button id="down" class="dpad-btn" style="grid-column: 2; grid-row: 3;">↓</button> |
| </div> |
|
|
| <div id="action-container" style="display: flex; flex-direction: column; align-items: flex-end; gap: 20px;"> |
| <div style="display: flex; gap: 10px;"> |
| <button id="l" class="shoulder-btn">L</button> |
| <button id="r" class="shoulder-btn">R</button> |
| </div> |
| <div style="display: flex; gap: 25px; margin-right: 10px;"> |
| <button id="b" class="action-btn">B</button> |
| <button id="a" class="action-btn">A</button> |
| </div> |
| <div style="display: flex; gap: 15px;"> |
| <button id="select" class="meta-btn">SELECT</button> |
| <button id="start" class="meta-btn">START</button> |
| </div> |
| </div> |
| </div> |
|
|
| </body> |
| </html> |
|
|