Add button bar, some buttons, states, screenshot

This commit is contained in:
Ethan O'Brien 2023-06-22 11:00:13 -05:00
parent 91b25a20fa
commit 86d28b867a
5 changed files with 314 additions and 36 deletions

View file

@ -6,7 +6,6 @@
EJS_player = '#game';
EJS_core = 'nes';
EJS_gameUrl = 'mega_mountain.nes';
EJS_pathtodata = 'data/';
EJS_DEBUG_XX = true;
</script>
<script src='src/loader.js'></script>

77
src/GameManager.js Normal file
View file

@ -0,0 +1,77 @@
class EJS_GameManager {
constructor(Module) {
this.Module = Module;
this.FS = this.Module.FS;
this.functions = {
restart: this.Module.cwrap('system_restart', '', []),
getStateInfo: this.Module.cwrap('get_state_info', 'string', []), //these names are dumb
saveStateInfo: this.Module.cwrap('save_state_info', 'null', []),
loadState: this.Module.cwrap('load_state', 'number', ['string', 'number']),
screenshot: this.Module.cwrap('cmd_take_screenshot', '', [])
}
}
restart() {
this.functions.restart();
}
getState() {
return new Promise(async (resolve, reject) => {
const stateInfo = (await this.getStateInfo()).split('|')
let state;
let size = stateInfo[0] >> 0;
if (size > 0) {
state = new Uint8Array(size);
let start = stateInfo[1] >> 0;
for (let i=0; i<size; i++) state[i] = this.Module.getValue(start + i);
}
resolve(state);
})
}
getStateInfo() {
this.functions.saveStateInfo();
return new Promise((resolve, reject) => {
let a;
let b = setInterval(() => {
a = this.functions.getStateInfo();
if (a) {
clearInterval(b);
resolve(a);
}
}, 50)
});
}
loadState(state) {
try {
this.FS.unlink('game.state');
} catch(e){}
this.FS.writeFile('/game.state', state);
this.functions.loadState("game.state", 0);
setTimeout(() => {
this.FS.unlink('game.state');
}, 5000)
}
screenshot() {
this.functions.screenshot();
return this.FS.readFile('screenshot.png');
}
quickSave() {
(async () => {
let slot = 0;
let name = slot + '-quick.state';
try {
this.FS.unlink(name);
} catch (e) {}
let data = await this.getState();
this.FS.writeFile('/'+name, data);
})();
}
quickLoad() {
(async () => {
let slot = 0;
let name = slot + '-quick.state';
this.functions.loadState(name, 0);
})();
}
}
window.EJS_GameManager = EJS_GameManager;

View file

@ -3,7 +3,7 @@
overflow: hidden;
--ejs-primary-color: 26,175,255;
position: relative;
font-family: Avenir,"Avenir Next","Helvetica Neue","Segoe UI",Helvetica,Arial,sans-serif;
font-family: Avenir, "Avenir Next", "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@ -105,3 +105,96 @@
box-shadow: 0 0 0 5px rgba(var(--ejs-primary-color),0.5);
outline: 0;
}
.ejs_menu_bar {
padding: 15px 10px 10px;
background: linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0.7));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
bottom: 0;
color: #fff;
left: 0;
position: absolute;
right: 0;
transition: opacity .4s ease-in-out,transform .4s ease-in-out;
z-index: 3;
align-items: center;
display: flex;
justify-content: flex-start;
text-align: center;
}
.ejs_menu_bar svg {
display: block;
fill: currentColor;
height: 18px;
pointer-events: none;
width: 18px;
}
.ejs_menu_bar_hidden {
opacity: 0;
pointer-events: none;
transform: translateY(100%);
}
.ejs_menu_button {
margin-right: 2px;
touch-action: manipulation;
background: transparent;
border: 0;
border-radius: 3px;
color: inherit;
cursor: pointer;
flex-shrink: 0;
overflow: visible;
padding: 7px;
position: relative;
transition: all .3s ease;
}
.ejs_menu_button:hover {
background: rgba(var(--ejs-primary-color),1);
color: #fff;
}
.ejs_menu_button:hover .ejs_menu_text {
transform: translate(0,0) scale(1);
opacity: 1;
}
.ejs_menu_text {
left: 0;
background: rgba(255,255,255,0.9);
border-radius: 3px;
bottom: 100%;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
color: #4f5b5f;
font-size: 14px;
font-weight: 500;
line-height: 1.3;
margin-bottom: 10px;
opacity: 0;
padding: 5px 7.5px;
pointer-events: none;
position: absolute;
transform: translate(0,10px) scale(0.8);
transform-origin: 0 100%;
transition: transform .2s .1s ease,opacity .2s .1s ease;
white-space: nowrap;
z-index: 2;
}
.ejs_menu_text::before {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid rgba(255,255,255,0.9);
bottom: -4px;
content: '';
height: 0;
left: 16px;
position: absolute;
transform: translateX(-50%);
width: 0;
z-index: 2;
}

View file

@ -2,6 +2,13 @@ class EmulatorJS {
createElement(type) {
return document.createElement(type);
}
addEventListener(element, listener, callback) {
const listeners = listener.split(" ");
for (let i=0; i<listeners.length; i++) {
element.addEventListener(listeners[i], callback);
this.listeners.push({cb:callback, elem:element, listener:listeners[i]});
}
}
downloadFile(path, cb, progressCB, notWithPath, opts) {
const basePath = notWithPath ? '' : this.config.dataPath;
path = basePath + path;
@ -61,8 +68,10 @@ class EmulatorJS {
}
}
constructor(element, config) {
this.debug = (window.EJS_DEBUG_XX === true);
this.setElements(element);
this.started = false;
this.paused = true;
this.listeners = [];
this.config = config;
this.canvas = this.createElement('canvas');
@ -84,10 +93,6 @@ class EmulatorJS {
}
this.elements.parent.classList.add("ejs_parent");
}
addEventListener(element, listener, callback) {
element.addEventListener(listener, callback);
this.listeners.push({cb:callback, elem:element, listener:listener});
}
// Start button
createStartButton() {
const button = this.createElement("div");
@ -223,6 +228,8 @@ class EmulatorJS {
}
}
downloadRom() {
this.gameManager = new window.EJS_GameManager(this.Module);
this.textElem.innerText = this.localization("Download Game Data");
this.downloadFile(this.config.gameUrl, (res) => {
if (res === -1) {
@ -247,13 +254,13 @@ class EmulatorJS {
'preRun': [],
'postRun': [],
'canvas': this.canvas,
'print': function(msg) {
if (window.EJS_DEBUG_XX === true) {
'print': (msg) => {
if (this.debug) {
console.log(msg);
}
},
'printErr': function(msg) {
if (window.EJS_DEBUG_XX === true) {
'printErr': (msg) => {
if (this.debug) {
console.log(msg);
}
},
@ -269,6 +276,7 @@ class EmulatorJS {
console.log(a, b, c)
}
};
this.Module = window.Module;
}
startGame() {
this.textElem.remove();
@ -276,11 +284,12 @@ class EmulatorJS {
this.game.classList.remove("ejs_game");
this.game.appendChild(this.canvas);
const args = [];
if (window.EJS_DEBUG_XX === true) args.push('-v');
if (this.debug) args.push('-v');
args.push('/game');
Module.callMain(args);
Module.resumeMainLoop();
this.started = true;
this.paused = false;
Module.setCanvasSize(800, 600);
let i=0;
// this needs to be fixed. Ugh.
@ -292,6 +301,7 @@ class EmulatorJS {
}
bindListeners() {
this.createContextMenu();
this.createBottomMenuBar();
//keyboard, etc...
}
createContextMenu() {
@ -305,41 +315,139 @@ class EmulatorJS {
}
e.preventDefault();
})
const hideMenu = () => {
this.elements.contextmenu.style.display = "none";
}
this.addEventListener(this.elements.contextmenu, 'contextmenu', (e) => e.preventDefault());
this.addEventListener(this.elements.parent, 'contextmenu', (e) => e.preventDefault());
this.addEventListener(this.game, 'mousedown', (e) => {
this.elements.contextmenu.style.display = "none";
})
let contextHtml = ['<ul>', '</ul>']
let contextFunctions = []
this.addEventListener(this.game, 'mousedown', hideMenu);
const parent = this.createElement("ul");
const addButton = (title, hidden, functi0n) => {
//<li><a href="#" onclick="return false">'+title+'</a></li>
const li = this.createElement("li");
if (hidden) li.hidden = true;
const a = this.createElement("a");
if (functi0n instanceof Function) {
contextFunctions.push(functi0n);
} else {
contextFunctions.push(() => {});
}
let i = contextHtml.length - 1;
if (hidden) {
contextHtml.splice(i, 0, '<li hidden><a href="#" onclick="return false">'+title+'</a></li>');
} else {
contextHtml.splice(i, 0, '<li><a href="#" onclick="return false">'+title+'</a></li>');
this.addEventListener(li, 'click', functi0n);
}
a.href = "#";
a.onclick = "return false";
a.innerText = title;
li.appendChild(a);
parent.appendChild(li);
hideMenu();
}
addButton("test 1", false, () => console.log("1"));
addButton("test 2", false, () => console.log("2"));
addButton("test 3", false, () => console.log("3"));
addButton("test 4", false, () => console.log("4"));
let screenshotUrl;
addButton("Take Screenshot", false, () => {
if (screenshotUrl) URL.revokeObjectURL(screenshotUrl);
const screenshot = this.gameManager.screenshot();
const blob = new Blob([screenshot]);
screenshotUrl = URL.createObjectURL(blob);
const a = this.createElement("a");
a.href = screenshotUrl;
a.download = "screenshot.png";
a.click();
hideMenu();
});
addButton("Quick Save", false, () => {
this.gameManager.quickSave();
hideMenu();
});
addButton("Quick Load", false, () => {
this.gameManager.quickLoad();
hideMenu();
});
addButton("EmulatorJS", false, () => console.log("4"));
this.elements.contextmenu.innerHTML = contextHtml.join('');
let buttons = this.elements.contextmenu.getElementsByTagName('li')
for (let i=0; i<buttons.length; i++) {
this.addEventListener(buttons[i], 'click', contextFunctions[i]);
}
this.elements.contextmenu.appendChild(parent);
this.elements.parent.appendChild(this.elements.contextmenu);
}
selectFile() {
return new Promise((resolve, reject) => {
const file = this.createElement("input");
file.type = "file";
this.addEventListener(file, "change", (e) => {
resolve(e.target.files[0]);
})
file.click();
})
}
createBottomMenuBar() {
this.elements.menu = this.createElement("div");
this.elements.menu.classList.add("ejs_menu_bar");
this.elements.menu.classList.add("ejs_menu_bar_hidden");
let timeout = null;
const hide = () => {
if (this.paused) return;
this.elements.menu.classList.add("ejs_menu_bar_hidden");
}
this.addEventListener(this.elements.parent, 'mousemove click', (e) => {
if (!this.started) return;
if (timeout !== null) clearTimeout(timeout);
timeout = setTimeout(hide, 3000);
this.elements.menu.classList.remove("ejs_menu_bar_hidden");
})
this.menu = {
close: () => {
if (!this.started) return;
if (timeout !== null) clearTimeout(timeout);
this.elements.menu.classList.remove("ejs_menu_bar_hidden");
},
open: () => {
if (!this.started) return;
if (timeout !== null) clearTimeout(timeout);
timeout = setTimeout(hide, 3000);
this.elements.menu.classList.remove("ejs_menu_bar_hidden");
}
}
this.elements.parent.appendChild(this.elements.menu);
//Now add buttons
const addButton = (title, image, callback) => {
const button = this.createElement("button");
button.type = "button";
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute("role", "presentation");
svg.setAttribute("focusable", "false");
svg.innerHTML = image;
const text = this.createElement("span");
text.innerText = title;
text.classList.add("ejs_menu_text");
button.classList.add("ejs_menu_button");
button.appendChild(svg);
button.appendChild(text);
this.elements.menu.appendChild(button);
if (callback instanceof Function) {
this.addEventListener(button, 'click', callback);
}
}
//todo. Center text on not restart button
addButton("Restart", '<svg viewBox="0 0 512 512"><path d="M496 48V192c0 17.69-14.31 32-32 32H320c-17.69 0-32-14.31-32-32s14.31-32 32-32h63.39c-29.97-39.7-77.25-63.78-127.6-63.78C167.7 96.22 96 167.9 96 256s71.69 159.8 159.8 159.8c34.88 0 68.03-11.03 95.88-31.94c14.22-10.53 34.22-7.75 44.81 6.375c10.59 14.16 7.75 34.22-6.375 44.81c-39.03 29.28-85.36 44.86-134.2 44.86C132.5 479.9 32 379.4 32 256s100.5-223.9 223.9-223.9c69.15 0 134 32.47 176.1 86.12V48c0-17.69 14.31-32 32-32S496 30.31 496 48z"/></svg>', () => {
this.gameManager.restart();
});
let stateUrl;
addButton("Save State", '<svg viewBox="0 0 448 512"><path fill="currentColor" d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"/></svg>', async () => {
if (stateUrl) URL.revokeObjectURL(stateUrl);
const state = await this.gameManager.getState();
const blob = new Blob([state]);
stateUrl = URL.createObjectURL(blob);
const a = this.createElement("a");
a.href = stateUrl;
a.download = "game.state";
a.click();
});
addButton("Load State", '<svg viewBox="0 0 576 512"><path fill="currentColor" d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"/></svg>', async () => {
const file = await this.selectFile();
const state = new Uint8Array(await file.arrayBuffer());
this.gameManager.loadState(state);
});
}
}

View file

@ -35,6 +35,7 @@
if (('undefined' != typeof EJS_DEBUG_XX && true === EJS_DEBUG_XX) || true) {
await loadScript('emulator.js');
await loadScript('GameManager.js');
await loadStyle('css/main.css');
}
const config = {};