mirror of
https://github.com/koute/pinky.git
synced 2024-06-01 02:48:08 -04:00
Add pinky-web
This commit is contained in:
parent
b915572ccc
commit
0bd8e1fabe
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,6 +2,8 @@ pinky-devui/target
|
|||
pinky-devui/Cargo.lock
|
||||
pinky-libretro/target
|
||||
pinky-libretro/Cargo.lock
|
||||
pinky-web/target
|
||||
pinky-web/Cargo.lock
|
||||
nes-testsuite/target
|
||||
nes-testsuite/Cargo.lock
|
||||
mos6502/target
|
||||
|
|
30
pinky-web/Cargo.toml
Normal file
30
pinky-web/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
|||
[package]
|
||||
name = "pinky-web"
|
||||
version = "0.1.0"
|
||||
authors = ["Jan Bujak <j@exia.io>"]
|
||||
|
||||
[dependencies]
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
|
||||
[dependencies.stdweb]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.nes]
|
||||
path = "../nes"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 2
|
||||
debug = true
|
||||
rpath = false
|
||||
lto = false
|
||||
debug-assertions = true
|
||||
codegen-units = 4
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
rpath = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
47
pinky-web/README.md
Normal file
47
pinky-web/README.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
This is the Web frontend for Pinky.
|
||||
|
||||
It's mostly meant as a demo for Rust's WebAssembly capabilities
|
||||
and the [stdweb] project.
|
||||
|
||||
[stdweb]: https://github.com/koute/stdweb
|
||||
|
||||
[![Become a patron](https://koute.github.io/img/become_a_patron_button.png)](https://www.patreon.com/koute)
|
||||
|
||||
### See it in action
|
||||
|
||||
* A version [compiled with Rust's native WebAssembly backend] (recommended!).
|
||||
* A version [compiled to WebAssembly with Emscripten].
|
||||
* A version [compiled to asm.js with Emscripten].
|
||||
|
||||
[compiled with Rust's native WebAssembly backend]: https://koute.github.io/pinky-web
|
||||
[compiled to WebAssembly with Emscripten]: https://koute.github.io/pinky-web-webasm-emscripten
|
||||
[compiled to asm.js with Emscripten]: https://koute.github.io/pinky-web-asmjs-emscripten
|
||||
|
||||
### Building (using Rust's native WebAssembly backend)
|
||||
|
||||
1. Install newest nightly Rust:
|
||||
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
|
||||
2. Install WebAssembly target:
|
||||
|
||||
$ rustup target add wasm32-unknown-unknown
|
||||
|
||||
3. Install [cargo-web]:
|
||||
|
||||
$ cargo install -f cargo-web
|
||||
|
||||
4. Build it:
|
||||
|
||||
$ cargo web start --target-webasm --release
|
||||
|
||||
5. Visit `http://localhost:8000` with your browser.
|
||||
|
||||
[cargo-web]: https://github.com/koute/cargo-web
|
||||
|
||||
### Building for other backends
|
||||
|
||||
Replace `--target-webasm` with `--target-webasm-emscripten` or `--target-asmjs-emscripten`
|
||||
if you want to build it using another backend. You will also have to install the
|
||||
corresponding targets with `rustup` - `wasm32-unknown-emscripten` and `asmjs-unknown-emscripten`
|
||||
respectively.
|
657
pinky-web/src/main.rs
Normal file
657
pinky-web/src/main.rs
Normal file
|
@ -0,0 +1,657 @@
|
|||
#![recursion_limit="2048"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate stdweb;
|
||||
extern crate nes;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::ops::DerefMut;
|
||||
use std::cmp::max;
|
||||
use std::error::Error;
|
||||
|
||||
use stdweb::web::{
|
||||
self,
|
||||
IEventTarget,
|
||||
INode,
|
||||
IElement,
|
||||
FileReader,
|
||||
FileReaderResult,
|
||||
Element,
|
||||
ArrayBuffer
|
||||
};
|
||||
|
||||
use stdweb::web::event::{
|
||||
IEvent,
|
||||
IKeyboardEvent,
|
||||
ClickEvent,
|
||||
ChangeEvent,
|
||||
ProgressLoadEvent,
|
||||
KeydownEvent,
|
||||
KeyupEvent,
|
||||
KeyboardLocation
|
||||
};
|
||||
|
||||
use stdweb::web::html_element::InputElement;
|
||||
use stdweb::unstable::TryInto;
|
||||
use stdweb::{Value, UnsafeTypedArray, Once};
|
||||
|
||||
macro_rules! enclose {
|
||||
( [$( $x:ident ),*] $y:expr ) => {
|
||||
{
|
||||
$(let $x = $x.clone();)*
|
||||
$y
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn palette_to_abgr( palette: &nes::Palette ) -> [u32; 64] {
|
||||
let mut output = [0; 64];
|
||||
for (index, out) in output.iter_mut().enumerate() {
|
||||
let (r, g, b) = palette.get_rgb( index as u8 );
|
||||
*out = ((b as u32) << 16) |
|
||||
((g as u32) << 8) |
|
||||
((r as u32) ) |
|
||||
0xFF000000;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
struct PinkyWeb {
|
||||
state: nes::State,
|
||||
palette: [u32; 64],
|
||||
framebuffer: [u32; 256 * 240],
|
||||
audio_buffer: Vec< f32 >,
|
||||
audio_chunk_counter: u32,
|
||||
audio_underrun: Option< usize >,
|
||||
paused: bool,
|
||||
busy: bool,
|
||||
js_ctx: Value
|
||||
}
|
||||
|
||||
impl nes::Context for PinkyWeb {
|
||||
#[inline]
|
||||
fn state_mut( &mut self ) -> &mut nes::State {
|
||||
&mut self.state
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn state( &self ) -> &nes::State {
|
||||
&self.state
|
||||
}
|
||||
|
||||
// Ugh, trying to get gapless low-latency PCM playback
|
||||
// out of the Web Audio APIs is like driving a rusty
|
||||
// nail through my hand.
|
||||
//
|
||||
// This blog post probably sums it up pretty well:
|
||||
// http://blog.mecheye.net/2017/09/i-dont-know-who-the-web-audio-api-is-designed-for/
|
||||
//
|
||||
// So, this is not perfect, but it's as good as I can make it.
|
||||
#[inline]
|
||||
fn on_audio_sample( &mut self, sample: f32 ) {
|
||||
self.audio_buffer.push( sample );
|
||||
if self.audio_buffer.len() == 2048 {
|
||||
self.audio_chunk_counter += 1;
|
||||
let audio_buffered: f64 = js! {
|
||||
var h = @{&self.js_ctx};
|
||||
var samples = @{unsafe { UnsafeTypedArray::new( &self.audio_buffer ) }};
|
||||
var sample_rate = 44100;
|
||||
var sample_count = samples.length;
|
||||
var latency = 0.032;
|
||||
|
||||
var audio_buffer;
|
||||
if( h.empty_audio_buffers.length === 0 ) {
|
||||
audio_buffer = h.audio.createBuffer( 1, sample_count, sample_rate );
|
||||
} else {
|
||||
audio_buffer = h.empty_audio_buffers.pop();
|
||||
}
|
||||
|
||||
audio_buffer.getChannelData( 0 ).set( samples );
|
||||
|
||||
var node = h.audio.createBufferSource();
|
||||
node.connect( h.audio.destination );
|
||||
node.buffer = audio_buffer;
|
||||
node.onended = function() {
|
||||
h.empty_audio_buffers.push( audio_buffer );
|
||||
};
|
||||
|
||||
var buffered = h.play_timestamp - (h.audio.currentTime + latency);
|
||||
var play_timestamp = Math.max( h.audio.currentTime + latency, h.play_timestamp );
|
||||
node.start( play_timestamp );
|
||||
h.play_timestamp = play_timestamp + sample_count / sample_rate;
|
||||
|
||||
return buffered;
|
||||
}.try_into().unwrap();
|
||||
|
||||
// Since we're using `request_animation_frame` for synchronization
|
||||
// we **will** go out of sync with the audio sooner or later,
|
||||
// which will result in sudden, and very unpleasant, audio pops.
|
||||
//
|
||||
// So here we check how much audio exactly we have queued up,
|
||||
// and if we don't have enough then we'll try to compensate
|
||||
// by running the emulator for a few more cycles at the end
|
||||
// of the current frame.
|
||||
if audio_buffered < 0.000 {
|
||||
self.audio_underrun = Some( max( self.audio_underrun.unwrap_or( 0 ), 3 ) );
|
||||
} else if audio_buffered < 0.010 {
|
||||
self.audio_underrun = Some( max( self.audio_underrun.unwrap_or( 0 ), 2 ) );
|
||||
} else if audio_buffered < 0.020 {
|
||||
self.audio_underrun = Some( max( self.audio_underrun.unwrap_or( 0 ), 1 ) );
|
||||
}
|
||||
|
||||
self.audio_buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This creates a really basic WebGL context for blitting a single texture.
|
||||
// On some web browsers this is faster than using a 2d canvas.
|
||||
fn setup_webgl( canvas: &Element ) -> Value {
|
||||
const FRAGMENT_SHADER: &'static str = r#"
|
||||
precision mediump float;
|
||||
varying vec2 v_texcoord;
|
||||
uniform sampler2D u_sampler;
|
||||
void main() {
|
||||
gl_FragColor = vec4( texture2D( u_sampler, vec2( v_texcoord.s, v_texcoord.t ) ).rgb, 1.0 );
|
||||
}
|
||||
"#;
|
||||
|
||||
const VERTEX_SHADER: &'static str = r#"
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_texcoord;
|
||||
uniform mat4 u_matrix;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
gl_Position = u_matrix * vec4( a_position, 0.0, 1.0 );
|
||||
v_texcoord = a_texcoord;
|
||||
}
|
||||
"#;
|
||||
|
||||
fn ortho( left: f64, right: f64, bottom: f64, top: f64 ) -> Vec< f64 > {
|
||||
let mut m = vec![ 1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0 ];
|
||||
|
||||
m[ 0 * 4 + 0 ] = 2.0 / (right - left);
|
||||
m[ 1 * 4 + 1 ] = 2.0 / (top - bottom);
|
||||
m[ 3 * 4 + 0 ] = (right + left) / (right - left) * -1.0;
|
||||
m[ 3 * 4 + 1 ] = (top + bottom) / (top - bottom) * -1.0;
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
js!(
|
||||
var gl;
|
||||
var webgl_names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
|
||||
for( var i = 0; i < webgl_names.length; ++i ) {
|
||||
var name = webgl_names[ i ];
|
||||
try {
|
||||
gl = @{canvas}.getContext( name );
|
||||
} catch( err ) {}
|
||||
|
||||
if( gl ) {
|
||||
console.log( "WebGL support using context:", name );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var vertex_shader = gl.createShader( gl.VERTEX_SHADER );
|
||||
var fragment_shader = gl.createShader( gl.FRAGMENT_SHADER );
|
||||
gl.shaderSource( vertex_shader, @{VERTEX_SHADER} );
|
||||
gl.shaderSource( fragment_shader, @{FRAGMENT_SHADER} );
|
||||
gl.compileShader( vertex_shader );
|
||||
gl.compileShader( fragment_shader );
|
||||
|
||||
if( !gl.getShaderParameter( vertex_shader, gl.COMPILE_STATUS ) ) {
|
||||
console.error( "WebGL vertex shader compilation failed:", gl.getShaderInfoLog( vertex_shader ) );
|
||||
return null;
|
||||
}
|
||||
|
||||
if( !gl.getShaderParameter( fragment_shader, gl.COMPILE_STATUS ) ) {
|
||||
console.error( "WebGL fragment shader compilation failed:", gl.getShaderInfoLog( fragment_shader ) );
|
||||
return null;
|
||||
}
|
||||
|
||||
var program = gl.createProgram();
|
||||
gl.attachShader( program, vertex_shader );
|
||||
gl.attachShader( program, fragment_shader );
|
||||
gl.linkProgram( program );
|
||||
if( !gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
|
||||
console.error( "WebGL program linking failed!" );
|
||||
return null;
|
||||
}
|
||||
|
||||
gl.useProgram( program );
|
||||
|
||||
var vertex_attr = gl.getAttribLocation( program, "a_position" );
|
||||
var texcoord_attr = gl.getAttribLocation( program, "a_texcoord" );
|
||||
|
||||
gl.enableVertexAttribArray( vertex_attr );
|
||||
gl.enableVertexAttribArray( texcoord_attr );
|
||||
|
||||
var sampler_uniform = gl.getUniformLocation( program, "u_sampler" );
|
||||
gl.uniform1i( sampler_uniform, 0 );
|
||||
|
||||
var matrix = @{ortho( 0.0, 256.0, 240.0, 0.0 )};
|
||||
var matrix_uniform = gl.getUniformLocation( program, "u_matrix" );
|
||||
gl.uniformMatrix4fv( matrix_uniform, false, matrix );
|
||||
|
||||
var 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, new Uint8Array( 256 * 256 * 4 ) );
|
||||
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST );
|
||||
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
|
||||
|
||||
var vertex_buffer = gl.createBuffer();
|
||||
gl.bindBuffer( gl.ARRAY_BUFFER, vertex_buffer );
|
||||
var vertices = [
|
||||
0.0, 0.0,
|
||||
0.0, 240.0,
|
||||
256.0, 0.0,
|
||||
256.0, 240.0
|
||||
];
|
||||
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( vertices ), gl.STATIC_DRAW );
|
||||
gl.vertexAttribPointer( vertex_attr, 2, gl.FLOAT, false, 0, 0 );
|
||||
|
||||
var texcoord_buffer = gl.createBuffer();
|
||||
gl.bindBuffer( gl.ARRAY_BUFFER, texcoord_buffer );
|
||||
var texcoords = [
|
||||
0.0, 0.0,
|
||||
0.0, 240.0 / 256.0,
|
||||
1.0, 0.0,
|
||||
1.0, 240.0 / 256.0
|
||||
];
|
||||
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( texcoords ), gl.STATIC_DRAW );
|
||||
gl.vertexAttribPointer( texcoord_attr, 2, gl.FLOAT, false, 0, 0 );
|
||||
|
||||
var index_buffer = gl.createBuffer();
|
||||
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, index_buffer );
|
||||
var indices = [
|
||||
0, 1, 2,
|
||||
2, 3, 1
|
||||
];
|
||||
gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( indices ), gl.STATIC_DRAW );
|
||||
|
||||
gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
|
||||
gl.enable( gl.DEPTH_TEST );
|
||||
gl.viewport( 0, 0, 256, 240 );
|
||||
|
||||
return gl;
|
||||
)
|
||||
}
|
||||
|
||||
impl PinkyWeb {
|
||||
fn new( canvas: &Element ) -> Self {
|
||||
let gl = setup_webgl( &canvas );
|
||||
|
||||
let js_ctx = js!(
|
||||
var h = {};
|
||||
var canvas = @{canvas};
|
||||
|
||||
h.gl = @{gl};
|
||||
h.audio = new AudioContext();
|
||||
h.empty_audio_buffers = [];
|
||||
h.play_timestamp = 0;
|
||||
|
||||
if( !h.gl ) {
|
||||
console.log( "No WebGL; using Canvas API" );
|
||||
|
||||
// If the WebGL **is** supported but something else
|
||||
// went wrong the web browser won't let us create
|
||||
// a normal canvas context on a WebGL-ified canvas,
|
||||
// so we recreate a new canvas here to work around that.
|
||||
var new_canvas = canvas.cloneNode( true );
|
||||
canvas.parentNode.replaceChild( new_canvas, canvas );
|
||||
canvas = new_canvas;
|
||||
|
||||
h.ctx = canvas.getContext( "2d" );
|
||||
h.img = h.ctx.createImageData( 256, 240 );
|
||||
h.buffer = new Uint32Array( h.img.data.buffer );
|
||||
}
|
||||
|
||||
return h;
|
||||
);
|
||||
|
||||
PinkyWeb {
|
||||
state: nes::State::new(),
|
||||
palette: palette_to_abgr( &nes::Palette::default() ),
|
||||
framebuffer: [0; 256 * 240],
|
||||
audio_buffer: Vec::with_capacity( 44100 ),
|
||||
audio_chunk_counter: 0,
|
||||
audio_underrun: None,
|
||||
paused: true,
|
||||
busy: false,
|
||||
js_ctx
|
||||
}
|
||||
}
|
||||
|
||||
fn pause( &mut self ) {
|
||||
self.paused = true;
|
||||
}
|
||||
|
||||
fn unpause( &mut self ) {
|
||||
self.paused = false;
|
||||
self.busy = false;
|
||||
}
|
||||
|
||||
// This will run the emulator either until we've finished
|
||||
// a frame, or until we've generated one audio chunk,
|
||||
// in which case we'll temporairly give back the control
|
||||
// of the main thread back to the web browser so that
|
||||
// it can handle other events and process audio.
|
||||
fn run_a_bit( &mut self ) -> Result< bool, Box< Error > > {
|
||||
if self.paused {
|
||||
return Ok( true );
|
||||
}
|
||||
|
||||
let audio_chunk_counter = self.audio_chunk_counter;
|
||||
loop {
|
||||
let result = nes::Interface::execute_cycle( self );
|
||||
match result {
|
||||
Ok( processed_whole_frame ) => {
|
||||
if processed_whole_frame {
|
||||
return Ok( true );
|
||||
} else if self.audio_chunk_counter != audio_chunk_counter {
|
||||
return Ok( false );
|
||||
}
|
||||
},
|
||||
Err( error ) => {
|
||||
js!( console.error( "Execution error:", @{format!( "{}", error )} ); );
|
||||
self.pause();
|
||||
|
||||
return Err( error );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw( &mut self ) {
|
||||
let framebuffer = self.state.framebuffer();
|
||||
if !self.paused {
|
||||
for (pixel_in, pixel_out) in framebuffer.iter().zip( self.framebuffer.iter_mut() ) {
|
||||
*pixel_out = self.palette[ pixel_in.color_in_system_palette_index() as usize ];
|
||||
}
|
||||
}
|
||||
|
||||
js! {
|
||||
var h = @{&self.js_ctx};
|
||||
var framebuffer = @{unsafe { UnsafeTypedArray::new( &self.framebuffer ) }};
|
||||
if( h.gl ) {
|
||||
var data = new Uint8Array( framebuffer.buffer, framebuffer.byteOffset, framebuffer.byteLength );
|
||||
h.gl.texSubImage2D( h.gl.TEXTURE_2D, 0, 0, 0, 256, 240, h.gl.RGBA, h.gl.UNSIGNED_BYTE, data );
|
||||
h.gl.drawElements( h.gl.TRIANGLES, 6, h.gl.UNSIGNED_SHORT, 0 );
|
||||
} else {
|
||||
h.buffer.set( framebuffer );
|
||||
h.ctx.putImageData( h.img, 0, 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_key( &mut self, key: &str, location: KeyboardLocation, is_pressed: bool ) -> bool {
|
||||
let button = match (key, location) {
|
||||
("Enter", _) => nes::Button::Start,
|
||||
("Shift", KeyboardLocation::Right) => nes::Button::Select,
|
||||
("ArrowUp", _) => nes::Button::Up,
|
||||
("ArrowLeft", _) => nes::Button::Left,
|
||||
("ArrowRight", _) => nes::Button::Right,
|
||||
("ArrowDown", _) => nes::Button::Down,
|
||||
|
||||
// On Edge the arrows have different names
|
||||
// for some reason.
|
||||
("Up", _) => nes::Button::Up,
|
||||
("Left", _) => nes::Button::Left,
|
||||
("Right", _) => nes::Button::Right,
|
||||
("Down", _) => nes::Button::Down,
|
||||
|
||||
("z", _) => nes::Button::A,
|
||||
("x", _) => nes::Button::B,
|
||||
|
||||
// For those using the Dvorak layout.
|
||||
(";", _) => nes::Button::A,
|
||||
("q", _) => nes::Button::B,
|
||||
|
||||
// For those using the Dvorak layout **and** Microsoft Edge.
|
||||
//
|
||||
// On `keydown` we get ";" as we should, but on `keyup`
|
||||
// we get "Unidentified". Seriously Microsoft, how buggy can
|
||||
// your browser be?
|
||||
("Unidentified", _) if is_pressed == false => nes::Button::A,
|
||||
|
||||
_ => return false
|
||||
};
|
||||
|
||||
nes::Interface::set_button_state( self, nes::ControllerPort::First, button, is_pressed );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fn emulate_for_a_single_frame( pinky: Rc< RefCell< PinkyWeb > > ) {
|
||||
pinky.borrow_mut().busy = true;
|
||||
|
||||
web::set_timeout( enclose!( [pinky] move || {
|
||||
let finished_frame = match pinky.borrow_mut().run_a_bit() {
|
||||
Ok( result ) => result,
|
||||
Err( error ) => {
|
||||
handle_error( error );
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !finished_frame {
|
||||
web::set_timeout( move || { emulate_for_a_single_frame( pinky ); }, 0 );
|
||||
} else {
|
||||
let mut pinky = pinky.borrow_mut();
|
||||
if let Some( count ) = pinky.audio_underrun.take() {
|
||||
for _ in 0..count {
|
||||
if let Err( error ) = pinky.run_a_bit() {
|
||||
handle_error( error );
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pinky.busy = false;
|
||||
}
|
||||
}), 0 );
|
||||
}
|
||||
|
||||
fn main_loop( pinky: Rc< RefCell< PinkyWeb > > ) {
|
||||
// If we're running too slowly there is no point
|
||||
// in queueing up even more work.
|
||||
if !pinky.borrow_mut().busy {
|
||||
emulate_for_a_single_frame( pinky.clone() );
|
||||
}
|
||||
|
||||
pinky.borrow_mut().draw();
|
||||
web::window().request_animation_frame( move |_| {
|
||||
main_loop( pinky );
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RomEntry {
|
||||
name: String,
|
||||
file: String
|
||||
}
|
||||
|
||||
js_deserializable!( RomEntry );
|
||||
|
||||
fn show( id: &str ) {
|
||||
web::document().get_element_by_id( id ).unwrap().class_list().remove( "hidden" );
|
||||
}
|
||||
|
||||
fn hide( id: &str ) {
|
||||
web::document().get_element_by_id( id ).unwrap().class_list().add( "hidden" );
|
||||
}
|
||||
|
||||
fn fetch_builtin_rom_list< F: FnOnce( Vec< RomEntry > ) + 'static >( callback: F ) {
|
||||
let on_rom_list_loaded = Once( move |mut roms: Vec< RomEntry >| {
|
||||
roms.sort_by( |a, b| a.name.cmp( &b.name ) );
|
||||
callback( roms );
|
||||
});
|
||||
|
||||
js! {
|
||||
var req = new XMLHttpRequest();
|
||||
req.addEventListener( "load" , function() {
|
||||
var cb = @{on_rom_list_loaded};
|
||||
cb( JSON.parse( req.responseText ) );
|
||||
cb.drop();
|
||||
});
|
||||
req.open( "GET", "roms/index.json" );
|
||||
req.send();
|
||||
}
|
||||
}
|
||||
|
||||
fn support_builtin_roms( roms: Vec< RomEntry >, pinky: Rc< RefCell< PinkyWeb > > ) {
|
||||
let entries = web::document().get_element_by_id( "rom-list" ).unwrap();
|
||||
for rom in roms {
|
||||
let entry = web::document().create_element( "button" );
|
||||
let name = rom.name;
|
||||
let file = rom.file;
|
||||
|
||||
entry.set_text_content( &name );
|
||||
entries.append_child( &entry );
|
||||
entry.add_event_listener( enclose!( [pinky] move |_: ClickEvent| {
|
||||
hide( "change-rom-menu" );
|
||||
hide( "side-text" );
|
||||
show( "loading" );
|
||||
|
||||
let builtin_rom_loaded = Once( enclose!( [pinky] move |array_buffer: ArrayBuffer| {
|
||||
let rom_data: Vec< u8 > = array_buffer.into();
|
||||
load_rom( &pinky, &rom_data );
|
||||
}));
|
||||
js! {
|
||||
var req = new XMLHttpRequest();
|
||||
req.addEventListener( "load" , function() {
|
||||
@{builtin_rom_loaded}( req.response );
|
||||
});
|
||||
req.open( "GET", "roms/" + @{&file} );
|
||||
req.responseType = "arraybuffer";
|
||||
req.send();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn support_custom_roms( pinky: Rc< RefCell< PinkyWeb > > ) {
|
||||
let browse_for_roms_button = web::document().get_element_by_id( "browse-for-roms" ).unwrap();
|
||||
browse_for_roms_button.add_event_listener( move |event: ChangeEvent| {
|
||||
let input: InputElement = event.target().unwrap().try_into().unwrap();
|
||||
let files = input.files().unwrap();
|
||||
let file = match files.iter().next() {
|
||||
Some( file ) => file,
|
||||
None => return
|
||||
};
|
||||
|
||||
hide( "change-rom-menu" );
|
||||
hide( "side-text" );
|
||||
show( "loading" );
|
||||
|
||||
let reader = FileReader::new();
|
||||
reader.add_event_listener( enclose!( [pinky, reader] move |_: ProgressLoadEvent| {
|
||||
let rom_data: Vec< u8 > = match reader.result().unwrap() {
|
||||
FileReaderResult::ArrayBuffer( buffer ) => buffer,
|
||||
_ => unreachable!()
|
||||
}.into();
|
||||
|
||||
load_rom( &pinky, &rom_data );
|
||||
}));
|
||||
|
||||
reader.read_as_array_buffer( &file );
|
||||
});
|
||||
}
|
||||
|
||||
fn support_rom_changing( pinky: Rc< RefCell< PinkyWeb > > ) {
|
||||
let change_rom_button = web::document().get_element_by_id( "change-rom-button" ).unwrap();
|
||||
change_rom_button.add_event_listener( enclose!( [pinky] move |_: ClickEvent| {
|
||||
pinky.borrow_mut().pause();
|
||||
hide( "viewport" );
|
||||
hide( "change-rom-button" );
|
||||
show( "change-rom-menu" );
|
||||
show( "rom-menu-close" );
|
||||
}));
|
||||
|
||||
let rom_menu_close_button = web::document().get_element_by_id( "rom-menu-close" ).unwrap();
|
||||
rom_menu_close_button.add_event_listener( move |_: ClickEvent| {
|
||||
pinky.borrow_mut().unpause();
|
||||
show( "viewport" );
|
||||
show( "change-rom-button" );
|
||||
hide( "change-rom-menu" );
|
||||
hide( "rom-menu-close" );
|
||||
});
|
||||
}
|
||||
|
||||
fn support_input( pinky: Rc< RefCell< PinkyWeb > > ) {
|
||||
web::window().add_event_listener( enclose!( [pinky] move |event: KeydownEvent| {
|
||||
let handled = pinky.borrow_mut().on_key( &event.key(), event.location(), true );
|
||||
if handled {
|
||||
event.prevent_default();
|
||||
}
|
||||
}));
|
||||
|
||||
web::window().add_event_listener( enclose!( [pinky] move |event: KeyupEvent| {
|
||||
let handled = pinky.borrow_mut().on_key( &event.key(), event.location(), false );
|
||||
if handled {
|
||||
event.prevent_default();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn load_rom( pinky: &Rc< RefCell< PinkyWeb > >, rom_data: &[u8] ) {
|
||||
hide( "loading" );
|
||||
hide( "error" );
|
||||
|
||||
let mut pinky = pinky.borrow_mut();
|
||||
let pinky = pinky.deref_mut();
|
||||
if let Err( err ) = nes::Interface::load_rom_from_memory( pinky, rom_data ) {
|
||||
handle_error( err );
|
||||
return;
|
||||
}
|
||||
pinky.unpause();
|
||||
|
||||
show( "viewport" );
|
||||
show( "change-rom-button" );
|
||||
}
|
||||
|
||||
fn handle_error< E: Into< Box< Error > > >( error: E ) {
|
||||
let error_message = format!( "{}", error.into() );
|
||||
web::document().get_element_by_id( "error-description" ).unwrap().set_text_content( &error_message );
|
||||
|
||||
hide( "viewport" );
|
||||
hide( "change-rom-button" );
|
||||
hide( "rom-menu-close" );
|
||||
show( "change-rom-menu" );
|
||||
show( "error" );
|
||||
}
|
||||
|
||||
fn main() {
|
||||
stdweb::initialize();
|
||||
|
||||
let canvas = web::document().get_element_by_id( "viewport" ).unwrap();
|
||||
let pinky = Rc::new( RefCell::new( PinkyWeb::new( &canvas ) ) );
|
||||
|
||||
support_custom_roms( pinky.clone() );
|
||||
support_rom_changing( pinky.clone() );
|
||||
|
||||
fetch_builtin_rom_list( enclose!( [pinky] |roms| {
|
||||
support_builtin_roms( roms, pinky );
|
||||
|
||||
hide( "loading" );
|
||||
show( "change-rom-menu" );
|
||||
}));
|
||||
|
||||
support_input( pinky.clone() );
|
||||
|
||||
web::window().request_animation_frame( move |_| {
|
||||
main_loop( pinky );
|
||||
});
|
||||
|
||||
stdweb::event_loop();
|
||||
}
|
447
pinky-web/static/css/normalize.css
vendored
Normal file
447
pinky-web/static/css/normalize.css
vendored
Normal file
|
@ -0,0 +1,447 @@
|
|||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in
|
||||
* IE on Windows Phone and in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers (opinionated).
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
footer,
|
||||
header,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in IE.
|
||||
*/
|
||||
|
||||
figcaption,
|
||||
figure,
|
||||
main { /* 1 */
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct margin in IE 8.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Remove the gray background on active links in IE 10.
|
||||
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent; /* 1 */
|
||||
-webkit-text-decoration-skip: objects; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font style in Android 4.3-.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct background and color in IE 9-.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
audio,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in iOS 4-7.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10-.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the overflow in IE.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers (opinionated).
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: sans-serif; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||
* controls in Android 4.
|
||||
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
html [type="button"], /* 1 */
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct display in IE 9-.
|
||||
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10-.
|
||||
* 2. Remove the padding in IE 10-.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in IE 9-.
|
||||
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||
*/
|
||||
|
||||
details, /* 1 */
|
||||
menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Scripting
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 9-.
|
||||
*/
|
||||
|
||||
canvas {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hidden
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10-.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
279
pinky-web/static/index.html
Normal file
279
pinky-web/static/index.html
Normal file
|
@ -0,0 +1,279 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1' name='viewport' />
|
||||
<link rel="stylesheet" type="text/css" href="css/normalize.css" media="screen" />
|
||||
<style media="screen" type="text/css">
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: black;
|
||||
color: #999;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
#viewport {
|
||||
text-align: center;
|
||||
display: box;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
#change-rom-menu {
|
||||
text-align: center;
|
||||
background-color: #111;
|
||||
max-width: 25em;
|
||||
min-width: 20em;
|
||||
margin: auto;
|
||||
padding-bottom: 1.5em;
|
||||
|
||||
display: flex;
|
||||
flex-flow:column;
|
||||
justify-content: flex-start;
|
||||
|
||||
max-height: 100%;
|
||||
}
|
||||
#change-rom-menu-header {
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
font-size: 19pt;
|
||||
font-style: italic;
|
||||
color: #bbb;
|
||||
background-color: black;
|
||||
margin-top: 0;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
#browse-for-roms-button {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
#browse-for-roms-button, #rom-list {
|
||||
margin-left: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
#change-rom-menu * {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#rom-list {
|
||||
flex-grow: 0;
|
||||
flex-basis: 1;
|
||||
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-flow:column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#browse-for-roms {
|
||||
position: fixed;
|
||||
top: -1000px;
|
||||
}
|
||||
|
||||
button, #browse-for-roms-button {
|
||||
font-size: 11pt;
|
||||
color: #bbb;
|
||||
padding: 8px 16px;
|
||||
background: #000;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover, #browse-for-roms-button:hover {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
button, #browse-for-roms-button, #rom-list, #change-rom-menu {
|
||||
border-radius: 4px;
|
||||
border-bottom: 1px solid #222;
|
||||
border-top: 1px solid #222;
|
||||
border-left: 1px solid #101010;
|
||||
border-right: 1px solid #101010;
|
||||
box-shadow: 0px 1px 3px #060606;
|
||||
}
|
||||
|
||||
#rom-list button {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#rom-menu-close {
|
||||
float: right;
|
||||
margin-left: -1.8em;
|
||||
padding-left: 0.8em;
|
||||
padding-right: 0.8em;
|
||||
}
|
||||
|
||||
#rom-menu-close:hover {
|
||||
color: #fff;
|
||||
transition: color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 16pt;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
#horizontal-container {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#horizontal-container #side-text {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
#horizontal-container #change-rom-menu {
|
||||
flex-basis: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#side-text {
|
||||
max-width: 30em;
|
||||
min-width: 20em;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
#loading {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#error-description {
|
||||
font-style: italic;
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
||||
<title>Pinky - an NES emulator</title>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="viewport" width="256" height="240" class="hidden"></canvas>
|
||||
<div id="menu">
|
||||
<button id="change-rom-button" class="hidden">Change ROM</button>
|
||||
</div>
|
||||
<div id="horizontal-container">
|
||||
<div id="side-text">
|
||||
<h1>Pinky - an NES emulator written in Rust</h1>
|
||||
<p>
|
||||
Hi there!
|
||||
</p>
|
||||
<p>
|
||||
This is an NES emulator written in <a href="https://www.rust-lang.org/">Rust</a> completely
|
||||
from scratch purely based on publicly available documentation.
|
||||
</p>
|
||||
<p>
|
||||
What you see here is its port compiled to <a href="https://en.wikipedia.org/wiki/WebAssembly">WebAssembly</a>
|
||||
using the magic of <a href="https://github.com/koute/stdweb">the stdweb crate</a> and Rust's native WebAssembly
|
||||
backend, without the use of Emscripten.
|
||||
</p>
|
||||
<!--
|
||||
<p>
|
||||
What you see here is its port compiled to <a href="https://en.wikipedia.org/wiki/WebAssembly">WebAssembly</a>
|
||||
using the magic of <a href="https://github.com/koute/stdweb">the stdweb crate</a> and <a href="http://kripken.github.io/emscripten-site">Emscripten</a>.
|
||||
</p>
|
||||
<p>
|
||||
What you see here is its port compiled to <a href="https://en.wikipedia.org/wiki/Asm.js">asm.js</a>
|
||||
using the magic of <a href="https://github.com/koute/stdweb">the stdweb crate</a> and <a href="http://kripken.github.io/emscripten-site">Emscripten</a>.
|
||||
</p>
|
||||
-->
|
||||
<p>
|
||||
You can take a peek at the <a href="https://github.com/koute/pinky/tree/master/pinky-web">code</a> if you're
|
||||
interested, but first I invite you to take it for a spin and play a few games!
|
||||
</p>
|
||||
<p>
|
||||
You can chose between a few fun freeware NES games, but if you have the ROMs then games like
|
||||
Super Mario Brothers, Contra, Legend of Zelda, or Final Fantasy will work too!
|
||||
</p>
|
||||
<p>
|
||||
Controls are <span class="highlight">Z</span> and <span class="highlight">X</span>, and <span class="highlight">the arrow keys</span>!
|
||||
</p>
|
||||
<!--
|
||||
<p>
|
||||
You might also want to check out the version compiled *with* Emscripten <a href="https://koute.github.io/pinky-web-webasm-emscripten">to WebAssembly</a> or <a href="https://koute.github.io/pinky-web-asmjs-emscripten">to asm.js</a>.
|
||||
</p>
|
||||
<p>
|
||||
You might also want to check out the version compiled to WebAssembly *without* Emscripten <a href="https://koute.github.io/pinky-web">here</a>.
|
||||
</p>
|
||||
-->
|
||||
</div>
|
||||
<div id="loading">Loading...</div>
|
||||
<div id="unsupported" class="hidden">
|
||||
<h1>Sorry, your browser is unsupported!</h1>
|
||||
<p>Maybe try something newer which supports WebAssembly?</p>
|
||||
</div>
|
||||
<div id="error" class="hidden">
|
||||
<h1>Encountered an error!</h1>
|
||||
<p>The error message is as follows:</p>
|
||||
<p id="error-description"></p>
|
||||
<p>Sorry about that! Maybe try another ROM?</p>
|
||||
</div>
|
||||
<div id="change-rom-menu" class="hidden">
|
||||
<div id="change-rom-menu-header"><div id="rom-menu-close" class="hidden">×</div>Select a ROM to load</div>
|
||||
<label id="browse-for-roms-button">
|
||||
<input id="browse-for-roms" type="file" required />
|
||||
<span>Load your own ROM...</span>
|
||||
</label>
|
||||
<div style="margin-bottom: 0.5em;">Builtin ROMs</div>
|
||||
<div id="rom-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
if( typeof Module !== "object" ) { // If not running under Emscripten.
|
||||
var webassembly_supported = typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
|
||||
if( !webassembly_supported ) {
|
||||
document.getElementById( "unsupported" ).className = "";
|
||||
document.getElementById( "loading" ).className = "hidden";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
pinky-web/static/roms/alter_ego.nes
Normal file
BIN
pinky-web/static/roms/alter_ego.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/cheril_the_goddess.nes
Normal file
BIN
pinky-web/static/roms/cheril_the_goddess.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/dushlan.nes
Normal file
BIN
pinky-web/static/roms/dushlan.nes
Normal file
Binary file not shown.
50
pinky-web/static/roms/index.json
Normal file
50
pinky-web/static/roms/index.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
[
|
||||
{
|
||||
"name": "Alter Ego",
|
||||
"file": "alter_ego.nes"
|
||||
},
|
||||
{
|
||||
"name": "Cheril the Goddess",
|
||||
"file": "cheril_the_goddess.nes"
|
||||
},
|
||||
{
|
||||
"name": "Dushlan",
|
||||
"file": "dushlan.nes"
|
||||
},
|
||||
{
|
||||
"name": "Lawn Mower",
|
||||
"file": "lawn_mower.nes"
|
||||
},
|
||||
{
|
||||
"name": "Mad Wizard",
|
||||
"file": "mad_wizard.nes"
|
||||
},
|
||||
{
|
||||
"name": "Micro Knight 4",
|
||||
"file": "micro_knight_4_v1_02.nes"
|
||||
},
|
||||
{
|
||||
"name": "Nebs 'n Debs",
|
||||
"file": "nebs_n_debs_2_15_2017.nes"
|
||||
},
|
||||
{
|
||||
"name": "Owlia",
|
||||
"file": "owlia.nes"
|
||||
},
|
||||
{
|
||||
"name": "Streemerz",
|
||||
"file": "streemerz_v02.nes"
|
||||
},
|
||||
{
|
||||
"name": "Super Painter",
|
||||
"file": "super_painter_1_0.nes"
|
||||
},
|
||||
{
|
||||
"name": "Tiger Jenny",
|
||||
"file": "tiger_jenny.nes"
|
||||
},
|
||||
{
|
||||
"name": "Yun",
|
||||
"file": "yun.nes"
|
||||
}
|
||||
]
|
BIN
pinky-web/static/roms/lawn_mower.nes
Normal file
BIN
pinky-web/static/roms/lawn_mower.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/mad_wizard.nes
Normal file
BIN
pinky-web/static/roms/mad_wizard.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/micro_knight_4_v1_02.nes
Normal file
BIN
pinky-web/static/roms/micro_knight_4_v1_02.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/nebs_n_debs_2_15_2017.nes
Normal file
BIN
pinky-web/static/roms/nebs_n_debs_2_15_2017.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/owlia.nes
Normal file
BIN
pinky-web/static/roms/owlia.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/streemerz_v02.nes
Normal file
BIN
pinky-web/static/roms/streemerz_v02.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/super_painter_1_0.nes
Normal file
BIN
pinky-web/static/roms/super_painter_1_0.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/tiger_jenny.nes
Normal file
BIN
pinky-web/static/roms/tiger_jenny.nes
Normal file
Binary file not shown.
BIN
pinky-web/static/roms/yun.nes
Normal file
BIN
pinky-web/static/roms/yun.nes
Normal file
Binary file not shown.
Loading…
Reference in a new issue