diff --git a/Cargo.toml b/Cargo.toml
index 444a27d7bd0e9ee54b62e8a112bb55f5e84701d7..46eec6a6e6cc72110995b8aa5ea0fc78dbe96d9a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,8 +10,12 @@ edition = "2018"
 [lib]
 crate-type = ["cdylib", "rlib"]
 
+[features]
+wasm = ["wasm-bindgen"]
+
 [dependencies]
 getrandom = { version = "0.2", features = ["js"] }
+wasm-bindgen = { version = "0.2", optional = true }
 
 [profile.release]
 debug = false
diff --git a/README.md b/README.md
index f9e947d70ac1ce7e84db101c36e6ca6b8740d95f..fb0e7965bcb562226953fd79b9123c9d676445a0 100644
--- a/README.md
+++ b/README.md
@@ -23,11 +23,29 @@ The work of this emulator was inspired/started by [jc-chip8](https://github.com/
 * Full compliant with test CHIP-8 ROMs
 * RAM snapshot saving and loading
 
+## Build
+
+### WASM for Node.js
+
+```bash
+cargo install wasm-pack
+wasm-pack build --release --target=nodejs -- --features wasm
+```
+
+### WASM for Web
+
+```bash
+cargo install wasm-pack
+wasm-pack build --release --target=web --out-dir=examples/web -- --features wasm
+cd examples/web
+python3 -m http.server
+```
+
 ## Reason
 
 And... yes this is the real inspiration behind the emulator's name:
 
-<img src="resources/chips-ahoy.jpeg" alt="Chips Ahoy" width="200" />
+<img src="res/chips-ahoy.jpeg" alt="Chips Ahoy" width="200" />
 
 ## Inspiration
 
diff --git a/examples/benchmark/resources/pong.ch8 b/examples/benchmark/res/pong.ch8
similarity index 100%
rename from examples/benchmark/resources/pong.ch8
rename to examples/benchmark/res/pong.ch8
diff --git a/examples/benchmark/src/main.rs b/examples/benchmark/src/main.rs
index 5dfefe1a37c319b7fbdfe27610f630b3d0ab5283..cb5c7d1274d6cb45b505f807905d72981e697853 100644
--- a/examples/benchmark/src/main.rs
+++ b/examples/benchmark/src/main.rs
@@ -8,7 +8,7 @@ const CYCLE_COUNT: u64 = 5_000_000_000;
 fn main() {
     let chips: [Box<dyn Chip8>; 2] = [Box::new(Chip8Classic::new()), Box::new(Chip8Neo::new())];
 
-    let rom_path = "./resources/pong.ch8";
+    let rom_path = "./res/pong.ch8";
     let rom = read_file(rom_path);
 
     for mut chip8 in chips {
diff --git a/examples/sdl/resources/OpenSans-Bold.ttf b/examples/sdl/res/OpenSans-Bold.ttf
similarity index 100%
rename from examples/sdl/resources/OpenSans-Bold.ttf
rename to examples/sdl/res/OpenSans-Bold.ttf
diff --git a/examples/sdl/resources/Roboto-Bold.ttf b/examples/sdl/res/Roboto-Bold.ttf
similarity index 100%
rename from examples/sdl/resources/Roboto-Bold.ttf
rename to examples/sdl/res/Roboto-Bold.ttf
diff --git a/examples/sdl/resources/RobotoMono-Bold.ttf b/examples/sdl/res/RobotoMono-Bold.ttf
similarity index 100%
rename from examples/sdl/resources/RobotoMono-Bold.ttf
rename to examples/sdl/res/RobotoMono-Bold.ttf
diff --git a/examples/sdl/resources/VT323-Regular.ttf b/examples/sdl/res/VT323-Regular.ttf
similarity index 100%
rename from examples/sdl/resources/VT323-Regular.ttf
rename to examples/sdl/res/VT323-Regular.ttf
diff --git a/examples/sdl/resources/icon.png b/examples/sdl/res/icon.png
similarity index 100%
rename from examples/sdl/resources/icon.png
rename to examples/sdl/res/icon.png
diff --git a/examples/sdl/src/main.rs b/examples/sdl/src/main.rs
index fee2d737401c60acce335b379f0c85347576e504..88257adf63a6be9cdd869b49bbfc6df3a1e2820f 100644
--- a/examples/sdl/src/main.rs
+++ b/examples/sdl/src/main.rs
@@ -160,7 +160,7 @@ fn main() {
     // loads the font that is going to be used in the drawing
     // process cycle if necessary
     let mut font = ttf_context
-        .load_font(format!("./resources/{}", FONT_NAME), FONT_SIZE)
+        .load_font(format!("./res/{}", FONT_NAME), FONT_SIZE)
         .unwrap();
     font.set_style(sdl2::ttf::FontStyle::BOLD);
     font.set_hinting(Hinting::Light);
@@ -181,7 +181,7 @@ fn main() {
 
     // updates the icon of the window to reflect the image
     // and style of the emulator
-    let surface = Surface::from_file("./resources/icon.png").unwrap();
+    let surface = Surface::from_file("./res/icon.png").unwrap();
     window.set_icon(&surface);
 
     let mut canvas = window.into_canvas().accelerated().build().unwrap();
diff --git a/examples/web/chip_ahoyto.d.ts b/examples/web/chip_ahoyto.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77a84e7b09140f1e0bc08057123af9e331d6602c
--- /dev/null
+++ b/examples/web/chip_ahoyto.d.ts
@@ -0,0 +1,91 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+*/
+export class Chip8Classic {
+  free(): void;
+/**
+*/
+  constructor();
+}
+/**
+*/
+export class Chip8Neo {
+  free(): void;
+/**
+*/
+  constructor();
+/**
+* @param {Uint8Array} rom
+*/
+  load_rom_ws(rom: Uint8Array): void;
+/**
+*/
+  reset_ws(): void;
+/**
+*/
+  reset_hard_ws(): void;
+/**
+* @returns {Uint8Array}
+*/
+  vram_ws(): Uint8Array;
+/**
+*/
+  clock_ws(): void;
+/**
+*/
+  clock_dt_ws(): void;
+/**
+*/
+  clock_st_ws(): void;
+/**
+* @param {number} key
+*/
+  key_press_ws(key: number): void;
+/**
+* @param {number} key
+*/
+  key_lift_ws(key: number): void;
+}
+
+export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
+
+export interface InitOutput {
+  readonly memory: WebAssembly.Memory;
+  readonly __wbg_chip8neo_free: (a: number) => void;
+  readonly chip8neo_new: () => number;
+  readonly chip8neo_load_rom_ws: (a: number, b: number, c: number) => void;
+  readonly chip8neo_reset_ws: (a: number) => void;
+  readonly chip8neo_reset_hard_ws: (a: number) => void;
+  readonly chip8neo_vram_ws: (a: number, b: number) => void;
+  readonly chip8neo_clock_ws: (a: number) => void;
+  readonly chip8neo_clock_dt_ws: (a: number) => void;
+  readonly chip8neo_clock_st_ws: (a: number) => void;
+  readonly chip8neo_key_press_ws: (a: number, b: number) => void;
+  readonly chip8neo_key_lift_ws: (a: number, b: number) => void;
+  readonly __wbg_chip8classic_free: (a: number) => void;
+  readonly chip8classic_new: () => number;
+  readonly __wbindgen_malloc: (a: number) => number;
+  readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
+  readonly __wbindgen_free: (a: number, b: number) => void;
+  readonly __wbindgen_exn_store: (a: number) => void;
+}
+
+/**
+* Synchronously compiles the given `bytes` and instantiates the WebAssembly module.
+*
+* @param {BufferSource} bytes
+*
+* @returns {InitOutput}
+*/
+export function initSync(bytes: BufferSource): InitOutput;
+
+/**
+* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
+* for everything else, calls `WebAssembly.instantiate` directly.
+*
+* @param {InitInput | Promise<InitInput>} module_or_path
+*
+* @returns {Promise<InitOutput>}
+*/
+export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
diff --git a/examples/web/chip_ahoyto.js b/examples/web/chip_ahoyto.js
new file mode 100644
index 0000000000000000000000000000000000000000..c83bdce964192eb55dedb6c28076f9889d7a43fa
--- /dev/null
+++ b/examples/web/chip_ahoyto.js
@@ -0,0 +1,386 @@
+
+let wasm;
+
+const heap = new Array(32).fill(undefined);
+
+heap.push(undefined, null, true, false);
+
+function getObject(idx) { return heap[idx]; }
+
+let heap_next = heap.length;
+
+function dropObject(idx) {
+    if (idx < 36) return;
+    heap[idx] = heap_next;
+    heap_next = idx;
+}
+
+function takeObject(idx) {
+    const ret = getObject(idx);
+    dropObject(idx);
+    return ret;
+}
+
+function addHeapObject(obj) {
+    if (heap_next === heap.length) heap.push(heap.length + 1);
+    const idx = heap_next;
+    heap_next = heap[idx];
+
+    heap[idx] = obj;
+    return idx;
+}
+
+const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+let cachedUint8Memory0;
+function getUint8Memory0() {
+    if (cachedUint8Memory0.byteLength === 0) {
+        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+    }
+    return cachedUint8Memory0;
+}
+
+function getStringFromWasm0(ptr, len) {
+    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
+}
+
+let WASM_VECTOR_LEN = 0;
+
+function passArray8ToWasm0(arg, malloc) {
+    const ptr = malloc(arg.length * 1);
+    getUint8Memory0().set(arg, ptr / 1);
+    WASM_VECTOR_LEN = arg.length;
+    return ptr;
+}
+
+let cachedInt32Memory0;
+function getInt32Memory0() {
+    if (cachedInt32Memory0.byteLength === 0) {
+        cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+    }
+    return cachedInt32Memory0;
+}
+
+function getArrayU8FromWasm0(ptr, len) {
+    return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
+}
+
+function handleError(f, args) {
+    try {
+        return f.apply(this, args);
+    } catch (e) {
+        wasm.__wbindgen_exn_store(addHeapObject(e));
+    }
+}
+/**
+*/
+export class Chip8Classic {
+
+    static __wrap(ptr) {
+        const obj = Object.create(Chip8Classic.prototype);
+        obj.ptr = ptr;
+
+        return obj;
+    }
+
+    __destroy_into_raw() {
+        const ptr = this.ptr;
+        this.ptr = 0;
+
+        return ptr;
+    }
+
+    free() {
+        const ptr = this.__destroy_into_raw();
+        wasm.__wbg_chip8classic_free(ptr);
+    }
+    /**
+    */
+    constructor() {
+        const ret = wasm.chip8classic_new();
+        return Chip8Classic.__wrap(ret);
+    }
+}
+/**
+*/
+export class Chip8Neo {
+
+    static __wrap(ptr) {
+        const obj = Object.create(Chip8Neo.prototype);
+        obj.ptr = ptr;
+
+        return obj;
+    }
+
+    __destroy_into_raw() {
+        const ptr = this.ptr;
+        this.ptr = 0;
+
+        return ptr;
+    }
+
+    free() {
+        const ptr = this.__destroy_into_raw();
+        wasm.__wbg_chip8neo_free(ptr);
+    }
+    /**
+    */
+    constructor() {
+        const ret = wasm.chip8neo_new();
+        return Chip8Neo.__wrap(ret);
+    }
+    /**
+    * @param {Uint8Array} rom
+    */
+    load_rom_ws(rom) {
+        const ptr0 = passArray8ToWasm0(rom, wasm.__wbindgen_malloc);
+        const len0 = WASM_VECTOR_LEN;
+        wasm.chip8neo_load_rom_ws(this.ptr, ptr0, len0);
+    }
+    /**
+    */
+    reset_ws() {
+        wasm.chip8neo_reset_ws(this.ptr);
+    }
+    /**
+    */
+    reset_hard_ws() {
+        wasm.chip8neo_reset_hard_ws(this.ptr);
+    }
+    /**
+    * @returns {Uint8Array}
+    */
+    vram_ws() {
+        try {
+            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+            wasm.chip8neo_vram_ws(retptr, this.ptr);
+            var r0 = getInt32Memory0()[retptr / 4 + 0];
+            var r1 = getInt32Memory0()[retptr / 4 + 1];
+            var v0 = getArrayU8FromWasm0(r0, r1).slice();
+            wasm.__wbindgen_free(r0, r1 * 1);
+            return v0;
+        } finally {
+            wasm.__wbindgen_add_to_stack_pointer(16);
+        }
+    }
+    /**
+    */
+    clock_ws() {
+        wasm.chip8neo_clock_ws(this.ptr);
+    }
+    /**
+    */
+    clock_dt_ws() {
+        wasm.chip8neo_clock_dt_ws(this.ptr);
+    }
+    /**
+    */
+    clock_st_ws() {
+        wasm.chip8neo_clock_st_ws(this.ptr);
+    }
+    /**
+    * @param {number} key
+    */
+    key_press_ws(key) {
+        wasm.chip8neo_key_press_ws(this.ptr, key);
+    }
+    /**
+    * @param {number} key
+    */
+    key_lift_ws(key) {
+        wasm.chip8neo_key_lift_ws(this.ptr, key);
+    }
+}
+
+async function load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+function getImports() {
+    const imports = {};
+    imports.wbg = {};
+    imports.wbg.__wbg_randomFillSync_91e2b39becca6147 = function() { return handleError(function (arg0, arg1, arg2) {
+        getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2));
+    }, arguments) };
+    imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
+        takeObject(arg0);
+    };
+    imports.wbg.__wbg_getRandomValues_b14734aa289bc356 = function() { return handleError(function (arg0, arg1) {
+        getObject(arg0).getRandomValues(getObject(arg1));
+    }, arguments) };
+    imports.wbg.__wbg_process_e56fd54cf6319b6c = function(arg0) {
+        const ret = getObject(arg0).process;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbindgen_is_object = function(arg0) {
+        const val = getObject(arg0);
+        const ret = typeof(val) === 'object' && val !== null;
+        return ret;
+    };
+    imports.wbg.__wbg_versions_77e21455908dad33 = function(arg0) {
+        const ret = getObject(arg0).versions;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_node_0dd25d832e4785d5 = function(arg0) {
+        const ret = getObject(arg0).node;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbindgen_is_string = function(arg0) {
+        const ret = typeof(getObject(arg0)) === 'string';
+        return ret;
+    };
+    imports.wbg.__wbg_static_accessor_NODE_MODULE_26b231378c1be7dd = function() {
+        const ret = module;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_require_0db1598d9ccecb30 = function() { return handleError(function (arg0, arg1, arg2) {
+        const ret = getObject(arg0).require(getStringFromWasm0(arg1, arg2));
+        return addHeapObject(ret);
+    }, arguments) };
+    imports.wbg.__wbg_crypto_b95d7173266618a9 = function(arg0) {
+        const ret = getObject(arg0).crypto;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_msCrypto_5a86d77a66230f81 = function(arg0) {
+        const ret = getObject(arg0).msCrypto;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_newnoargs_fc5356289219b93b = function(arg0, arg1) {
+        const ret = new Function(getStringFromWasm0(arg0, arg1));
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_call_4573f605ca4b5f10 = function() { return handleError(function (arg0, arg1) {
+        const ret = getObject(arg0).call(getObject(arg1));
+        return addHeapObject(ret);
+    }, arguments) };
+    imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
+        const ret = getObject(arg0);
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_self_ba1ddafe9ea7a3a2 = function() { return handleError(function () {
+        const ret = self.self;
+        return addHeapObject(ret);
+    }, arguments) };
+    imports.wbg.__wbg_window_be3cc430364fd32c = function() { return handleError(function () {
+        const ret = window.window;
+        return addHeapObject(ret);
+    }, arguments) };
+    imports.wbg.__wbg_globalThis_56d9c9f814daeeee = function() { return handleError(function () {
+        const ret = globalThis.globalThis;
+        return addHeapObject(ret);
+    }, arguments) };
+    imports.wbg.__wbg_global_8c35aeee4ac77f2b = function() { return handleError(function () {
+        const ret = global.global;
+        return addHeapObject(ret);
+    }, arguments) };
+    imports.wbg.__wbindgen_is_undefined = function(arg0) {
+        const ret = getObject(arg0) === undefined;
+        return ret;
+    };
+    imports.wbg.__wbg_buffer_de1150f91b23aa89 = function(arg0) {
+        const ret = getObject(arg0).buffer;
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_new_97cf52648830a70d = function(arg0) {
+        const ret = new Uint8Array(getObject(arg0));
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_set_a0172b213e2469e9 = function(arg0, arg1, arg2) {
+        getObject(arg0).set(getObject(arg1), arg2 >>> 0);
+    };
+    imports.wbg.__wbg_length_e09c0b925ab8de5d = function(arg0) {
+        const ret = getObject(arg0).length;
+        return ret;
+    };
+    imports.wbg.__wbg_newwithlength_e833b89f9db02732 = function(arg0) {
+        const ret = new Uint8Array(arg0 >>> 0);
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbg_subarray_9482ae5cd5cd99d3 = function(arg0, arg1, arg2) {
+        const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0);
+        return addHeapObject(ret);
+    };
+    imports.wbg.__wbindgen_throw = function(arg0, arg1) {
+        throw new Error(getStringFromWasm0(arg0, arg1));
+    };
+    imports.wbg.__wbindgen_memory = function() {
+        const ret = wasm.memory;
+        return addHeapObject(ret);
+    };
+
+    return imports;
+}
+
+function initMemory(imports, maybe_memory) {
+
+}
+
+function finalizeInit(instance, module) {
+    wasm = instance.exports;
+    init.__wbindgen_wasm_module = module;
+    cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
+    cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
+
+
+    return wasm;
+}
+
+function initSync(bytes) {
+    const imports = getImports();
+
+    initMemory(imports);
+
+    const module = new WebAssembly.Module(bytes);
+    const instance = new WebAssembly.Instance(module, imports);
+
+    return finalizeInit(instance, module);
+}
+
+async function init(input) {
+    if (typeof input === 'undefined') {
+        input = new URL('chip_ahoyto_bg.wasm', import.meta.url);
+    }
+    const imports = getImports();
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+    initMemory(imports);
+
+    const { instance, module } = await load(await input, imports);
+
+    return finalizeInit(instance, module);
+}
+
+export { initSync }
+export default init;
diff --git a/examples/web/chip_ahoyto_bg.wasm b/examples/web/chip_ahoyto_bg.wasm
new file mode 100644
index 0000000000000000000000000000000000000000..8ebcfa0bd0912d5f934cf9ea18f88daeb2dd8275
Binary files /dev/null and b/examples/web/chip_ahoyto_bg.wasm differ
diff --git a/examples/web/chip_ahoyto_bg.wasm.d.ts b/examples/web/chip_ahoyto_bg.wasm.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d5813e8d652675613b57214d6de048b5aa3d2b01
--- /dev/null
+++ b/examples/web/chip_ahoyto_bg.wasm.d.ts
@@ -0,0 +1,20 @@
+/* tslint:disable */
+/* eslint-disable */
+export const memory: WebAssembly.Memory;
+export function __wbg_chip8neo_free(a: number): void;
+export function chip8neo_new(): number;
+export function chip8neo_load_rom_ws(a: number, b: number, c: number): void;
+export function chip8neo_reset_ws(a: number): void;
+export function chip8neo_reset_hard_ws(a: number): void;
+export function chip8neo_vram_ws(a: number, b: number): void;
+export function chip8neo_clock_ws(a: number): void;
+export function chip8neo_clock_dt_ws(a: number): void;
+export function chip8neo_clock_st_ws(a: number): void;
+export function chip8neo_key_press_ws(a: number, b: number): void;
+export function chip8neo_key_lift_ws(a: number, b: number): void;
+export function __wbg_chip8classic_free(a: number): void;
+export function chip8classic_new(): number;
+export function __wbindgen_malloc(a: number): number;
+export function __wbindgen_add_to_stack_pointer(a: number): number;
+export function __wbindgen_free(a: number, b: number): void;
+export function __wbindgen_exn_store(a: number): void;
diff --git a/examples/web/index.html b/examples/web/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..6def33c85844e6a68f7dbfbebf19e9de1da75642
--- /dev/null
+++ b/examples/web/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>CHIP-Ahoyto</title>
+        <link rel="icon" href="res/icon.png" />
+        <link rel="stylesheet" href="index.css" />
+    </head>
+    <body>
+        <canvas id="chip-canvas" width="960" height="480"></canvas>
+        <div id="overlay" class="overlay">
+            Drag to load ROM
+        </div>
+        <script type="module" src="index.js"></script>
+    </body>
+</html>
diff --git a/examples/web/index.js b/examples/web/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..26b77b7efb66220d42c5a245560052c88e4c2a4a
--- /dev/null
+++ b/examples/web/index.js
@@ -0,0 +1,187 @@
+import {
+    default as wasm,
+    Chip8Neo
+} from "./chip_ahoyto.js";
+
+const PIXEL_SET_COLOR = 0x50cb93ff;
+const PIXEL_UNSET_COLOR = 0x1b1a17ff;
+
+let LOGIC_HZ = 480;
+const TIMER_HZ = 60;
+const VISUAL_HZ = 60;
+
+const KEYS = {
+    "1": 0x01,
+    "2": 0x02,
+    "3": 0x03,
+    "4": 0x0c,
+    "q": 0x04,
+    "w": 0x05,
+    "e": 0x06,
+    "r": 0x0d,
+    "a": 0x07,
+    "s": 0x08,
+    "d": 0x09,
+    "f": 0x0e,
+    "z": 0x0a,
+    "x": 0x00,
+    "c": 0x0b,
+    "v": 0x0f
+}
+
+const ROM = "res/roms/pong.ch8";
+
+const state = {
+    chip8: null,
+    canvas: null,
+    canvasScaled: null,
+    canvasCtx: null,
+    canvasScaledCtx: null,
+    image: null,
+    videoBuff: null
+};
+
+(async () => {
+    // initializes the WASM module, this is required
+    // so that the global symbols become available
+    await wasm();
+
+    // initializes the complete set of sub-systems
+    // and registers the event handlers
+    init();
+    register();
+
+    // loads the ROM data and converts it into the
+    // target u8 array bufffer
+    const response = await fetch(ROM);
+    const blob = await response.blob();
+    const arrayBuffer = await blob.arrayBuffer();
+    const data = new Uint8Array(arrayBuffer);
+
+    // creates the CHIP-8 instance and resets it
+    state.chip8 = new Chip8Neo();
+    state.chip8.reset_hard_ws();
+    state.chip8.load_rom_ws(data);
+
+    // runs the sequence as an infinite loop, running
+    // the associated CPU cycles accordingly
+    while (true) {        
+        const ratioLogic = LOGIC_HZ / VISUAL_HZ;
+        for(let i = 0; i < ratioLogic; i++) {
+            state.chip8.clock_ws();
+        }
+
+        const ratioTimer = TIMER_HZ / VISUAL_HZ;
+        for(let i = 0; i < ratioTimer; i++) {
+            state.chip8.clock_dt_ws();
+            state.chip8.clock_st_ws();
+        }
+
+        // updates the canvas object with the new
+        // visual information comming in
+        updateCanvas(state.chip8.vram_ws());
+        
+        // waits a little bit for the next frame to be draw
+        // @todo need to define target time for draw
+        await new Promise((resolve, reject) => {
+            setTimeout(resolve, 1000 / VISUAL_HZ);
+        })
+    }
+})();
+
+const register = () => {
+    registerDrop();
+    registerKeys();
+}
+
+const registerDrop = () => {
+    document.addEventListener("drop", async (event) => {
+        event.preventDefault();
+        event.stopPropagation();
+
+        const overlay = document.getElementById("overlay");
+        overlay.classList.remove("visible");
+
+        if (!event.dataTransfer.files) return;
+
+        const file = event.dataTransfer.files[0];
+
+        const arrayBuffer = await file.arrayBuffer();
+        const data = new Uint8Array(arrayBuffer);
+
+        state.chip8.reset_hard_ws();
+        state.chip8.load_rom_ws(data);
+    });
+    document.addEventListener("dragover", async (event) => {
+        event.preventDefault();
+
+        const overlay = document.getElementById("overlay");
+        overlay.classList.add("visible");
+    });
+    document.addEventListener("dragenter", async (event) => {
+        const overlay = document.getElementById("overlay");
+        overlay.classList.add("visible");
+    });
+    document.addEventListener("dragleave", async (event) => {
+        const overlay = document.getElementById("overlay");
+        overlay.classList.remove("visible");
+    });
+};
+
+const registerKeys = () => {
+    document.addEventListener("keydown", (event) => {
+        const keyCode = KEYS[event.key];
+        if (keyCode !== undefined) {
+            state.chip8.key_press_ws(keyCode);
+            return;
+        }
+
+        switch(event.key) {
+            case "+":
+                LOGIC_HZ += 60;
+                break;
+
+            case "-":
+                LOGIC_HZ += 60;
+                break;
+        }
+    });
+
+    document.addEventListener("keyup", (event) => {
+        const keyCode = KEYS[event.key];
+        if (keyCode !== undefined) {
+            state.chip8.key_lift_ws(keyCode);
+            return;
+        }
+    });
+}
+
+const init = () => {
+    initCanvas();
+}
+
+const initCanvas = () => {
+    // initializes the off-screen canvas that is going to be
+    // used in the drawing proces
+    state.canvas = document.createElement("canvas");
+    state.canvas.width = 64;
+    state.canvas.height = 32;
+    state.canvasCtx = state.canvas.getContext("2d");
+
+    state.canvasScaled = document.getElementById("chip-canvas");
+    state.canvasScaledCtx = state.canvasScaled.getContext("2d");
+
+    state.canvasScaledCtx.scale(state.canvasScaled.width / state.canvas.width, state.canvasScaled.height / state.canvas.height);
+    state.canvasScaledCtx.imageSmoothingEnabled = false;
+
+    state.image = state.canvasCtx.createImageData(state.canvas.width, state.canvas.height);
+    state.videoBuff = new DataView(state.image.data.buffer);
+};
+
+const updateCanvas = (pixels) => {
+    for (let i = 0; i < pixels.length; i++) {
+        state.videoBuff.setUint32(i * 4, pixels[i] ? PIXEL_SET_COLOR : PIXEL_UNSET_COLOR);
+    }
+    state.canvasCtx.putImageData(state.image, 0, 0);
+    state.canvasScaledCtx.drawImage(state.canvas, 0, 0);
+};
diff --git a/resources/chips-ahoy.jpeg b/res/chips-ahoy.jpeg
similarity index 100%
rename from resources/chips-ahoy.jpeg
rename to res/chips-ahoy.jpeg
diff --git a/resources/roms/bc_test.ch8 b/res/roms/bc_test.ch8
similarity index 100%
rename from resources/roms/bc_test.ch8
rename to res/roms/bc_test.ch8
diff --git a/resources/roms/bmp.ch8 b/res/roms/bmp.ch8
similarity index 100%
rename from resources/roms/bmp.ch8
rename to res/roms/bmp.ch8
diff --git a/resources/roms/breakout.ch8 b/res/roms/breakout.ch8
similarity index 100%
rename from resources/roms/breakout.ch8
rename to res/roms/breakout.ch8
diff --git a/resources/roms/chip8_picture.ch8 b/res/roms/chip8_picture.ch8
similarity index 100%
rename from resources/roms/chip8_picture.ch8
rename to res/roms/chip8_picture.ch8
diff --git a/resources/roms/chipwar.ch8 b/res/roms/chipwar.ch8
similarity index 100%
rename from resources/roms/chipwar.ch8
rename to res/roms/chipwar.ch8
diff --git a/resources/roms/ibm_logo.ch8 b/res/roms/ibm_logo.ch8
similarity index 100%
rename from resources/roms/ibm_logo.ch8
rename to res/roms/ibm_logo.ch8
diff --git a/resources/roms/keypad.ch8 b/res/roms/keypad.ch8
similarity index 100%
rename from resources/roms/keypad.ch8
rename to res/roms/keypad.ch8
diff --git a/resources/roms/maze.ch8 b/res/roms/maze.ch8
similarity index 100%
rename from resources/roms/maze.ch8
rename to res/roms/maze.ch8
diff --git a/resources/roms/octojam_6_title.ch8 b/res/roms/octojam_6_title.ch8
similarity index 100%
rename from resources/roms/octojam_6_title.ch8
rename to res/roms/octojam_6_title.ch8
diff --git a/resources/roms/pong.ch8 b/res/roms/pong.ch8
similarity index 100%
rename from resources/roms/pong.ch8
rename to res/roms/pong.ch8
diff --git a/resources/roms/pong_paul.ch8 b/res/roms/pong_paul.ch8
similarity index 100%
rename from resources/roms/pong_paul.ch8
rename to res/roms/pong_paul.ch8
diff --git a/resources/roms/sirpinski.ch8 b/res/roms/sirpinski.ch8
similarity index 100%
rename from resources/roms/sirpinski.ch8
rename to res/roms/sirpinski.ch8
diff --git a/resources/roms/snake.ch8 b/res/roms/snake.ch8
similarity index 100%
rename from resources/roms/snake.ch8
rename to res/roms/snake.ch8
diff --git a/resources/roms/soccer.ch8 b/res/roms/soccer.ch8
similarity index 100%
rename from resources/roms/soccer.ch8
rename to res/roms/soccer.ch8
diff --git a/resources/roms/space_invaders.ch8 b/res/roms/space_invaders.ch8
similarity index 100%
rename from resources/roms/space_invaders.ch8
rename to res/roms/space_invaders.ch8
diff --git a/resources/roms/spacejam.ch8 b/res/roms/spacejam.ch8
similarity index 100%
rename from resources/roms/spacejam.ch8
rename to res/roms/spacejam.ch8
diff --git a/resources/roms/test_opcode.ch8 b/res/roms/test_opcode.ch8
similarity index 100%
rename from resources/roms/test_opcode.ch8
rename to res/roms/test_opcode.ch8
diff --git a/resources/roms/tetris.ch8 b/res/roms/tetris.ch8
similarity index 100%
rename from resources/roms/tetris.ch8
rename to res/roms/tetris.ch8
diff --git a/resources/roms/tic_tac_toe.ch8 b/res/roms/tic_tac_toe.ch8
similarity index 100%
rename from resources/roms/tic_tac_toe.ch8
rename to res/roms/tic_tac_toe.ch8
diff --git a/src/chip8_classic.rs b/src/chip8_classic.rs
index 5b6917e902e3bdb77026f38117a06afef77f7b77..a89ddcc5fe296d13f90bff20daf1fca91379bae1 100644
--- a/src/chip8_classic.rs
+++ b/src/chip8_classic.rs
@@ -2,7 +2,7 @@ use std::fmt::Display;
 
 use crate::{chip8::Chip8, util::random};
 
-#[cfg(feature = "web")]
+#[cfg(feature = "wasm")]
 use wasm_bindgen::prelude::*;
 
 /// The width of the screen in pixels.
@@ -50,7 +50,7 @@ static FONT_SET: [u8; 80] = [
     0xf0, 0x80, 0xf0, 0x80, 0x80, // F
 ];
 
-#[cfg_attr(feature = "web", wasm_bindgen)]
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
 pub struct Chip8Classic {
     vram: [u8; SCREEN_PIXEL_WIDTH * SCREEN_PIXEL_HEIGHT],
     ram: [u8; RAM_SIZE],
@@ -66,7 +66,6 @@ pub struct Chip8Classic {
     keys: [bool; NUM_KEYS],
 }
 
-#[cfg_attr(feature = "web", wasm_bindgen)]
 impl Chip8 for Chip8Classic {
     fn name(&self) -> &str {
         "classic"
@@ -152,8 +151,9 @@ impl Chip8 for Chip8Classic {
     }
 }
 
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
 impl Chip8Classic {
-    #[cfg_attr(feature = "web", wasm_bindgen(constructor))]
+    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
     pub fn new() -> Chip8Classic {
         let mut chip8 = Chip8Classic {
             vram: [0u8; SCREEN_PIXEL_WIDTH * SCREEN_PIXEL_HEIGHT],
diff --git a/src/chip8_neo.rs b/src/chip8_neo.rs
index 0e8aecac4717915ed7b13ae17adb8a876a9d01c4..fa81ef540b7b1412a4d6e964e06172e64ca5e932 100644
--- a/src/chip8_neo.rs
+++ b/src/chip8_neo.rs
@@ -2,6 +2,9 @@ use std::io::{Cursor, Read};
 
 use crate::{chip8::Chip8, util::random};
 
+#[cfg(feature = "wasm")]
+use wasm_bindgen::prelude::*;
+
 pub const DISPLAY_WIDTH: usize = 64;
 pub const DISPLAY_HEIGHT: usize = 32;
 
@@ -33,6 +36,7 @@ static FONT_SET: [u8; 80] = [
     0xf0, 0x80, 0xf0, 0x80, 0x80, // F
 ];
 
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
 pub struct Chip8Neo {
     ram: [u8; RAM_SIZE],
     vram: [u8; DISPLAY_WIDTH * DISPLAY_HEIGHT],
@@ -47,7 +51,6 @@ pub struct Chip8Neo {
     last_key: u8,
 }
 
-#[cfg_attr(feature = "web", wasm_bindgen)]
 impl Chip8 for Chip8Neo {
     fn name(&self) -> &str {
         "neo"
@@ -72,6 +75,89 @@ impl Chip8 for Chip8Neo {
         self.reset();
     }
 
+    fn beep(&self) -> bool {
+        self.st > 0
+    }
+
+    fn pc(&self) -> u16 {
+        self.pc
+    }
+
+    fn sp(&self) -> u8 {
+        self.sp
+    }
+
+    fn ram(&self) -> Vec<u8> {
+        self.ram.to_vec()
+    }
+
+    fn vram(&self) -> Vec<u8> {
+        self.vram.to_vec()
+    }
+
+    fn get_state(&self) -> Vec<u8> {
+        let mut buffer: Vec<u8> = Vec::new();
+        buffer.extend(self.ram.iter());
+        buffer.extend(self.vram.iter());
+        buffer.extend(self.stack.map(|v| v.to_le_bytes()).iter().flatten());
+        buffer.extend(self.regs.iter());
+        buffer.extend(self.pc.to_le_bytes().iter());
+        buffer.extend(self.i.to_le_bytes().iter());
+        buffer.extend(self.sp.to_le_bytes().iter());
+        buffer.extend(self.dt.to_le_bytes().iter());
+        buffer.extend(self.st.to_le_bytes().iter());
+        buffer.extend(self.keys.map(|v| v as u8).iter());
+        buffer.extend(self.last_key.to_le_bytes().iter());
+        buffer
+    }
+
+    fn set_state(&mut self, state: &[u8]) {
+        let mut u8_buffer = [0u8; 1];
+        let mut u16_buffer = [0u8; 2];
+        let mut regs_buffer = [0u8; REGISTERS_SIZE * 2];
+        let mut keys_buffer = [0u8; KEYS_SIZE];
+
+        let mut cursor = Cursor::new(state.to_vec());
+
+        cursor.read_exact(&mut self.ram).unwrap();
+        cursor.read_exact(&mut self.vram).unwrap();
+        cursor.read_exact(&mut regs_buffer).unwrap();
+        self.stack.clone_from_slice(
+            regs_buffer
+                .chunks(2)
+                .map(|v| {
+                    u16_buffer.clone_from_slice(&v[0..2]);
+                    u16::from_le_bytes(u16_buffer)
+                })
+                .collect::<Vec<u16>>()
+                .as_slice(),
+        );
+        cursor.read_exact(&mut self.regs).unwrap();
+        cursor.read_exact(&mut u16_buffer).unwrap();
+        self.pc = u16::from_le_bytes(u16_buffer);
+        cursor.read_exact(&mut u16_buffer).unwrap();
+        self.i = u16::from_le_bytes(u16_buffer);
+        cursor.read_exact(&mut u8_buffer).unwrap();
+        self.sp = u8::from_le_bytes(u8_buffer);
+        cursor.read_exact(&mut u8_buffer).unwrap();
+        self.dt = u8::from_le_bytes(u8_buffer);
+        cursor.read_exact(&mut u8_buffer).unwrap();
+        self.st = u8::from_le_bytes(u8_buffer);
+        cursor.read_exact(&mut keys_buffer).unwrap();
+        self.keys.clone_from_slice(
+            keys_buffer
+                .map(|v| if v == 1 { true } else { false })
+                .iter()
+                .as_slice(),
+        );
+        cursor.read_exact(&mut u8_buffer).unwrap();
+        self.last_key = u8::from_le_bytes(u8_buffer);
+    }
+
+    fn load_rom(&mut self, rom: &[u8]) {
+        self.ram[ROM_START..ROM_START + rom.len()].clone_from_slice(&rom);
+    }
+
     fn clock(&mut self) {
         // fetches the current instruction and increments
         // the PC (program counter) accordingly
@@ -221,93 +307,11 @@ impl Chip8 for Chip8Neo {
         }
         self.keys[key as usize] = false;
     }
-
-    fn load_rom(&mut self, rom: &[u8]) {
-        self.ram[ROM_START..ROM_START + rom.len()].clone_from_slice(&rom);
-    }
-
-    fn beep(&self) -> bool {
-        self.st > 0
-    }
-
-    fn pc(&self) -> u16 {
-        self.pc
-    }
-
-    fn sp(&self) -> u8 {
-        self.sp
-    }
-
-    fn ram(&self) -> Vec<u8> {
-        self.ram.to_vec()
-    }
-
-    fn vram(&self) -> Vec<u8> {
-        self.vram.to_vec()
-    }
-
-    fn get_state(&self) -> Vec<u8> {
-        let mut buffer: Vec<u8> = Vec::new();
-        buffer.extend(self.ram.iter());
-        buffer.extend(self.vram.iter());
-        buffer.extend(self.stack.map(|v| v.to_le_bytes()).iter().flatten());
-        buffer.extend(self.regs.iter());
-        buffer.extend(self.pc.to_le_bytes().iter());
-        buffer.extend(self.i.to_le_bytes().iter());
-        buffer.extend(self.sp.to_le_bytes().iter());
-        buffer.extend(self.dt.to_le_bytes().iter());
-        buffer.extend(self.st.to_le_bytes().iter());
-        buffer.extend(self.keys.map(|v| v as u8).iter());
-        buffer.extend(self.last_key.to_le_bytes().iter());
-        buffer
-    }
-
-    fn set_state(&mut self, state: &[u8]) {
-        let mut u8_buffer = [0u8; 1];
-        let mut u16_buffer = [0u8; 2];
-        let mut regs_buffer = [0u8; REGISTERS_SIZE * 2];
-        let mut keys_buffer = [0u8; KEYS_SIZE];
-
-        let mut cursor = Cursor::new(state.to_vec());
-
-        cursor.read_exact(&mut self.ram).unwrap();
-        cursor.read_exact(&mut self.vram).unwrap();
-        cursor.read_exact(&mut regs_buffer).unwrap();
-        self.stack.clone_from_slice(
-            regs_buffer
-                .chunks(2)
-                .map(|v| {
-                    u16_buffer.clone_from_slice(&v[0..2]);
-                    u16::from_le_bytes(u16_buffer)
-                })
-                .collect::<Vec<u16>>()
-                .as_slice(),
-        );
-        cursor.read_exact(&mut self.regs).unwrap();
-        cursor.read_exact(&mut u16_buffer).unwrap();
-        self.pc = u16::from_le_bytes(u16_buffer);
-        cursor.read_exact(&mut u16_buffer).unwrap();
-        self.i = u16::from_le_bytes(u16_buffer);
-        cursor.read_exact(&mut u8_buffer).unwrap();
-        self.sp = u8::from_le_bytes(u8_buffer);
-        cursor.read_exact(&mut u8_buffer).unwrap();
-        self.dt = u8::from_le_bytes(u8_buffer);
-        cursor.read_exact(&mut u8_buffer).unwrap();
-        self.st = u8::from_le_bytes(u8_buffer);
-        cursor.read_exact(&mut keys_buffer).unwrap();
-        self.keys.clone_from_slice(
-            keys_buffer
-                .map(|v| if v == 1 { true } else { false })
-                .iter()
-                .as_slice(),
-        );
-        cursor.read_exact(&mut u8_buffer).unwrap();
-        self.last_key = u8::from_le_bytes(u8_buffer);
-    }
 }
 
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
 impl Chip8Neo {
-    #[cfg_attr(feature = "web", wasm_bindgen(constructor))]
+    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
     pub fn new() -> Chip8Neo {
         let mut chip8 = Chip8Neo {
             ram: [0u8; RAM_SIZE],
@@ -359,3 +363,48 @@ impl Chip8Neo {
         }
     }
 }
+
+#[cfg_attr(feature = "wasm", wasm_bindgen)]
+impl Chip8Neo {
+    pub fn load_rom_ws(&mut self, rom: &[u8]) {
+        self.load_rom(rom)
+    }
+
+    pub fn reset_ws(&mut self) {
+        self.reset()
+    }
+
+    pub fn reset_hard_ws(&mut self) {
+        self.reset_hard()
+    }
+
+    pub fn vram_ws(&self) -> Vec<u8> {
+        self.vram()
+    }
+
+    pub fn clock_ws(&mut self) {
+        self.clock()
+    }
+
+    pub fn clock_dt_ws(&mut self) {
+        self.clock_dt()
+    }
+
+    pub fn clock_st_ws(&mut self) {
+        self.clock_st()
+    }
+
+    pub fn key_press_ws(&mut self, key: u8) {
+        self.key_press(key)
+    }
+
+    pub fn key_lift_ws(&mut self, key: u8) {
+        self.key_lift(key)
+    }
+}
+
+impl Default for Chip8Neo {
+    fn default() -> Chip8Neo {
+        Chip8Neo::new()
+    }
+}