| | <html> |
| | <head> |
| | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| | <title>GBA.js - Joystick Edition</title> |
| | <link rel="stylesheet" href="resources/main.css"> |
| | <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> |
| |
|
| | <style> |
| | body, html { |
| | background: #1a1a1a; color: #fff; margin: 0; padding: 0; |
| | width: 100%; height: 100%; overflow: hidden; font-family: sans-serif; |
| | display: flex; flex-direction: column; |
| | } |
| | |
| | #screen-container { |
| | flex: 1; display: flex; align-items: center; justify-content: center; |
| | background: #000; position: relative; |
| | } |
| | |
| | canvas#screen { |
| | max-width: 100%; max-height: 100%; |
| | image-rendering: pixelated; |
| | border: 2px solid #333; |
| | } |
| | |
| | |
| | #touch-controls { |
| | height: 45vh; |
| | background: radial-gradient(circle, #2c2c2c 0%, #1a1a1a 100%); |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | position: relative; |
| | border-top: 4px solid #444; |
| | } |
| | |
| | |
| | .joystick-zone { display: flex; align-items: center; justify-content: center; position: relative; } |
| | |
| | #joystick-base { |
| | width: 150px; height: 150px; |
| | background: rgba(255, 255, 255, 0.05); |
| | border: 3px solid rgba(255, 255, 255, 0.2); |
| | border-radius: 50%; position: relative; |
| | touch-action: none; |
| | } |
| | |
| | #joystick-handle { |
| | width: 70px; height: 70px; |
| | background: linear-gradient(145deg, #555, #222); |
| | border: 2px solid #666; border-radius: 50%; |
| | position: absolute; top: 50%; left: 50%; |
| | transform: translate(-50%, -50%); |
| | box-shadow: 0 8px 15px rgba(0,0,0,0.6); |
| | pointer-events: none; |
| | } |
| | |
| | |
| | .action-zone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; } |
| | |
| | .button-group { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; transform: rotate(-10deg); } |
| | |
| | .btn-circle { |
| | width: 70px; height: 70px; border-radius: 50%; |
| | display: flex; align-items: center; justify-content: center; |
| | font-weight: bold; font-size: 20px; color: #fff; |
| | border: 4px solid rgba(0,0,0,0.3); |
| | box-shadow: 0 5px 0 #000; |
| | } |
| | |
| | .btn-a { background: #e91e63; } |
| | .btn-b { background: #ffc107; color: #000; } |
| | .btn-circle:active { transform: translateY(4px) rotate(-10deg); box-shadow: 0 1px 0 #000; } |
| | |
| | .meta-btns { display: flex; gap: 20px; margin-top: 10px; } |
| | .pill { background: #333; padding: 10px 20px; border-radius: 20px; font-size: 11px; font-weight: bold; border: 1px solid #555; } |
| | |
| | #loader-overlay { position: absolute; z-index: 5; } |
| | .hidden { display: none !important; } |
| | </style> |
| | </head> |
| | <body> |
| |
|
| | <div id="screen-container"> |
| | <canvas id="screen" width="480" height="320"></canvas> |
| | |
| | <div id="loader-overlay"> |
| | <button class="pill" style="padding: 20px; font-size: 16px; color: #00eaff; border-color: #00eaff;" onclick="document.getElementById('loader').click()">SELECT ROM</button> |
| | <input id="loader" type="file" accept=".gba" class="hidden" onchange="run(this.files[0]);"> |
| | </div> |
| | </div> |
| |
|
| |
|
| | <div id="touch-controls"> |
| | <div class="joystick-zone"> |
| | <div id="joystick-base"> |
| | <div id="joystick-handle"></div> |
| | </div> |
| | </div> |
| |
|
| | <div class="action-zone"> |
| | <div class="button-group"> |
| | <div class="btn-circle btn-b" data-key="5">B</div> |
| | <div class="btn-circle btn-a" data-key="4">A</div> |
| | </div> |
| | |
| | <div class="meta-btns"> |
| | <div class="pill" data-key="10">SELECT</div> |
| | <div class="pill" data-key="11">START</div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | var gba; |
| | try { |
| | gba = new GameBoyAdvance(); |
| | gba.keypad.eatInput = true; |
| | gba.setCanvas(document.getElementById('screen')); |
| | loadRom('resources/bios.bin', function(bios) { gba.setBios(bios); }); |
| | } catch (e) { gba = null; } |
| | |
| | function run(file) { |
| | gba.loadRomFromFile(file, function(result) { |
| | if (result) { |
| | document.getElementById('loader-overlay').classList.add('hidden'); |
| | gba.runStable(); |
| | } |
| | }); |
| | } |
| | |
| | |
| | const base = document.getElementById('joystick-base'); |
| | const handle = document.getElementById('joystick-handle'); |
| | const centerX = 75; |
| | const centerY = 75; |
| | const limit = 55; |
| | |
| | let activeDirections = { 0: false, 1: false, 2: false, 3: false }; |
| | |
| | base.addEventListener('touchstart', moveJoystick, {passive: false}); |
| | base.addEventListener('touchmove', moveJoystick, {passive: false}); |
| | base.addEventListener('touchend', endJoystick, {passive: false}); |
| | |
| | function moveJoystick(e) { |
| | e.preventDefault(); |
| | const touch = e.touches[0]; |
| | const rect = base.getBoundingClientRect(); |
| | let x = touch.clientX - rect.left - centerX; |
| | let y = touch.clientY - rect.top - centerY; |
| | |
| | const distance = Math.sqrt(x*x + y*y); |
| | if (distance > limit) { |
| | x = x * (limit / distance); |
| | y = y * (limit / distance); |
| | } |
| | |
| | handle.style.left = (centerX + x) + 'px'; |
| | handle.style.top = (centerY + y) + 'px'; |
| | |
| | |
| | const deadzone = 15; |
| | updateKey(2, x < -deadzone); |
| | updateKey(3, x > deadzone); |
| | updateKey(0, y < -deadzone); |
| | updateKey(1, y > deadzone); |
| | } |
| | |
| | function endJoystick(e) { |
| | handle.style.left = '50%'; |
| | handle.style.top = '50%'; |
| | for (let key in activeDirections) updateKey(key, false); |
| | } |
| | |
| | function updateKey(key, isPressed) { |
| | if (isPressed && !activeDirections[key]) { |
| | gba.keypad.keydown(key); |
| | activeDirections[key] = true; |
| | } else if (!isPressed && activeDirections[key]) { |
| | gba.keypad.keyup(key); |
| | activeDirections[key] = false; |
| | } |
| | } |
| | |
| | |
| | document.querySelectorAll('[data-key]').forEach(btn => { |
| | const key = parseInt(btn.getAttribute('data-key')); |
| | btn.addEventListener('touchstart', (e) => { |
| | e.preventDefault(); |
| | gba.keypad.keydown(key); |
| | if(navigator.vibrate) navigator.vibrate(15); |
| | }); |
| | btn.addEventListener('touchend', (e) => { |
| | e.preventDefault(); |
| | gba.keypad.keyup(key); |
| | }); |
| | }); |
| | </script> |
| | </body> |
| | </html> |
| |
|