Created
February 2, 2021 06:17
-
-
Save skynet86/832e3135918b31e059e4ed0a4f2aab85 to your computer and use it in GitHub Desktop.
NES-RUST-WASM-EXAMPLE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/> | |
<style> | |
canvas { | |
background: #000; | |
margin: 10px; | |
} | |
.actual { | |
width: 256; | |
height: 240; | |
} | |
.double { | |
width: 512; | |
height: 480; | |
} | |
.quadruple { | |
width: 1024; | |
height: 960; | |
} | |
td { | |
padding: 5px; | |
} | |
</style> | |
</head> | |
<body> | |
<script type="module"> | |
import init, { WasmNes, Button } from './nes_rust_wasm.js'; | |
const setupAudio = (wasm, nes) => { | |
const audioContext = AudioContext || webkitAudioContext; | |
if (audioContext === undefined) { | |
throw new Error('This browser seems not to support AudioContext.'); | |
} | |
const bufferLength = 4096; | |
const context = new audioContext({sampleRate: 44100}); | |
const scriptProcessor = context.createScriptProcessor(bufferLength, 0, 1); | |
scriptProcessor.onaudioprocess = e => { | |
const data = e.outputBuffer.getChannelData(0); | |
nes.update_sample_buffer(data); | |
// Adjust volume | |
for (let i = 0; i < data.length; i++) { | |
data[i] *= 0.25; | |
} | |
}; | |
scriptProcessor.connect(context.destination); | |
}; | |
const start = romArrayBuffer => { | |
// @TODO: Call init beforehand | |
init() | |
.then(wasm => run(wasm, new Uint8Array(romArrayBuffer))) | |
.catch(error => console.error(error)); | |
}; | |
const run = (wasm, romContentArray) => { | |
const width = 256; | |
const height = 240; | |
const canvas = document.getElementById('nesCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const imageData = ctx.createImageData(width, height); | |
const pixels = new Uint8Array(imageData.data.buffer); | |
const nes = WasmNes.new(); | |
nes.set_rom(romContentArray); | |
setupAudio(wasm, nes); | |
nes.bootup(); | |
// FPS counter | |
let totalElapsedTime = 0.0; | |
let previousTime = performance.now(); | |
let frameCount = 0; | |
const fpsSpan = document.getElementById('fpsSpan'); | |
const countFps = () => { | |
frameCount++; | |
const currentTime = performance.now(); | |
const elapsedTime = currentTime - previousTime; | |
totalElapsedTime += elapsedTime; | |
previousTime = currentTime; | |
if ((frameCount % 60) === 0) { | |
fpsSpan.textContent = (1000.0 / (totalElapsedTime / 60)).toFixed(2); | |
totalElapsedTime = 0.0; | |
frameCount = 0; | |
} | |
} | |
// animation frame loop | |
const stepFrame = () => { | |
requestAnimationFrame(stepFrame); | |
countFps(); | |
nes.step_frame(); | |
nes.update_pixels(pixels); | |
ctx.putImageData(imageData, 0, 0); | |
}; | |
// joypad event listener setup | |
// @TODO: Mapping should be configurable | |
const getButton = keyCode => { | |
switch (keyCode) { | |
case 32: // space | |
return Button.Start; | |
case 37: // Left | |
return Button.Joypad1Left; | |
case 38: // Up | |
return Button.Joypad1Up; | |
case 39: // Right | |
return Button.Joypad1Right; | |
case 40: // Down | |
return Button.Joypad1Down; | |
case 50: // 2 | |
return Button.Joypad2Down; | |
case 52: // 4 | |
return Button.Joypad2Left; | |
case 54: // 6 | |
return Button.Joypad2Right; | |
case 56: // 8 | |
return Button.Joypad2Up; | |
case 65: // A | |
return Button.Joypad1A; | |
case 66: // B | |
return Button.Joypad1B; | |
case 82: // R | |
return Button.Reset; | |
case 83: // S | |
return Button.Select; | |
case 88: // X | |
return Button.Joypad2A; | |
case 90: // Z | |
return Button.Joypad2B; | |
default: | |
return null; | |
} | |
}; | |
window.addEventListener('keydown', event => { | |
const button = getButton(event.keyCode); | |
if (button === null) { | |
return; | |
} | |
nes.press_button(button); | |
event.preventDefault(); | |
}, false); | |
window.addEventListener('keyup', event => { | |
const button = getButton(event.keyCode); | |
if (button === null) { | |
return; | |
} | |
nes.release_button(button); | |
event.preventDefault(); | |
}, false); | |
stepFrame(); | |
}; | |
// rom load | |
let romSelected = false; | |
document.getElementById('romSelect').addEventListener('change', event => { | |
if (romSelected) return; | |
romSelected = true; | |
const select = event.target; | |
const option = select.selectedOptions[0]; | |
const filename = option.value; | |
if (!filename) { | |
return; | |
} | |
select.disabled = true; // @TODO: Reset Nes instead | |
fetch('./' + filename) | |
.then(result => result.arrayBuffer()) | |
.then(start) | |
.catch(error => console.error(error)); | |
}); | |
window.addEventListener('dragover', event => { | |
event.preventDefault(); | |
}, false); | |
window.addEventListener('drop', event => { | |
event.preventDefault(); | |
if (romSelected) return; | |
romSelected = true; | |
document.getElementById('romSelect').disabled = true; // @TODO: Reset Nes instead | |
const reader = new FileReader(); | |
reader.onload = e => { | |
start(e.target.result); | |
}; | |
reader.onerror = e => { | |
console.error(e); | |
}; | |
reader.readAsArrayBuffer(event.dataTransfer.files[0]); | |
}, false); | |
// screen size | |
document.getElementById('screenSizeSelect').addEventListener('change', event => { | |
const select = event.target; | |
const option = select.selectedOptions[0]; | |
const className = option.value; | |
if (!className) { | |
return; | |
} | |
const canvas = document.getElementById('nesCanvas'); | |
for (const name of ['actual', 'double', 'quadruple']) { | |
if (name === className) { | |
canvas.classList.add(name); | |
} else { | |
canvas.classList.remove(name); | |
} | |
} | |
}); | |
</script> | |
<div> | |
<select id="romSelect"> | |
<option value="" selected>-- select rom --</option> | |
<option value="Super_mario_brothers.nes">Super Mario</option> | |
<option value="Tetris.nes">Tetris</option> | |
</select> | |
or Drag and Drop your own rom file | |
</div> | |
<div> | |
<canvas id="nesCanvas" width="256" height="240"></canvas> | |
</div> | |
<div> | |
<select id="screenSizeSelect"> | |
<option value="actual" selected>256x240</optioin> | |
<option value="double">512x480</optioin> | |
<option value="quadruple">1024x960</optioin> | |
</select> | |
<span id="fpsSpan">--.--</span> fps | |
</div> | |
<div> | |
<table> | |
<tr> | |
<td>Down →</td> | |
<td>Down</td> | |
</tr> | |
<tr> | |
<td>Left →</td> | |
<td>Left</td> | |
</tr> | |
<tr> | |
<td>Right →</td> | |
<td>Right</td> | |
<!-- <td>6</td> --> | |
</tr> | |
<tr> | |
<td>Up →</td> | |
<td>Up</td> | |
<!-- <td>8</td> --> | |
</tr> | |
<tr> | |
<td>A →</td> | |
<td>A</td> | |
<!-- <td>X</td> --> | |
</tr> | |
<tr> | |
<td>B →</td> | |
<td>B</td> | |
<!-- <td>Z</td> --> | |
</tr> | |
<tr> | |
<td>Start →</td> | |
<td>Space</td> | |
<!-- <td>-</td> --> | |
</tr> | |
<tr> | |
<td>Select →</td> | |
<td>S</td> | |
<!-- <td>-</td> --> | |
</tr> | |
<tr> | |
<td>Reset →</td> | |
<td>R</td> | |
<!-- <td>-</td> --> | |
</tr> | |
</table> | |
</div> | |
<div> | |
<p>NES Roms Copyright | |
<a href="https://github.com/takahirox/nes-rust">NES emulator in Rust</a></p> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment