agb/examples/the-purple-night/html/emu.html

1039 lines
34 KiB
HTML
Raw Normal View History

2022-01-01 23:05:48 +11:00
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.2" />
<meta name="title" content="gba.ninja - Gameboy Advance emulator in the browser" />
<meta name="description"
content="Run Gameboy Advance games in a web browser. Based on the VisualBoyAdvance-M emulator." />
<title>gba.ninja</title>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script>function gtag() { }</script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("js", new Date());
gtag("config", "UA-45495852-6");
</script>
<script>
if (navigator.serviceWorker) {
navigator.serviceWorker.register("./sw.js", { scope: "./", });
navigator.serviceWorker.addEventListener("message", event => {
var msg = event.data.msg;
switch (msg.name) {
default:
console.log("unknown sw message", event);
gtag("event", "unknown_sw_message_1", {
name: msg.name,
});
return;
}
});
}
</script>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0px;
font-family: sans-serif;
background-color: #f3ddff;
}
canvas {
right: 0px;
left: 0px;
bottom: 0px;
top: 0px;
position: absolute;
width: 100%;
height: 100%;
}
</style>
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
varying highp vec2 v_textureCoord;
void main() {
/*
* This scales the quad so that the screen texture fits the viewport.
* The texture is 256 * 256, but only 240 * 160 is used. The quad is 2*2, centered on (0,0)
*/
gl_Position = vec4((a_position.x * 2.0 * 1.0666) - 1.0, (a_position.y * 2.0 * 1.6) * -1.0 + 1.0, 0, 1);
v_textureCoord = vec2(a_position.x, a_position.y);
}
</script>
<script id="2d-fragment-shader" type="x-shader/x-fragment">
varying highp vec2 v_textureCoord;
uniform sampler2D u_sampler;
void main(void) {
gl_FragColor = texture2D(u_sampler, vec2(v_textureCoord.s, v_textureCoord.t));
}
</script>
<script>
window.onerror = function (messageOrEvent, source, lineno, colno, error) {
try {
var str = "";
if (typeof messageOrEvent === "object") {
str += "Event: " + messageOrEvent.type + " " + messageOrEvent.message + " ;";
} else {
str += messageOrEvent + "; ";
}
if (source) {
str += " Source: " + source + "; ";
}
if (lineno !== void 0) {
str += " Line: " + lineno + "; ";
}
if (colno !== void 0) {
str += " Col: " + colno + "; ";
}
if (error) {
str += " Message: " + error.message + "; ";
try {
str += " StackTop: " + error.stack.split(/\n/g)[1].trim() + "; ";
} catch (e) { }
}
console.log("Remote logged: ", str);
gtag("event", "exception", {
description: str,
});
} catch (e) {
console.error(e);
}
};
</script>
<script>
"use strict";
try {
void new Image("/logo.png");
} catch (e) {
// Not sure why but on some browsers this crashes.
}
var qs = {
autorun: "./thepurplenight.gba", exclusive: true,
};
function escapeHtml(string) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#039;',
};
return string.replace(/[&<>"']/g, function (s) {
return entityMap[s] || s;
});
};
function unescapeHtml(string) {
var reverseEntityMap = {
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
'&quot;': '"',
'&#039;': "'",
};
return string.replace(/&.+?;/g, function (s) {
return reverseEntityMap[s] || s;
});
};
// Disable backspace navigation
var backspaceHandler = function (e) {
if (e.which === 8) {
if (!/INPUT|SELECT|TEXTAREA/i.test(e.target.tagName) || e.target.disabled || e.target.readOnly) {
e.preventDefault();
}
}
}.bind(this);
document.addEventListener("keydown", backspaceHandler);
document.addEventListener("keypress", backspaceHandler);
// Shim performance.now
window.performance = window.performance || {};
performance.now = (function () {
return performance.now ||
performance.mozNow ||
performance.msNow ||
performance.oNow ||
performance.webkitNow ||
function () {
return new Date().getTime();
};
})();
// Shim localstorage
if (!window.localStorage) {
window.localStorage = {};
}
try {
localStorage._ = 1;
} catch (e) {
window.isShittyLocalstorage = true;
}
var readyHandlers = {};
var onReady = function (what, fn) {
if (readyHandlers[what] === null) {
setTimeout(function () { fn(); });
return;
}
if (readyHandlers[what] === void 0) {
readyHandlers[what] = [];
}
readyHandlers[what].push(fn);
};
var triggerReady = function (what) {
if (readyHandlers[what]) {
readyHandlers[what].forEach(function (v) {
v();
});
}
readyHandlers[what] = null;
};
function isPowerOf2(x) {
return (x != 0) && ((x & (x - 1)) == 0);
}
var modalEls;
var modalRefcount = 0;
function modal(text, options) {
modalRefcount++;
modalEls = modalEls || {
modal: document.querySelector(".modal"),
modalTitle: document.querySelector(".modal-title"),
modalTitleText: document.querySelector(".modal-title").childNodes[0],
modalText: document.querySelector(".modal-text"),
modalTextText: document.querySelector(".modal-text").childNodes[0],
modalLeftButton: document.querySelector(".modal-button-left"),
modalLeftButtonText: document.querySelector(".modal-button-left").childNodes[0],
modalRightButton: document.querySelector(".modal-button-right"),
modalRightButtonText: document.querySelector(".modal-button-right").childNodes[0],
modalProgress: document.querySelector(".modal-progress"),
modalInput: document.querySelector(".modal-input"),
};
var removeEvents;
function hideModal() {
modalRefcount--;
if (modalRefcount <= 0) {
modalRefcount = 0;
document.body.style.overflow = "";
}
modalEls.modal.style.display = "none";
removeEvents();
}
function setProgress(n) {
modalEls.modalProgress.style.display = "block";
modalEls.modalProgress.style.width = n + "%";
}
function getInputValue() {
return modalEls.modalInput.value;
}
options = options || {};
options = {
title: options.title || null,
text: text,
leftButtonText: options.leftButtonText || "OK",
leftButtonFn: options.hasOwnProperty("leftButtonFn") ?
options.leftButtonFn : function () { },
rightButtonText: options.rightButtonText || "OK",
rightButtonFn: options.rightButtonFn || null,
input: typeof options.input === "string" ? options.input : null,
};
modalEls.modal.style.display = "block";
document.body.style.overflow = "hidden";
window.scrollTo(0, 0);
if (options.title) {
modalEls.modalTitle.style.display = "block";
modalEls.modalTitleText.textContent = options.title;
} else {
modalEls.modalTitle.style.display = "none";
}
modalEls.modalTextText.textContent = options.text;
modalEls.modalLeftButtonText.textContent = options.leftButtonText;
modalEls.modalRightButtonText.textContent = options.rightButtonText;
if (options.leftButtonFn) {
modalEls.modalLeftButton.style.display = "";
} else {
modalEls.modalLeftButton.style.display = "none";
}
if (options.rightButtonFn) {
modalEls.modalRightButton.style.display = "";
} else {
modalEls.modalRightButton.style.display = "none";
}
modalEls.modalProgress.style.display = "none";
modalEls.modalInput.value = "";
if (typeof options.input === "string") {
modalEls.modalInput.style.display = "";
} else {
modalEls.modalInput.style.display = "none";
}
var leftHandler = function () {
if (!options.leftButtonFn || options.leftButtonFn() !== false) {
hideModal();
}
};
modalEls.modalLeftButton.addEventListener("click", leftHandler);
var rightHandler = function () {
if (!options.rightButtonFn || options.rightButtonFn() !== false) {
hideModal();
}
};
modalEls.modalRightButton.addEventListener("click", rightHandler);
removeEvents = function () {
modalEls.modalLeftButton.removeEventListener("click", leftHandler);
modalEls.modalRightButton.removeEventListener("click", rightHandler);
};
return {
hideModal: hideModal,
setProgress: setProgress,
getInputValue: getInputValue,
};
}
function stringToCharCodes(str) {
return str.split("").map(function (c) {
return c.charCodeAt(0);
});
}
var emuReady = false;
function hasEmuModule() {
return !!emuReady;
}
var romBuffer8 = null;
window.loadRomFromBuffer = function (_romBuffer8, filename) {
var errorOpts = { title: "Error" };
if (_romBuffer8.length < 512) {
gtag("event", "load_tiny_rom_1", {
event_label: filename,
});
return modal("That file isn't a GBA ROM. (It's too small to be a ROM.)", errorOpts);
}
// Check if it's a real rom
var romCode = String.fromCharCode(
_romBuffer8[0xAC], _romBuffer8[0xAD], _romBuffer8[0xAE], _romBuffer8[0xAF]
);
var gbMagic = [
_romBuffer8[0x0104], _romBuffer8[0x0105], _romBuffer8[0x0106], _romBuffer8[0x0107],
_romBuffer8[0x0108], _romBuffer8[0x0109], _romBuffer8[0x010A], _romBuffer8[0x010B],
].map(function (v) {
return v.toString(16);
}).join();
if (filename.search(/\.zip$/i) !== -1) {
gtag("event", "load_zip_rom_1", {
event_label: filename,
});
return modal("You need to extract the rom file from the zip.", errorOpts);
}
if (String.fromCharCode(_romBuffer8[0], _romBuffer8[1]) === "PK") {
gtag("event", "load_zip_rom_1", {
event_label: filename + " (non-dot-zip-file)",
});
return modal("You need to extract the rom file.", errorOpts);
}
if (filename.search(/\.sav$/i) !== -1) {
gtag("event", "load_sav_rom_1", {
event_label: filename,
});
return modal("That's not a ROM, it's a savegame file. GBA ROM files usually end in '.gba'.", errorOpts);
}
if (filename.search(/\.smc$/i) !== -1 || filename.search(/\.sfc$/i) !== -1) {
gtag("event", "load_smc_rom_1", {
event_label: filename,
});
return modal("That's a SNES ROM, this emulator runs Gameboy Advance ROMs.", errorOpts);
}
if (gbMagic === "ce,ed,66,66,cc,d,0,b") {
gtag("event", "load_gb_rom_1", {
event_label: filename,
});
var colorMaybe = "";
if (filename.search(/\.gbc$/i) !== -1) {
colorMaybe = "Color ";
}
return modal("That's a Gameboy " + colorMaybe + "ROM, this emulator only runs Gameboy Advance ROMs.", errorOpts);
}
if (!isPowerOf2(_romBuffer8.length)) {
// Some roms are actually non-pot, so don't enforce this.
gtag("event", "non_pot_rom_1", {
event_label: stringToCharCodes(romCode) + " " + filename + " " + _romBuffer8.length,
});
// Don't return
}
function ok() {
romBuffer8 = _romBuffer8;
triggerReady("cartridge");
}
function waitForEmuLoad() {
if (hasEmuModule()) {
ok();
} else {
gtag("event", "emu_file_missing_at_load_rom_1", {});
var interval;
var modalOpts = modal("The emulator module isn't loaded yet. Give it a moment.", {
title: "Waiting For Emulator Module",
leftButtonText: "Back",
leftButtonFn: function () {
clearInterval(interval);
},
});
function progress() {
if (window.emuScriptProgress === -1) {
modalOpts.hideModal();
return modal("There was an error while loading the emulator module. You'll need to refresh the page.", {
title: "Error",
leftButtonText: "Ok",
leftButtonFn: function () {
clearInterval(interval);
},
});
} else {
modalOpts.setProgress(window.emuScriptProgress);
}
}
progress();
interval = setInterval(function () {
progress();
if (window.emuScriptProgress >= 100) {
clearInterval(interval);
modalOpts.hideModal();
ok();
}
}, 100);
}
}
if (romCode.search(/^[A-Z0-9]{4}$/) && !qs.ignoreInvalidRomCode) {
gtag("event", "invalid_rom_code_1", {
event_label: stringToCharCodes(romCode) + " " + filename,
});
return modal("That file doesn't look like a GBA ROM. (Couldn't find a rom code in the file.)", {
title: "Error",
rightButtonText: "Run it anyway",
rightButtonFn: waitForEmuLoad,
});
} else {
waitForEmuLoad();
}
};
window.loadRomFromFile = function (e) {
var binaryFile = e.currentTarget.files[0];
var filename = binaryFile.name;
e.currentTarget.form.reset();
if (!binaryFile) {
return;
}
var modalOpts = modal(filename, {
title: "Loading File",
leftButtonFn: null,
});
var fr = new FileReader();
fr.readAsArrayBuffer(binaryFile);
fr.onload = function () {
modalOpts.hideModal();
var _romBuffer8 = new Uint8Array(fr.result);
loadRomFromBuffer(_romBuffer8, binaryFile.name);
};
};
window.loadRomFromNetwork = function (url) {
var xhr = new XMLHttpRequest();
let loadingModalSettings = {
title: "Loading",
leftButtonText: "Cancel",
leftButtonFn: function () {
xhr.abort();
}
};
if (qs.exclusive) {
loadingModalSettings.leftButtonFn = null;
}
var modalOpts = modal("Loading " + url, loadingModalSettings);
modalOpts.setProgress(0);
xhr.onload = function (e) {
modalOpts.hideModal();
window.loadRomFromBuffer(new Uint8Array(xhr.response), url);
};
xhr.onprogress = function (e) {
modalOpts.setProgress((e.loaded / e.total) * 100);
};
xhr.onerror = function (e) {
modalOpts.hideModal();
let errorModalSettings = {
title: "Error",
leftButtonText: "Ok",
};
if (qs.exclusive) {
errorModalSettings.leftButtonFn = function () {
location.reload();
}
errorModalSettings.leftButtonText = "Reload Page";
}
modal("There was an error loading the ROM.", errorModalSettings);
};
xhr.open("GET", url);
xhr.responseType = "arraybuffer";
xhr.send();
};
window.gbaninja = {
onRuntimeInitialized: function () {
triggerReady("emu");
},
};
document.addEventListener("DOMContentLoaded", function () {
triggerReady("document");
});
document.addEventListener("mousedown", function () {
if (window.vbaSound.audioCtx.state === "suspended") {
window.vbaSound.audioCtx.resume();
}
});
onReady("document", function () {
if (window.init) {
window.init();
} else {
document.querySelector(".pixels").innerHTML = "<p style='margin: 20px;'>A required file failed to load.</p>";
}
if (qs.autorun) {
loadRomFromNetwork(qs.autorun);
}
});
onReady("emu", function () {
emuReady = true;
onReady("document", function () {
onReady("cartridge", function () {
window.start();
});
});
});
// ------ VBA ENTRY POINTS -------
var VBAInterface = {};
VBAInterface.VBA_get_emulating = function () {
return gbaninja.ccall("VBA_get_emulating", "int", [], []);
};
VBAInterface.VBA_start = function () {
return gbaninja.ccall("VBA_start", "int", [], []);
};
VBAInterface.VBA_do_cycles = function (cycles) {
return gbaninja.ccall("VBA_do_cycles", "int", ["int"], [cycles]);
};
VBAInterface.VBA_stop = function () {
return gbaninja.ccall("VBA_stop", "int", [], []);
};
VBAInterface.VBA_get_bios = function () {
return gbaninja.ccall("VBA_get_bios", "int", [], []);
};
VBAInterface.VBA_get_rom = function () {
return gbaninja.ccall("VBA_get_rom", "int", [], []);
};
VBAInterface.VBA_get_internalRAM = function () {
return gbaninja.ccall("VBA_get_internalRAM", "int", [], []);
};
VBAInterface.VBA_get_workRAM = function () {
return gbaninja.ccall("VBA_get_workRAM", "int", [], []);
};
VBAInterface.VBA_get_paletteRAM = function () {
return gbaninja.ccall("VBA_get_paletteRAM", "int", [], []);
};
VBAInterface.VBA_get_vram = function () {
return gbaninja.ccall("VBA_get_vram", "int", [], []);
};
VBAInterface.VBA_get_pix = function () {
return gbaninja.ccall("VBA_get_pix", "int", [], []);
};
VBAInterface.VBA_get_oam = function () {
return gbaninja.ccall("VBA_get_oam", "int", [], []);
};
VBAInterface.VBA_get_ioMem = function () {
return gbaninja.ccall("VBA_get_ioMem", "int", [], []);
};
VBAInterface.VBA_get_systemColorMap16 = function () {
return gbaninja.ccall("VBA_get_systemColorMap16", "int", [], []);
};
VBAInterface.VBA_get_systemColorMap32 = function () {
return gbaninja.ccall("VBA_get_systemColorMap32", "int", [], []);
};
VBAInterface.VBA_get_systemFrameSkip = function () {
return gbaninja.ccall("VBA_get_systemFrameSkip", "int", [], []);
};
VBAInterface.VBA_set_systemFrameSkip = function (n) {
return gbaninja.ccall("VBA_set_systemFrameSkip", "int", ["int"], [n]);
};
VBAInterface.VBA_get_systemSaveUpdateCounter = function () {
return gbaninja.ccall("VBA_get_systemSaveUpdateCounter", "int", [], []);
};
VBAInterface.VBA_reset_systemSaveUpdateCounter = function () {
return gbaninja.ccall("VBA_reset_systemSaveUpdateCounter", "int", [], []);
};
VBAInterface.VBA_emuWriteBattery = function () {
return gbaninja.ccall("VBA_emuWriteBattery", "int", [], []);
};
VBAInterface.VBA_agbPrintFlush = function () {
return gbaninja.ccall("VBA_agbPrintFlush", "int", [], []);
};
// ------- VBA EXIT POINTS --------
VBAInterface.NYI = function (feature) {
console.log("Feature is NYI: ", feature);
};
VBAInterface.getAudioSampleRate = function () {
return window.vbaSound.getSampleRate();
};
VBAInterface.getRomSize = function (startPointer8) {
return romBuffer8.byteLength;
};
VBAInterface.copyRomToMemory = function (startPointer8) {
var gbaHeap8 = gbaninja.HEAP8;
var byteLength = romBuffer8.byteLength;
for (var i = 0; i < byteLength; i++) {
gbaHeap8[startPointer8 + i] = romBuffer8[i];
}
};
VBAInterface.renderFrame = function (pixPointer8) {
window.vbaGraphics.drawGBAFrame(pixPointer8);
};
VBAInterface.initSound = function () {
};
VBAInterface.pauseSound = function () {
};
VBAInterface.resetSound = function () {
window.vbaSound.resetSound();
};
VBAInterface.resumeSound = function () {
};
VBAInterface.writeSound = function (pointer8, length16) {
return window.vbaSound.writeSound(pointer8, length16);
};
VBAInterface.setThrottleSound = function (pointer8, length16) {
};
VBAInterface.getSaveSize = function () {
return vbaSaves.getSaveSize();
};
VBAInterface.commitFlash = VBAInterface.commitEeprom = function (pointer8, size) {
return vbaSaves.softCommit(pointer8, size);
};
VBAInterface.restoreSaveMemory = function (pointer8, targetBufferSize) {
return vbaSaves.restoreSaveMemory(pointer8, targetBufferSize);
};
VBAInterface.getJoypad = function (joypadNum) {
return vbaInput.getJoypad(joypadNum);
};
VBAInterface.dbgOutput = function (textPointer8, unknownPointer8) {
return console.log("dbgOutput", textPointer8, unknownPointer8);
};
function ajaxScript(url, progressCallback) {
var xhr = new XMLHttpRequest();
xhr.onload = function (e) {
var script = document.createElement('script');
script.text = xhr.responseText;
document.head.appendChild(script);
};
xhr.onprogress = function (e) {
progressCallback((e.loaded / e.total) * 100);
};
xhr.onerror = function () {
progressCallback(-1);
gtag("event", "emu_load_error", {});
};
xhr.open("GET", url);
xhr.responseType = "text";
xhr.send();
}
window.emuScriptProgress = 0;
ajaxScript("./emu.js", function (progress, text) {
window.emuScriptProgress = progress;
});
</script>
<script src="./app.js"></script>
</head>
<body>
<style>
.ui {
color: #4d2990;
}
.ui-border-1 {
float: left;
border: transparent 20px solid;
}
.ui-border-2 {
padding: 20px;
border: 13px solid #aa17fe;
background-color: white;
float: left;
border-left-width: 5px;
border-right-width: 5px;
position: relative;
}
section {
margin: 23px 0px;
}
h2 {
margin: 0;
font-size: 14px;
font-style: italic;
text-decoration: underline;
}
table {
margin: 0;
}
p,
label {
margin: 0;
font-size: 14px;
}
.gap {
margin-top: 10px;
}
input[type=file] {
position: absolute;
left: -1000000px;
}
#load-rom-from-url {
height: 23px;
border-radius: 3px;
border: solid 2px black;
margin-left: 16px;
padding: 3px 13px;
}
.btn {
display: inline-block;
border: 1px #9b69b7 solid;
border-width: 4px 2px;
padding: 5px 13px;
background-color: #3a3a3a;
color: #ece6ff;
font-weight: 600;
font-size: 13px;
min-width: 60px;
text-align: center;
}
.btn:hover {
cursor: pointer;
border-color: #b09cbb;
background-color: #7a7994;
}
.empty-table {
font-size: 11px;
}
table {
border-collapse: collapse;
font-size: 11px;
}
td {
padding: 3px 11px;
border: 2px solid #aa17fe;
border-top: none;
border-bottom: none;
}
a {
color: #a89be8;
}
.report-bug-button {
font-size: 11px;
}
.modal {
display: none;
}
.modal-background {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
opacity: 0.8;
background-color: black;
}
.modal-body {
width: 400px;
margin: 0 auto;
position: absolute;
border: 10px solid #aa17fe;
background-color: white;
border-left-width: 3px;
border-right-width: 3px;
left: 50%;
margin-left: -218px;
/* =(400 + 2*15 + 2*3) / 2 */
margin-top: 60px;
padding: 15px;
}
.modal-title {
font-size: 16px;
}
.modal-buttons {
text-align: center;
}
.modal-button-left,
.modal-button-right {
margin: 6px 4px 3px 4px;
}
.modal-progress {
height: 4px;
margin-bottom: 10px;
background-color: #9b69b7;
}
.modal-input {
width: 100%;
padding: 6px;
box-sizing: border-box;
margin: 10px 0;
border: 2px solid #9b69b7;
border-radius: 5px;
font-size: 15px;
color: #4d2990;
}
.perf {
position: fixed;
bottom: 0;
color: white;
padding: 4px;
margin: 4px;
background-color: rgba(0, 0, 0, 0.5);
}
.perf-left {
display: inline-block;
width: 200px;
}
</style>
<div class="pixels" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;"></div>
<div style="display: none;" class="ui">
<div class="ui-border-1">
<div class="ui-border-2">
<img src="./logo.png" style="height: 50px; padding-bottom: 6px; padding-right: 90px;" />
<section class="load-rom-section">
<h2>Load a Gameboy Advance ROM</h2>
<div class="gap"></div>
<label class="btn" for="load-rom-from-file">From File</label>
<div class="gap"></div>
<form>
<input id="load-rom-from-file" type="file" onchange="window.loadRomFromFile(event);" />
</form>
</section>
<section class="paused-section" style="display: none;">
<h2>Paused</h2>
<p style="padding-top: 8px;">
Press <span class="unpause-key-prompt"></span> to resume.
</p>
</section>
<!--
<div>
<label class="btn" for="load-rom-from-url" onclick="window.loadCartridgeFromURL(event);window.onReady('cartridge', function () {start()});">From URL</label>
<input id="load-rom-from-url" type="text"/>
</div>
-->
<section class="savegames-section">
<h2>Saves</h2>
<div class="gap"></div>
<div class="saves-list"></div>
<div class="gap"></div>
<label class="btn" for="import-save-file">Import Save File</label>
<form>
<input id="import-save-file" type="file"
onchange="vbaSaves.onFileImportInputChanged(event, window.vbaUI.reset.bind(vbaUI));" />
</form>
</section>
<section>
<h2>Keyboard Bindings</h2>
<div class="gap"></div>
<div class="keyboard-bindings"></div>
<div class="gap"></div>
<button class="btn reset-bindings-button" onclick="window.vbaUI.resetBindings();">Reset
Bindings</button>
</section>
<div style="position: absolute; right: 10px; bottom: 10px;">
<a class="report-bug-button" target="_blank"
href="https://github.com/simon-paris/gba.ninja/blob/master/embed.md">embed gba.ninja</a>
&nbsp;
<a class="report-bug-button" target="_blank"
href="https://github.com/simon-paris/gba.ninja/issues">report a bug</a>
</div>
</div>
</div>
</div>
<div class="modal">
<div class="modal-background"></div>
<div class="modal-body">
<h2 class="modal-title">Title</h2>
<div class="gap"></div>
<p class="modal-text">Text</p>
<div class="gap"></div>
<div class="modal-progress"></div>
<input class="modal-input"></input>
<div class="modal-buttons">
<div class="btn modal-button-left">Left</div>
<div class="btn modal-button-right">Right</div>
</div>
</div>
</div>
<div class="toast">
</div>
<div class="perf" style="display: none;">
<span class="perf-left">Game</span><span class="perf-right perf-game">-</span><br />
<span class="perf-left">Speed</span><span class="perf-right perf-percentage">-</span><br />
<span class="perf-left">On-Time Renders</span><span class="perf-right perf-render-deadlines">-</span><br />
<span class="perf-left">On-Time Audio Events</span><span class="perf-right perf-audio-deadlines">-</span><br />
<span class="perf-left">Timesteps/Second</span><span class="perf-right perf-timesteps">-</span><br />
<span class="perf-left">Audio Lag</span><span class="perf-right perf-audio-lag">-</span><br />
</div>
</body>
<script>
window.addEventListener("keydown", (e) => {
if ([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
e.preventDefault();
}
}, false);
</script>
</html>