diff --git a/resen/pages.js b/resen/pages.js index 812e9ed..7a3cefb 100644 --- a/resen/pages.js +++ b/resen/pages.js @@ -6,12 +6,13 @@ const pages = { "/": {title: "רֶסֶן", alt: "Resen", author: "resen", logo: "media/resen.svg", skip: true}, // "open-call-kmeot/": {title: "קול קורא: קמעות", alt: "Open call: Talismans", author: "resen"}, + "screen/": {title: "קמעות צג", alt: "Screen talismans", author: "eakoukli", kw: [1, "interactive", "live code", "poem", "software", "sound", "visual"]}, "fateful/": {title: "שהות הרת־גורל", alt: "Fateful stay", author: "shoeyraz", kw: [1]}, "psychosophy/": {title: "פסיכוסופיה: פתיחה", alt: "Psychsophy: a reading", author: ["avinoamsternheim", "menahemgoldenberg"], kw: [1, "visual"]}, "talismother/": {title: "אימא קמעית", alt: "Talis-Mother", author: "sandravalabregue", kw: [1, "visual"]}, "umbilical/": {title: "טבוּר", alt: "Umbilical", author: "nettalevtov", kw: [1, "live code", "poem", "visual"]}, "taase/": {title: "תעשה", alt: "Ta-a-se", author: "ayaamikam", kw: [1, "biblical", "cipher", "interactive", "visual"]}, - "kaddish/": {title: "קדיש (שלד)", alt: "Kaddish (skeleton)", author: "omridanino", kw: [1, "biblical", "interactive", "poem", "visual"]}, + "kaddish/": {title: "קדיש (שלד)", alt: "Kaddish (skeleton)", author: "omridanino", kw: [1, "biblical", "interactive", "poem", "sound", "visual"]}, "water/": {title: "קוד מים | תרשימי זרימה", alt: "Code of water | flow charts", author: "arikweiss", kw: [1, "2d 3d", "biblical", "interactive", "visual"]}, "talismetta/": {title: "טליס־מטא", alt: "Talis-Metta", author: ["neorashem", "nimrodkerrett"], kw: [1, "software", "visual"]}, "snark/": {title: "כרחש אבמ\"ח וכרחש אדו\"ש", alt: "ABMḤ snark and ADWŠ", author: "eyalgruss", kw: [1, "2d 3d", "combinatorial", "interactive", "new constraint", "pangram", "software", "sound", "visual"]}, @@ -72,6 +73,10 @@ const authors = { "brunogrife": { "name": {"": "עידן ברונו גרייף", "en": "Bruno Grife"}, }, + "eakoukli": { + "name": {"": "א. קוּקלי", "en": "E. A. Koukly"}, + "web": "toonsnake.net", + }, "eyalgruss": { "name": {"": "איל יהוה גרוּס", "en": "Eyal Yehowa Gruss"}, "mail": "@gmail.com", diff --git a/resen/screen/README.md b/resen/screen/README.md new file mode 100644 index 0000000..0679159 --- /dev/null +++ b/resen/screen/README.md @@ -0,0 +1,25 @@ +# binjgb + +Fork of binji's Game Boy emulator built as a WebAssembly module. + +It includes changes from [Daid's fork](https://github.com/daid/binjgb) and others to better support GB Studio. + +## License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/resen/screen/assets/Assistant-VariableFont_wght.ttf b/resen/screen/assets/Assistant-VariableFont_wght.ttf new file mode 100644 index 0000000..e4ad938 Binary files /dev/null and b/resen/screen/assets/Assistant-VariableFont_wght.ttf differ diff --git a/resen/screen/assets/Assistant_OFL.txt b/resen/screen/assets/Assistant_OFL.txt new file mode 100644 index 0000000..5dee9a3 --- /dev/null +++ b/resen/screen/assets/Assistant_OFL.txt @@ -0,0 +1,95 @@ +Copyright 2020 The Assistant Project Authors (https://github.com/hafontia/Assistant). +Copyright 2010 The Source Sans Pro Authors (https://github.com/adobe-fonts/source-sans-pro), with Reserved Font Name 'Source'. +Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resen/screen/binjgb.js b/resen/screen/binjgb.js new file mode 100644 index 0000000..e32ce2d --- /dev/null +++ b/resen/screen/binjgb.js @@ -0,0 +1,21 @@ + +var Binjgb = (function() { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; + + return ( +function(Binjgb) { + Binjgb = Binjgb || {}; + +var Module=typeof Binjgb!=="undefined"?Binjgb:{};var readyPromiseResolve,readyPromiseReject;Module["ready"]=new Promise(function(resolve,reject){readyPromiseResolve=resolve;readyPromiseReject=reject});var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var arguments_=[];var thisProgram="./this.program";var quit_=function(status,toThrow){throw toThrow};var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!=="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=function(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=function(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=function(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=function(title){document.title=title}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!=="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;var UTF8Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(heap,idx,maxBytesToRead){var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heap[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heap.subarray&&UTF8Decoder){return UTF8Decoder.decode(heap.subarray(idx,endPtr))}else{var str="";while(idx>10,56320|ch&1023)}}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferAndViews(buf){buffer=buf;Module["HEAP8"]=HEAP8=new Int8Array(buf);Module["HEAP16"]=HEAP16=new Int16Array(buf);Module["HEAP32"]=HEAP32=new Int32Array(buf);Module["HEAPU8"]=HEAPU8=new Uint8Array(buf);Module["HEAPU16"]=HEAPU16=new Uint16Array(buf);Module["HEAPU32"]=HEAPU32=new Uint32Array(buf);Module["HEAPF32"]=HEAPF32=new Float32Array(buf);Module["HEAPF64"]=HEAPF64=new Float64Array(buf)}var INITIAL_MEMORY=Module["INITIAL_MEMORY"]||16777216;var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;__ATINIT__.push({func:function(){___wasm_call_ctors()}});function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what+="";err(what);ABORT=true;EXITSTATUS=1;what="abort("+what+"). Build with -s ASSERTIONS=1 for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}function hasPrefix(str,prefix){return String.prototype.startsWith?str.startsWith(prefix):str.indexOf(prefix)===0}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return hasPrefix(filename,dataURIPrefix)}var wasmBinaryFile="binjgb.wasm";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}else{throw"both async and sync fetching of the wasm failed"}}catch(err){abort(err)}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch==="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){if(!response["ok"]){throw"failed to load wasm binary file at '"+wasmBinaryFile+"'"}return response["arrayBuffer"]()}).catch(function(){return getBinary(wasmBinaryFile)})}}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var info={"a":asmLibraryArg};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["g"];updateGlobalBufferAndViews(wasmMemory.buffer);wasmTable=Module["asm"]["H"];removeRunDependency("wasm-instantiate")}addRunDependency("wasm-instantiate");function receiveInstantiatedSource(output){receiveInstance(output["instance"])}function instantiateArrayBuffer(receiver){return getBinaryPromise().then(function(binary){return WebAssembly.instantiate(binary,info)}).then(receiver,function(reason){err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(){if(!wasmBinary&&typeof WebAssembly.instantiateStreaming==="function"&&!isDataURI(wasmBinaryFile)&&typeof fetch==="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){var result=WebAssembly.instantiateStreaming(response,info);return result.then(receiveInstantiatedSource,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(receiveInstantiatedSource)})})}else{return instantiateArrayBuffer(receiveInstantiatedSource)}}if(Module["instantiateWasm"]){try{var exports=Module["instantiateWasm"](info,receiveInstance);return exports}catch(e){err("Module.instantiateWasm callback failed with error: "+e);return false}}instantiateAsync().catch(readyPromiseReject);return{}}function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback(Module);continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){wasmTable.get(func)()}else{wasmTable.get(func)(callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src,src+num)}function abortOnCannotGrowMemory(requestedSize){abort("OOM")}function _emscripten_resize_heap(requestedSize){abortOnCannotGrowMemory(requestedSize)}function _exit(status){exit(status)}var SYSCALLS={mappings:{},buffers:[null,[],[]],printChar:function(stream,curr){var buffer=SYSCALLS.buffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}},varargs:undefined,get:function(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(ptr){var ret=UTF8ToString(ptr);return ret},get64:function(low,high){return low}};function _fd_close(fd){return 0}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){}function _fd_write(fd,iov,iovcnt,pnum){var num=0;for(var i=0;i>2];var len=HEAP32[iov+(i*8+4)>>2];for(var j=0;j>2]=num;return 0}var asmLibraryArg={"d":_emscripten_memcpy_big,"e":_emscripten_resize_heap,"b":_exit,"f":_fd_close,"c":_fd_seek,"a":_fd_write};var asm=createWasm();var ___wasm_call_ctors=Module["___wasm_call_ctors"]=function(){return(___wasm_call_ctors=Module["___wasm_call_ctors"]=Module["asm"]["h"]).apply(null,arguments)};var _malloc=Module["_malloc"]=function(){return(_malloc=Module["_malloc"]=Module["asm"]["i"]).apply(null,arguments)};var _emulator_set_builtin_palette=Module["_emulator_set_builtin_palette"]=function(){return(_emulator_set_builtin_palette=Module["_emulator_set_builtin_palette"]=Module["asm"]["j"]).apply(null,arguments)};var _emulator_was_ext_ram_updated=Module["_emulator_was_ext_ram_updated"]=function(){return(_emulator_was_ext_ram_updated=Module["_emulator_was_ext_ram_updated"]=Module["asm"]["k"]).apply(null,arguments)};var _emulator_read_ext_ram=Module["_emulator_read_ext_ram"]=function(){return(_emulator_read_ext_ram=Module["_emulator_read_ext_ram"]=Module["asm"]["l"]).apply(null,arguments)};var _emulator_write_ext_ram=Module["_emulator_write_ext_ram"]=function(){return(_emulator_write_ext_ram=Module["_emulator_write_ext_ram"]=Module["asm"]["m"]).apply(null,arguments)};var _file_data_delete=Module["_file_data_delete"]=function(){return(_file_data_delete=Module["_file_data_delete"]=Module["asm"]["n"]).apply(null,arguments)};var _free=Module["_free"]=function(){return(_free=Module["_free"]=Module["asm"]["o"]).apply(null,arguments)};var _emulator_delete=Module["_emulator_delete"]=function(){return(_emulator_delete=Module["_emulator_delete"]=Module["asm"]["p"]).apply(null,arguments)};var _emulator_get_PC=Module["_emulator_get_PC"]=function(){return(_emulator_get_PC=Module["_emulator_get_PC"]=Module["asm"]["q"]).apply(null,arguments)};var _emulator_get_A=Module["_emulator_get_A"]=function(){return(_emulator_get_A=Module["_emulator_get_A"]=Module["asm"]["r"]).apply(null,arguments)};var _emulator_get_BC=Module["_emulator_get_BC"]=function(){return(_emulator_get_BC=Module["_emulator_get_BC"]=Module["asm"]["s"]).apply(null,arguments)};var _emulator_get_DE=Module["_emulator_get_DE"]=function(){return(_emulator_get_DE=Module["_emulator_get_DE"]=Module["asm"]["t"]).apply(null,arguments)};var _emulator_get_HL=Module["_emulator_get_HL"]=function(){return(_emulator_get_HL=Module["_emulator_get_HL"]=Module["asm"]["u"]).apply(null,arguments)};var _emulator_get_F=Module["_emulator_get_F"]=function(){return(_emulator_get_F=Module["_emulator_get_F"]=Module["asm"]["v"]).apply(null,arguments)};var _emulator_get_SP=Module["_emulator_get_SP"]=function(){return(_emulator_get_SP=Module["_emulator_get_SP"]=Module["asm"]["w"]).apply(null,arguments)};var _emulator_set_PC=Module["_emulator_set_PC"]=function(){return(_emulator_set_PC=Module["_emulator_set_PC"]=Module["asm"]["x"]).apply(null,arguments)};var _emulator_set_breakpoint=Module["_emulator_set_breakpoint"]=function(){return(_emulator_set_breakpoint=Module["_emulator_set_breakpoint"]=Module["asm"]["y"]).apply(null,arguments)};var _emulator_clear_breakpoints=Module["_emulator_clear_breakpoints"]=function(){return(_emulator_clear_breakpoints=Module["_emulator_clear_breakpoints"]=Module["asm"]["z"]).apply(null,arguments)};var _emulator_render_vram=Module["_emulator_render_vram"]=function(){return(_emulator_render_vram=Module["_emulator_render_vram"]=Module["asm"]["A"]).apply(null,arguments)};var _emulator_render_background=Module["_emulator_render_background"]=function(){return(_emulator_render_background=Module["_emulator_render_background"]=Module["asm"]["B"]).apply(null,arguments)};var _emulator_get_wram_ptr=Module["_emulator_get_wram_ptr"]=function(){return(_emulator_get_wram_ptr=Module["_emulator_get_wram_ptr"]=Module["asm"]["C"]).apply(null,arguments)};var _emulator_get_hram_ptr=Module["_emulator_get_hram_ptr"]=function(){return(_emulator_get_hram_ptr=Module["_emulator_get_hram_ptr"]=Module["asm"]["D"]).apply(null,arguments)};var _emulator_read_mem=Module["_emulator_read_mem"]=function(){return(_emulator_read_mem=Module["_emulator_read_mem"]=Module["asm"]["E"]).apply(null,arguments)};var _emulator_write_mem=Module["_emulator_write_mem"]=function(){return(_emulator_write_mem=Module["_emulator_write_mem"]=Module["asm"]["F"]).apply(null,arguments)};var _set_audio_channel_mute=Module["_set_audio_channel_mute"]=function(){return(_set_audio_channel_mute=Module["_set_audio_channel_mute"]=Module["asm"]["G"]).apply(null,arguments)};var _joypad_new=Module["_joypad_new"]=function(){return(_joypad_new=Module["_joypad_new"]=Module["asm"]["I"]).apply(null,arguments)};var _joypad_delete=Module["_joypad_delete"]=function(){return(_joypad_delete=Module["_joypad_delete"]=Module["asm"]["J"]).apply(null,arguments)};var _rewind_append=Module["_rewind_append"]=function(){return(_rewind_append=Module["_rewind_append"]=Module["asm"]["K"]).apply(null,arguments)};var _rewind_delete=Module["_rewind_delete"]=function(){return(_rewind_delete=Module["_rewind_delete"]=Module["asm"]["L"]).apply(null,arguments)};var _emulator_new_simple=Module["_emulator_new_simple"]=function(){return(_emulator_new_simple=Module["_emulator_new_simple"]=Module["asm"]["M"]).apply(null,arguments)};var _emulator_get_ticks_f64=Module["_emulator_get_ticks_f64"]=function(){return(_emulator_get_ticks_f64=Module["_emulator_get_ticks_f64"]=Module["asm"]["N"]).apply(null,arguments)};var _emulator_run_until_f64=Module["_emulator_run_until_f64"]=function(){return(_emulator_run_until_f64=Module["_emulator_run_until_f64"]=Module["asm"]["O"]).apply(null,arguments)};var _rewind_get_newest_ticks_f64=Module["_rewind_get_newest_ticks_f64"]=function(){return(_rewind_get_newest_ticks_f64=Module["_rewind_get_newest_ticks_f64"]=Module["asm"]["P"]).apply(null,arguments)};var _rewind_get_oldest_ticks_f64=Module["_rewind_get_oldest_ticks_f64"]=function(){return(_rewind_get_oldest_ticks_f64=Module["_rewind_get_oldest_ticks_f64"]=Module["asm"]["Q"]).apply(null,arguments)};var _emulator_set_default_joypad_callback=Module["_emulator_set_default_joypad_callback"]=function(){return(_emulator_set_default_joypad_callback=Module["_emulator_set_default_joypad_callback"]=Module["asm"]["R"]).apply(null,arguments)};var _emulator_set_bw_palette_simple=Module["_emulator_set_bw_palette_simple"]=function(){return(_emulator_set_bw_palette_simple=Module["_emulator_set_bw_palette_simple"]=Module["asm"]["S"]).apply(null,arguments)};var _rewind_new_simple=Module["_rewind_new_simple"]=function(){return(_rewind_new_simple=Module["_rewind_new_simple"]=Module["asm"]["T"]).apply(null,arguments)};var _rewind_begin=Module["_rewind_begin"]=function(){return(_rewind_begin=Module["_rewind_begin"]=Module["asm"]["U"]).apply(null,arguments)};var _emulator_set_rewind_joypad_callback=Module["_emulator_set_rewind_joypad_callback"]=function(){return(_emulator_set_rewind_joypad_callback=Module["_emulator_set_rewind_joypad_callback"]=Module["asm"]["V"]).apply(null,arguments)};var _rewind_to_ticks_wrapper=Module["_rewind_to_ticks_wrapper"]=function(){return(_rewind_to_ticks_wrapper=Module["_rewind_to_ticks_wrapper"]=Module["asm"]["W"]).apply(null,arguments)};var _rewind_end=Module["_rewind_end"]=function(){return(_rewind_end=Module["_rewind_end"]=Module["asm"]["X"]).apply(null,arguments)};var _set_joyp_up=Module["_set_joyp_up"]=function(){return(_set_joyp_up=Module["_set_joyp_up"]=Module["asm"]["Y"]).apply(null,arguments)};var _set_joyp_down=Module["_set_joyp_down"]=function(){return(_set_joyp_down=Module["_set_joyp_down"]=Module["asm"]["Z"]).apply(null,arguments)};var _set_joyp_left=Module["_set_joyp_left"]=function(){return(_set_joyp_left=Module["_set_joyp_left"]=Module["asm"]["_"]).apply(null,arguments)};var _set_joyp_right=Module["_set_joyp_right"]=function(){return(_set_joyp_right=Module["_set_joyp_right"]=Module["asm"]["$"]).apply(null,arguments)};var _set_joyp_B=Module["_set_joyp_B"]=function(){return(_set_joyp_B=Module["_set_joyp_B"]=Module["asm"]["aa"]).apply(null,arguments)};var _set_joyp_A=Module["_set_joyp_A"]=function(){return(_set_joyp_A=Module["_set_joyp_A"]=Module["asm"]["ba"]).apply(null,arguments)};var _set_joyp_start=Module["_set_joyp_start"]=function(){return(_set_joyp_start=Module["_set_joyp_start"]=Module["asm"]["ca"]).apply(null,arguments)};var _set_joyp_select=Module["_set_joyp_select"]=function(){return(_set_joyp_select=Module["_set_joyp_select"]=Module["asm"]["da"]).apply(null,arguments)};var _get_frame_buffer_ptr=Module["_get_frame_buffer_ptr"]=function(){return(_get_frame_buffer_ptr=Module["_get_frame_buffer_ptr"]=Module["asm"]["ea"]).apply(null,arguments)};var _get_frame_buffer_size=Module["_get_frame_buffer_size"]=function(){return(_get_frame_buffer_size=Module["_get_frame_buffer_size"]=Module["asm"]["fa"]).apply(null,arguments)};var _get_sgb_frame_buffer_ptr=Module["_get_sgb_frame_buffer_ptr"]=function(){return(_get_sgb_frame_buffer_ptr=Module["_get_sgb_frame_buffer_ptr"]=Module["asm"]["ga"]).apply(null,arguments)};var _get_sgb_frame_buffer_size=Module["_get_sgb_frame_buffer_size"]=function(){return(_get_sgb_frame_buffer_size=Module["_get_sgb_frame_buffer_size"]=Module["asm"]["ha"]).apply(null,arguments)};var _get_audio_buffer_ptr=Module["_get_audio_buffer_ptr"]=function(){return(_get_audio_buffer_ptr=Module["_get_audio_buffer_ptr"]=Module["asm"]["ia"]).apply(null,arguments)};var _get_audio_buffer_capacity=Module["_get_audio_buffer_capacity"]=function(){return(_get_audio_buffer_capacity=Module["_get_audio_buffer_capacity"]=Module["asm"]["ja"]).apply(null,arguments)};var _ext_ram_file_data_new=Module["_ext_ram_file_data_new"]=function(){return(_ext_ram_file_data_new=Module["_ext_ram_file_data_new"]=Module["asm"]["ka"]).apply(null,arguments)};var _get_file_data_ptr=Module["_get_file_data_ptr"]=function(){return(_get_file_data_ptr=Module["_get_file_data_ptr"]=Module["asm"]["la"]).apply(null,arguments)};var _get_file_data_size=Module["_get_file_data_size"]=function(){return(_get_file_data_size=Module["_get_file_data_size"]=Module["asm"]["ma"]).apply(null,arguments)};var calledRun;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(args){args=args||arguments_;if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}Module["run"]=run;function exit(status,implicit){if(implicit&&noExitRuntime&&status===0){return}if(noExitRuntime){}else{EXITSTATUS=status;exitRuntime();if(Module["onExit"])Module["onExit"](status);ABORT=true}quit_(status,new ExitStatus(status))}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run(); + + + return Binjgb.ready +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') + module.exports = Binjgb; +else if (typeof define === 'function' && define['amd']) + define([], function() { return Binjgb; }); +else if (typeof exports === 'object') + exports["Binjgb"] = Binjgb; diff --git a/resen/screen/binjgb.wasm b/resen/screen/binjgb.wasm new file mode 100644 index 0000000..81fe205 Binary files /dev/null and b/resen/screen/binjgb.wasm differ diff --git a/resen/screen/css/style.css b/resen/screen/css/style.css new file mode 100644 index 0000000..ccf9d0c --- /dev/null +++ b/resen/screen/css/style.css @@ -0,0 +1,183 @@ +body { + --bg: #1d0212 !important; +} + +#game { + width: 100%; + max-width: 480px; + margin-inline: auto; +} + +canvas { + width: 100%; + margin-inline: auto; + image-rendering: crisp-edges; +} + +#controller { + display: block; + height: 230px; + width: 100%; + margin-inline: auto; + max-width: 480px; + touch-action: none; + text-align: center; + position: relative; + +} +#fullScreenButton span.material-symbols-outlined { + color: #f1dcab; + vertical-align: bottom; +} +#controller_dpad { + position: absolute; + bottom: 10px; + left: 0px; + width: 184px; + height: 184px; +} +#xyzLogo { + height: 100px; + padding: 25px 0; +} +#controller_left, +#controller_right, +#controller_up, +#controller_down, +#controller_a, +#controller_b, +#controller_start, +#controller_select { + cursor: pointer; +} + +#controller_dpad:before { + content: ""; + display: block; + width: 42px; + height: 42px; + background: #e38c89; + position: absolute; + left: 62px; + top: 68px; +} + +#controller_left { + position: absolute; + left: 20px; + top: 68px; + width: 42px; + height: 42px; + background: #e38c89; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +#controller_right { + position: absolute; + left: 104px; + top: 68px; + width: 42px; + height: 42px; + background: #e38c89; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +#controller_up { + position: absolute; + left: 62px; + top: 26px; + width: 42px; + height: 42px; + background: #e38c89; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +#controller_down { + position: absolute; + left: 62px; + top: 110px; + width: 42px; + height: 42px; + background: #e38c89; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +#controller_a { + position: absolute; + bottom: 110px; + right: 20px; +} + +#controller_b { + position: absolute; + bottom: 80px; + right: 90px; +} +ul.navigation { + width: 85%; + direction: rtl; + right: 0; + margin: 0; + padding: 0; +} +#logo, +ul.navigation { + display: inline-flex; +} +ul.navigation li { + margin: 0; + padding: 0; +} +.roundBtn { + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 26px; + line-height: 56px; + width: 56px; + height: 56px; + border-radius: 56px; + color: #9e3230; + background: #e38c89; + border: 2px solid #9e3230; + + box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2); +} + +.capsuleBtn { + font-weight: bold; + font-size: 10px; + color: #9e3230; + display: flex; + justify-content: center; + align-items: center; + line-height: 20px; + text-transform: uppercase; + width: 56px; + height: 25px; + border-radius: 40px; + background: #e38c89; + box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2); + vertical-align: middle; +} + +#controller_start { + position: absolute; + bottom: 20px; + right: 15px; +} + +#controller_select { + position: absolute; + bottom: 20px; + right: 90px; +} + +.btnPressed { + opacity: 0.5; +} diff --git a/resen/screen/css/style_keyboard_controls.css b/resen/screen/css/style_keyboard_controls.css new file mode 100644 index 0000000..fba8e08 --- /dev/null +++ b/resen/screen/css/style_keyboard_controls.css @@ -0,0 +1,1259 @@ +body { + text-align: center; +} +body.he { + direction: rtl; + /* text-align: right; */ +} +body { + font-size: 16px; +} +body .ignore-content { + display: none; +} +p { + text-align: center; +} +body.ignore .ignore-content { + display: block; +} + +svg .cls-1 { + fill: #87c170; +} + +#main { + margin-top: 30px; + margin-bottom: 200px; + padding-top: 30px; + display: block; + position: relative; + max-width: 1200px; + width: 90vw; + float: none; + margin: 0 auto; +} + +svg .cls-2 { + fill: #3b0213; +} + +svg .cls-3 { + fill: #e1efd0; +} + +svg .cls-4 { + fill: #346958; +} + +svg.ignore { + height: 1.2em; + width: 1.2em; + display: inline; +} + +body.ignore .hebrew-content, +body.ignore .english-content { + display: none; +} + +.he p { + text-align: center; +} + +.en-US .hebrew-content { + display: none; +} + +.he .english-content { + display: none; +} + +img.ignore-image { + display: inline; +} + +img#xyzFist { + position: relative; + display: block; + width: 100px; + margin: 100px auto 20px; +} + +#mobilelogo { + display: none; +} + +.mobile-only, +.touch-only { + display: none; +} + +nav { + transition: height 200ms ease; +} + +#expand img, +#collapse img { + display: none; + width: 17px; + margin: 10px auto; + cursor: pointer; +} + +li.footnote { + list-style: none; + font-style: italic; + font-size: 0.8em; + color: #f1dcab; +} + +span.footnote { + color: #f1dcab; +} + +.footnote:before { + content: "* "; +} + +.date { + font-style: italic; + font-size: 0.8em; + display: block; +} + +#collapse img { + margin-top: 50px; +} + +nav.expanded #expand { + display: none; +} + +nav.expanded #collapse img { + display: block; +} + +.heb p { + text-align: right; +} + +body { + /* font-family: "Miriam Libre", sans-serif; */ + /* font-family: "pauza", sans-serif; */ + font-family: "narkiss-yair-variable", sans-serif; + color: #86c06c; + background-color: #3b0213; + overflow-x: hidden; + width: 100vw; +} + +ul.navigation li a.nav-active span { + font-weight: bold; + font-variation-settings: "MONO" 0, "wght" 600; + background-color: #86c06c; + color: #3b0213; + border-radius: 0px; +} + +ul.navigation li a span { + padding: 5px 20px; +} + +#logo { + position: fixed; +} + +#logo { + left: 0; +} + +#lang { + right: 0; +} + +#lang a { + text-align: center; + display: block; + border-radius: 7px; + font-weight: bold; + font-variation-settings: "MONO" 0, "wght" 600; +} + +#lang a.active { + color: #f1dcab; + box-shadow: inset 0 -200px 0 0 #9e3230; +} + +#lang a:not(.active):hover { + color: #9e3230; +} + +#lang a { + padding: 7px 10px; + box-shadow: inset 0 0 0 0 #9e3230; + color: #3b0213; + font-size: 1em; + text-decoration: none; +} + +#logo svg { + width: 100px; +} + +h1 { + margin-top: 0; + font-size: 0.9em; +} + +h2 { + font-size: 1.5em; +} + +.navigation a { + font-size: 0.9em; +} + +h1 span { + display: block; + /* font-weight: normal; */ + font-variation-settings: "MONO" 0, "wght" 400; + color: #86c06c; + text-shadow: 1px 1px #3b0213; +} + +#logo { + display: block; + height: fit-content; +} + +#logo > * { + display: inline-block; +} + +ul.team li img { + border: 4px solid #f1dcab; + border-radius: 10px; + width: 50%; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; + image-rendering: pixelated; +} + +h2, +h3, +h4, +h5, +h6, +strong { + color: #f1dcab; + text-shadow: 1px 1px #9e3230; + font-variation-settings: "MONO" 0, "wght" 600; + text-transform: none; +} + +.hero h2 { + text-shadow: 1px 1px #3b0213; +} +.hero h1 span { + font-variation-settings: "MONO" 0, "wght" 700; +} +h4 { + margin: 10px 0; + font-size: 1em; +} +h3 { + margin: 10px 0 15px; + font-size: 1.3em; +} + +a.nav-active { + color: #9e3230; +} + +a { + color: #86c06c; +} + +body { + position: relative; + font-size: 16px; + margin: 0; + padding: 0; +} + +a { + color: #f1dcab; +} + +ul.q-and-a { + list-style-type: square; + padding: 0; + margin: 30px auto 10px; + width: 75%; +} + +ul.q-and-a li:not(.question) { + margin-bottom: 30px; + line-height: 1.2; + text-align: justify; +} + +#contact a span { + text-decoration: underline; +} + +nav { + padding-left: 4vw; + display: block; + width: 85vw; + position: relative; + height: 100vh; + float: left; + max-width: 400px; + overflow-y: auto; + padding: 15px 2%; + height: 80px; + margin: 0 auto; + left: 50%; + transform: translateX(-50%); + max-width: 1200px; +} + +#main section { + width: 100%; + margin: 20px auto 100px; + display: block; + position: relative; + height: unset; +} +#main { + overflow: hidden; +} + +#main section#mailinglist { + margin-bottom: 300px; +} + +h2 { + margin: 10px 0; +} + +#logo img { + width: 100px; +} + +#staticlogo { + position: absolute; +} + +small { + font-style: italic; + color: #86bf6c63; +} + +.description { + /* background: #f1dcab; + border-color: #3b0213; + color: #3b0213; + text-shadow: 1px 1px #86c26c; */ + + font-size: 1.2em; + padding: 20px 15px; + width: 45%; + margin: 0 auto; +} + +.ignore-content object { + height: 1em; +} + +ul.navigation li { + list-style: none; + font-style: normal; +} + +ul.navigation li a { + text-decoration: none; + color: #3b0213; + font-variation-settings: "MONO" 0, "wght" 600; + position: relative; + top: 50%; + transform: translateY(-50%); + vertical-align: middle; + display: flex; + width: 100%; +} + +ul.navigation { + font-size: 1.2em; + display: flex; + position: relative; + margin: 0 auto; + height: 100%; + width: 70%; +} + +ul.navigation li { + flex: fit-content; + position: relative; + height: 100%; +} + +.heb * { + direction: rtl; + /* text-align: right; */ +} + +.heb ul.team li, +.heb ul.team li > * { + text-align: center; +} + +.heb nav { + padding-right: 4vw; + padding-left: 0; + float: right; + right: 0; +} + +@media screen and (min-width: 1400px) { + body { + font-size: 18px; + } +} + +@media screen and (min-width: 801px) { + #lang { + position: fixed; + font-size: 20px; + top: 50%; + transform: translateY(-50%); + } + + #staticlogo:hover { + opacity: 0; + } + + footer { + width: 100vw; + position: fixed; + bottom: calc(-100% + 200px); + bottom: 0; + background-color: #f1dcab; + z-index: 2; + } + + nav.expanded #collapse, + #expand { + display: none; + } +} + +@media screen and (max-width: 800px) { + nav:not(.expanded) #lang { + display: none; + } + + #iframe-container { + height: 480px; + } + + #lang a { + border-radius: 0; + } + + ul.navigation li a span { + padding: 0; + } + + ul.navigation li a.nav-active span { + padding: 15px; + } + + #swipe { + display: block; + text-align: center; + margin-bottom: 50px; + } + #swipe small { + color: rgba(255, 255, 255, 0.549); + } + nav.expanded #collapse, + #expand { + display: block; + position: fixed; + bottom: 76px; + left: 50%; + transform: translateX(-50%); + } + + nav { + width: 100vw; + position: fixed; + bottom: calc(-100% + 200px); + bottom: 0; + background-color: #f1dcab; + } + + nav.expanded, + .heb nav.expanded { + height: 100vh; + } + + .mobile-only { + display: block; + } + + .hero { + display: block; + /* margin-bottom: 110%; */ + } + + ul.navigation li a { + display: block; + font-size: 1.2em; + } + + .hero > img { + width: 100%; + object-fit: contain; + } + + .hero > * { + position: relative; + display: block; + } + + .description, + .mailchimp { + margin: 0 auto 20px; + display: block; + position: relative; + width: calc(80vw - 60px); + } + .brandingLogo img { + object-fit: fill; + max-width: 100%; + } + + .he p.description, + .en-US p.description { + text-align: center; + font-size: 1em; + padding: 30px 15px; + } + + .navigation a { + font-size: 0.8em; + } + + ul.team { + display: block; + padding: 0; + } + + .team li { + margin: 20px 0; + } + + #collapse { + display: none; + } + + nav.expanded #expand { + display: none; + } + + #expand { + display: block; + padding-top: 0; + } + + #collapse { + padding-bottom: 0; + } + + #expand img, + #collapse img { + display: block; + } + + #mobilelogo img { + width: 55px; + } + h2 { + font-size: 1.3em; + } + /* h1 { + font-size: 0.75em; + } */ + + .heb #lang { + direction: ltr; + } + p.brandingLogo { + width: 100%; + } + p { + width: 90%; + margin: 0 auto 15px; + } + + nav { + width: 100%; + max-width: 100%; + padding: 0; + position: fixed; + height: auto; + float: none; + font-size: 12px; + z-index: 999; + } + + nav, + nav .navigation li, + nav a, + #contact li { + text-align: center; + } + + #contact h2 { + text-align: center; + } + + #contact li a img, + .heb #contact li a img { + float: none; + height: 0.8em; + } + + nav, + .heb nav { + padding-right: 0; + padding-left: 0; + padding-bottom: 2vh; + overflow: hidden; + height: 100px; + } + + #lang { + text-align: center; + } + + #logo { + text-align: center; + display: block; + width: 50%; + margin: 0 auto; + } + + #xyz ul { + width: 85%; + margin: 0 auto; + } + + #lang { + position: fixed; + z-index: 999; + bottom: 125px; + } + + .he #lang { + right: 50%; + transform: translateX(50%); + width: 50%; + } + + .en-US #lang { + left: 50%; + transform: translateX(-50%); + width: 50%; + } + + #main section { + width: calc(90vw - 40px); + display: block; + position: relative; + padding: 0 20px; + margin: 0 0 50px 0; + right: 0; + } + + ul.navigation { + font-size: 20px; + padding: 50px 0; + width: 100%; + position: relative; + display: none; + text-align: center; + } + + nav.expanded ul.navigation { + display: flex; + width: 75%; + height: auto; + flex-direction: column; + top: 50%; + transform: translateY(-50%); + } + + nav.expanded ul.navigation li { + padding: 0 0 5px; + width: 100%; + position: relative; + display: block; + flex: 1 1 content; + flex: unset; + display: block; + width: 100%; + position: relative; + height: unset; + text-align: center; + margin-bottom: 30px; + } + + #logocontainer { + display: none; + } + + #mobilelogo { + display: block; + position: fixed; + bottom: 16px; + left: calc(50% + -4px); + transform: translateX(-50%); + } + + #lang a { + padding: 5px 10px; + box-shadow: unset; + -webkit-transition: ease 0.5s; + -moz-transition: ease 0.5s; + transition: ease 0.5s; + } + + #lang a.active { + background: #9e3230; + box-shadow: unset; + } + + #lang a { + padding: 5px 10px; + box-shadow: unset; + -webkit-transition: ease 0.5s; + transition: ease 0.5s; + font-size: 1.6em; + } +} + +@media screen and (max-height: 450px) { + nav.expanded ul.navigation li { + margin-bottom: 15px; + } + ul.navigation { + font-size: 15px; + } +} + +body { + background: #3b0213; + color: #fff; + /* font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", + Helvetica, Arial, "Lucida Grande", sans-serif; */ + font-family: "narkiss-yair-variable", sans-serif; + font-weight: 300; + margin: 0; + padding: 0; + touch-action: none; + -webkit-touch-callout: none; + user-select: none; + -webkit-user-select: none; + overflow: hidden; +} + +.cky-modal, +.cky-consent-container { + direction: ltr !important; +} + +footer { + color: #3b0213; +} +strong { + color: #3b0213; + text-shadow: none; +} +#char1Logo { + height: 70px; + padding: 15px 0; +} +#game { + display: flex; + flex-direction: column; + position: fixed; + width: 100%; + height: calc(100% - 300px); + touch-action: none; +} +body:not(.fullscreen) #game { + top: calc(50% - 55px); + transform: translateY(-50%); + height: calc( + 100% - 220px + ); /* Subtracting footer height and some extra space */ + /* max-height: calc(100vh - 160px); */ +} +.buttonIndicator, +span#fullScreenButton { + max-width: 50%; + margin: 0 auto 10px; + color: #9e3230; + background: #e38c89; + padding: 4px; + border-radius: 100%; + border: 2px solid #9e3230; + display: block; + line-height: 1.2em; + /* text-shadow: 0 1px #e1eacd; */ + font-weight: bold; + color: #9e3230; +} + +body.fullscreen #mainCanvas { + position: fixed; + width: 100%; + height: 100%; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +body.fullscreen #gameFooter, +body.fullscreen #char1Logo { + display: none; +} + +.buttonIndicator:not(.square) { + width: 1.2em; + height: 1.2em; +} +/* .buttonIndicator.square { + border-radius: 4px; +} */ +.buttonIndicator.square, +span#fullScreenButton { + border-radius: 7px; + max-width: 120px; +} +span#fullScreenButton { + cursor: pointer; +} +#game canvas { + object-fit: contain; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + image-rendering: pixelated; + image-rendering: crisp-edges; + height: 100%; +} + +#controller { + display: none; + position: fixed; + bottom: 0px; + height: 230px; + width: 100%; + touch-action: none; +} +#fullScreenButton span.material-symbols-outlined { + color: #f1dcab; + vertical-align: bottom; +} +#controller_dpad { + position: absolute; + bottom: 10px; + left: 0px; + width: 184px; + height: 184px; +} +#xyzLogo { + height: 100px; + padding: 25px 0; +} +#controller_dpad:before { + /* content: ""; + display: block; + width: 48px; + height: 48px; + background: #e38c89; + + position: absolute; + left: 68px; + top: 68px; */ + content: ""; + display: block; + width: 42px; + height: 42px; + background: #e38c89; + position: absolute; + left: 62px; + top: 68px; +} + +#controller_left { + /* position: absolute; + left: 20px; + top: 68px; + width: 48px; + height: 48px; + background: #e38c89; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; */ + position: absolute; + left: 20px; + top: 68px; + width: 42px; + height: 42px; + background: #e38c89; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +#controller_right { + /* position: absolute; + left: 116px; + top: 68px; + width: 48px; + height: 48px; + background: #e38c89; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; */ + position: absolute; + left: 104px; + top: 68px; + width: 42px; + height: 42px; + background: #e38c89; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +#controller_up { + /* position: absolute; + left: 68px; + top: 20px; + width: 48px; + height: 48px; + background: #e38c89; + border-top-left-radius: 4px; + border-top-right-radius: 4px; */ + position: absolute; + left: 62px; + top: 26px; + width: 42px; + height: 42px; + background: #e38c89; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +#controller_down { + /* position: absolute; + left: 68px; + top: 116px; + width: 48px; + height: 48px; + background: #e38c89; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; */ + position: absolute; + left: 62px; + top: 110px; + width: 42px; + height: 42px; + background: #e38c89; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +#controller_a { + position: absolute; + bottom: 110px; + right: 20px; +} + +#controller_b { + position: absolute; + bottom: 80px; + right: 90px; +} +/* ul.navigation { + width: 85%; + direction: rtl; +} */ +ul.navigation { + width: 85%; + direction: rtl; + right: 0; + margin: 0; + padding: 0; +} +#logo, +ul.navigation { + display: inline-flex; +} +ul.navigation li { + margin: 0; + padding: 0; +} +.roundBtn { + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 26px; + line-height: 56px; + width: 56px; + height: 56px; + border-radius: 56px; + color: #9e3230; + background: #e38c89; + border: 2px solid #9e3230; + + box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2); +} + +.capsuleBtn { + font-weight: bold; + font-size: 10px; + color: #9e3230; + display: flex; + justify-content: center; + align-items: center; + line-height: 20px; + text-transform: uppercase; + width: 56px; + height: 25px; + border-radius: 40px; + background: #e38c89; + box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2); + vertical-align: middle; +} + +#controller_start { + position: absolute; + bottom: 20px; + right: 15px; +} + +#controller_select { + position: absolute; + bottom: 20px; + right: 90px; +} + +.btnPressed { + opacity: 0.5; +} + +.spinner { + height: 50px; + width: 50px; + margin: 0px auto; + -webkit-animation: rotation 0.8s linear infinite; + -moz-animation: rotation 0.8s linear infinite; + -o-animation: rotation 0.8s linear infinite; + animation: rotation 0.8s linear infinite; + border-left: 10px solid #9e3230; + border-right: 10px solid #9e3230; + border-bottom: 10px solid #9e3230; + border-top: 10px solid #88c070; + border-radius: 100%; + background-color: #031921; +} +@-webkit-keyframes rotation { + from { + -webkit-transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + } +} +@-moz-keyframes rotation { + from { + -moz-transform: rotate(0deg); + } + to { + -moz-transform: rotate(360deg); + } +} +@-o-keyframes rotation { + from { + -o-transform: rotate(0deg); + } + to { + -o-transform: rotate(360deg); + } +} +@keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media only screen and (max-width: 560px) { + #game canvas { + margin-top: 0px; + width: 100%; + /* max-width: 512px; */ + border: 0px; + border-radius: 0px; + } + #game { + width: 100%; + height: calc(100% - 150px); + } + #char1Logo { + /* height: 70px; */ + padding: 15px 0; + } + nav { + height: 50px; + top: 0; + } +} +@media only screen and (max-width: 800px) { + nav { + display: none; + } +} +@media only screen and (max-device-width: 1080px) { + #char1Logo { + height: 50px; + padding: 10px; + } + + #mobile-notice { + display: block; + } + footer { + display: none; + } +} +@media only screen and (max-device-width: 1080px) and (orientation: landscape) { + #char1Logo { + display: none; + } + #game { + height: 100%; + } +} +@media only screen and (max-device-width: 845px) and (orientation: landscape) { + #char1Logo { + height: 50px; + } + #mobile-notice { + font-size: 0.67rem; + /* bottom: 0; */ + } + #game { + height: calc(100% - 100px); + } + footer { + display: none; + } +} + +@media only screen and (max-device-width: 845px) and (orientation: portrait) { + body { + margin: 0; + } + + #game { + width: 100%; + position: fixed; + touch-action: none; + } + + #game canvas { + margin: 0; + display: block; + width: 100% !important; + height: auto !important; + } +} + +@media only screen and (max-device-width: 320px) and (orientation: portrait) { + #controller_dpad { + left: -5px; + bottom: -5px; + } + + #controller_a { + right: 5px; + bottom: 95px; + } + + #controller_b { + right: 80px; + } + + #controller_start { + right: 5px; + } + + #controller_select { + right: 80px; + } +} + +@media only screen and (max-width: 500px) and (max-height: 400px) { + #controller { + display: none; + } +} + +/* Small devices in landscape */ +@media only screen and (max-device-width: 300px) and (orientation: landscape) { + html, + body { + height: 100%; + } + body { + display: flex; + justify-content: center; + align-items: center; + } + + #game:after { + content: "PLEASE ROTATE ↻"; + font-size: 24px; + font-weight: bold; + color: #fff; + } + + #game canvas { + display: none; + max-width: 480px; + } + + #controller { + display: none; + } +} + +/* Devices large enough for landscape */ +@media only screen and (min-width: 300px) and (orientation: landscape) { + #controller { + bottom: 50%; + transform: translateY(50%); + } + a#logo { + overflow: hidden; + height: 80px; + } +} diff --git a/resen/screen/imgs/logo.png b/resen/screen/imgs/logo.png new file mode 100644 index 0000000..76d7bb0 Binary files /dev/null and b/resen/screen/imgs/logo.png differ diff --git a/resen/screen/index.html b/resen/screen/index.html new file mode 100644 index 0000000..c9415e7 --- /dev/null +++ b/resen/screen/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+
+
Select
+
Start
+
B
+
A
+
+ + + + + + + \ No newline at end of file diff --git a/resen/screen/index_keyboard_controls.html b/resen/screen/index_keyboard_controls.html new file mode 100644 index 0000000..476c440 --- /dev/null +++ b/resen/screen/index_keyboard_controls.html @@ -0,0 +1,102 @@ + + + + + + + + XYZ + + + + + + + + + + + +
+ No Canvas + Support +
+
+ +
+
+
+
+
+
+
Select
+
Start
+ +
B
+
A
+
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/resen/screen/js/additions.js b/resen/screen/js/additions.js new file mode 100644 index 0000000..1a8e831 --- /dev/null +++ b/resen/screen/js/additions.js @@ -0,0 +1,18 @@ +function fullScreen() { + const body = document.getElementById("gameBody"); // Replace 'canvas' with your canvas element's ID + + if (body) { + body.classList.add("fullscreen"); // Replace 'your-class-name' with the class you want to add + } +} + +// Function to remove a class from the canvas element when the "Esc" key is pressed +document.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + const body = document.getElementById("gameBody"); // Replace 'canvas' with your canvas element's ID + + if (body) { + body.classList.remove("fullscreen"); // Replace 'your-class-name' with the class you want to remove + } + } +}); diff --git a/resen/screen/js/script.js b/resen/screen/js/script.js new file mode 100644 index 0000000..5665553 --- /dev/null +++ b/resen/screen/js/script.js @@ -0,0 +1,1519 @@ +/* + * Copyright (C) 2017 Ben Smith + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +"use strict"; + +// User configurable. +let ROM_FILENAME = "rom/kmeot_zag.gb"; + +const ENABLE_REWIND = true; +const ENABLE_PAUSE = false; +const ENABLE_SWITCH_PALETTES = true; +const OSGP_DEADZONE = 0.1; // On screen gamepad deadzone range +const CGB_COLOR_CURVE = 2; // 0: none, 1: Sameboy "Emulate Hardware" 2: Gambatte/Gameboy Online + +// List of DMG palettes to switch between. By default it includes all 84 +// built-in palettes. If you want to restrict this, change it to an array of +// the palettes you want to use and change DEFAULT_PALETTE_IDX to the index of the +// default palette in that list. +// +// Example: (only allow one palette with index 16): +// const DEFAULT_PALETTE_IDX = 0; +// const PALETTES = [16]; +// +// Example: (allow three palettes, 16, 32, 64, with default 32): +// const DEFAULT_PALETTE_IDX = 1; +// const PALETTES = [16, 32, 64]; +// +const DEFAULT_PALETTE_IDX = 83; +const PALETTES = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, + 79, 80, 81, 82, 83, +]; + +const RESULT_OK = 0; +const RESULT_ERROR = 1; +const SCREEN_WIDTH = 160; +const SCREEN_HEIGHT = 144; +const SGB_SCREEN_WIDTH = 256; +const SGB_SCREEN_HEIGHT = 224; +const SGB_SCREEN_LEFT = (SGB_SCREEN_WIDTH - SCREEN_WIDTH) >> 1; +const SGB_SCREEN_RIGHT = (SGB_SCREEN_WIDTH + SCREEN_WIDTH) >> 1; +const SGB_SCREEN_TOP = (SGB_SCREEN_HEIGHT - SCREEN_HEIGHT) >> 1; +const SGB_SCREEN_BOTTOM = (SGB_SCREEN_HEIGHT + SCREEN_HEIGHT) >> 1; +const AUDIO_FRAMES = 4096; +const AUDIO_LATENCY_SEC = 0.1; +const MAX_UPDATE_SEC = 5 / 60; +const CPU_TICKS_PER_SECOND = 4194304; +const EVENT_NEW_FRAME = 1; +const EVENT_AUDIO_BUFFER_FULL = 2; +const EVENT_UNTIL_TICKS = 4; +const REWIND_FRAMES_PER_BASE_STATE = 45; +const REWIND_BUFFER_CAPACITY = 4 * 1024 * 1024; +const REWIND_FACTOR = 1.5; +const REWIND_UPDATE_MS = 16; +const GAMEPAD_POLLING_INTERVAL = 1000 / 60 / 4; // When activated, poll for gamepad input about ~4 times per gameboy frame (~240 times second) +const GAMEPAD_KEYMAP_STANDARD_STR = "standard"; // Try to use "standard" HTML5 mapping config if available + +const $ = document.querySelector.bind(document); +let emulator = null; + +const controllerEl = $("#controller"); +const dpadEl = $("#controller_dpad"); +const selectEl = $("#controller_select"); +const startEl = $("#controller_start"); +const bEl = $("#controller_b"); +const aEl = $("#controller_a"); + +const binjgbPromise = Binjgb(); + +const sgbEnabled = window.location.href.includes("sgb=true"); +if (sgbEnabled) { + $("canvas").width = SGB_SCREEN_WIDTH; + $("canvas").height = SGB_SCREEN_HEIGHT; +} else { + $("canvas").width = SCREEN_WIDTH; + $("canvas").height = SCREEN_HEIGHT; +} + +// Extract stuff from the vue.js implementation in demo.js. +class VM { + constructor() { + this.ticks = 0; + this.extRamUpdated = false; + this.paused_ = false; + this.volume = 0.5; + this.palIdx = DEFAULT_PALETTE_IDX; + this.canvas = { + show: true, + useSgbBorder: sgbEnabled, + scale: 3, + }; + this.rewind = { + minTicks: 0, + maxTicks: 0, + }; + setInterval(() => { + if (this.extRamUpdated) { + this.updateExtRam(); + this.extRamUpdated = false; + } + }, 1000); + } + + get paused() { + return this.paused_; + } + set paused(newPaused) { + let oldPaused = this.paused_; + this.paused_ = newPaused; + if (!emulator) return; + if (newPaused == oldPaused) return; + if (newPaused) { + emulator.pause(); + this.ticks = emulator.ticks; + this.rewind.minTicks = emulator.rewind.oldestTicks; + this.rewind.maxTicks = emulator.rewind.newestTicks; + } else { + emulator.resume(); + } + } + + togglePause() { + this.paused = !this.paused; + } + + updateExtRam() { + if (!emulator) return; + const extram = emulator.getExtRam(); + localStorage.setItem("extram", JSON.stringify(Array.from(extram))); + } +} +// // Function to detect if the user is using an iPhone or iPad +// function isIOS() { +// const userAgent = window.navigator.userAgent; +// return /iPad|iPhone|iPod/.test(userAgent); +// } + +const vm = new VM(); + +// Load a ROM. +(async function go() { + // if (isIOS()) { + // ROM_FILENAME = "rom/tzag_kmeot_ios.gb"; + // } + let response = await fetch(ROM_FILENAME); + let romBuffer = await response.arrayBuffer(); + const extRam = new Uint8Array(JSON.parse(localStorage.getItem("extram"))); + Emulator.start(await binjgbPromise, romBuffer, extRam); + emulator.setBuiltinPalette(vm.palIdx); +})(); + +function makeWasmBuffer(module, ptr, size) { + return new Uint8Array(module.HEAP8.buffer, ptr, size); +} + +class Emulator { + static start(module, romBuffer, extRamBuffer) { + Emulator.stop(); + emulator = new Emulator(module, romBuffer, extRamBuffer); + emulator.run(); + } + + static stop() { + if (emulator) { + emulator.destroy(); + emulator = null; + } + } + + constructor(module, romBuffer, extRamBuffer) { + this.module = module; + this.romDataPtr = this.module._malloc(romBuffer.byteLength); + makeWasmBuffer(this.module, this.romDataPtr, romBuffer.byteLength).set( + new Uint8Array(romBuffer) + ); + this.e = this.module._emulator_new_simple( + this.romDataPtr, + romBuffer.byteLength, + Audio.ctx.sampleRate, + AUDIO_FRAMES, + CGB_COLOR_CURVE + ); + if (this.e == 0) { + throw new Error("Invalid ROM."); + } + + this.gamepad = new Gamepad(module, this.e); + this.audio = new Audio(module, this.e); + this.video = new Video(module, this.e, $("canvas")); + this.rewind = new Rewind(module, this.e); + this.rewindIntervalId = 0; + + this.lastRafSec = 0; + this.leftoverTicks = 0; + this.fps = 60; + + if (extRamBuffer) { + this.loadExtRam(extRamBuffer); + } + + // this.bindKeys(); + this.bindTouch(); + + this.touchEnabled = "ontouchstart" in document.documentElement; + // this.updateOnscreenGamepad(); + + this.gamepad.init(); + } + + destroy() { + this.gamepad.shutdown(); + this.unbindTouch(); + // this.unbindKeys(); + this.cancelAnimationFrame(); + clearInterval(this.rewindIntervalId); + this.rewind.destroy(); + this.module._emulator_delete(this.e); + this.module._free(this.romDataPtr); + } + + withNewFileData(cb) { + const fileDataPtr = this.module._ext_ram_file_data_new(this.e); + const buffer = makeWasmBuffer( + this.module, + this.module._get_file_data_ptr(fileDataPtr), + this.module._get_file_data_size(fileDataPtr) + ); + const result = cb(fileDataPtr, buffer); + this.module._file_data_delete(fileDataPtr); + return result; + } + + loadExtRam(extRamBuffer) { + this.withNewFileData((fileDataPtr, buffer) => { + if (buffer.byteLength === extRamBuffer.byteLength) { + buffer.set(new Uint8Array(extRamBuffer)); + this.module._emulator_read_ext_ram(this.e, fileDataPtr); + } + }); + } + + getExtRam() { + return this.withNewFileData((fileDataPtr, buffer) => { + this.module._emulator_write_ext_ram(this.e, fileDataPtr); + return new Uint8Array(buffer); + }); + } + + get isPaused() { + return this.rafCancelToken === null; + } + + pause() { + if (!this.isPaused) { + this.cancelAnimationFrame(); + this.audio.pause(); + this.beginRewind(); + } + } + + resume() { + if (this.isPaused) { + this.endRewind(); + this.requestAnimationFrame(); + this.audio.resume(); + } + } + + setBuiltinPalette(palIdx) { + this.module._emulator_set_builtin_palette(this.e, PALETTES[palIdx]); + } + + get isRewinding() { + return this.rewind.isRewinding; + } + + beginRewind() { + this.rewind.beginRewind(); + } + + rewindToTicks(ticks) { + if (this.rewind.rewindToTicks(ticks)) { + this.runUntil(ticks); + this.video.renderTexture(); + } + } + + endRewind() { + this.rewind.endRewind(); + this.lastRafSec = 0; + this.leftoverTicks = 0; + this.audio.startSec = 0; + } + + set autoRewind(enabled) { + if (enabled) { + this.rewindIntervalId = setInterval(() => { + const oldest = this.rewind.oldestTicks; + const start = this.ticks; + const delta = + ((REWIND_FACTOR * REWIND_UPDATE_MS) / 1000) * CPU_TICKS_PER_SECOND; + const rewindTo = Math.max(oldest, start - delta); + this.rewindToTicks(rewindTo); + vm.ticks = emulator.ticks; + }, REWIND_UPDATE_MS); + } else { + clearInterval(this.rewindIntervalId); + this.rewindIntervalId = 0; + } + } + + requestAnimationFrame() { + this.rafCancelToken = requestAnimationFrame(this.rafCallback.bind(this)); + } + + cancelAnimationFrame() { + cancelAnimationFrame(this.rafCancelToken); + this.rafCancelToken = null; + } + + run() { + this.requestAnimationFrame(); + } + + get ticks() { + return this.module._emulator_get_ticks_f64(this.e); + } + + runUntil(ticks) { + while (true) { + const event = this.module._emulator_run_until_f64(this.e, ticks); + if (event & EVENT_NEW_FRAME) { + this.rewind.pushBuffer(); + this.video.uploadTexture(); + } + if (event & EVENT_AUDIO_BUFFER_FULL && !this.isRewinding) { + this.audio.pushBuffer(); + } + if (event & EVENT_UNTIL_TICKS) { + break; + } + } + if (this.module._emulator_was_ext_ram_updated(this.e)) { + vm.extRamUpdated = true; + } + } + + rafCallback(startMs) { + this.requestAnimationFrame(); + let deltaSec = 0; + if (!this.isRewinding) { + const startSec = startMs / 1000; + deltaSec = Math.max(startSec - (this.lastRafSec || startSec), 0); + const startTicks = this.ticks; + const deltaTicks = + Math.min(deltaSec, MAX_UPDATE_SEC) * CPU_TICKS_PER_SECOND; + const runUntilTicks = startTicks + deltaTicks - this.leftoverTicks; + this.runUntil(runUntilTicks); + this.leftoverTicks = (this.ticks - runUntilTicks) | 0; + this.lastRafSec = startSec; + } + const lerp = (from, to, alpha) => alpha * from + (1 - alpha) * to; + this.fps = lerp(this.fps, Math.min(1 / deltaSec, 10000), 0.3); + this.video.renderTexture(); + } + + updateOnscreenGamepad() { + $("#controller").style.display = this.touchEnabled ? "block" : "none"; + } + + bindTouch() { + console.log("bindTouch"); + this.touchFuncs = { + controller_b: this.setJoypB.bind(this), + controller_a: this.setJoypA.bind(this), + controller_start: this.setJoypStart.bind(this), + controller_select: this.setJoypSelect.bind(this), + }; + + this.boundButtonTouchStart = this.buttonTouchStart.bind(this); + this.boundButtonTouchEnd = this.buttonTouchEnd.bind(this); + selectEl.addEventListener("touchstart", this.boundButtonTouchStart); + selectEl.addEventListener("touchend", this.boundButtonTouchEnd); + selectEl.addEventListener("mousedown", this.boundButtonTouchStart); + selectEl.addEventListener("mouseup", this.boundButtonTouchEnd); + + startEl.addEventListener("touchstart", this.boundButtonTouchStart); + startEl.addEventListener("touchend", this.boundButtonTouchEnd); + startEl.addEventListener("mousedown", this.boundButtonTouchStart); + startEl.addEventListener("mouseup", this.boundButtonTouchEnd); + + bEl.addEventListener("touchstart", this.boundButtonTouchStart); + bEl.addEventListener("touchend", this.boundButtonTouchEnd); + bEl.addEventListener("mousedown", this.boundButtonTouchStart); + bEl.addEventListener("mouseup", this.boundButtonTouchEnd); + + aEl.addEventListener("touchstart", this.boundButtonTouchStart); + aEl.addEventListener("touchend", this.boundButtonTouchEnd); + aEl.addEventListener("mousedown", this.boundButtonTouchStart); + aEl.addEventListener("mouseup", this.boundButtonTouchEnd); + + this.boundDpadTouchStartMove = this.dpadTouchStartMove.bind(this); + this.boundDpadTouchEnd = this.dpadTouchEnd.bind(this); + dpadEl.addEventListener("touchstart", this.boundDpadTouchStartMove); + dpadEl.addEventListener("touchmove", this.boundDpadTouchStartMove); + dpadEl.addEventListener("touchend", this.boundDpadTouchEnd); + dpadEl.addEventListener("mousedown", this.boundDpadTouchStartMove); + dpadEl.addEventListener("mousemove", this.boundDpadTouchStartMove); + dpadEl.addEventListener("mouseup", this.boundDpadTouchEnd); + + this.boundTouchRestore = this.touchRestore.bind(this); + window.addEventListener("touchstart", this.boundTouchRestore); + window.addEventListener("mousedown", this.boundTouchRestore); + } + + unbindTouch() { + console.log("unbindTouch"); + + selectEl.removeEventListener("touchstart", this.boundButtonTouchStart); + selectEl.removeEventListener("touchend", this.boundButtonTouchEnd); + startEl.removeEventListener("touchstart", this.boundButtonTouchStart); + startEl.removeEventListener("touchend", this.boundButtonTouchEnd); + bEl.removeEventListener("touchstart", this.boundButtonTouchStart); + bEl.removeEventListener("touchend", this.boundButtonTouchEnd); + aEl.removeEventListener("touchstart", this.boundButtonTouchStart); + aEl.removeEventListener("touchend", this.boundButtonTouchEnd); + + dpadEl.removeEventListener("touchstart", this.boundDpadTouchStartMove); + dpadEl.removeEventListener("touchmove", this.boundDpadTouchStartMove); + dpadEl.removeEventListener("touchend", this.boundDpadTouchEnd); + + window.removeEventListener("touchstart", this.boundTouchRestore); + dpadEl.addEventListener("mousedown", this.boundDpadTouchStartMove); + dpadEl.addEventListener("mousemove", this.boundDpadTouchStartMove); + dpadEl.addEventListener("mouseup", this.boundDpadTouchEnd); + dpadEl.addEventListener("mouseleave", this.boundDpadTouchEnd); + } + + buttonTouchStart(event) { + if (event.currentTarget.id in this.touchFuncs) { + this.touchFuncs[event.currentTarget.id](true); + event.currentTarget.classList.add("btnPressed"); + event.preventDefault(); + } + } + + buttonTouchEnd(event) { + if (event.currentTarget.id in this.touchFuncs) { + this.touchFuncs[event.currentTarget.id](false); + event.currentTarget.classList.remove("btnPressed"); + event.preventDefault(); + } + } + // original code + // dpadTouchStartMove(event) { + // const rect = event.currentTarget.getBoundingClientRect(); + + // const x = + // (2 * (event.targetTouches[0].clientX - rect.left)) / rect.width - 1; + // const y = + // (2 * (event.targetTouches[0].clientY - rect.top)) / rect.height - 1; + // if (event.targetTouches && event.targetTouches[0]) { + // x = (2 * (event.targetTouches[0].clientX - rect.left)) / rect.width - 1; + // y = (2 * (event.targetTouches[0].clientY - rect.top)) / rect.height - 1; + // } else if (event.clientX && event.clientY) { + // // Handle mouse events + // x = (2 * (event.clientX - rect.left)) / rect.width - 1; + // y = (2 * (event.clientY - rect.top)) / rect.height - 1; + // } else { + // return; // Exit if we can't get coordinates + // } + + // // Rest of the function using x and y... + + // if (Math.abs(x) > OSGP_DEADZONE) { + // if (y > x && y < -x) { + // this.setJoypLeft(true); + // this.setJoypRight(false); + // } else if (y < x && y > -x) { + // this.setJoypLeft(false); + // this.setJoypRight(true); + // } + // } else { + // this.setJoypLeft(false); + // this.setJoypRight(false); + // } + + // if (Math.abs(y) > OSGP_DEADZONE) { + // if (x > y && x < -y) { + // this.setJoypUp(true); + // this.setJoypDown(false); + // } else if (x < y && x > -y) { + // this.setJoypUp(false); + // this.setJoypDown(true); + // } + // } else { + // this.setJoypUp(false); + // this.setJoypDown(false); + // } + // event.preventDefault(); + // } + + dpadTouchStartMove(event) { + // works but has unwanted diagonal movement + // Ignore mousemove events if the mouse button isn't pressed + if (event.type === "mousemove" && event.buttons !== 1) { + return; + } + + const rect = event.currentTarget.getBoundingClientRect(); + let x, y; + + if (event.targetTouches && event.targetTouches[0]) { + x = (2 * (event.targetTouches[0].clientX - rect.left)) / rect.width - 1; + y = (2 * (event.targetTouches[0].clientY - rect.top)) / rect.height - 1; + } else if (event.clientX && event.clientY) { + x = (2 * (event.clientX - rect.left)) / rect.width - 1; + y = (2 * (event.clientY - rect.top)) / rect.height - 1; + } else { + return; // Exit if we can't get coordinates + } + + // Rest of the function using x and y... + if (Math.abs(x) > OSGP_DEADZONE) { + if (y > x && y < -x) { + this.setJoypLeft(true); + this.setJoypRight(false); + } else if (y < x && y > -x) { + this.setJoypLeft(false); + this.setJoypRight(true); + } + } else { + this.setJoypLeft(false); + this.setJoypRight(false); + } + + if (Math.abs(y) > OSGP_DEADZONE) { + if (x > y && x < -y) { + this.setJoypUp(true); + this.setJoypDown(false); + } else if (x < y && x > -y) { + this.setJoypUp(false); + this.setJoypDown(true); + } + } else { + this.setJoypUp(false); + this.setJoypDown(false); + } + event.preventDefault(); + } + + //original code + // dpadTouchEnd(event) { + // this.setJoypLeft(false); + // this.setJoypRight(false); + // this.setJoypUp(false); + // this.setJoypDown(false); + // event.preventDefault(); + // } + dpadTouchEnd(event) { + this.setJoypLeft(false); + this.setJoypRight(false); + this.setJoypUp(false); + this.setJoypDown(false); + event.preventDefault(); + } + touchRestore() { + this.touchEnabled = true; + // this.updateOnscreenGamepad(); + } + + bindKeys() { + console.log("bindKeys"); + + this.keyFuncs = { + Backspace: this.keyRewind.bind(this), + " ": this.keyPause.bind(this), + "[": this.keyPrevPalette.bind(this), + "]": this.keyNextPalette.bind(this), + }; + + if (customControls.down && customControls.down.length > 0) { + customControls.down.forEach((k) => { + this.keyFuncs[k] = this.setJoypDown.bind(this); + }); + } else { + // keys assigned to DOWN button on game boy + this.keyFuncs["ArrowDown"] = this.setJoypDown.bind(this); + this.keyFuncs["s"] = this.setJoypDown.bind(this); + this.keyFuncs["S"] = this.setJoypDown.bind(this); + + this.keyFuncs["ד"] = this.setJoypDown.bind(this); + } + + if (customControls.left && customControls.left.length > 0) { + customControls.left.forEach((k) => { + this.keyFuncs[k] = this.setJoypLeft.bind(this); + }); + } else { + // keys assigned to LEFT button on game boy + this.keyFuncs["ArrowLeft"] = this.setJoypLeft.bind(this); + this.keyFuncs["a"] = this.setJoypLeft.bind(this); + this.keyFuncs["ש"] = this.setJoypLeft.bind(this); + this.keyFuncs["A"] = this.setJoypDown.bind(this); + } + + if (customControls.right && customControls.right.length > 0) { + customControls.right.forEach((k) => { + this.keyFuncs[k] = this.setJoypRight.bind(this); + }); + } else { + // keys assigned to RIGHT button on game boy + this.keyFuncs["ArrowRight"] = this.setJoypRight.bind(this); + this.keyFuncs["d"] = this.setJoypRight.bind(this); + this.keyFuncs["ג"] = this.setJoypRight.bind(this); + this.keyFuncs["D"] = this.setJoypDown.bind(this); + } + + if (customControls.up && customControls.up.length > 0) { + customControls.up.forEach((k) => { + this.keyFuncs[k] = this.setJoypUp.bind(this); + }); + } else { + // keys assigned to UP button on game boy + + this.keyFuncs["ArrowUp"] = this.setJoypUp.bind(this); + this.keyFuncs["w"] = this.setJoypUp.bind(this); + this.keyFuncs["W"] = this.setJoypUp.bind(this); + this.keyFuncs["'"] = this.setJoypUp.bind(this); + } + + if (customControls.a && customControls.a.length > 0) { + customControls.a.forEach((k) => { + this.keyFuncs[k] = this.setJoypA.bind(this); + }); + } else { + // keys assigned to A button on game boy + + this.keyFuncs["z"] = this.setJoypA.bind(this); + this.keyFuncs["ז"] = this.setJoypA.bind(this); + this.keyFuncs["ח"] = this.setJoypA.bind(this); + this.keyFuncs["j"] = this.setJoypA.bind(this); + this.keyFuncs["Z"] = this.setJoypA.bind(this); + this.keyFuncs["j"] = this.setJoypA.bind(this); + // this.keyFuncs["Alt"] = this.setJoypA.bind(this); + } + + if (customControls.b && customControls.b.length > 0) { + customControls.b.forEach((k) => { + this.keyFuncs[k] = this.setJoypB.bind(this); + }); + } else { + // keys assigned to B button on game boy + this.keyFuncs["x"] = this.setJoypB.bind(this); + this.keyFuncs["X"] = this.setJoypB.bind(this); + this.keyFuncs["ס"] = this.setJoypB.bind(this); + this.keyFuncs["ל"] = this.setJoypB.bind(this); + this.keyFuncs["k"] = this.setJoypB.bind(this); + this.keyFuncs["K"] = this.setJoypB.bind(this); + // this.keyFuncs["Control"] = this.setJoypB.bind(this); + } + + if (customControls.start && customControls.start.length > 0) { + customControls.start.forEach((k) => { + this.keyFuncs[k] = this.setJoypStart.bind(this); + }); + } else { + // keys assigned to START button on game boy + + this.keyFuncs["Enter"] = this.setJoypStart.bind(this); + } + + if (customControls.select && customControls.select.length > 0) { + customControls.select.forEach((k) => { + this.keyFuncs[k] = this.setJoypSelect.bind(this); + }); + } else { + // keys assigned to SELECT button on game boy + + this.keyFuncs["Shift"] = this.setJoypSelect.bind(this); + } + + this.boundKeyDown = this.keyDown.bind(this); + this.boundKeyUp = this.keyUp.bind(this); + + window.addEventListener("keydown", this.boundKeyDown); + window.addEventListener("keyup", this.boundKeyUp); + } + + unbindKeys() { + console.log("unbindKeys"); + + window.removeEventListener("keydown", this.boundKeyDown); + window.removeEventListener("keyup", this.boundKeyUp); + } + + keyDown(event) { + if (event.key === "w" && (event.metaKey || event.ctrlKey)) { + return; + } + if (event.key in this.keyFuncs) { + if (this.touchEnabled) { + this.touchEnabled = false; + // this.updateOnscreenGamepad(); + } + this.keyFuncs[event.key](true); + event.preventDefault(); + } + } + + keyUp(event) { + if (event.key in this.keyFuncs) { + this.keyFuncs[event.key](false); + event.preventDefault(); + } + } + + keyRewind(isKeyDown) { + if (!ENABLE_REWIND) { + return; + } + if (this.isRewinding !== isKeyDown) { + if (isKeyDown) { + vm.paused = true; + this.autoRewind = true; + } else { + this.autoRewind = false; + vm.paused = false; + } + } + } + + keyPause(isKeyDown) { + if (!ENABLE_PAUSE) { + return; + } + if (isKeyDown) vm.togglePause(); + } + + keyPrevPalette(isKeyDown) { + if (!ENABLE_SWITCH_PALETTES) { + return; + } + if (isKeyDown) { + vm.palIdx = (vm.palIdx + PALETTES.length - 1) % PALETTES.length; + emulator.setBuiltinPalette(vm.palIdx); + } + } + + keyNextPalette(isKeyDown) { + if (!ENABLE_SWITCH_PALETTES) { + return; + } + if (isKeyDown) { + vm.palIdx = (vm.palIdx + 1) % PALETTES.length; + emulator.setBuiltinPalette(vm.palIdx); + } + } + + setJoypDown(set) { + this.module._set_joyp_down(this.e, set); + } + setJoypUp(set) { + this.module._set_joyp_up(this.e, set); + } + setJoypLeft(set) { + this.module._set_joyp_left(this.e, set); + } + setJoypRight(set) { + this.module._set_joyp_right(this.e, set); + } + setJoypSelect(set) { + this.module._set_joyp_select(this.e, set); + } + setJoypStart(set) { + this.module._set_joyp_start(this.e, set); + } + setJoypB(set) { + this.module._set_joyp_B(this.e, set); + } + setJoypA(set) { + this.module._set_joyp_A(this.e, set); + } +} + +class Gamepad { + constructor(module, e) { + this.module = module; + this.e = e; + } + + // Load a key map for gamepad-to-gameboy buttons + // bindKeys(strMapping) { + // this.GAMEPAD_KEYMAP_STANDARD = [ + // { + // gb_key: "b", + // gp_button: 0, + // type: "button", + // gp_bind: this.module._set_joyp_B.bind(null, this.e), + // }, + // { + // gb_key: "a", + // gp_button: 1, + // type: "button", + // gp_bind: this.module._set_joyp_A.bind(null, this.e), + // }, + // { + // gb_key: "select", + // gp_button: 8, + // type: "button", + // gp_bind: this.module._set_joyp_select.bind(null, this.e), + // }, + // { + // gb_key: "start", + // gp_button: 9, + // type: "button", + // gp_bind: this.module._set_joyp_start.bind(null, this.e), + // }, + // { + // gb_key: "up", + // gp_button: 12, + // type: "button", + // gp_bind: this.module._set_joyp_up.bind(null, this.e), + // }, + // { + // gb_key: "down", + // gp_button: 13, + // type: "button", + // gp_bind: this.module._set_joyp_down.bind(null, this.e), + // }, + // { + // gb_key: "left", + // gp_button: 14, + // type: "button", + // gp_bind: this.module._set_joyp_left.bind(null, this.e), + // }, + // { + // gb_key: "right", + // gp_button: 15, + // type: "button", + // gp_bind: this.module._set_joyp_right.bind(null, this.e), + // }, + // ]; + + // this.GAMEPAD_KEYMAP_DEFAULT = [ + // { + // gb_key: "a", + // gp_button: 0, + // type: "button", + // gp_bind: this.module._set_joyp_A.bind(null, this.e), + // }, + // { + // gb_key: "b", + // gp_button: 1, + // type: "button", + // gp_bind: this.module._set_joyp_B.bind(null, this.e), + // }, + // { + // gb_key: "select", + // gp_button: 2, + // type: "button", + // gp_bind: this.module._set_joyp_select.bind(null, this.e), + // }, + // { + // gb_key: "start", + // gp_button: 3, + // type: "button", + // gp_bind: this.module._set_joyp_start.bind(null, this.e), + // }, + // { + // gb_key: "up", + // gp_button: 2, + // type: "axis", + // gp_bind: this.module._set_joyp_up.bind(null, this.e), + // }, + // { + // gb_key: "down", + // gp_button: 3, + // type: "axis", + // gp_bind: this.module._set_joyp_down.bind(null, this.e), + // }, + // { + // gb_key: "left", + // gp_button: 0, + // type: "axis", + // gp_bind: this.module._set_joyp_left.bind(null, this.e), + // }, + // { + // gb_key: "right", + // gp_button: 1, + // type: "axis", + // gp_bind: this.module._set_joyp_right.bind(null, this.e), + // }, + // ]; + + // // Try to use the w3c "standard" gamepad mapping if available + // // (Chrome/V8 seems to do that better than Firefox) + // // + // // Otherwise use a default mapping that assigns + // // A/B/Select/Start to the first four buttons, + // // and U/D/L/R to the first two axes. + // if (strMapping === GAMEPAD_KEYMAP_STANDARD_STR) { + // this.gp.keybinds = this.GAMEPAD_KEYMAP_STANDARD; + // } else { + // this.gp.keybinds = this.GAMEPAD_KEYMAP_DEFAULT; + // } + // } + + cacheValues(gamepad) { + // Read Buttons + for (let k = 0; k < gamepad.buttons.length; k++) { + // .value is for analog, .pressed is for boolean buttons + this.gp.buttons.cur[k] = + gamepad.buttons[k].value > 0 || gamepad.buttons[k].pressed == true; + + // Update state changed if not on first input pass + if (this.gp.buttons.last !== undefined) { + this.gp.buttons.changed[k] = + this.gp.buttons.cur[k] != this.gp.buttons.last[k]; + } + } + + // Read Axes + for (let k = 0; k < gamepad.axes.length; k++) { + // Decode each dpad axis into two buttons, one for each direction + this.gp.axes.cur[k * 2] = gamepad.axes[k] < 0; + this.gp.axes.cur[k * 2 + 1] = gamepad.axes[k] > 0; + + // Update state changed if not on first input pass + if (this.gp.axes.last !== undefined) { + this.gp.axes.changed[k * 2] = + this.gp.axes.cur[k * 2] != this.gp.axes.last[k * 2]; + this.gp.axes.changed[k * 2 + 1] = + this.gp.axes.cur[k * 2 + 1] != this.gp.axes.last[k * 2 + 1]; + } + } + + // Save current state for comparison on next input + this.gp.axes.last = this.gp.axes.cur.slice(0); + this.gp.buttons.last = this.gp.buttons.cur.slice(0); + } + + handleButton(keyBind) { + let buttonCache; + + // Select button / axis cache based on key bind type + if (keyBind.type === "button") { + buttonCache = this.gp.buttons; + } else if (keyBind.type === "axis") { + buttonCache = this.gp.axes; + } + + // Make sure the button exists in the cache array + if (keyBind.gp_button < buttonCache.changed.length) { + // Send the button state if it's changed + if (buttonCache.changed[keyBind.gp_button]) { + if (buttonCache.cur[keyBind.gp_button]) { + // Gamepad Button Down + keyBind.gp_bind(true); + } else { + // Gamepad Button Up + keyBind.gp_bind(false); + } + } + } + } + + getCurrent() { + // Chrome requires retrieving a new gamepad object + // every time button state is queried (the existing object + // will have stale button state). Just do that for all browsers + let gamepad = navigator.getGamepads()[this.gp.apiID]; + + if (gamepad) { + if (gamepad.connected) { + return gamepad; + } + } + + return undefined; + } + + update() { + let gamepad = this.getCurrent(); + + if (gamepad !== undefined) { + // Cache gamepad input values + this.cacheValues(gamepad); + + // Loop through buttons and send changes if needed + for (let i = 0; i < this.gp.keybinds.length; i++) { + this.handleButton(this.gp.keybinds[i]); + } + } else { + // Gamepad is no longer present, disconnect + this.releaseGamepad(); + } + } + + startGamepad(gamepad) { + // Make sure it has enough buttons and axes + if ( + gamepad.mapping === GAMEPAD_KEYMAP_STANDARD_STR || + (gamepad.axes.length >= 2 && gamepad.buttons.length >= 4) + ) { + // Save API index for polling (required by Chrome/V8) + this.gp.apiID = gamepad.index; + + // Assign gameboy keys to the gamepad + this.bindKeys(gamepad.mapping); + + // Start polling the gamepad for input + this.gp.timerID = setInterval( + () => this.update(), + GAMEPAD_POLLING_INTERVAL + ); + } + } + + releaseGamepad() { + // Stop polling the gamepad for input + if (this.gp.timerID !== undefined) { + clearInterval(this.gp.timerID); + } + + // Clear previous button history and controller info + this.gp.axes.last = undefined; + this.gp.buttons.last = undefined; + this.gp.keybinds = undefined; + + this.gp.apiID = undefined; + } + + // If a gamepad was already connected on this page + // and released, it won't fire another connect event. + // So try to find any that might be present + checkAlreadyConnected() { + let gamepads = navigator.getGamepads(); + + // If any gamepads are already attached to the page, + // use the first one that is connected + for (let idx = 0; idx < gamepads.length; idx++) { + if (gamepads[idx] !== undefined && gamepads[idx] !== null) { + if (gamepads[idx].connected === true) { + this.startGamepad(gamepads[idx]); + } + } + } + } + + // Event handler for when a gamepad is connected + eventConnected(event) { + this.startGamepad(navigator.getGamepads()[event.gamepad.index]); + } + + // Event handler for when a gamepad is disconnected + eventDisconnected(event) { + this.releaseGamepad(); + } + + // Register event connection handlers for gamepads + init() { + // gamepad related vars + this.gp = { + apiID: undefined, + timerID: undefined, + keybinds: undefined, + axes: { last: undefined, cur: [], changed: [] }, + buttons: { last: undefined, cur: [], changed: [] }, + }; + + // Check for previously attached gamepads that might + // not emit a gamepadconnected() event + this.checkAlreadyConnected(); + + this.boundGamepadConnected = this.eventConnected.bind(this); + this.boundGamepadDisconnected = this.eventDisconnected.bind(this); + + // When a gamepad connects, start polling it for input + window.addEventListener("gamepadconnected", this.boundGamepadConnected); + + // When a gamepad disconnects, shut down polling for input + window.addEventListener( + "gamepaddisconnected", + this.boundGamepadDisconnected + ); + } + + // Release event connection handlers and settings + shutdown() { + this.releaseGamepad(); + window.removeEventListener("gamepadconnected", this.boundGamepadConnected); + window.removeEventListener( + "gamepaddisconnected", + this.boundGamepadDisconnected + ); + } +} + +class Audio { + constructor(module, e) { + this.started = false; + this.module = module; + this.buffer = makeWasmBuffer( + this.module, + this.module._get_audio_buffer_ptr(e), + this.module._get_audio_buffer_capacity(e) + ); + this.startSec = 0; + this.resume(); + + this.boundStartPlayback = this.startPlayback.bind(this); + window.addEventListener("keydown", this.boundStartPlayback, true); + window.addEventListener("click", this.boundStartPlayback, true); + window.addEventListener("touchend", this.boundStartPlayback, true); + } + + startPlayback() { + window.removeEventListener("touchend", this.boundStartPlayback, true); + window.removeEventListener("keydown", this.boundStartPlayback, true); + window.removeEventListener("click", this.boundStartPlayback, true); + this.started = true; + this.resume(); + } + + get sampleRate() { + return Audio.ctx.sampleRate; + } + + pushBuffer() { + if (!this.started) { + return; + } + const nowSec = Audio.ctx.currentTime; + const nowPlusLatency = nowSec + AUDIO_LATENCY_SEC; + const volume = vm.volume; + this.startSec = this.startSec || nowPlusLatency; + if (this.startSec >= nowSec) { + const buffer = Audio.ctx.createBuffer(2, AUDIO_FRAMES, this.sampleRate); + const channel0 = buffer.getChannelData(0); + const channel1 = buffer.getChannelData(1); + for (let i = 0; i < AUDIO_FRAMES; i++) { + channel0[i] = (this.buffer[2 * i] * volume) / 255; + channel1[i] = (this.buffer[2 * i + 1] * volume) / 255; + } + const bufferSource = Audio.ctx.createBufferSource(); + bufferSource.buffer = buffer; + bufferSource.connect(Audio.ctx.destination); + bufferSource.start(this.startSec); + const bufferSec = AUDIO_FRAMES / this.sampleRate; + this.startSec += bufferSec; + } else { + console.log( + "Resetting audio (" + + this.startSec.toFixed(2) + + " < " + + nowSec.toFixed(2) + + ")" + ); + this.startSec = nowPlusLatency; + } + } + + pause() { + if (!this.started) { + return; + } + Audio.ctx.suspend(); + } + + resume() { + if (!this.started) { + return; + } + Audio.ctx.resume(); + } +} + +Audio.ctx = new AudioContext(); + +class Video { + constructor(module, e, el) { + this.module = module; + // Both iPhone and Desktop Safari dont't upscale using image-rendering: pixelated + // on webgl canvases. See https://bugs.webkit.org/show_bug.cgi?id=193895. + // For now, default to Canvas2D. + if (window.navigator.userAgent.match(/iPhone|iPad|15.[0-9] Safari/)) { + this.renderer = new Canvas2DRenderer(el); + } else { + try { + this.renderer = new WebGLRenderer(el); + } catch (error) { + console.log(`Error creating WebGLRenderer: ${error}`); + this.renderer = new Canvas2DRenderer(el); + } + } + this.buffer = makeWasmBuffer( + this.module, + this.module._get_frame_buffer_ptr(e), + this.module._get_frame_buffer_size(e) + ); + this.sgbBuffer = makeWasmBuffer( + this.module, + this.module._get_sgb_frame_buffer_ptr(e), + this.module._get_sgb_frame_buffer_size(e) + ); + } + + uploadTexture() { + this.renderer.uploadTextures(this.buffer, this.sgbBuffer); + } + + renderTexture() { + this.renderer.renderTextures(); + } +} + +class Canvas2DRenderer { + constructor(el) { + this.ctx = el.getContext("2d"); + this.imageData = this.ctx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT); + this.sgbImageData = this.ctx.createImageData( + SGB_SCREEN_WIDTH, + SGB_SCREEN_HEIGHT + ); + + this.overlayCanvas = document.createElement("canvas"); + this.overlayCanvas.width = SGB_SCREEN_WIDTH; + this.overlayCanvas.height = SGB_SCREEN_HEIGHT; + this.overlayCtx = this.overlayCanvas.getContext("2d"); + } + + uploadTextures(buffer, sgbBuffer) { + this.imageData.data.set(buffer); + this.sgbImageData.data.set(sgbBuffer); + } + + renderTextures() { + if (vm.canvas.useSgbBorder) { + this.ctx.putImageData(this.imageData, SGB_SCREEN_LEFT, SGB_SCREEN_TOP); + this.overlayCtx.putImageData(this.sgbImageData, 0, 0); + this.ctx.drawImage(this.overlayCanvas, 0, 0); + } else { + this.ctx.putImageData(this.imageData, 0, 0); + } + } +} + +class WebGLRenderer { + constructor(el) { + const gl = (this.gl = el.getContext("webgl", { + preserveDrawingBuffer: true, + })); + if (gl === null) { + throw new Error("unable to create webgl context"); + } + + function compileShader(type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error(`compileShader failed: ${gl.getShaderInfoLog(shader)}`); + } + return shader; + } + + const vertexShader = compileShader( + gl.VERTEX_SHADER, + `attribute vec2 aPos; + attribute vec2 aTexCoord; + varying highp vec2 vTexCoord; + void main(void) { + gl_Position = vec4(aPos, 0.0, 1.0); + vTexCoord = aTexCoord; + }` + ); + const fragmentShader = compileShader( + gl.FRAGMENT_SHADER, + `varying highp vec2 vTexCoord; + uniform sampler2D uSampler; + void main(void) { + gl_FragColor = texture2D(uSampler, vTexCoord); + }` + ); + + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(`program link failed: ${gl.getProgramInfoLog(program)}`); + } + gl.useProgram(program); + + this.aPos = gl.getAttribLocation(program, "aPos"); + this.aTexCoord = gl.getAttribLocation(program, "aTexCoord"); + this.uSampler = gl.getUniformLocation(program, "uSampler"); + + this.fbTexture = this.createTexture(); + this.sgbFbTexture = this.createTexture(); + + const invLerpClipSpace = (x, max) => 2 * (x / max) - 1; + const l = invLerpClipSpace(SGB_SCREEN_LEFT, SGB_SCREEN_WIDTH); + const r = invLerpClipSpace(SGB_SCREEN_RIGHT, SGB_SCREEN_WIDTH); + const t = -invLerpClipSpace(SGB_SCREEN_TOP, SGB_SCREEN_HEIGHT); + const b = -invLerpClipSpace(SGB_SCREEN_BOTTOM, SGB_SCREEN_HEIGHT); + const w = SCREEN_WIDTH / 256, + sw = SGB_SCREEN_WIDTH / 256; + const h = SCREEN_HEIGHT / 256, + sh = SGB_SCREEN_HEIGHT / 256; + + const verts = new Float32Array([ + // fb only + -1, + -1, + 0, + h, + +1, + -1, + w, + h, + -1, + +1, + 0, + 0, + +1, + +1, + w, + 0, + + // sgb fb + l, + b, + 0, + h, + r, + b, + w, + h, + l, + t, + 0, + 0, + r, + t, + w, + 0, + + // sgb border + -1, + -1, + 0, + sh, + +1, + -1, + sw, + sh, + -1, + +1, + 0, + 0, + +1, + +1, + sw, + 0, + ]); + + const buffer = gl.createBuffer(); + this.gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW); + + gl.enableVertexAttribArray(this.aPos); + gl.enableVertexAttribArray(this.aTexCoord); + gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, gl.FALSE, 16, 0); + gl.vertexAttribPointer(this.aTexCoord, 2, gl.FLOAT, gl.FALSE, 16, 8); + gl.uniform1i(this.uSampler, 0); + } + + createTexture() { + const gl = this.gl; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 256, + 256, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + return texture; + } + + uploadTextures(buffer, sgbBuffer) { + const gl = this.gl; + gl.bindTexture(gl.TEXTURE_2D, this.fbTexture); + gl.texSubImage2D( + gl.TEXTURE_2D, + 0, + 0, + 0, + SCREEN_WIDTH, + SCREEN_HEIGHT, + gl.RGBA, + gl.UNSIGNED_BYTE, + buffer + ); + + gl.bindTexture(gl.TEXTURE_2D, this.sgbFbTexture); + gl.texSubImage2D( + gl.TEXTURE_2D, + 0, + 0, + 0, + SGB_SCREEN_WIDTH, + SGB_SCREEN_HEIGHT, + gl.RGBA, + gl.UNSIGNED_BYTE, + sgbBuffer + ); + } + + renderTextures() { + const gl = this.gl; + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.clearColor(0.5, 0.5, 0.5, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + if (vm.canvas.useSgbBorder) { + gl.bindTexture(gl.TEXTURE_2D, this.fbTexture); + gl.drawArrays(gl.TRIANGLE_STRIP, 4, 4); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.bindTexture(gl.TEXTURE_2D, this.sgbFbTexture); + gl.drawArrays(gl.TRIANGLE_STRIP, 8, 4); + gl.disable(gl.BLEND); + } else { + gl.bindTexture(gl.TEXTURE_2D, this.fbTexture); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + } +} + +class Rewind { + constructor(module, e) { + this.module = module; + this.e = e; + this.joypadBufferPtr = this.module._joypad_new(); + this.statePtr = 0; + this.bufferPtr = this.module._rewind_new_simple( + e, + REWIND_FRAMES_PER_BASE_STATE, + REWIND_BUFFER_CAPACITY + ); + this.module._emulator_set_default_joypad_callback(e, this.joypadBufferPtr); + } + + destroy() { + this.module._rewind_delete(this.bufferPtr); + this.module._joypad_delete(this.joypadBufferPtr); + } + + get oldestTicks() { + return this.module._rewind_get_oldest_ticks_f64(this.bufferPtr); + } + + get newestTicks() { + return this.module._rewind_get_newest_ticks_f64(this.bufferPtr); + } + + pushBuffer() { + if (!this.isRewinding) { + this.module._rewind_append(this.bufferPtr, this.e); + } + } + + get isRewinding() { + return this.statePtr !== 0; + } + + beginRewind() { + if (this.isRewinding) return; + this.statePtr = this.module._rewind_begin( + this.e, + this.bufferPtr, + this.joypadBufferPtr + ); + } + + rewindToTicks(ticks) { + if (!this.isRewinding) return; + return ( + this.module._rewind_to_ticks_wrapper(this.statePtr, ticks) === RESULT_OK + ); + } + + endRewind() { + if (!this.isRewinding) return; + this.module._emulator_set_default_joypad_callback( + this.e, + this.joypadBufferPtr + ); + this.module._rewind_end(this.statePtr); + this.statePtr = 0; + } +} diff --git a/resen/screen/js/script_keyboard_controls.js b/resen/screen/js/script_keyboard_controls.js new file mode 100644 index 0000000..8685469 --- /dev/null +++ b/resen/screen/js/script_keyboard_controls.js @@ -0,0 +1,1424 @@ +/* + * Copyright (C) 2017 Ben Smith + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +"use strict"; + +// User configurable. +let ROM_FILENAME = "rom/kmeot_zag.gb"; + +const ENABLE_REWIND = true; +const ENABLE_PAUSE = false; +const ENABLE_SWITCH_PALETTES = true; +const OSGP_DEADZONE = 0.1; // On screen gamepad deadzone range +const CGB_COLOR_CURVE = 2; // 0: none, 1: Sameboy "Emulate Hardware" 2: Gambatte/Gameboy Online + +// List of DMG palettes to switch between. By default it includes all 84 +// built-in palettes. If you want to restrict this, change it to an array of +// the palettes you want to use and change DEFAULT_PALETTE_IDX to the index of the +// default palette in that list. +// +// Example: (only allow one palette with index 16): +// const DEFAULT_PALETTE_IDX = 0; +// const PALETTES = [16]; +// +// Example: (allow three palettes, 16, 32, 64, with default 32): +// const DEFAULT_PALETTE_IDX = 1; +// const PALETTES = [16, 32, 64]; +// +const DEFAULT_PALETTE_IDX = 83; +const PALETTES = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, + 79, 80, 81, 82, 83, +]; + +const RESULT_OK = 0; +const RESULT_ERROR = 1; +const SCREEN_WIDTH = 160; +const SCREEN_HEIGHT = 144; +const SGB_SCREEN_WIDTH = 256; +const SGB_SCREEN_HEIGHT = 224; +const SGB_SCREEN_LEFT = (SGB_SCREEN_WIDTH - SCREEN_WIDTH) >> 1; +const SGB_SCREEN_RIGHT = (SGB_SCREEN_WIDTH + SCREEN_WIDTH) >> 1; +const SGB_SCREEN_TOP = (SGB_SCREEN_HEIGHT - SCREEN_HEIGHT) >> 1; +const SGB_SCREEN_BOTTOM = (SGB_SCREEN_HEIGHT + SCREEN_HEIGHT) >> 1; +const AUDIO_FRAMES = 4096; +const AUDIO_LATENCY_SEC = 0.1; +const MAX_UPDATE_SEC = 5 / 60; +const CPU_TICKS_PER_SECOND = 4194304; +const EVENT_NEW_FRAME = 1; +const EVENT_AUDIO_BUFFER_FULL = 2; +const EVENT_UNTIL_TICKS = 4; +const REWIND_FRAMES_PER_BASE_STATE = 45; +const REWIND_BUFFER_CAPACITY = 4 * 1024 * 1024; +const REWIND_FACTOR = 1.5; +const REWIND_UPDATE_MS = 16; +const GAMEPAD_POLLING_INTERVAL = 1000 / 60 / 4; // When activated, poll for gamepad input about ~4 times per gameboy frame (~240 times second) +const GAMEPAD_KEYMAP_STANDARD_STR = "standard"; // Try to use "standard" HTML5 mapping config if available + +const $ = document.querySelector.bind(document); +let emulator = null; + +const controllerEl = $("#controller"); +const dpadEl = $("#controller_dpad"); +const selectEl = $("#controller_select"); +const startEl = $("#controller_start"); +const bEl = $("#controller_b"); +const aEl = $("#controller_a"); + +const binjgbPromise = Binjgb(); + +const sgbEnabled = window.location.href.includes("sgb=true"); +if (sgbEnabled) { + $("canvas").width = SGB_SCREEN_WIDTH; + $("canvas").height = SGB_SCREEN_HEIGHT; +} else { + $("canvas").width = SCREEN_WIDTH; + $("canvas").height = SCREEN_HEIGHT; +} + +// Extract stuff from the vue.js implementation in demo.js. +class VM { + constructor() { + this.ticks = 0; + this.extRamUpdated = false; + this.paused_ = false; + this.volume = 0.5; + this.palIdx = DEFAULT_PALETTE_IDX; + this.canvas = { + show: true, + useSgbBorder: sgbEnabled, + scale: 3, + }; + this.rewind = { + minTicks: 0, + maxTicks: 0, + }; + setInterval(() => { + if (this.extRamUpdated) { + this.updateExtRam(); + this.extRamUpdated = false; + } + }, 1000); + } + + get paused() { + return this.paused_; + } + set paused(newPaused) { + let oldPaused = this.paused_; + this.paused_ = newPaused; + if (!emulator) return; + if (newPaused == oldPaused) return; + if (newPaused) { + emulator.pause(); + this.ticks = emulator.ticks; + this.rewind.minTicks = emulator.rewind.oldestTicks; + this.rewind.maxTicks = emulator.rewind.newestTicks; + } else { + emulator.resume(); + } + } + + togglePause() { + this.paused = !this.paused; + } + + updateExtRam() { + if (!emulator) return; + const extram = emulator.getExtRam(); + localStorage.setItem("extram", JSON.stringify(Array.from(extram))); + } +} +// // Function to detect if the user is using an iPhone or iPad +// function isIOS() { +// const userAgent = window.navigator.userAgent; +// return /iPad|iPhone|iPod/.test(userAgent); +// } + +const vm = new VM(); + +// Load a ROM. +(async function go() { + // if (isIOS()) { + // ROM_FILENAME = "rom/tzag_kmeot_ios.gb"; + // } + let response = await fetch(ROM_FILENAME); + let romBuffer = await response.arrayBuffer(); + const extRam = new Uint8Array(JSON.parse(localStorage.getItem("extram"))); + Emulator.start(await binjgbPromise, romBuffer, extRam); + emulator.setBuiltinPalette(vm.palIdx); +})(); + +function makeWasmBuffer(module, ptr, size) { + return new Uint8Array(module.HEAP8.buffer, ptr, size); +} + +class Emulator { + static start(module, romBuffer, extRamBuffer) { + Emulator.stop(); + emulator = new Emulator(module, romBuffer, extRamBuffer); + emulator.run(); + } + + static stop() { + if (emulator) { + emulator.destroy(); + emulator = null; + } + } + + constructor(module, romBuffer, extRamBuffer) { + this.module = module; + this.romDataPtr = this.module._malloc(romBuffer.byteLength); + makeWasmBuffer(this.module, this.romDataPtr, romBuffer.byteLength).set( + new Uint8Array(romBuffer) + ); + this.e = this.module._emulator_new_simple( + this.romDataPtr, + romBuffer.byteLength, + Audio.ctx.sampleRate, + AUDIO_FRAMES, + CGB_COLOR_CURVE + ); + if (this.e == 0) { + throw new Error("Invalid ROM."); + } + + this.gamepad = new Gamepad(module, this.e); + this.audio = new Audio(module, this.e); + this.video = new Video(module, this.e, $("canvas")); + this.rewind = new Rewind(module, this.e); + this.rewindIntervalId = 0; + + this.lastRafSec = 0; + this.leftoverTicks = 0; + this.fps = 60; + + if (extRamBuffer) { + this.loadExtRam(extRamBuffer); + } + + this.bindKeys(); + this.bindTouch(); + + this.touchEnabled = "ontouchstart" in document.documentElement; + this.updateOnscreenGamepad(); + + this.gamepad.init(); + } + + destroy() { + this.gamepad.shutdown(); + this.unbindTouch(); + this.unbindKeys(); + this.cancelAnimationFrame(); + clearInterval(this.rewindIntervalId); + this.rewind.destroy(); + this.module._emulator_delete(this.e); + this.module._free(this.romDataPtr); + } + + withNewFileData(cb) { + const fileDataPtr = this.module._ext_ram_file_data_new(this.e); + const buffer = makeWasmBuffer( + this.module, + this.module._get_file_data_ptr(fileDataPtr), + this.module._get_file_data_size(fileDataPtr) + ); + const result = cb(fileDataPtr, buffer); + this.module._file_data_delete(fileDataPtr); + return result; + } + + loadExtRam(extRamBuffer) { + this.withNewFileData((fileDataPtr, buffer) => { + if (buffer.byteLength === extRamBuffer.byteLength) { + buffer.set(new Uint8Array(extRamBuffer)); + this.module._emulator_read_ext_ram(this.e, fileDataPtr); + } + }); + } + + getExtRam() { + return this.withNewFileData((fileDataPtr, buffer) => { + this.module._emulator_write_ext_ram(this.e, fileDataPtr); + return new Uint8Array(buffer); + }); + } + + get isPaused() { + return this.rafCancelToken === null; + } + + pause() { + if (!this.isPaused) { + this.cancelAnimationFrame(); + this.audio.pause(); + this.beginRewind(); + } + } + + resume() { + if (this.isPaused) { + this.endRewind(); + this.requestAnimationFrame(); + this.audio.resume(); + } + } + + setBuiltinPalette(palIdx) { + this.module._emulator_set_builtin_palette(this.e, PALETTES[palIdx]); + } + + get isRewinding() { + return this.rewind.isRewinding; + } + + beginRewind() { + this.rewind.beginRewind(); + } + + rewindToTicks(ticks) { + if (this.rewind.rewindToTicks(ticks)) { + this.runUntil(ticks); + this.video.renderTexture(); + } + } + + endRewind() { + this.rewind.endRewind(); + this.lastRafSec = 0; + this.leftoverTicks = 0; + this.audio.startSec = 0; + } + + set autoRewind(enabled) { + if (enabled) { + this.rewindIntervalId = setInterval(() => { + const oldest = this.rewind.oldestTicks; + const start = this.ticks; + const delta = + ((REWIND_FACTOR * REWIND_UPDATE_MS) / 1000) * CPU_TICKS_PER_SECOND; + const rewindTo = Math.max(oldest, start - delta); + this.rewindToTicks(rewindTo); + vm.ticks = emulator.ticks; + }, REWIND_UPDATE_MS); + } else { + clearInterval(this.rewindIntervalId); + this.rewindIntervalId = 0; + } + } + + requestAnimationFrame() { + this.rafCancelToken = requestAnimationFrame(this.rafCallback.bind(this)); + } + + cancelAnimationFrame() { + cancelAnimationFrame(this.rafCancelToken); + this.rafCancelToken = null; + } + + run() { + this.requestAnimationFrame(); + } + + get ticks() { + return this.module._emulator_get_ticks_f64(this.e); + } + + runUntil(ticks) { + while (true) { + const event = this.module._emulator_run_until_f64(this.e, ticks); + if (event & EVENT_NEW_FRAME) { + this.rewind.pushBuffer(); + this.video.uploadTexture(); + } + if (event & EVENT_AUDIO_BUFFER_FULL && !this.isRewinding) { + this.audio.pushBuffer(); + } + if (event & EVENT_UNTIL_TICKS) { + break; + } + } + if (this.module._emulator_was_ext_ram_updated(this.e)) { + vm.extRamUpdated = true; + } + } + + rafCallback(startMs) { + this.requestAnimationFrame(); + let deltaSec = 0; + if (!this.isRewinding) { + const startSec = startMs / 1000; + deltaSec = Math.max(startSec - (this.lastRafSec || startSec), 0); + const startTicks = this.ticks; + const deltaTicks = + Math.min(deltaSec, MAX_UPDATE_SEC) * CPU_TICKS_PER_SECOND; + const runUntilTicks = startTicks + deltaTicks - this.leftoverTicks; + this.runUntil(runUntilTicks); + this.leftoverTicks = (this.ticks - runUntilTicks) | 0; + this.lastRafSec = startSec; + } + const lerp = (from, to, alpha) => alpha * from + (1 - alpha) * to; + this.fps = lerp(this.fps, Math.min(1 / deltaSec, 10000), 0.3); + this.video.renderTexture(); + } + + updateOnscreenGamepad() { + $("#controller").style.display = this.touchEnabled ? "block" : "none"; + } + + bindTouch() { + this.touchFuncs = { + controller_b: this.setJoypB.bind(this), + controller_a: this.setJoypA.bind(this), + controller_start: this.setJoypStart.bind(this), + controller_select: this.setJoypSelect.bind(this), + }; + + this.boundButtonTouchStart = this.buttonTouchStart.bind(this); + this.boundButtonTouchEnd = this.buttonTouchEnd.bind(this); + selectEl.addEventListener("touchstart", this.boundButtonTouchStart); + selectEl.addEventListener("touchend", this.boundButtonTouchEnd); + startEl.addEventListener("touchstart", this.boundButtonTouchStart); + startEl.addEventListener("touchend", this.boundButtonTouchEnd); + bEl.addEventListener("touchstart", this.boundButtonTouchStart); + bEl.addEventListener("touchend", this.boundButtonTouchEnd); + aEl.addEventListener("touchstart", this.boundButtonTouchStart); + aEl.addEventListener("touchend", this.boundButtonTouchEnd); + + this.boundDpadTouchStartMove = this.dpadTouchStartMove.bind(this); + this.boundDpadTouchEnd = this.dpadTouchEnd.bind(this); + dpadEl.addEventListener("touchstart", this.boundDpadTouchStartMove); + dpadEl.addEventListener("touchmove", this.boundDpadTouchStartMove); + dpadEl.addEventListener("touchend", this.boundDpadTouchEnd); + + this.boundTouchRestore = this.touchRestore.bind(this); + window.addEventListener("touchstart", this.boundTouchRestore); + } + + unbindTouch() { + selectEl.removeEventListener("touchstart", this.boundButtonTouchStart); + selectEl.removeEventListener("touchend", this.boundButtonTouchEnd); + startEl.removeEventListener("touchstart", this.boundButtonTouchStart); + startEl.removeEventListener("touchend", this.boundButtonTouchEnd); + bEl.removeEventListener("touchstart", this.boundButtonTouchStart); + bEl.removeEventListener("touchend", this.boundButtonTouchEnd); + aEl.removeEventListener("touchstart", this.boundButtonTouchStart); + aEl.removeEventListener("touchend", this.boundButtonTouchEnd); + + dpadEl.removeEventListener("touchstart", this.boundDpadTouchStartMove); + dpadEl.removeEventListener("touchmove", this.boundDpadTouchStartMove); + dpadEl.removeEventListener("touchend", this.boundDpadTouchEnd); + + window.removeEventListener("touchstart", this.boundTouchRestore); + } + + buttonTouchStart(event) { + if (event.currentTarget.id in this.touchFuncs) { + this.touchFuncs[event.currentTarget.id](true); + event.currentTarget.classList.add("btnPressed"); + event.preventDefault(); + } + } + + buttonTouchEnd(event) { + if (event.currentTarget.id in this.touchFuncs) { + this.touchFuncs[event.currentTarget.id](false); + event.currentTarget.classList.remove("btnPressed"); + event.preventDefault(); + } + } + + dpadTouchStartMove(event) { + const rect = event.currentTarget.getBoundingClientRect(); + const x = + (2 * (event.targetTouches[0].clientX - rect.left)) / rect.width - 1; + const y = + (2 * (event.targetTouches[0].clientY - rect.top)) / rect.height - 1; + + if (Math.abs(x) > OSGP_DEADZONE) { + if (y > x && y < -x) { + this.setJoypLeft(true); + this.setJoypRight(false); + } else if (y < x && y > -x) { + this.setJoypLeft(false); + this.setJoypRight(true); + } + } else { + this.setJoypLeft(false); + this.setJoypRight(false); + } + + if (Math.abs(y) > OSGP_DEADZONE) { + if (x > y && x < -y) { + this.setJoypUp(true); + this.setJoypDown(false); + } else if (x < y && x > -y) { + this.setJoypUp(false); + this.setJoypDown(true); + } + } else { + this.setJoypUp(false); + this.setJoypDown(false); + } + event.preventDefault(); + } + + dpadTouchEnd(event) { + this.setJoypLeft(false); + this.setJoypRight(false); + this.setJoypUp(false); + this.setJoypDown(false); + event.preventDefault(); + } + + touchRestore() { + this.touchEnabled = true; + this.updateOnscreenGamepad(); + } + + bindKeys() { + this.keyFuncs = { + Backspace: this.keyRewind.bind(this), + " ": this.keyPause.bind(this), + "[": this.keyPrevPalette.bind(this), + "]": this.keyNextPalette.bind(this), + }; + + if (customControls.down && customControls.down.length > 0) { + customControls.down.forEach((k) => { + this.keyFuncs[k] = this.setJoypDown.bind(this); + }); + } else { + // keys assigned to DOWN button on game boy + this.keyFuncs["ArrowDown"] = this.setJoypDown.bind(this); + this.keyFuncs["s"] = this.setJoypDown.bind(this); + this.keyFuncs["S"] = this.setJoypDown.bind(this); + + this.keyFuncs["ד"] = this.setJoypDown.bind(this); + } + + if (customControls.left && customControls.left.length > 0) { + customControls.left.forEach((k) => { + this.keyFuncs[k] = this.setJoypLeft.bind(this); + }); + } else { + // keys assigned to LEFT button on game boy + this.keyFuncs["ArrowLeft"] = this.setJoypLeft.bind(this); + this.keyFuncs["a"] = this.setJoypLeft.bind(this); + this.keyFuncs["ש"] = this.setJoypLeft.bind(this); + this.keyFuncs["A"] = this.setJoypDown.bind(this); + } + + if (customControls.right && customControls.right.length > 0) { + customControls.right.forEach((k) => { + this.keyFuncs[k] = this.setJoypRight.bind(this); + }); + } else { + // keys assigned to RIGHT button on game boy + this.keyFuncs["ArrowRight"] = this.setJoypRight.bind(this); + this.keyFuncs["d"] = this.setJoypRight.bind(this); + this.keyFuncs["ג"] = this.setJoypRight.bind(this); + this.keyFuncs["D"] = this.setJoypDown.bind(this); + } + + if (customControls.up && customControls.up.length > 0) { + customControls.up.forEach((k) => { + this.keyFuncs[k] = this.setJoypUp.bind(this); + }); + } else { + // keys assigned to UP button on game boy + + this.keyFuncs["ArrowUp"] = this.setJoypUp.bind(this); + this.keyFuncs["w"] = this.setJoypUp.bind(this); + this.keyFuncs["W"] = this.setJoypUp.bind(this); + this.keyFuncs["'"] = this.setJoypUp.bind(this); + } + + if (customControls.a && customControls.a.length > 0) { + customControls.a.forEach((k) => { + this.keyFuncs[k] = this.setJoypA.bind(this); + }); + } else { + // keys assigned to A button on game boy + + this.keyFuncs["z"] = this.setJoypA.bind(this); + this.keyFuncs["ז"] = this.setJoypA.bind(this); + this.keyFuncs["ח"] = this.setJoypA.bind(this); + this.keyFuncs["j"] = this.setJoypA.bind(this); + this.keyFuncs["Z"] = this.setJoypA.bind(this); + this.keyFuncs["j"] = this.setJoypA.bind(this); + // this.keyFuncs["Alt"] = this.setJoypA.bind(this); + } + + if (customControls.b && customControls.b.length > 0) { + customControls.b.forEach((k) => { + this.keyFuncs[k] = this.setJoypB.bind(this); + }); + } else { + // keys assigned to B button on game boy + this.keyFuncs["x"] = this.setJoypB.bind(this); + this.keyFuncs["X"] = this.setJoypB.bind(this); + this.keyFuncs["ס"] = this.setJoypB.bind(this); + this.keyFuncs["ל"] = this.setJoypB.bind(this); + this.keyFuncs["k"] = this.setJoypB.bind(this); + this.keyFuncs["K"] = this.setJoypB.bind(this); + // this.keyFuncs["Control"] = this.setJoypB.bind(this); + } + + if (customControls.start && customControls.start.length > 0) { + customControls.start.forEach((k) => { + this.keyFuncs[k] = this.setJoypStart.bind(this); + }); + } else { + // keys assigned to START button on game boy + + this.keyFuncs["Enter"] = this.setJoypStart.bind(this); + } + + if (customControls.select && customControls.select.length > 0) { + customControls.select.forEach((k) => { + this.keyFuncs[k] = this.setJoypSelect.bind(this); + }); + } else { + // keys assigned to SELECT button on game boy + + this.keyFuncs["Shift"] = this.setJoypSelect.bind(this); + } + + this.boundKeyDown = this.keyDown.bind(this); + this.boundKeyUp = this.keyUp.bind(this); + + window.addEventListener("keydown", this.boundKeyDown); + window.addEventListener("keyup", this.boundKeyUp); + } + + unbindKeys() { + window.removeEventListener("keydown", this.boundKeyDown); + window.removeEventListener("keyup", this.boundKeyUp); + } + + keyDown(event) { + if (event.key === "w" && (event.metaKey || event.ctrlKey)) { + return; + } + if (event.key in this.keyFuncs) { + if (this.touchEnabled) { + this.touchEnabled = false; + this.updateOnscreenGamepad(); + } + this.keyFuncs[event.key](true); + event.preventDefault(); + } + } + + keyUp(event) { + if (event.key in this.keyFuncs) { + this.keyFuncs[event.key](false); + event.preventDefault(); + } + } + + keyRewind(isKeyDown) { + if (!ENABLE_REWIND) { + return; + } + if (this.isRewinding !== isKeyDown) { + if (isKeyDown) { + vm.paused = true; + this.autoRewind = true; + } else { + this.autoRewind = false; + vm.paused = false; + } + } + } + + keyPause(isKeyDown) { + if (!ENABLE_PAUSE) { + return; + } + if (isKeyDown) vm.togglePause(); + } + + keyPrevPalette(isKeyDown) { + if (!ENABLE_SWITCH_PALETTES) { + return; + } + if (isKeyDown) { + vm.palIdx = (vm.palIdx + PALETTES.length - 1) % PALETTES.length; + emulator.setBuiltinPalette(vm.palIdx); + } + } + + keyNextPalette(isKeyDown) { + if (!ENABLE_SWITCH_PALETTES) { + return; + } + if (isKeyDown) { + vm.palIdx = (vm.palIdx + 1) % PALETTES.length; + emulator.setBuiltinPalette(vm.palIdx); + } + } + + setJoypDown(set) { + this.module._set_joyp_down(this.e, set); + } + setJoypUp(set) { + this.module._set_joyp_up(this.e, set); + } + setJoypLeft(set) { + this.module._set_joyp_left(this.e, set); + } + setJoypRight(set) { + this.module._set_joyp_right(this.e, set); + } + setJoypSelect(set) { + this.module._set_joyp_select(this.e, set); + } + setJoypStart(set) { + this.module._set_joyp_start(this.e, set); + } + setJoypB(set) { + this.module._set_joyp_B(this.e, set); + } + setJoypA(set) { + this.module._set_joyp_A(this.e, set); + } +} + +class Gamepad { + constructor(module, e) { + this.module = module; + this.e = e; + } + + // Load a key map for gamepad-to-gameboy buttons + bindKeys(strMapping) { + this.GAMEPAD_KEYMAP_STANDARD = [ + { + gb_key: "b", + gp_button: 0, + type: "button", + gp_bind: this.module._set_joyp_B.bind(null, this.e), + }, + { + gb_key: "a", + gp_button: 1, + type: "button", + gp_bind: this.module._set_joyp_A.bind(null, this.e), + }, + { + gb_key: "select", + gp_button: 8, + type: "button", + gp_bind: this.module._set_joyp_select.bind(null, this.e), + }, + { + gb_key: "start", + gp_button: 9, + type: "button", + gp_bind: this.module._set_joyp_start.bind(null, this.e), + }, + { + gb_key: "up", + gp_button: 12, + type: "button", + gp_bind: this.module._set_joyp_up.bind(null, this.e), + }, + { + gb_key: "down", + gp_button: 13, + type: "button", + gp_bind: this.module._set_joyp_down.bind(null, this.e), + }, + { + gb_key: "left", + gp_button: 14, + type: "button", + gp_bind: this.module._set_joyp_left.bind(null, this.e), + }, + { + gb_key: "right", + gp_button: 15, + type: "button", + gp_bind: this.module._set_joyp_right.bind(null, this.e), + }, + ]; + + this.GAMEPAD_KEYMAP_DEFAULT = [ + { + gb_key: "a", + gp_button: 0, + type: "button", + gp_bind: this.module._set_joyp_A.bind(null, this.e), + }, + { + gb_key: "b", + gp_button: 1, + type: "button", + gp_bind: this.module._set_joyp_B.bind(null, this.e), + }, + { + gb_key: "select", + gp_button: 2, + type: "button", + gp_bind: this.module._set_joyp_select.bind(null, this.e), + }, + { + gb_key: "start", + gp_button: 3, + type: "button", + gp_bind: this.module._set_joyp_start.bind(null, this.e), + }, + { + gb_key: "up", + gp_button: 2, + type: "axis", + gp_bind: this.module._set_joyp_up.bind(null, this.e), + }, + { + gb_key: "down", + gp_button: 3, + type: "axis", + gp_bind: this.module._set_joyp_down.bind(null, this.e), + }, + { + gb_key: "left", + gp_button: 0, + type: "axis", + gp_bind: this.module._set_joyp_left.bind(null, this.e), + }, + { + gb_key: "right", + gp_button: 1, + type: "axis", + gp_bind: this.module._set_joyp_right.bind(null, this.e), + }, + ]; + + // Try to use the w3c "standard" gamepad mapping if available + // (Chrome/V8 seems to do that better than Firefox) + // + // Otherwise use a default mapping that assigns + // A/B/Select/Start to the first four buttons, + // and U/D/L/R to the first two axes. + if (strMapping === GAMEPAD_KEYMAP_STANDARD_STR) { + this.gp.keybinds = this.GAMEPAD_KEYMAP_STANDARD; + } else { + this.gp.keybinds = this.GAMEPAD_KEYMAP_DEFAULT; + } + } + + cacheValues(gamepad) { + // Read Buttons + for (let k = 0; k < gamepad.buttons.length; k++) { + // .value is for analog, .pressed is for boolean buttons + this.gp.buttons.cur[k] = + gamepad.buttons[k].value > 0 || gamepad.buttons[k].pressed == true; + + // Update state changed if not on first input pass + if (this.gp.buttons.last !== undefined) { + this.gp.buttons.changed[k] = + this.gp.buttons.cur[k] != this.gp.buttons.last[k]; + } + } + + // Read Axes + for (let k = 0; k < gamepad.axes.length; k++) { + // Decode each dpad axis into two buttons, one for each direction + this.gp.axes.cur[k * 2] = gamepad.axes[k] < 0; + this.gp.axes.cur[k * 2 + 1] = gamepad.axes[k] > 0; + + // Update state changed if not on first input pass + if (this.gp.axes.last !== undefined) { + this.gp.axes.changed[k * 2] = + this.gp.axes.cur[k * 2] != this.gp.axes.last[k * 2]; + this.gp.axes.changed[k * 2 + 1] = + this.gp.axes.cur[k * 2 + 1] != this.gp.axes.last[k * 2 + 1]; + } + } + + // Save current state for comparison on next input + this.gp.axes.last = this.gp.axes.cur.slice(0); + this.gp.buttons.last = this.gp.buttons.cur.slice(0); + } + + handleButton(keyBind) { + let buttonCache; + + // Select button / axis cache based on key bind type + if (keyBind.type === "button") { + buttonCache = this.gp.buttons; + } else if (keyBind.type === "axis") { + buttonCache = this.gp.axes; + } + + // Make sure the button exists in the cache array + if (keyBind.gp_button < buttonCache.changed.length) { + // Send the button state if it's changed + if (buttonCache.changed[keyBind.gp_button]) { + if (buttonCache.cur[keyBind.gp_button]) { + // Gamepad Button Down + keyBind.gp_bind(true); + } else { + // Gamepad Button Up + keyBind.gp_bind(false); + } + } + } + } + + getCurrent() { + // Chrome requires retrieving a new gamepad object + // every time button state is queried (the existing object + // will have stale button state). Just do that for all browsers + let gamepad = navigator.getGamepads()[this.gp.apiID]; + + if (gamepad) { + if (gamepad.connected) { + return gamepad; + } + } + + return undefined; + } + + update() { + let gamepad = this.getCurrent(); + + if (gamepad !== undefined) { + // Cache gamepad input values + this.cacheValues(gamepad); + + // Loop through buttons and send changes if needed + for (let i = 0; i < this.gp.keybinds.length; i++) { + this.handleButton(this.gp.keybinds[i]); + } + } else { + // Gamepad is no longer present, disconnect + this.releaseGamepad(); + } + } + + startGamepad(gamepad) { + // Make sure it has enough buttons and axes + if ( + gamepad.mapping === GAMEPAD_KEYMAP_STANDARD_STR || + (gamepad.axes.length >= 2 && gamepad.buttons.length >= 4) + ) { + // Save API index for polling (required by Chrome/V8) + this.gp.apiID = gamepad.index; + + // Assign gameboy keys to the gamepad + this.bindKeys(gamepad.mapping); + + // Start polling the gamepad for input + this.gp.timerID = setInterval( + () => this.update(), + GAMEPAD_POLLING_INTERVAL + ); + } + } + + releaseGamepad() { + // Stop polling the gamepad for input + if (this.gp.timerID !== undefined) { + clearInterval(this.gp.timerID); + } + + // Clear previous button history and controller info + this.gp.axes.last = undefined; + this.gp.buttons.last = undefined; + this.gp.keybinds = undefined; + + this.gp.apiID = undefined; + } + + // If a gamepad was already connected on this page + // and released, it won't fire another connect event. + // So try to find any that might be present + checkAlreadyConnected() { + let gamepads = navigator.getGamepads(); + + // If any gamepads are already attached to the page, + // use the first one that is connected + for (let idx = 0; idx < gamepads.length; idx++) { + if (gamepads[idx] !== undefined && gamepads[idx] !== null) { + if (gamepads[idx].connected === true) { + this.startGamepad(gamepads[idx]); + } + } + } + } + + // Event handler for when a gamepad is connected + eventConnected(event) { + this.startGamepad(navigator.getGamepads()[event.gamepad.index]); + } + + // Event handler for when a gamepad is disconnected + eventDisconnected(event) { + this.releaseGamepad(); + } + + // Register event connection handlers for gamepads + init() { + // gamepad related vars + this.gp = { + apiID: undefined, + timerID: undefined, + keybinds: undefined, + axes: { last: undefined, cur: [], changed: [] }, + buttons: { last: undefined, cur: [], changed: [] }, + }; + + // Check for previously attached gamepads that might + // not emit a gamepadconnected() event + this.checkAlreadyConnected(); + + this.boundGamepadConnected = this.eventConnected.bind(this); + this.boundGamepadDisconnected = this.eventDisconnected.bind(this); + + // When a gamepad connects, start polling it for input + window.addEventListener("gamepadconnected", this.boundGamepadConnected); + + // When a gamepad disconnects, shut down polling for input + window.addEventListener( + "gamepaddisconnected", + this.boundGamepadDisconnected + ); + } + + // Release event connection handlers and settings + shutdown() { + this.releaseGamepad(); + window.removeEventListener("gamepadconnected", this.boundGamepadConnected); + window.removeEventListener( + "gamepaddisconnected", + this.boundGamepadDisconnected + ); + } +} + +class Audio { + constructor(module, e) { + this.started = false; + this.module = module; + this.buffer = makeWasmBuffer( + this.module, + this.module._get_audio_buffer_ptr(e), + this.module._get_audio_buffer_capacity(e) + ); + this.startSec = 0; + this.resume(); + + this.boundStartPlayback = this.startPlayback.bind(this); + window.addEventListener("keydown", this.boundStartPlayback, true); + window.addEventListener("click", this.boundStartPlayback, true); + window.addEventListener("touchend", this.boundStartPlayback, true); + } + + startPlayback() { + window.removeEventListener("touchend", this.boundStartPlayback, true); + window.removeEventListener("keydown", this.boundStartPlayback, true); + window.removeEventListener("click", this.boundStartPlayback, true); + this.started = true; + this.resume(); + } + + get sampleRate() { + return Audio.ctx.sampleRate; + } + + pushBuffer() { + if (!this.started) { + return; + } + const nowSec = Audio.ctx.currentTime; + const nowPlusLatency = nowSec + AUDIO_LATENCY_SEC; + const volume = vm.volume; + this.startSec = this.startSec || nowPlusLatency; + if (this.startSec >= nowSec) { + const buffer = Audio.ctx.createBuffer(2, AUDIO_FRAMES, this.sampleRate); + const channel0 = buffer.getChannelData(0); + const channel1 = buffer.getChannelData(1); + for (let i = 0; i < AUDIO_FRAMES; i++) { + channel0[i] = (this.buffer[2 * i] * volume) / 255; + channel1[i] = (this.buffer[2 * i + 1] * volume) / 255; + } + const bufferSource = Audio.ctx.createBufferSource(); + bufferSource.buffer = buffer; + bufferSource.connect(Audio.ctx.destination); + bufferSource.start(this.startSec); + const bufferSec = AUDIO_FRAMES / this.sampleRate; + this.startSec += bufferSec; + } else { + console.log( + "Resetting audio (" + + this.startSec.toFixed(2) + + " < " + + nowSec.toFixed(2) + + ")" + ); + this.startSec = nowPlusLatency; + } + } + + pause() { + if (!this.started) { + return; + } + Audio.ctx.suspend(); + } + + resume() { + if (!this.started) { + return; + } + Audio.ctx.resume(); + } +} + +Audio.ctx = new AudioContext(); + +class Video { + constructor(module, e, el) { + this.module = module; + // Both iPhone and Desktop Safari dont't upscale using image-rendering: pixelated + // on webgl canvases. See https://bugs.webkit.org/show_bug.cgi?id=193895. + // For now, default to Canvas2D. + if (window.navigator.userAgent.match(/iPhone|iPad|15.[0-9] Safari/)) { + this.renderer = new Canvas2DRenderer(el); + } else { + try { + this.renderer = new WebGLRenderer(el); + } catch (error) { + console.log(`Error creating WebGLRenderer: ${error}`); + this.renderer = new Canvas2DRenderer(el); + } + } + this.buffer = makeWasmBuffer( + this.module, + this.module._get_frame_buffer_ptr(e), + this.module._get_frame_buffer_size(e) + ); + this.sgbBuffer = makeWasmBuffer( + this.module, + this.module._get_sgb_frame_buffer_ptr(e), + this.module._get_sgb_frame_buffer_size(e) + ); + } + + uploadTexture() { + this.renderer.uploadTextures(this.buffer, this.sgbBuffer); + } + + renderTexture() { + this.renderer.renderTextures(); + } +} + +class Canvas2DRenderer { + constructor(el) { + this.ctx = el.getContext("2d"); + this.imageData = this.ctx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT); + this.sgbImageData = this.ctx.createImageData( + SGB_SCREEN_WIDTH, + SGB_SCREEN_HEIGHT + ); + + this.overlayCanvas = document.createElement("canvas"); + this.overlayCanvas.width = SGB_SCREEN_WIDTH; + this.overlayCanvas.height = SGB_SCREEN_HEIGHT; + this.overlayCtx = this.overlayCanvas.getContext("2d"); + } + + uploadTextures(buffer, sgbBuffer) { + this.imageData.data.set(buffer); + this.sgbImageData.data.set(sgbBuffer); + } + + renderTextures() { + if (vm.canvas.useSgbBorder) { + this.ctx.putImageData(this.imageData, SGB_SCREEN_LEFT, SGB_SCREEN_TOP); + this.overlayCtx.putImageData(this.sgbImageData, 0, 0); + this.ctx.drawImage(this.overlayCanvas, 0, 0); + } else { + this.ctx.putImageData(this.imageData, 0, 0); + } + } +} + +class WebGLRenderer { + constructor(el) { + const gl = (this.gl = el.getContext("webgl", { + preserveDrawingBuffer: true, + })); + if (gl === null) { + throw new Error("unable to create webgl context"); + } + + function compileShader(type, source) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error(`compileShader failed: ${gl.getShaderInfoLog(shader)}`); + } + return shader; + } + + const vertexShader = compileShader( + gl.VERTEX_SHADER, + `attribute vec2 aPos; + attribute vec2 aTexCoord; + varying highp vec2 vTexCoord; + void main(void) { + gl_Position = vec4(aPos, 0.0, 1.0); + vTexCoord = aTexCoord; + }` + ); + const fragmentShader = compileShader( + gl.FRAGMENT_SHADER, + `varying highp vec2 vTexCoord; + uniform sampler2D uSampler; + void main(void) { + gl_FragColor = texture2D(uSampler, vTexCoord); + }` + ); + + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(`program link failed: ${gl.getProgramInfoLog(program)}`); + } + gl.useProgram(program); + + this.aPos = gl.getAttribLocation(program, "aPos"); + this.aTexCoord = gl.getAttribLocation(program, "aTexCoord"); + this.uSampler = gl.getUniformLocation(program, "uSampler"); + + this.fbTexture = this.createTexture(); + this.sgbFbTexture = this.createTexture(); + + const invLerpClipSpace = (x, max) => 2 * (x / max) - 1; + const l = invLerpClipSpace(SGB_SCREEN_LEFT, SGB_SCREEN_WIDTH); + const r = invLerpClipSpace(SGB_SCREEN_RIGHT, SGB_SCREEN_WIDTH); + const t = -invLerpClipSpace(SGB_SCREEN_TOP, SGB_SCREEN_HEIGHT); + const b = -invLerpClipSpace(SGB_SCREEN_BOTTOM, SGB_SCREEN_HEIGHT); + const w = SCREEN_WIDTH / 256, + sw = SGB_SCREEN_WIDTH / 256; + const h = SCREEN_HEIGHT / 256, + sh = SGB_SCREEN_HEIGHT / 256; + + const verts = new Float32Array([ + // fb only + -1, + -1, + 0, + h, + +1, + -1, + w, + h, + -1, + +1, + 0, + 0, + +1, + +1, + w, + 0, + + // sgb fb + l, + b, + 0, + h, + r, + b, + w, + h, + l, + t, + 0, + 0, + r, + t, + w, + 0, + + // sgb border + -1, + -1, + 0, + sh, + +1, + -1, + sw, + sh, + -1, + +1, + 0, + 0, + +1, + +1, + sw, + 0, + ]); + + const buffer = gl.createBuffer(); + this.gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW); + + gl.enableVertexAttribArray(this.aPos); + gl.enableVertexAttribArray(this.aTexCoord); + gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, gl.FALSE, 16, 0); + gl.vertexAttribPointer(this.aTexCoord, 2, gl.FLOAT, gl.FALSE, 16, 8); + gl.uniform1i(this.uSampler, 0); + } + + createTexture() { + const gl = this.gl; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 256, + 256, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + return texture; + } + + uploadTextures(buffer, sgbBuffer) { + const gl = this.gl; + gl.bindTexture(gl.TEXTURE_2D, this.fbTexture); + gl.texSubImage2D( + gl.TEXTURE_2D, + 0, + 0, + 0, + SCREEN_WIDTH, + SCREEN_HEIGHT, + gl.RGBA, + gl.UNSIGNED_BYTE, + buffer + ); + + gl.bindTexture(gl.TEXTURE_2D, this.sgbFbTexture); + gl.texSubImage2D( + gl.TEXTURE_2D, + 0, + 0, + 0, + SGB_SCREEN_WIDTH, + SGB_SCREEN_HEIGHT, + gl.RGBA, + gl.UNSIGNED_BYTE, + sgbBuffer + ); + } + + renderTextures() { + const gl = this.gl; + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.clearColor(0.5, 0.5, 0.5, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + if (vm.canvas.useSgbBorder) { + gl.bindTexture(gl.TEXTURE_2D, this.fbTexture); + gl.drawArrays(gl.TRIANGLE_STRIP, 4, 4); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.bindTexture(gl.TEXTURE_2D, this.sgbFbTexture); + gl.drawArrays(gl.TRIANGLE_STRIP, 8, 4); + gl.disable(gl.BLEND); + } else { + gl.bindTexture(gl.TEXTURE_2D, this.fbTexture); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + } +} + +class Rewind { + constructor(module, e) { + this.module = module; + this.e = e; + this.joypadBufferPtr = this.module._joypad_new(); + this.statePtr = 0; + this.bufferPtr = this.module._rewind_new_simple( + e, + REWIND_FRAMES_PER_BASE_STATE, + REWIND_BUFFER_CAPACITY + ); + this.module._emulator_set_default_joypad_callback(e, this.joypadBufferPtr); + } + + destroy() { + this.module._rewind_delete(this.bufferPtr); + this.module._joypad_delete(this.joypadBufferPtr); + } + + get oldestTicks() { + return this.module._rewind_get_oldest_ticks_f64(this.bufferPtr); + } + + get newestTicks() { + return this.module._rewind_get_newest_ticks_f64(this.bufferPtr); + } + + pushBuffer() { + if (!this.isRewinding) { + this.module._rewind_append(this.bufferPtr, this.e); + } + } + + get isRewinding() { + return this.statePtr !== 0; + } + + beginRewind() { + if (this.isRewinding) return; + this.statePtr = this.module._rewind_begin( + this.e, + this.bufferPtr, + this.joypadBufferPtr + ); + } + + rewindToTicks(ticks) { + if (!this.isRewinding) return; + return ( + this.module._rewind_to_ticks_wrapper(this.statePtr, ticks) === RESULT_OK + ); + } + + endRewind() { + if (!this.isRewinding) return; + this.module._emulator_set_default_joypad_callback( + this.e, + this.joypadBufferPtr + ); + this.module._rewind_end(this.statePtr); + this.statePtr = 0; + } +} diff --git a/resen/screen/rom/kmeot_zag.gb b/resen/screen/rom/kmeot_zag.gb new file mode 100644 index 0000000..9b60577 Binary files /dev/null and b/resen/screen/rom/kmeot_zag.gb differ diff --git a/resen/screen/snippet b/resen/screen/snippet new file mode 100644 index 0000000..01482ed Binary files /dev/null and b/resen/screen/snippet differ