jsnes-ninjapad

This commit is contained in:
2023-12-03 17:15:59 +01:00
parent e7e2e3f593
commit b32a7982d0
16 changed files with 9676 additions and 0 deletions

View File

@ -0,0 +1,108 @@
<html><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://unpkg.com/image-map-resizer@1.0.10/js/imageMapResizer.min.js"></script>
<script src="jsnes.js"></script>
<script src="nes-embed.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
canvas {
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: -o-crisp-edges;
image-rendering: crisp-edges;
-ms-interpolation-mode: nearest-neighbor;
image-rendering: pixelated;
width: 100%;
height: 100%;
}
#main {
width: 100%;
height: 100%;
background-color: #222;
justify-content: center;
align-items: center;
display: flex;
}
#emu-screen {
width: 256px;
max-width: 768px;
height: 240px;
max-height: 720px;
position: relative;
margin: auto;
}
</style>
</head>
<body>
<div id="main">
<div id="emu-screen">
<canvas id="emu-canvas" width="256" height="240"></canvas>
</div>
</div>
<!-- NinjaPad -->
<div id="ninjaPad">
<script src="ninjapad-config.js"></script>
<script src="ninjapad/lib/sha256.js"></script>
<script src="ninjapad/lib/fflate.min.js"></script>
<script src="ninjapad/lib/uint8-to-utf16.js"></script>
<script src="ninjapad/index.js"></script>
<script src="ninjapad/utils.js"></script>
<script src="ninjapad/layout.js"></script>
<script src="ninjapad/menu.js"></script>
<script src="ninjapad/pause.js"></script>
<script src="ninjapad/gamepad.js"></script>
<script src="ninjapad/interface.js"></script>
<script src="ninjapad/main.js"></script>
<link rel="stylesheet" href="ninjapad/ninjapad.css">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<div id="SCREEN">
<div id="OSD"></div>
</div>
<div id="GAMEPAD">
<div id="GAMEPAD-BUTTONS">
<div id="DPAD" class="dirButtons">
<div class="dpad">
<div id="MULTI_UL" class="box"></div>
<div id="BUTTON_UP" class="box"></div>
<div id="MULTI_UR" class="box"></div>
<div id="BUTTON_LEFT" class="box"></div>
<div id="DEADZONE" class="box"></div>
<div id="BUTTON_RIGHT" class="box"></div>
<div id="MULTI_DL" class="box"></div>
<div id="BUTTON_DOWN" class="box"></div>
<div id="MULTI_DR" class="box"></div>
</div>
</div>
<div id="ANALOG" class="dirButtons" style="display: none">
<div id="RING" class="analogRing"></div>
<div id="ANALOG_STICK" class="analogStick"></div>
</div>
<div id="ACTION" class="actionButtons">
<div id="MULTI_AB" class="button_AB"></div>
<div id="BUTTON_B" class="button_B"></div>
<div id="BUTTON_A" class="button_A"></div>
</div>
<div id="FUNCTIONAL" class="middleButtons">
<div id="BUTTON_SELECT" class="button_SELECT"></div>
<div id="BUTTON_START" class="button_START"></div>
<div id="analogSwitch" class="analogSwitchButton"></div>
<div id="menu" class="menuButton">
<input type="file" accept=".nes" id="upload" style="display:none"/>
</div>
</div>
</div>
</div>
</div>
<!-- End of NinjaPad -->
</body></html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
var DEBUG = false;
var DEADZONE = 2;
var DISPLAY = "emu-canvas";
var SCREEN = "emu-screen";
var EMULATOR = "jsnes";
var SINGLE_ROM = true;
var DEFAULT_ROM = JSON.parse(new URLSearchParams(window.location.hash).get('#')).RomUrl;

View File

@ -0,0 +1,219 @@
ninjapad.gamepad = function() {
// Handle single-touch multiple button presses
const MULTIPRESS = {
"UR": ["BUTTON_UP", "BUTTON_RIGHT"],
"DR": ["BUTTON_DOWN", "BUTTON_RIGHT"],
"UL": ["BUTTON_UP", "BUTTON_LEFT" ],
"DL": ["BUTTON_DOWN", "BUTTON_LEFT" ],
"AB": ["BUTTON_A", "BUTTON_B" ]
};
const DPAD_BUTTONS = [
["BUTTON_LEFT" ],
["BUTTON_UP", "BUTTON_LEFT" ],
["BUTTON_UP", ],
["BUTTON_UP", "BUTTON_RIGHT"],
["BUTTON_RIGHT" ],
["BUTTON_DOWN", "BUTTON_RIGHT"],
["BUTTON_DOWN" ],
["BUTTON_DOWN", "BUTTON_LEFT" ],
]
// This object is necessary to handle the user
// sliding their finger from one button to another
var childButton = {};
var analog = {
active: false,
touchX: undefined,
touchY: undefined,
deltaX: undefined,
deltaY: undefined,
padBtn: undefined
};
function isButtonDown(eventType) {
return eventType.endsWith("start") || eventType.endsWith("move");
}
function fnButtonPress(eventType) {
return isButtonDown(eventType) ? ninjapad.emulator.buttonDown : ninjapad.emulator.buttonUp;
}
function pressButtons(fn, buttons) {
for (const b of buttons) fn(b);
}
function analogReset(element) {
element.css("transform", "translate(0, 0)");
}
return {
analogTouch: function(event) {
event.preventDefault();
event.stopPropagation();
for (const touch of event.changedTouches) {
// Ignore any touches where the target
// element doesn't match the source element
if (touch.target.id != event.target.id) continue;
switch (event.type) {
case "touchstart":
analog.touchX = touch.clientX;
analog.touchY = touch.clientY;
break;
case "touchmove":
analog.deltaX = touch.clientX - analog.touchX;
analog.deltaY = touch.clientY - analog.touchY;
let r = ninjapad.utils.angle(analog.deltaX, analog.deltaY);
let d = Math.min(ninjapad.utils.vw(10), ninjapad.utils.dist(analog.deltaX, analog.deltaY));
let dx = Math.cos(r) * d;
let dy = Math.sin(r) * d;
ninjapad.jQElement.analogStick.css(
"transform",
"translate(" + dx + "px, " + dy + "px)"
);
let btnIndex = Math.floor(((180 + (45/2) + (r * 180 / Math.PI)) % 360) / 45);
analog.padBtn && pressButtons(ninjapad.emulator.buttonUp, analog.padBtn);
analog.padBtn = d < ninjapad.utils.vw(DEADZONE) ? null : DPAD_BUTTONS[btnIndex];
analog.padBtn && pressButtons(ninjapad.emulator.buttonDown, analog.padBtn);
break;
default:
analog.padBtn && pressButtons(ninjapad.emulator.buttonUp, analog.padBtn);
analogReset(ninjapad.jQElement.analogStick);
}
}
},
buttonPress: function(event) {
// Prevent all the shenanigans that happen with a "long-press" on mobile
event.preventDefault();
// Get the source element
target = event.target;
// Handle the touch
for (const touch of event.changedTouches) {
// Ignore any touches where the target
// element doesn't match the source element
if (touch.target.id != target.id) continue;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Get the element (either a button or the empty area of the gamepad)
// the user is physically touching right now
let element = $(document.elementFromPoint(touch.clientX, touch.clientY))[0];
// If it's a new touch, set the child button to its parent
if (event.type == "touchstart") {
childButton[target.id] = element;
}
// Otherwise, if the user is sliding its finger from one button to another
// or simply stops touching the screen with that finger
else if (childButton[target.id].id != element.id) {
//else if (element.id && childButton[target.id].id != element.id) {
// Check which button (if any) the user had its finger on previously
let lastButton = childButton[target.id];
// If the user was actually pressing a button before
if (lastButton.id.startsWith("BUTTON")) {
// Tell the emulator to release that button
ninjapad.emulator.buttonUp(lastButton.id);
$(lastButton).css("border-style", "outset");
DEBUG && console.log("Released", lastButton.id); // Debug
}
// Otherwise, if it was a multipress
else if (lastButton.id.startsWith("MULTI")) {
// Get buttons
let key = lastButton.id.split("_").pop();
for (const d of MULTIPRESS[key]) {
ninjapad.emulator.buttonUp(d);
}
$(lastButton).css("background-color", "transparent");
DEBUG && console.log("Released", lastButton.id); // Debug
}
// Update the child button to be the one the user is touching right now
childButton[target.id] = element;
}
// If the user is actually interacting a button right now
if (element.id.startsWith("BUTTON")) {
// Press / release that button
fnButtonPress(event.type)(element.id);
// Show button presses / releases
if (isButtonDown(event.type)) {
$(element).css("border-style", "inset");
DEBUG && console.log("Pressed", element.id); // Debug
}
else {
$(element).css("border-style", "outset");
DEBUG && console.log("Released", element.id); // Debug
}
}
// Otherwise, if it's actually two buttons at the same time
else if (element.id.startsWith("MULTI")) {
// Get the correct function call
let fn = fnButtonPress(event.type)
// Get buttons and press / release them
let key = element.id.split("_").pop();
for (const d of MULTIPRESS[key]) {
fn(d);
}
// Resume emulation and show button presses / releases
if (isButtonDown(event.type)) {
$(element).css("background-color", "#444");
DEBUG && console.log("Pressed", element.id); // Debug
}
else {
$(element).css("background-color", "transparent");
DEBUG && console.log("Released", element.id); // Debug
}
}
}
},
analogSwitch: function(event) {
event.preventDefault();
if (event.type == "touchstart") {
ninjapad.jQElement.analogSwitch.css("border-style", "inset");
return;
}
ninjapad.jQElement.analogSwitch.css("border-style", "outset");
if (ninjapad.jQElement.analog.css("display") == "none") {
analog.active = true;
ninjapad.jQElement.dpad.hide();
ninjapad.jQElement.analog.show();
analogReset(ninjapad.jQElement.analog);
return;
}
analog.active = false;
ninjapad.jQElement.analog.hide();
ninjapad.jQElement.dpad.show();
},
toggleMenu: function(event) {
event.preventDefault();
if (event.type == "touchstart") {
ninjapad.jQElement.menu.css("border-style", "inset");
return;
}
ninjapad.jQElement.menu.css("border-style", "outset");
ninjapad.menu.toggleMenu();
},
// Doesn't work on iOS
toggleFullScreen: function(event) {
event.preventDefault();
let element = document.getElementById("ninjaPad");
ninjapad.utils.isFullScreen() ? ninjapad.utils.exitFullScreen() : ninjapad.utils.enterFullscreen(element);
},
}
}();

View File

@ -0,0 +1 @@
const ninjapad = {};

View File

@ -0,0 +1,430 @@
ninjapad.interface = {
jsnes: function() {
const BUTTON_A = 0;
const BUTTON_B = 2;
const SCREEN_WIDTH = 256;
const SCREEN_HEIGHT = 240;
const FRAMEBUFFER_SIZE = SCREEN_WIDTH*SCREEN_HEIGHT;
const AUDIO_BUFFERING = 512;
const SAMPLE_COUNT = 4*1024;
const SAMPLE_MASK = SAMPLE_COUNT - 1;
var canvas_ctx, image;
var framebuffer_u8, framebuffer_u32;
var audio_ctx, script_processor;
var audio_samples_L = new Float32Array(SAMPLE_COUNT);
var audio_samples_R = new Float32Array(SAMPLE_COUNT);
var audio_write_cursor = 0, audio_read_cursor = 0;
const nes = new jsnes.NES({
onFrame: function(framebuffer_24){
for(var i = 0; i < FRAMEBUFFER_SIZE; i++) framebuffer_u32[i] = 0xFF000000 | framebuffer_24[i];
},
onAudioSample: function(l, r){
audio_samples_L[audio_write_cursor] = l;
audio_samples_R[audio_write_cursor] = r;
audio_write_cursor = (audio_write_cursor + 1) & SAMPLE_MASK;
},
// sampleRate: getSampleRate()
});
function getSampleRate()
{
if (!window.AudioContext) {
return 44100;
}
let myCtx = new window.AudioContext();
let sampleRate = myCtx.sampleRate;
myCtx.close();
return sampleRate;
}
function onAnimationFrame(){
window.setTimeout(onAnimationFrame, 1000/60);
image.data.set(framebuffer_u8);
canvas_ctx.putImageData(image, 0, 0);
nes.frame();
}
function audio_remain(){
return (audio_write_cursor - audio_read_cursor) & SAMPLE_MASK;
}
function audio_callback(event) {
if (nes.rom == null) return;
var dst = event.outputBuffer;
var len = dst.length;
// Attempt to avoid buffer underruns.
if(audio_remain() < AUDIO_BUFFERING) nes.frame();
var dst_l = dst.getChannelData(0);
var dst_r = dst.getChannelData(1);
for(var i = 0; i < len; i++){
var src_idx = (audio_read_cursor + i) & SAMPLE_MASK;
dst_l[i] = audio_samples_L[src_idx];
dst_r[i] = audio_samples_R[src_idx];
}
audio_read_cursor = (audio_read_cursor + len) & SAMPLE_MASK;
}
function keyboard(callback, event) {
var player = 1;
var prevent = true;
switch(event.keyCode){
case 38: // UP
case 87: // W
callback(player, jsnes.Controller.BUTTON_UP); break;
case 40: // Down
case 83: // S
callback(player, jsnes.Controller.BUTTON_DOWN); break;
case 37: // Left
case 65: // A
callback(player, jsnes.Controller.BUTTON_LEFT); break;
case 39: // Right
case 68: // D
callback(player, jsnes.Controller.BUTTON_RIGHT); break;
case 18: // 'alt'
case 88: // 'x'
callback(player, jsnes.Controller.BUTTON_A); break;
case 90: // 'z'
case 17: // 'ctrl'
callback(player, jsnes.Controller.BUTTON_B); break;
case 32: // Space
case 16: // Right Shift
callback(player, jsnes.Controller.BUTTON_SELECT); break;
case 13: // Return
callback(player, jsnes.Controller.BUTTON_START); break;
default:
prevent = false; break;
}
if (prevent){
event.preventDefault();
}
}
function nes_init(canvas_id){
var canvas = document.getElementById(canvas_id);
canvas_ctx = canvas.getContext("2d");
image = canvas_ctx.getImageData(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
canvas_ctx.fillStyle = "black";
canvas_ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Allocate framebuffer array.
var buffer = new ArrayBuffer(image.data.length);
framebuffer_u8 = new Uint8ClampedArray(buffer);
framebuffer_u32 = new Uint32Array(buffer);
// Setup audio.
audio_ctx = new window.AudioContext();
script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2);
script_processor.onaudioprocess = audio_callback;
script_processor.connect(audio_ctx.destination);
document.addEventListener('touchstart', () => { audio_ctx.resume() });
document.addEventListener('keydown', () => { audio_ctx.resume() });
}
function nes_boot(rom_data){
nes.loadROM(rom_data);
window.requestAnimationFrame(onAnimationFrame);
}
function nes_load_data(canvas_id, rom_data){
nes_init(canvas_id);
nes_boot(rom_data);
}
function nes_load_url(canvas_id, path){
nes_init(canvas_id);
var req = new XMLHttpRequest();
req.open("GET", path);
req.overrideMimeType("text/plain; charset=x-user-defined");
req.onerror = () => console.log(`Error loading ${path}: ${req.statusText}`);
req.onload = function() {
if (this.status === 200) {
nes_boot(this.responseText);
} else if (this.status === 0) {
// Aborted, so ignore error
} else {
req.onerror();
}
};
req.send();
}
document.addEventListener('keydown', (event) => {keyboard(nes.buttonDown, event)});
document.addEventListener('keyup', (event) => {keyboard(nes.buttonUp, event)});
/////////////////////
// GAMEPAD SUPPORT
//
// Based on documentation here: https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
/////////////////////
var haveEvents = 'ongamepadconnected' in window;
var controllers = {};
// Once the presses a button on one of the controllers, this will store
// the index of that controller so that only that controller is checked
// each frame. This is to avoid additional controllers triggering key_up
// events when they are just sitting there inactive.
var cur_controller_index = -1;
function connecthandler(e) {
addgamepad(e.gamepad);
}
function addgamepad(gamepad) {
controllers[gamepad.index] = gamepad;
requestAnimationFrame(updateStatus);
}
function disconnecthandler(e) {
removegamepad(e.gamepad);
}
function removegamepad(gamepad) {
delete controllers[gamepad.index];
}
// Check all controllers to see if player has pressed any buttons.
// If they have, store that as the current controller.
function findController()
{
var i = 0;
var j;
for (j in controllers)
{
var controller = controllers[j];
for (i = 0; i < controller.buttons.length; i++)
{
var val = controller.buttons[i];
var pressed = val == 1.0;
if (typeof(val) == "object")
{
pressed = val.pressed;
val = val.value;
}
if (pressed)
{
cur_controller_index = j;
}
}
}
}
function updateStatus()
{
if (!haveEvents)
{
scangamepads();
}
// If a controller has not yet been chosen, check for one now.
if (cur_controller_index == -1)
{
findController();
}
// Allow for case where controller was chosen this frame
if (cur_controller_index != -1)
{
var i = 0;
var j;
var controller = controllers[cur_controller_index];
for (i = 0; i < controller.buttons.length; i++)
{
var val = controller.buttons[i];
var pressed = val == 1.0;
if (typeof(val) == "object")
{
pressed = val.pressed;
val = val.value;
}
var player = 1 //parseInt(j,10) + 1;
if (pressed)
{
if (audio_ctx) audio_ctx.resume();
var callback = nes.buttonDown;
switch(i)
{
case 12: // UP
callback(player, jsnes.Controller.BUTTON_UP); break;
case 13: // Down
callback(player, jsnes.Controller.BUTTON_DOWN); break;
case 14: // Left
callback(player, jsnes.Controller.BUTTON_LEFT); break;
case 15: // Right
callback(player, jsnes.Controller.BUTTON_RIGHT); break;
case BUTTON_A: // 'A'
callback(player, jsnes.Controller.BUTTON_A); break;
case BUTTON_B: // 'B'
callback(player, jsnes.Controller.BUTTON_B); break;
case 8: // Select
callback(player, jsnes.Controller.BUTTON_SELECT); break;
case 9: // Start
callback(player, jsnes.Controller.BUTTON_START); break;
}
}
else
{
var callback = nes.buttonUp;
switch(i)
{
case 12: // UP
callback(player, jsnes.Controller.BUTTON_UP); break;
case 13: // Down
callback(player, jsnes.Controller.BUTTON_DOWN); break;
case 14: // Left
callback(player, jsnes.Controller.BUTTON_LEFT); break;
case 15: // Right
callback(player, jsnes.Controller.BUTTON_RIGHT); break;
case BUTTON_A: // 'A'
callback(player, jsnes.Controller.BUTTON_A); break;
case BUTTON_B: // 'B'
callback(player, jsnes.Controller.BUTTON_B); break;
case 8: // Select
callback(player, jsnes.Controller.BUTTON_SELECT); break;
case 9: // Start
callback(player, jsnes.Controller.BUTTON_START); break;
}
}
// var axes = d.getElementsByClassName("axis");
// for (i = 0; i < controller.axes.length; i++)
// {
// var a = axes[i];
// a.innerHTML = i + ": " + controller.axes[i].toFixed(4);
// a.setAttribute("value", controller.axes[i] + 1);
// }
}
}
requestAnimationFrame(updateStatus);
}
function scangamepads() {
var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
for (var i = 0; i < gamepads.length; i++) {
if (gamepads[i]) {
if (gamepads[i].index in controllers) {
controllers[gamepads[i].index] = gamepads[i];
} else {
addgamepad(gamepads[i]);
}
}
}
}
window.addEventListener("gamepadconnected", connecthandler);
window.addEventListener("gamepaddisconnected", disconnecthandler);
if (!haveEvents) {
setInterval(scangamepads, 500);
}
return {
display: {
width: 256,
height: 240
},
core: function() {
return nes;
}(),
buttonDown: function(b) {
nes.buttonDown(1, eval("jsnes.Controller." + b));
},
buttonUp: function(b) {
nes.buttonUp(1, eval("jsnes.Controller." + b));
},
pause: function() {
function _pause() {
if (nes.break) return;
// - - - - - - - - - - - - - - - - - - - - - - -
if (audio_ctx && audio_ctx.suspend) {
audio_ctx.suspend();
}
audio_ctx = {
resume: function(){},
isNull: true
};
nes.break = true;
if (typeof enforcePause === 'undefined') {
enforcePause = setInterval(_pause, 16);
}
}
_pause();
},
resume: function() {
if (!nes.rom.header) return;
clearInterval(enforcePause);
enforcePause = undefined;
if (audio_ctx.isNull) {
audio_ctx = new window.AudioContext();
script_processor = audio_ctx.createScriptProcessor(AUDIO_BUFFERING, 0, 2);
script_processor.onaudioprocess = audio_callback;
script_processor.connect(audio_ctx.destination);
}
audio_ctx.resume();
nes.break = false;
},
loadROMData: function(d) {
nes.loadROM(d);
},
reloadROM: function() {
nes.reloadROM();
},
getROMData: function() {
return nes.romData;
},
saveState: function() {
const o = nes.toJSON();
const s = JSON.stringify(o);
return ninjapad.utils.zip(s);
},
loadState: function(z) {
const s = ninjapad.utils.unzip(z);
const o = JSON.parse(s);
nes.fromJSON(o);
},
isROMLoaded: function() {
return !!nes.rom.header
},
initialize: function() {
nes_load_url(DISPLAY, DEFAULT_ROM);
}
// ...
};
}()
};

View File

@ -0,0 +1,117 @@
ninjapad.layout = function() {
var coldStart = true;
function setOSDLayout() {
ninjapad.jQElement.osd.empty();
ninjapad.jQElement.osd.detach().appendTo(ninjapad.jQElement.screen);
ninjapad.jQElement.osd.css("top", 0);
ninjapad.jQElement.osd.css("left", 0);
ninjapad.jQElement.osd.css("height", ninjapad.jQElement.screen.height());
ninjapad.jQElement.osd.css("width", ninjapad.jQElement.screen.width());
ninjapad.jQElement.osd.css("visibility", ninjapad.pause.pauseScreen.visibility);
ninjapad.jQElement.osd.append(ninjapad.pause.pauseScreen.content);
}
function setEmulationScreenLayout() {
ninjapad.jQElement.screen.removeAttr("style");
ninjapad.jQElement.screen.css("width", ninjapad.emulator.display.width);
ninjapad.jQElement.screen.css("height", ninjapad.emulator.display.height);
ninjapad.jQElement.screen.css("margin", "auto");
ninjapad.jQElement.screen.css("position", "relative");
}
function setDesktopLayout() {
DEBUG && console.log("Desktop mode");
let useJQuery = !ninjapad.utils.isFullScreen() || ninjapad.utils.isIOSDevice();
let width = useJQuery ? $(window).width() : window.innerWidth;
let height = useJQuery ? $(window).height() : window.innerHeight;
if (width > height) {
ninjapad.jQElement.screen.height("100%");
let newHeight = ninjapad.jQElement.screen.height();
ninjapad.jQElement.screen.width(256 * (newHeight / 240));
}
else {
ninjapad.jQElement.screen.width("100%");
let newWidth = ninjapad.jQElement.screen.width();
ninjapad.jQElement.screen.height(240 * (newWidth / 256));
}
ninjapad.jQElement.gamepad.height("0%");
ninjapad.jQElement.controller.hide();
}
function setMobileLayout() {
DEBUG && console.log("Mobile mode");
if (coldStart) {
DEBUG && console.log("Cold start");
$("#ninjaPad").css("height", "100%");
$("body").removeAttr("style").css("margin", "0%");
setEmulationScreenLayout();
ninjapad.jQElement.screen.detach().appendTo("#SCREEN");
$("body *").not("#ninjaPad *").not("#ninjaPad").remove();
coldStart = false;
}
let useJQuery = !ninjapad.utils.isFullScreen() || ninjapad.utils.isIOSDevice();
let width = useJQuery ? $(window).width() : window.innerWidth;
let height = useJQuery ? $(window).height() : window.innerHeight;
if (height >= width || window.matchMedia("(orientation: portrait)").matches) {
let opacity = 1;
let bottom = "auto";
ninjapad.jQElement.screen.width("100%");
let newWidth = ninjapad.jQElement.screen.width();
ninjapad.jQElement.screen.height(240 * (newWidth / 256));
let padHeight = ninjapad.utils.vw(47.5);
let remainingHeight = height - ninjapad.jQElement.screen.height();
ninjapad.jQElement.gamepad.height(Math.max(padHeight, remainingHeight));
let difference = remainingHeight - padHeight;
if (difference < 0) {
opacity += (difference / (padHeight * 2));
bottom = 0;
}
ninjapad.jQElement.gamepad.css("bottom", bottom);
ninjapad.jQElement.gamepad.css("display", "block");
ninjapad.jQElement.controller.css("opacity", opacity);
ninjapad.jQElement.controller.show();
if (ninjapad.pause.state.cannotResume) {
ninjapad.pause.state.cannotResume = false;
ninjapad.pause.pauseEmulation();
}
DEBUG && console.log("Show touch controls");
}
else {
setDesktopLayout();
handleLandscapeMode();
DEBUG && console.log("Hide touch controls");
}
}
function handleLandscapeMode() {
ninjapad.pause.state.cannotResume = true;
ninjapad.pause.pauseEmulation(
ninjapad.utils.html(
"span", "pauseScreenContent",
`Landscape mode<br/>
not supported yet<br/>
<br/>
Turn your device<br/>
upright to play`
)
);
}
return {
setPageLayout: function() {
ninjapad.utils.isMobileDevice() ? setMobileLayout() : setDesktopLayout();
setOSDLayout();
}
};
}();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,468 @@
/**
* [js-sha256]{@link https://github.com/emn178/js-sha256}
*
* @version 0.9.0
* @author Chen, Yi-Cyuan [emn178@gmail.com]
* @copyright Chen, Yi-Cyuan 2014-2017
* @license MIT
*/
/*jslint bitwise: true */
(function() {
'use strict';
var ERROR = 'input is invalid type';
var WINDOW = typeof window === 'object';
var root = WINDOW ? window : {};
if (root.JS_SHA256_NO_WINDOW) {
WINDOW = false;
}
var WEB_WORKER = !WINDOW && typeof self === 'object';
var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
if (NODE_JS) {
root = global;
} else if (WEB_WORKER) {
root = self;
}
var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports;
var AMD = typeof define === 'function' && define.amd;
var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined';
var HEX_CHARS = '0123456789abcdef'.split('');
var EXTRA = [-2147483648, 8388608, 32768, 128];
var SHIFT = [24, 16, 8, 0];
var K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
var blocks = [];
if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) {
Array.isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
}
if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
ArrayBuffer.isView = function(obj) {
return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
};
}
var createOutputMethod = function(outputType, is224) {
return function(message) {
return new Sha256(is224, true).update(message)[outputType]();
};
};
var createMethod = function(is224) {
var method = createOutputMethod('hex', is224);
if (NODE_JS) {
method = nodeWrap(method, is224);
}
method.create = function() {
return new Sha256(is224);
};
method.update = function(message) {
return method.create().update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createOutputMethod(type, is224);
}
return method;
};
var nodeWrap = function(method, is224) {
var crypto = eval("require('crypto')");
var Buffer = eval("require('buffer').Buffer");
var algorithm = is224 ? 'sha224' : 'sha256';
var nodeMethod = function(message) {
if (typeof message === 'string') {
return crypto.createHash(algorithm).update(message, 'utf8').digest('hex');
} else {
if (message === null || message === undefined) {
throw new Error(ERROR);
} else if (message.constructor === ArrayBuffer) {
message = new Uint8Array(message);
}
}
if (Array.isArray(message) || ArrayBuffer.isView(message) || message.constructor === Buffer) {
return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex');
} else {
return method(message);
}
};
return nodeMethod;
};
var createHmacOutputMethod = function(outputType, is224) {
return function(key, message) {
return new HmacSha256(key, is224, true).update(message)[outputType]();
};
};
var createHmacMethod = function(is224) {
var method = createHmacOutputMethod('hex', is224);
method.create = function(key) {
return new HmacSha256(key, is224);
};
method.update = function(key, message) {
return method.create(key).update(message);
};
for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
var type = OUTPUT_TYPES[i];
method[type] = createHmacOutputMethod(type, is224);
}
return method;
};
function Sha256(is224, sharedMemory) {
if (sharedMemory) {
blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
this.blocks = blocks;
} else {
this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
}
if (is224) {
this.h0 = 0xc1059ed8;
this.h1 = 0x367cd507;
this.h2 = 0x3070dd17;
this.h3 = 0xf70e5939;
this.h4 = 0xffc00b31;
this.h5 = 0x68581511;
this.h6 = 0x64f98fa7;
this.h7 = 0xbefa4fa4;
} else { // 256
this.h0 = 0x6a09e667;
this.h1 = 0xbb67ae85;
this.h2 = 0x3c6ef372;
this.h3 = 0xa54ff53a;
this.h4 = 0x510e527f;
this.h5 = 0x9b05688c;
this.h6 = 0x1f83d9ab;
this.h7 = 0x5be0cd19;
}
this.block = this.start = this.bytes = this.hBytes = 0;
this.finalized = this.hashed = false;
this.first = true;
this.is224 = is224;
}
Sha256.prototype.update = function(message) {
if (this.finalized) {
return;
}
var notString, type = typeof message;
if (type !== 'string') {
if (type === 'object') {
if (message === null) {
throw new Error(ERROR);
} else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
message = new Uint8Array(message);
} else if (!Array.isArray(message)) {
if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
throw new Error(ERROR);
}
}
} else {
throw new Error(ERROR);
}
notString = true;
}
var code, index = 0,
i, length = message.length,
blocks = this.blocks;
while (index < length) {
if (this.hashed) {
this.hashed = false;
blocks[0] = this.block;
blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
}
if (notString) {
for (i = this.start; index < length && i < 64; ++index) {
blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
}
} else {
for (i = this.start; index < length && i < 64; ++index) {
code = message.charCodeAt(index);
if (code < 0x80) {
blocks[i >> 2] |= code << SHIFT[i++ & 3];
} else if (code < 0x800) {
blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
} else if (code < 0xd800 || code >= 0xe000) {
blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
} else {
code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
}
}
}
this.lastByteIndex = i;
this.bytes += i - this.start;
if (i >= 64) {
this.block = blocks[16];
this.start = i - 64;
this.hash();
this.hashed = true;
} else {
this.start = i;
}
}
if (this.bytes > 4294967295) {
this.hBytes += this.bytes / 4294967296 << 0;
this.bytes = this.bytes % 4294967296;
}
return this;
};
Sha256.prototype.finalize = function() {
if (this.finalized) {
return;
}
this.finalized = true;
var blocks = this.blocks,
i = this.lastByteIndex;
blocks[16] = this.block;
blocks[i >> 2] |= EXTRA[i & 3];
this.block = blocks[16];
if (i >= 56) {
if (!this.hashed) {
this.hash();
}
blocks[0] = this.block;
blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
}
blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
blocks[15] = this.bytes << 3;
this.hash();
};
Sha256.prototype.hash = function() {
var a = this.h0,
b = this.h1,
c = this.h2,
d = this.h3,
e = this.h4,
f = this.h5,
g = this.h6,
h = this.h7,
blocks = this.blocks,
j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
for (j = 16; j < 64; ++j) {
// rightrotate
t1 = blocks[j - 15];
s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3);
t1 = blocks[j - 2];
s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10);
blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0;
}
bc = b & c;
for (j = 0; j < 64; j += 4) {
if (this.first) {
if (this.is224) {
ab = 300032;
t1 = blocks[0] - 1413257819;
h = t1 - 150054599 << 0;
d = t1 + 24177077 << 0;
} else {
ab = 704751109;
t1 = blocks[0] - 210244248;
h = t1 - 1521486534 << 0;
d = t1 + 143694565 << 0;
}
this.first = false;
} else {
s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
ab = a & b;
maj = ab ^ (a & c) ^ bc;
ch = (e & f) ^ (~e & g);
t1 = h + s1 + ch + K[j] + blocks[j];
t2 = s0 + maj;
h = d + t1 << 0;
d = t1 + t2 << 0;
}
s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10));
s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7));
da = d & a;
maj = da ^ (d & b) ^ ab;
ch = (h & e) ^ (~h & f);
t1 = g + s1 + ch + K[j + 1] + blocks[j + 1];
t2 = s0 + maj;
g = c + t1 << 0;
c = t1 + t2 << 0;
s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10));
s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7));
cd = c & d;
maj = cd ^ (c & a) ^ da;
ch = (g & h) ^ (~g & e);
t1 = f + s1 + ch + K[j + 2] + blocks[j + 2];
t2 = s0 + maj;
f = b + t1 << 0;
b = t1 + t2 << 0;
s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10));
s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7));
bc = b & c;
maj = bc ^ (b & d) ^ cd;
ch = (f & g) ^ (~f & h);
t1 = e + s1 + ch + K[j + 3] + blocks[j + 3];
t2 = s0 + maj;
e = a + t1 << 0;
a = t1 + t2 << 0;
}
this.h0 = this.h0 + a << 0;
this.h1 = this.h1 + b << 0;
this.h2 = this.h2 + c << 0;
this.h3 = this.h3 + d << 0;
this.h4 = this.h4 + e << 0;
this.h5 = this.h5 + f << 0;
this.h6 = this.h6 + g << 0;
this.h7 = this.h7 + h << 0;
};
Sha256.prototype.hex = function() {
this.finalize();
var h0 = this.h0,
h1 = this.h1,
h2 = this.h2,
h3 = this.h3,
h4 = this.h4,
h5 = this.h5,
h6 = this.h6,
h7 = this.h7;
var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] + HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] + HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] + HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] + HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] + HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] + HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F];
if (!this.is224) {
hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] + HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] + HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] + HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F];
}
return hex;
};
Sha256.prototype.toString = Sha256.prototype.hex;
Sha256.prototype.digest = function() {
this.finalize();
var h0 = this.h0,
h1 = this.h1,
h2 = this.h2,
h3 = this.h3,
h4 = this.h4,
h5 = this.h5,
h6 = this.h6,
h7 = this.h7;
var arr = [
(h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF,
(h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF,
(h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF,
(h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF,
(h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF,
(h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF,
(h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF
];
if (!this.is224) {
arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF);
}
return arr;
};
Sha256.prototype.array = Sha256.prototype.digest;
Sha256.prototype.arrayBuffer = function() {
this.finalize();
var buffer = new ArrayBuffer(this.is224 ? 28 : 32);
var dataView = new DataView(buffer);
dataView.setUint32(0, this.h0);
dataView.setUint32(4, this.h1);
dataView.setUint32(8, this.h2);
dataView.setUint32(12, this.h3);
dataView.setUint32(16, this.h4);
dataView.setUint32(20, this.h5);
dataView.setUint32(24, this.h6);
if (!this.is224) {
dataView.setUint32(28, this.h7);
}
return buffer;
};
function HmacSha256(key, is224, sharedMemory) {
var i, type = typeof key;
if (type === 'string') {
var bytes = [],
length = key.length,
index = 0,
code;
for (i = 0; i < length; ++i) {
code = key.charCodeAt(i);
if (code < 0x80) {
bytes[index++] = code;
} else if (code < 0x800) {
bytes[index++] = (0xc0 | (code >> 6));
bytes[index++] = (0x80 | (code & 0x3f));
} else if (code < 0xd800 || code >= 0xe000) {
bytes[index++] = (0xe0 | (code >> 12));
bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
bytes[index++] = (0x80 | (code & 0x3f));
} else {
code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff));
bytes[index++] = (0xf0 | (code >> 18));
bytes[index++] = (0x80 | ((code >> 12) & 0x3f));
bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
bytes[index++] = (0x80 | (code & 0x3f));
}
}
key = bytes;
} else {
if (type === 'object') {
if (key === null) {
throw new Error(ERROR);
} else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) {
key = new Uint8Array(key);
} else if (!Array.isArray(key)) {
if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) {
throw new Error(ERROR);
}
}
} else {
throw new Error(ERROR);
}
}
if (key.length > 64) {
key = (new Sha256(is224, true)).update(key).array();
}
var oKeyPad = [],
iKeyPad = [];
for (i = 0; i < 64; ++i) {
var b = key[i] || 0;
oKeyPad[i] = 0x5c ^ b;
iKeyPad[i] = 0x36 ^ b;
}
Sha256.call(this, is224, sharedMemory);
this.update(iKeyPad);
this.oKeyPad = oKeyPad;
this.inner = true;
this.sharedMemory = sharedMemory;
}
HmacSha256.prototype = new Sha256();
HmacSha256.prototype.finalize = function() {
Sha256.prototype.finalize.call(this);
if (this.inner) {
this.inner = false;
var innerHash = this.array();
Sha256.call(this, this.is224, this.sharedMemory);
this.update(this.oKeyPad);
this.update(innerHash);
Sha256.prototype.finalize.call(this);
}
};
var exports = createMethod();
exports.sha256 = exports;
exports.sha224 = createMethod(true);
exports.sha256.hmac = createHmacMethod();
exports.sha224.hmac = createHmacMethod(true);
if (COMMON_JS) {
module.exports = exports;
} else {
root.sha256 = exports.sha256;
root.sha224 = exports.sha224;
if (AMD) {
define(function() {
return exports;
});
}
}
})();

View File

@ -0,0 +1,103 @@
/*
ISC License
Copyright (c) 2020, Andrea Giammarchi, @WebReflection
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
/*
Ninja Dynamics 20-Sep-2021:
- Modified both functions to allow for safe usage with localStorage
*/
var uint8ToUtf16 = (function (exports) {
'use strict';
/* Added by Ninja Dynamics */
function _isInvalidChar(char) {
/* https://stackoverflow.com/questions/11170716/are-there-any-characters-that-are-not-allowed-in-localstorage */
return (char < 0x20 || (char >= 0xD800 && char < 0xE000) || char > 0xFFFD);
}
/*! (c) Andrea Giammarchi @WebReflection */
var ceil = Math.ceil;
var fromCharCode = String.fromCharCode;
/* Modified by Ninja Dynamics */
var encode = function encode(uint8array) {
var char;
var extra = 0;
var output = [];
var modified = "";
var length = uint8array.length;
var len = ceil(length / 2);
for (var j = 0, i = 0; i < len; i++) {
char = (uint8array[j++] << 8) + (j < length ? uint8array[j++] : extra++);
if (_isInvalidChar(char)) {
char = (char + 0x800).mod(0x10000);
modified += (output.length.toString(16).padStart(4, '0'));
}
output.push(fromCharCode(char));
}
if (_isInvalidChar(extra)) {
extra = (extra + 0x800).mod(0x10000);
modified += (output.length.toString(16).padStart(4, '0'));
}
output.push(fromCharCode(extra));
return modified.length + '!' + modified + output.join('');
};
/* Modified by Ninja Dynamics */
var decode = function decode(chars) {
var indexes = []
var separator = chars.indexOf('!');
var modifiedStart = separator + 1;
var modifiedLength = parseInt(chars.slice(0, separator));
var modified = chars.slice(modifiedStart, modifiedStart + modifiedLength);
for (var i = 0; i < modifiedLength; i += 4) {
var hex = modified.slice(i, i + 4);
indexes.push(parseInt(hex, 16));
}
chars = chars.slice(modifiedStart + modifiedLength, chars.length);
var codes = [];
var length = chars.length - 1;
for (var i = 0; i < length; i++) {
var c = chars.charCodeAt(i);
if (i == indexes[0]) {
c = (c - 0x800).mod(0x10000);
indexes.shift();
}
codes.push(c >> 8, c & 0xFF);
}
c = chars.charCodeAt(length);
if (i == indexes.shift()) {
c = (c - 0x800).mod(0x10000);
}
if (c) codes.pop();
return Uint8Array.from(codes);
};
exports.decode = decode;
exports.encode = encode;
return exports;
}({}));

View File

@ -0,0 +1,56 @@
ninjapad.emulator = null;
ninjapad.jQElement = null;
ninjapad.initialize = function() {
ninjapad.jQElement = {
gamepad: $("#GAMEPAD"),
controller: $("#GAMEPAD-BUTTONS"),
analogSwitch: $("#analogSwitch"),
menu: $("#menu"),
upload: $("#upload"),
analogStick: $("#ANALOG_STICK"),
analog: $("#ANALOG"),
dpad: $("#DPAD"),
osd: $("#OSD"),
screen: $("#" + SCREEN),
};
// Page setup
ninjapad.layout.setPageLayout();
// Assign function calls to touch events
ninjapad.utils.assign(ninjapad.gamepad.toggleMenu, "menu", "start", "end");
ninjapad.utils.assign(ninjapad.gamepad.analogSwitch, "analogSwitch", "start", "end");
ninjapad.utils.assign(ninjapad.gamepad.buttonPress, "GAMEPAD-BUTTONS", "start", "move", "end");
ninjapad.utils.assign(ninjapad.gamepad.analogTouch, "ANALOG_STICK", "start", "move", "end");
ninjapad.utils.assign(ninjapad.gamepad.toggleFullScreen, SCREEN, "end");
ninjapad.utils.assign(null, "GAMEPAD");
};
$(document).ready(function() {
DEBUG && console.log("Document ready event");
// Load emulator
ninjapad.emulator = ninjapad.interface[EMULATOR];
// Pause on loss of focus
$(window).blur(function() {
!ninjapad.pause.state.isEmulationPaused &&
ninjapad.utils.isMobileDevice() &&
ninjapad.pause.pauseEmulation();
});
// Reload layout on orientation change
$(window).resize(function() {
DEBUG && console.log("Window resize event");
ninjapad.initialize();
});
// Use ESC key to open the menu
$(window).keyup(function(e) {
if (e.code == "Escape") ninjapad.menu.toggleMenu();
});
// Load a ROM and setup the page layout
ninjapad.emulator.initialize();
ninjapad.initialize();
});

View File

@ -0,0 +1,140 @@
ninjapad.menu = function() {
var state = { isOpen: false };
function allowUserInteraction(ontap=null) {
ninjapad.utils.allowInteraction("pauseScreenContent");
ninjapad.utils.assignNoPropagation(ontap, "OSD", ontap && "end");
}
function preventUserInteraction(ontap=null) {
ninjapad.utils.assign(null, "pauseScreenContent");
ninjapad.utils.assignNoPropagation(ontap, "OSD", ontap && "end");
}
function showError(msg) {
$("#pauseScreenContent").html(
ninjapad.utils.html("div", "error", msg)
);
preventUserInteraction(returnToMainMenu);
}
function mainMenu() {
const upload = "ninjapad.menu.uploadROM()";
const save = "ninjapad.menu.saveState();";
const load = "ninjapad.menu.loadState();";
const reset = "ninjapad.menu.reset();"
const about = "ninjapad.menu.showCredits()";
return ninjapad.utils.createMenu(null,
ninjapad.utils.link("Load ROM", js=upload, hide=SINGLE_ROM),
ninjapad.utils.link("Save State", js=save),
ninjapad.utils.link("Load State", js=load),
ninjapad.utils.link("Reset", js=reset),
ninjapad.utils.link("About", js=about)
);
}
function openMainMenu() {
ninjapad.pause.pauseEmulation(
ninjapad.utils.html("span", "pauseScreenContent", mainMenu())
);
allowUserInteraction();
state.isOpen = true;
}
function returnToMainMenu(event) {
event.stopPropagation();
$("#pauseScreenContent").html(
mainMenu()
);
allowUserInteraction();
}
return {
state: state,
loadState: function() {
const hash = sha256(ninjapad.emulator.getROMData());
const data = localStorage.getItem(hash);
if (!data) {
showError("No save data");
return;
}
try {
ninjapad.emulator.loadState(
uint8ToUtf16.decode(data)
);
ninjapad.pause.resumeEmulation();
}
catch (e) {
showError(`Error<br/><br/>${e.message}`);
DEBUG && console.log(e);
}
},
saveState: function() {
const hash = sha256(ninjapad.emulator.getROMData());
const data = ninjapad.emulator.saveState();
try {
const optimizedData = uint8ToUtf16.encode(data);
localStorage.setItem(hash, optimizedData);
ninjapad.pause.resumeEmulation();
}
catch (e) {
showError(`Error<br/><br/>${e.message}`);
DEBUG && console.log(e);
}
},
reset: function() {
ninjapad.emulator.reloadROM();
ninjapad.pause.resumeEmulation();
},
uploadROM: function() {
ninjapad.jQElement.upload.trigger("click");
const inputElement = document.getElementById("upload");
inputElement.addEventListener("change", handleFiles, false);
function handleFiles() {
let saveData = null;
if (ninjapad.emulator.isROMLoaded()) {
saveData = ninjapad.emulator.saveState();
}
let f = document.getElementById('upload').files[0];
let reader = new FileReader();
reader.onload = function () {
try {
ninjapad.emulator.loadROMData(reader.result);
ninjapad.pause.resumeEmulation();
}
catch (e) {
if (saveData) {
ninjapad.emulator.reloadROM();
ninjapad.emulator.loadState(saveData);
}
showError(`Error<br/><br/>${e.message.strip(".")}`);
DEBUG && console.log(e);
}
}
reader.readAsBinaryString(f);
}
},
showCredits: function() {
$("#pauseScreenContent").html(
ninjapad.utils.html("div", "about", ABOUT)
);
allowUserInteraction(returnToMainMenu)
},
toggleMenu: function() {
if (!ninjapad.pause.state.cannotResume && state.isOpen) {
ninjapad.pause.resumeEmulation();
state.isOpen = false;
return;
}
openMainMenu();
}
}
}();

View File

@ -0,0 +1,237 @@
#ninjaPad {
background-color: #222;
height: 0%;
}
#GAMEPAD {
position: absolute;
width: 100%;
display: none;
}
#OSD {
position: absolute;
text-align: center;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
font-family: KarmaticArcade;
font-size: 5vmin;
box-sizing: border-box;
padding: 2em;
word-spacing: 0.5em;
color: red;
visibility: hidden;
background-color: rgba(0, 0, 0, 0.8);
}
/* unvisited link */
#OSD a:link {
color: red;
text-decoration: none;
}
/* visited link */
#OSD a:visited {
color: red;
text-decoration: none;
}
/* mouse over link */
#OSD a:hover {
color: red;
text-decoration: none;
}
/* selected link */
#OSD a:active {
color: yellow;
text-decoration: none;
}
#DEADZONE {
background: #444;
}
.dirButtons {
position: absolute;
left: 4%;
top: 10vw;
width: 35vw;
height: 35vw;
display: flex;
justify-content: center;
align-items: center;
}
.actionButtons {
position: absolute;
right: 4%;
top: 10vw;
width: 35vw;
height: 35vw;
display: flex;
justify-content: center;
align-items: center;
}
.middleButtons {
position: absolute;
left: 45%;
top: 10vw;
width: 10vw;
height: 35vw;
display: flex;
justify-content: center;
align-items: center;
}
.dpad {
position: absolute;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
.analogRing {
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: 50%;
background-color: transparent;
border: 2.5vw;
border-style: inset;
}
.analogStick {
position: absolute;
width: 62%;
height: 62%;
box-sizing: border-box;
border-radius: 50%;
background-color: #666;
border: 2.5vw;
border-style: outset;
}
.button_B {
position: absolute;
left: 0%;
top: 25%;
width: 50%;
height: 50%;
box-sizing: border-box;
border-radius: 50%;
text-align: center;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
font-family: monospace;
}
.button_A {
position: absolute;
right: 0%;
top: -2.5%;
width: 50%;
height: 50%;
box-sizing: border-box;
border-radius: 50%;
text-align: center;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
font-family: monospace;
}
.button_AB {
position: absolute;
right: 0%;
bottom: -2.5%;
width: 50%;
height: 50%;
box-sizing: border-box;
border-radius: 50%;
text-align: center;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
font-family: monospace;
}
.button_SELECT {
position: absolute;
left: 0%;
top: 30%;
width: 100%;
height: 16%;
box-sizing: border-box;
align-items: center;
justify-content: center;
}
.button_START {
position: absolute;
left: 0%;
top: 55%;
width: 100%;
height: 16%;
box-sizing: border-box;
align-items: center;
justify-content: center;
}
.analogSwitchButton {
position: absolute;
left: 0%;
top: 0%;
width: 100%;
height: 16%;
box-sizing: border-box;
background-color: #666;
border: 1vw;
border-style: outset;
border-radius: 20%;
}
.menuButton {
position: absolute;
left: 0%;
bottom: 0%;
width: 100%;
height: 16%;
box-sizing: border-box;
background-color: #666;
border: 1vw;
border-style: outset;
border-radius: 20%;
}
.box {
display: flex;
align-items: center;
justify-content: center;
}
[id^="BUTTON"] {
background-color: #BBB;
border: 1vw;
border-style: outset;
}
@font-face {
font-family: 'KarmaticArcade';
src: url('res/ka1.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,48 @@
ninjapad.pause = function() {
var state = {
isEmulationPaused: false,
cannotResume: false
};
var pauseScreen = {
visibility: "hidden",
content: ""
};
function pauseText() {
let msg = "Emulation paused";
let resumeMsg = ninjapad.utils.isMobileDevice() ? "Tap" : "Click";
resumeMsg += " to resume";
return ninjapad.utils.html("span", "pauseScreenContent", msg + "<br/>" + resumeMsg);
}
return {
state: state,
pauseScreen: pauseScreen,
pauseEmulation: function(content=null) {
ninjapad.emulator.pause();
pauseScreen.visibility = "visible";
pauseScreen.content = content || pauseText();
ninjapad.jQElement.osd.empty();
ninjapad.jQElement.osd.append(pauseScreen.content);
ninjapad.jQElement.osd.css("visibility", pauseScreen.visibility);
state.isEmulationPaused = true;
ninjapad.utils.assign(null, "pauseScreenContent");
ninjapad.utils.assignNoPropagation(ninjapad.pause.resumeEmulation, "OSD", "end");
DEBUG && console.log("Emulation paused");
},
resumeEmulation: function(event) {
if (event) event.stopPropagation();
if (state.cannotResume) return;
ninjapad.emulator.resume();
pauseScreen.visibility = "hidden";
ninjapad.jQElement.osd.css("visibility", pauseScreen.visibility);
state.isEmulationPaused = false;
ninjapad.menu.state.isOpen = false;
DEBUG && console.log("Emulation resumed");
}
};
}();

Binary file not shown.

View File

@ -0,0 +1,162 @@
ninjapad.utils = function() {
const TOUCH_EVENTS = ["start", "move", "end"];
Number.prototype.mod = function(n) {
return ((this%n)+n)%n;
};
String.prototype.strip = function (string) {
var escaped = string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
return this.replace(RegExp("^[" + escaped + "]+|[" + escaped + "]+$", "gm"), '');
};
return {
preventDefaultWithoutPropagation: function(event) {
event.preventDefault();
event.stopPropagation();
},
preventDefault: function(event) {
event.preventDefault();
},
stopPropagation: function(event) {
event.stopPropagation();
},
isIOSDevice: function(){
return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
},
isMobileDevice: function() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
},
isFullScreen: function() {
return (
document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement
);
},
enterFullscreen: function(element) {
if (element.requestFullScreen) {
element.requestFullScreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.webkitEnterFullscreen) {
element.webkitEnterFullscreen(); //for iphone this code worked
}
},
exitFullScreen: function() {
if (document.cancelFullScreen) {
document.cancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
},
html: function(obj, id, text) {
return `<${obj} id='${id}'>${text}</${obj}>`;
},
link: function(content, js, hide) {
js = `${js}; return false;`;
return hide || `<a href="#" onclick="${js}">${content}</a>`;
},
createMenu: function(title, ...opts) {
opts = opts.filter(e => e !== true);
title = title ? `${title}<br/>` : "";
return (
`<div style="line-height: 2.2em;">
${title}
${opts.join("<br/>")}
</div>`
);
},
assign: function(fn, elementName, ...touchEvents) {
// Prevent default on all events
let element = document.getElementById(elementName);
for (const e of TOUCH_EVENTS) {
eval("element.ontouch" + e + " = ninjapad.utils.preventDefault");
}
// Assign function call to events
for (const e of touchEvents) {
eval("element.ontouch" + e + " = fn");
}
},
assignNoPropagation: function(fn, elementName, ...touchEvents) {
// Prevent default and stop propagation on all events
let element = document.getElementById(elementName);
for (const e of TOUCH_EVENTS) {
eval("element.ontouch" + e + " = ninjapad.utils.preventDefaultWithoutPropagation");
}
// Assign function call to events
for (const e of touchEvents) {
eval("element.ontouch" + e + " = fn");
}
},
allowInteraction: function(elementName) {
let element = document.getElementById(elementName);
for (const e of TOUCH_EVENTS) {
eval("element.ontouch" + e + " = ninjapad.utils.stopPropagation");
}
},
zip: function(data) {
const buf = fflate.strToU8(data);
return fflate.compressSync(buf, { level: 9, mem: 8 });
},
unzip: function(data) {
const decompressed = fflate.decompressSync(data);
return fflate.strFromU8(decompressed);
},
equal: function(buf1, buf2) {
var result = true;
if (buf1.byteLength != buf2.byteLength) {
DEBUG && console.log("size", buf1.byteLength, buf2.byteLength);
return false;
}
var dv1 = new Int8Array(buf1);
var dv2 = new Int8Array(buf2);
for (var i = 0 ; i != buf1.byteLength ; i++)
{
if (dv1[i] != dv2[i]) {
result = false;
DEBUG && console.log(i, dv1[i], dv2[i]);
}
}
return result;
},
vw: function(v) {
let w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
return (v * w) / 100;
},
dist: function(dx, dy) {
return Math.sqrt((dx * dx) + (dy * dy));
},
angle: function(dx, dy) {
return Math.atan2(dy, dx);
}
}
}();