| | <!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, viewport-fit=cover"> |
| | <title>GBA.js - Touch Enabled</title> |
| |
|
| | |
| | <link rel="stylesheet" href="resources/main.css"> |
| |
|
| | |
| | <style> |
| | * { margin:0; padding:0; box-sizing:border-box; } |
| | html, body { |
| | height:100%; |
| | overflow:hidden; |
| | background:#000; |
| | touch-action: manipulation; |
| | font-family: sans-serif; |
| | } |
| | #screen-container { |
| | position: relative; |
| | width: 100%; |
| | height: 100%; |
| | display: flex; |
| | justify-content: center; |
| | align-items: center; |
| | background: #111; |
| | } |
| | #screen { |
| | width: 100%; |
| | height: auto; |
| | max-width: 100%; |
| | max-height: 100%; |
| | image-rendering: pixelated; |
| | touch-action: none; |
| | } |
| | #controls { |
| | position: absolute; |
| | top: 10px; |
| | left: 10px; |
| | z-index: 20; |
| | color: white; |
| | background: rgba(0,0,0,0.6); |
| | padding: 10px; |
| | border-radius: 8px; |
| | display: none; |
| | } |
| | #controls.visible { display: block; } |
| | .bigbutton { |
| | padding: 10px 20px; |
| | font-size: 16px; |
| | margin: 5px; |
| | } |
| | #touchControls { |
| | position: absolute; |
| | inset: 0; |
| | pointer-events: none; |
| | display: none; |
| | z-index: 10; |
| | padding: 3vmin; |
| | } |
| | button { |
| | font-weight: bold; |
| | color: white; |
| | border: none; |
| | border-radius: 50%; |
| | opacity: 0.65; |
| | touch-action: manipulation; |
| | -webkit-tap-highlight-color: transparent; |
| | user-select: none; |
| | } |
| | button:active { opacity: 0.95; } |
| | #dpad { |
| | display: grid; |
| | grid-template-columns: repeat(3, 20vmin); |
| | grid-template-rows: repeat(3, 20vmin); |
| | gap: 1.5vmin; |
| | } |
| | #faceButtons { |
| | display: flex; |
| | flex-direction: column; |
| | align-items: flex-end; |
| | gap: 3vmin; |
| | } |
| | .shoulder { |
| | width: 20vmin; |
| | height: 12vmin; |
| | border-radius: 12vmin; |
| | background: rgba(220,220,220,0.6); |
| | font-size: 4vmin; |
| | } |
| | .startselect { |
| | width: 20vmin; |
| | height: 12vmin; |
| | border-radius: 10vmin; |
| | background: rgba(0,200,0,0.6); |
| | font-size: 3.5vmin; |
| | } |
| | #a { background: rgba(255,60,60,0.8); width: 24vmin; height: 24vmin; font-size: 6vmin; } |
| | #b { background: rgba(60,60,255,0.8); width: 24vmin; height: 24vmin; font-size: 6vmin; } |
| | |
| | |
| | @media (orientation: landscape) { |
| | #touchControls { |
| | display: flex; |
| | flex-direction: row; |
| | justify-content: space-between; |
| | align-items: flex-end; |
| | } |
| | #dpad { grid-template-columns: repeat(3, 16vmin); grid-template-rows: repeat(3, 16vmin); } |
| | #faceButtons { flex-direction: row; align-items: flex-end; gap: 5vmin; } |
| | #faceButtons > div { flex-direction: column; } |
| | } |
| | |
| | |
| | @media (orientation: portrait) { |
| | #touchControls { |
| | display: flex; |
| | flex-direction: column; |
| | justify-content: flex-end; |
| | gap: 4vmin; |
| | } |
| | #dpad { align-self: flex-start; } |
| | #faceButtons { align-self: flex-end; } |
| | } |
| | </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.className === 'dead') return; |
| | var crash = document.createElement('img'); |
| | crash.id = 'crash'; |
| | crash.src = 'resources/crash.png'; |
| | screen.parentElement.insertBefore(crash, screen); |
| | screen.className = '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) { |
| | if (bios) gba.setBios(bios); |
| | }); |
| | |
| | if (!gba.audio.context) { |
| | var soundbox = document.getElementById('sound'); |
| | if (soundbox) soundbox.parentElement.removeChild(soundbox); |
| | } |
| | |
| | if (navigator.userAgent.indexOf('MSIE') !== -1 || !!document.documentMode) { |
| | var pixelatedBox = document.getElementById('pixelated'); |
| | if (pixelatedBox) pixelatedBox.parentElement.removeChild(pixelatedBox); |
| | } |
| | |
| | |
| | if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { |
| | document.getElementById('touchControls').style.display = 'flex'; |
| | canvas.style.maxHeight = '75vh'; |
| | } |
| | |
| | setupTouchControls(); |
| | } else { |
| | var ctrls = document.getElementById('controls'); |
| | if (ctrls) ctrls.parentElement.removeChild(ctrls); |
| | } |
| | }; |
| | |
| | function loadRom(url, callback) { |
| | var xhr = new XMLHttpRequest(); |
| | xhr.open('GET', url, true); |
| | xhr.responseType = 'arraybuffer'; |
| | xhr.onload = function() { |
| | if (xhr.status === 200) callback(new Uint8Array(xhr.response)); |
| | else callback(null); |
| | }; |
| | xhr.send(); |
| | } |
| | |
| | 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.className = 'dead'; |
| | e.removeEventListener('transitionend', removeSelf); |
| | } |
| | if (e2) { |
| | e2.className = 'hidden'; |
| | setTimeout(() => e2.className = '', 0); |
| | } |
| | }; |
| | e.addEventListener('transitionend', removeSelf); |
| | e.className = 'hidden'; |
| | } |
| | |
| | function run(file) { |
| | document.getElementById('loader').value = ''; |
| | var loadBtn = document.getElementById('select'); |
| | loadBtn.textContent = 'Loading...'; |
| | loadBtn.removeAttribute('onclick'); |
| | document.getElementById('pause').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('controls').classList.add('visible'); |
| | } else { |
| | loadBtn.textContent = 'FAILED'; |
| | setTimeout(() => { |
| | loadBtn.textContent = 'SELECT'; |
| | loadBtn.onclick = () => document.getElementById('loader').click(); |
| | }, 3000); |
| | } |
| | }); |
| | } |
| | |
| | function reset() { |
| | gba.pause(); |
| | gba.reset(); |
| | document.getElementById('select').textContent = 'SELECT'; |
| | var crash = document.getElementById('crash'); |
| | if (crash) { |
| | crash.parentElement.removeChild(crash); |
| | 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() { |
| | var btn = document.getElementById('pause'); |
| | if (gba.paused) { |
| | gba.runStable(); |
| | btn.textContent = "PAUSE"; |
| | } else { |
| | gba.pause(); |
| | btn.textContent = "UNPAUSE"; |
| | } |
| | } |
| | |
| | function screenshot() { |
| | window.open(gba.indirectCanvas.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; |
| | } |
| | } |
| | |
| | function setupTouchControls() { |
| | 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 |
| | }; |
| | |
| | ['up','down','left','right','a','b','l','r','start','select'].forEach(key => { |
| | const el = document.getElementById(key); |
| | if (!el) return; |
| | el.addEventListener('touchstart', e => { e.preventDefault(); press(map[key]); }); |
| | el.addEventListener('touchend', e => { e.preventDefault(); release(map[key]); }); |
| | el.addEventListener('touchcancel',e => { e.preventDefault(); release(map[key]); }); |
| | }); |
| | } |
| | </script> |
| | </head> |
| | <body> |
| |
|
| | <div id="screen-container"> |
| | <canvas id="screen" width="480" height="320"></canvas> |
| | </div> |
| |
|
| | |
| | <section id="controls"> |
| | <div id="preload"> |
| | <button class="bigbutton" id="select" onclick="document.getElementById('loader').click()">SELECT</button> |
| | <input id="loader" type="file" accept=".gba" onchange="run(this.files[0])" style="display:none"> |
| | <button 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 onclick="gba.downloadSavedata()">Download Save</button> |
| | <button onclick="screenshot()">Screenshot</button> |
| | <label id="pixelated"> |
| | <input type="checkbox" onchange="setPixelated(this.checked)"> Pixelated |
| | </label> |
| | <div id="sound"> |
| | <input type="checkbox" checked onchange="gba.audio.masterEnable = this.checked"> Sound |
| | <input type="range" min="0" max="1" value="1" step="any" oninput="setVolume(this.value)"> |
| | </div> |
| | </div> |
| | </section> |
| |
|
| | |
| | <div id="touchControls"> |
| | |
| | <div id="dpad"> |
| | <button id="up">β</button> |
| | <button id="left">β</button> |
| | <button id="right">β</button> |
| | <button id="down">β</button> |
| | </div> |
| |
|
| | |
| | <div id="faceButtons"> |
| | <div> |
| | <button id="l" class="shoulder">L</button> |
| | <button id="r" class="shoulder">R</button> |
| | </div> |
| | <div> |
| | <button id="select" class="startselect">Select</button> |
| | <button id="start" class="startselect">Start</button> |
| | </div> |
| | <div> |
| | <button id="b">B</button> |
| | <button id="a">A</button> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | </body> |
| | </html> |