diff --git a/examples/web/index.html b/examples/web/index.html
index 3000b3a8a9e36d5376f86edaa2bd2b1334d4387c..cf2eabe02ff66a3186780d071d275b84ba009b82 100644
--- a/examples/web/index.html
+++ b/examples/web/index.html
@@ -13,6 +13,7 @@
 </head>
 
 <body>
+    <div id="app"></div>
     <div class="main">
         <div class="side-left">
             <div id="canvas-container" class="canvas-container">
diff --git a/examples/web/index.ts b/examples/web/index.ts
index 3784a69fb6c5d5aa451a19da4cf8bb0e89be84ac..366f76b8795ba67708b8697f48bba1753a381aff 100644
--- a/examples/web/index.ts
+++ b/examples/web/index.ts
@@ -1,3 +1,5 @@
+import { createApp } from "vue";
+import App from "./vue/app.vue";
 import { default as _wasm, GameBoy, PadKey, PpuMode } from "./lib/boytacean.js";
 import info from "./package.json";
 
@@ -16,9 +18,6 @@ const DISPLAY_RATIO = DISPLAY_WIDTH / DISPLAY_HEIGHT;
 
 const SAMPLE_RATE = 2;
 
-const SOUND_DATA =
-    "data:audio/mpeg;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAA";
-
 const BACKGROUNDS = [
     "264653",
     "1b1a17",
@@ -44,983 +43,1067 @@ const KEYS: Record<string, number> = {
 const ROM_PATH = require("../../res/roms/20y.gb");
 
 // Enumeration that describes the multiple pixel
-// formats and the associated byte size.
+// formats and the associated size in bytes.
 enum PixelFormat {
     RGB = 3,
     RGBA = 4
 }
 
-type State = {
-    gameBoy: GameBoy;
-    engine: string;
-    logicFrequency: number;
-    visualFrequency: number;
-    timerFrequency: number;
-    idleFrequency: number;
-    canvas: HTMLCanvasElement;
-    canvasScaled: HTMLCanvasElement;
-    canvasCtx: CanvasRenderingContext2D;
-    canvasScaledCtx: CanvasRenderingContext2D;
-    image: ImageData;
-    videoBuff: DataView;
-    toastTimeout: number;
-    paused: boolean;
-    background_index: number;
-    nextTickTime: number;
-    fps: number;
-    frameStart: number;
-    frameCount: number;
-    romName: string;
-    romData: Uint8Array;
-    romSize: number;
-};
-
-type Global = {
-    modalCallback: Function;
-};
-
-const state: State = {
-    gameBoy: null,
-    engine: null,
-    logicFrequency: LOGIC_HZ,
-    visualFrequency: VISUAL_HZ,
-    timerFrequency: TIMER_HZ,
-    idleFrequency: IDLE_HZ,
-    canvas: null,
-    canvasScaled: null,
-    canvasCtx: null,
-    canvasScaledCtx: null,
-    image: null,
-    videoBuff: null,
-    toastTimeout: null,
-    paused: false,
-    background_index: 0,
-    nextTickTime: 0,
-    fps: 0,
-    frameStart: new Date().getTime(),
-    frameCount: 0,
-    romName: null,
-    romData: null,
-    romSize: 0
-};
-
-const global: Global = {
-    modalCallback: null
-};
+class Emulator {
+    /**
+     * The Game Boy engine (probably coming from WASM) that
+     * is going to be used for the emulation.
+     */
+    private gameBoy: GameBoy | null = null;
+
+    /**
+     * The descriptive name of the engine that is currently
+     * in use to emulate the system.
+     */
+    private engine: string | null = null;
+
+    private logicFrequency: number = LOGIC_HZ;
+    private visualFrequency: number = VISUAL_HZ;
+    private timerFrequency: number = TIMER_HZ;
+    private idleFrequency: number = IDLE_HZ;
+
+    private canvas: HTMLCanvasElement | null = null;
+    private canvasScaled: HTMLCanvasElement | null = null;
+    private canvasCtx: CanvasRenderingContext2D | null = null;
+    private canvasScaledCtx: CanvasRenderingContext2D | null = null;
+    private image: ImageData | null = null;
+    private videoBuff: DataView | null = null;
+    private toastTimeout: number | null = null;
+    private paused: boolean = false;
+    private background_index: number = 0;
+    private nextTickTime: number = 0;
+    private fps: number = 0;
+    private frameStart: number = new Date().getTime();
+    private frameCount: number = 0;
+
+    private romName: string | null = null;
+    private romData: Uint8Array | null = null;
+    private romSize: number = 0;
+
+    async main() {
+        // 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
+        await this.buildVisuals();
+        await this.init();
+        await this.register();
+
+        // start the emulator subsystem with the initial
+        // ROM retrieved from a remote data source
+        await this.start({ loadRom: true });
+
+        // the counter that controls the overflowing cycles
+        // from tick to tick operation
+        let pending = 0;
+
+        // runs the sequence as an infinite loop, running
+        // the associated CPU cycles accordingly
+        while (true) {
+            // in case the machine is paused we must delay the execution
+            // a little bit until the paused state is recovered
+            if (this.paused) {
+                await new Promise((resolve) => {
+                    setTimeout(resolve, 1000 / this.idleFrequency);
+                });
+                continue;
+            }
 
-const sound = ((data = SOUND_DATA, volume = 0.2) => {
-    const sound = new Audio(data);
-    sound.volume = volume;
-    sound.muted = true;
-    return sound;
-})();
+            // obtains the current time, this value is going
+            // to be used to compute the need for tick computation
+            let currentTime = new Date().getTime();
+
+            try {
+                pending = this.tick(currentTime, pending);
+            } catch (err) {
+                // sets the default error message to be displayed
+                // to the user, this value may be overridden in case
+                // a better and more explicit message can be determined
+                let message = String(err);
+
+                // verifies if the current issue is a panic one
+                // and updates the message value if that's the case
+                const messageNormalized = (err as Error).message.toLowerCase();
+                const isPanic =
+                    messageNormalized.startsWith("unreachable") ||
+                    messageNormalized.startsWith("recursive use of an object");
+                if (isPanic) {
+                    message = "Unrecoverable error, restarting Game Boy";
+                }
 
-const wasm = async () => {
-    await _wasm();
-    GameBoy.set_panic_hook_ws();
-};
+                // displays the error information to both the end-user
+                // and the developer (for diagnostics)
+                this.showToast(message, true, 5000);
+                console.error(err);
+
+                // pauses the machine, allowing the end-user to act
+                // on the error in a proper fashion
+                this.pause();
+
+                // if we're talking about a panic, proper action must be taken
+                // which in this case it means restarting both the WASM sub
+                // system and the machine state (to be able to recover)
+                // also sets the default color on screen to indicate the issue
+                if (isPanic) {
+                    await this.clearCanvas(undefined, {
+                        // @ts-ignore: ts(2580)
+                        image: require("./res/storm.png"),
+                        imageScale: 0.2
+                    });
+
+                    await wasm();
+                    await this.start({ restore: false });
+                }
+            }
 
-(window as any).panic = (message: string) => {
-    console.error(message);
-};
+            // calculates the amount of time until the next draw operation
+            // this is the amount of time that is going to be pending
+            currentTime = new Date().getTime();
+            const pendingTime = Math.max(this.nextTickTime - currentTime, 0);
 
-const main = 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
-    await init();
-    await register();
-
-    // start the emulator subsystem with the initial
-    // ROM retrieved from a remote data source
-    await start({ loadRom: true });
-
-    // the counter that controls the overflowing cycles
-    // from tick to tick operation
-    let pending = 0;
-
-    // runs the sequence as an infinite loop, running
-    // the associated CPU cycles accordingly
-    while (true) {
-        // in case the machin is paused we must delay the execution
-        // a little bit until the paused state is recovered
-        if (state.paused) {
+            // waits a little bit for the next frame to be draw,
+            // this should control the flow of render
             await new Promise((resolve) => {
-                setTimeout(resolve, 1000 / state.idleFrequency);
+                setTimeout(resolve, pendingTime);
             });
-            continue;
         }
+    }
 
-        // obtains the current time, this value is going
-        // to be used to compute the need for tick computation
-        let currentTime = new Date().getTime();
-
-        try {
-            pending = tick(currentTime, pending);
-        } catch (err) {
-            // sets the default error message to be displayed
-            // to the user
-            let message = String(err);
-
-            // verifies if the current issue is a panic one
-            // and updates the message value if that's the case
-            const messageNormalized = (err as Error).message.toLowerCase();
-            const isPanic =
-                messageNormalized.startsWith("unreachable") ||
-                messageNormalized.startsWith("recursive use of an object");
-            if (isPanic) {
-                message = "Unrecoverable error, restarting Game Boy";
-            }
+    tick(currentTime: number, pending: number, cycles: number = 70224) {
+        // in case the time to draw the next frame has not been
+        // reached the flush of the "tick" logic is skipped
+        if (currentTime < this.nextTickTime) return pending;
+
+        // calculates the number of ticks that have elapsed since the
+        // last draw operation, this is critical to be able to properly
+        // operate the clock of the CPU in frame drop situations
+        if (this.nextTickTime === 0) this.nextTickTime = currentTime;
+        let ticks = Math.ceil(
+            (currentTime - this.nextTickTime) /
+                ((1 / this.visualFrequency) * 1000)
+        );
+        ticks = Math.max(ticks, 1);
 
-            // displays the error information to both the end-user
-            // and the developer (for dianostics)
-            showToast(message, true, 5000);
-            console.error(err);
-
-            // pauses the machine, allowing the end-user to act
-            // on the error in a proper fashion
-            pause();
-
-            // if we're talking about a panic proper action must be taken
-            // which in this case it means restarting both the WASM sub
-            // system and the machine state (to be able to recover)
-            // also sets the default color on screen to indicate the issue
-            if (isPanic) {
-                await clearCanvas(undefined, {
-                    // @ts-ignore: ts(2580)
-                    image: require("./res/storm.png"),
-                    imageScale: 0.2
-                });
+        // initializes the counter of cycles with the pending number
+        // of cycles coming from the previous tick
+        let counterCycles = pending;
+
+        let lastFrame = -1;
 
-                await wasm();
-                await start({ restore: false });
+        while (true) {
+            // limits the number of cycles to the provided
+            // cycle value passed as a parameter
+            if (counterCycles >= cycles) {
+                break;
+            }
+
+            // runs the Game Boy clock, this operations should
+            // include the advance of both the CPU and the PPU
+            counterCycles += this.gameBoy!.clock();
+
+            // in case the current PPU mode is VBlank and the
+            // frame is different from the previously rendered
+            // one then it's time to update the canvas
+            if (
+                this.gameBoy!.ppu_mode() == PpuMode.VBlank &&
+                this.gameBoy!.ppu_frame() != lastFrame
+            ) {
+                // updates the canvas object with the new
+                // visual information coming in
+                this.updateCanvas(
+                    this.gameBoy!.frame_buffer_eager(),
+                    PixelFormat.RGB
+                );
+                lastFrame = this.gameBoy!.ppu_frame();
             }
         }
 
-        // calculates the amount of time until the next draw operation
-        // this is the amount of time that is going to be pending
-        currentTime = new Date().getTime();
-        const pendingTime = Math.max(state.nextTickTime - currentTime, 0);
+        // increments the number of frames rendered in the current
+        // section, this value is going to be used to calculate FPS
+        this.frameCount += 1;
+
+        // in case the target number of frames for FPS control
+        // has been reached calculates the number of FPS and
+        // flushes the value to the screen
+        if (this.frameCount === this.visualFrequency * SAMPLE_RATE) {
+            const currentTime = new Date().getTime();
+            const deltaTime = (currentTime - this.frameStart) / 1000;
+            const fps = Math.round(this.frameCount / deltaTime);
+            this.setFps(fps);
+            this.frameCount = 0;
+            this.frameStart = currentTime;
+        }
 
-        // waits a little bit for the next frame to be draw,
-        // this should control the flow of render
-        await new Promise((resolve) => {
-            setTimeout(resolve, pendingTime);
-        });
+        // updates the next update time reference to the, so that it
+        // can be used to control the game loop
+        this.nextTickTime += (1000 / this.visualFrequency) * ticks;
+
+        // calculates the new number of pending (overflow) cycles
+        // that are going to be added to the next iteration
+        return counterCycles - cycles;
     }
-};
 
-const tick = (currentTime: number, pending: number, cycles: number = 70224) => {
-    // in case the time to draw the next frame has not been
-    // reached the flush of the "tick" logic is skiped
-    if (currentTime < state.nextTickTime) return pending;
-
-    // calculates the number of ticks that have elapsed since the
-    // last draw operation, this is critical to be able to properly
-    // operate the clock of the CPU in frame drop situations
-    if (state.nextTickTime === 0) state.nextTickTime = currentTime;
-    let ticks = Math.ceil(
-        (currentTime - state.nextTickTime) /
-            ((1 / state.visualFrequency) * 1000)
-    );
-    ticks = Math.max(ticks, 1);
-
-    // initializes the counter of cycles with the pending number
-    // of cycles coming from the previous tick
-    let counterCycles = pending;
-
-    let lastFrame = -1;
-
-    while (true) {
-        // limits the number of cycles to the provided
-        // cycle value passed as a parameter
-        if (counterCycles >= cycles) {
-            break;
+    /**
+     * Starts the current machine, setting the internal structure in
+     * a proper state to start drawing and receiving input.
+     *
+     * @param options The options that are going to be used in the
+     * starting of the machine.
+     */
+    async start({
+        engine = "neo",
+        restore = true,
+        loadRom = false,
+        romPath = ROM_PATH,
+        romName = null,
+        romData = null
+    }: {
+        engine?: string | null;
+        restore?: boolean;
+        loadRom?: boolean;
+        romPath?: string;
+        romName?: string | null;
+        romData?: Uint8Array | null;
+    } = {}) {
+        // in case a remote ROM loading operation has been
+        // requested then loads it from the remote origin
+        if (loadRom) {
+            [romName, romData] = await this.fetchRom(romPath);
+        } else if (romName === null || romData === null) {
+            [romName, romData] = [this.romName, this.romData];
         }
 
-        // runs the Game Boy clock, this operations should
-        // include the advance of both the CPU and the PPU
-        counterCycles += state.gameBoy.clock();
-
-        // in case the current PPU mode is VBlank and the
-        // fram is different from the previously rendered
-        // one then it's time to update the canvas
-        if (
-            state.gameBoy.ppu_mode() == PpuMode.VBlank &&
-            state.gameBoy.ppu_frame() != lastFrame
-        ) {
-            // updates the canvas object with the new
-            // visual information coming in
-            updateCanvas(state.gameBoy.frame_buffer_eager(), PixelFormat.RGB);
-            lastFrame = state.gameBoy.ppu_frame();
+        // selects the proper engine for execution
+        // and builds a new instance of it
+        switch (engine) {
+            case "neo":
+                this.gameBoy = new GameBoy();
+                break;
+
+            default:
+                if (!this.gameBoy) {
+                    throw new Error("No engine requested");
+                }
+                break;
         }
-    }
 
-    // increments the number of frames rendered in the current
-    // section, this value is going to be used to calculate FPS
-    state.frameCount += 1;
-
-    // in case the target number of frames for FPS control
-    // has been reached calculates the number of FPS and
-    // flushes the value to the screen
-    if (state.frameCount === state.visualFrequency * SAMPLE_RATE) {
-        const currentTime = new Date().getTime();
-        const deltaTime = (currentTime - state.frameStart) / 1000;
-        const fps = Math.round(state.frameCount / deltaTime);
-        setFps(fps);
-        state.frameCount = 0;
-        state.frameStart = currentTime;
+        // resets the Game Boy engine to restore it into
+        // a valid state ready to be used
+        this.gameBoy.reset();
+        this.gameBoy.load_boot_default();
+        const cartridge = this.gameBoy.load_rom_ws(romData!);
+
+        // updates the ROM name in case there's extra information
+        // coming from the cartridge
+        romName = cartridge.title() ? cartridge.title() : romName;
+
+        // updates the name of the currently selected engine
+        // to the one that has been provided (logic change)
+        if (engine) this.engine = engine;
+
+        // updates the complete set of global information that
+        // is going to be displayed
+        this.setEngine(this.engine!);
+        this.setRom(romName!, romData!);
+        this.setLogicFrequency(this.logicFrequency);
+        this.setFps(this.fps);
+
+        // in case the restore (state) flag is set
+        // then resumes the machine execution
+        if (restore) this.resume();
     }
 
-    // updates the next update time reference to the, so that it
-    // can be used to control the game loop
-    state.nextTickTime += (1000 / state.visualFrequency) * ticks;
+    async buildVisuals() {
+        /*     KeyValue.create("ROM", "-", { id: "diag:rom-name" }).mount(".diag");
+        KeyValue.create("ROM Size", "-", { id: "diag:rom-size" }).mount(
+            ".diag"
+        );
+        KeyValue.create("Framerate", "-", { id: "diag:framerate" }).mount(
+            ".diag"
+        );
+        KeyValue.create("ROM Type", "-", { id: "diag:rom-type" }).mount(
+            ".diag"
+        );
+        KeySwitch.create("Tobias", ["1", "2", "3"], {
+            id: "diag:tobias"
+        }).mount(".diag");
+
+        // @ts-ignore: ts(2580)
+        Button.create("Tobias", require("./res/close.svg"))
+            .bind("click", () => alert("Hello World"))
+            .mount(".button-area");*/
+    }
 
-    // calculates the new number of pending (overflow) cycles
-    // that are going to be added to the next iteration
-    return counterCycles - cycles;
-};
+    // @todo remove this method, or at least most of it
+    async register() {
+        await Promise.all([
+            this.registerDrop(),
+            this.registerKeys(),
+            this.registerButtons(),
+            this.registerKeyboard(),
+            this.registerCanvas(),
+            this.registerToast(),
+            this.registerModal()
+        ]);
+    }
 
-/**
- * Starts the current machine, setting the internal structure in
- * a proper state to start drwaing and receiving input.
- *
- * @param options The options that are going to be used in the
- * starting of the machine.
- */
-const start = async ({
-    engine = "neo",
-    restore = true,
-    loadRom = false,
-    romPath = ROM_PATH,
-    romName = null as string,
-    romData = null as Uint8Array
-} = {}) => {
-    // in case a remote ROM loading operation has been
-    // requested then loads it from the remote origin
-    if (loadRom) {
-        [romName, romData] = await fetchRom(romPath);
-    } else if (romName === null || romData === null) {
-        [romName, romData] = [state.romName, state.romData];
+    async init() {
+        await Promise.all([this.initBase(), this.initCanvas()]);
     }
 
-    // selects the proper engine for execution
-    // and builds a new instance of it
-    switch (engine) {
-        case "neo":
-            state.gameBoy = new GameBoy();
-            break;
+    registerDrop() {
+        document.addEventListener("drop", async (event) => {
+            if (
+                !event.dataTransfer!.files ||
+                event.dataTransfer!.files.length === 0
+            ) {
+                return;
+            }
 
-        default:
-            if (!state.gameBoy) {
-                throw new Error("No engine requested");
+            event.preventDefault();
+            event.stopPropagation();
+
+            const overlay = document.getElementById("overlay")!;
+            overlay.classList.remove("visible");
+
+            const file = event.dataTransfer!.files[0];
+
+            if (!file.name.endsWith(".gb")) {
+                this.showToast(
+                    "This is probably not a Game Boy ROM file!",
+                    true
+                );
+                return;
             }
-            break;
-    }
 
-    // resets the Game Boy engine to restore it into
-    // a valid state ready to be used
-    state.gameBoy.reset();
-    state.gameBoy.load_boot_default();
-    const cartridge = state.gameBoy.load_rom_ws(romData);
-
-    // updates the ROM name in case there's extra information
-    // comming from the cartridge
-    romName = cartridge.title() ? cartridge.title() : romName;
-
-    // updates the name of the currently selected engine
-    // to the one that has been provided (logic change)
-    if (engine) state.engine = engine;
-
-    // updates the complete set of global information that
-    // is going to be displayed
-    setEngine(state.engine);
-    setRom(romName, romData);
-    setLogicFrequency(state.logicFrequency);
-    setFps(state.fps);
-
-    // in case the restore (state) flag is set
-    // then resumes the machine execution
-    if (restore) resume();
-};
+            const arrayBuffer = await file.arrayBuffer();
+            const romData = new Uint8Array(arrayBuffer);
 
-const register = async () => {
-    await Promise.all([
-        registerDrop(),
-        registerKeys(),
-        registerButtons(),
-        registerKeyboard(),
-        registerCanvas(),
-        registerToast(),
-        registerModal()
-    ]);
-};
+            this.start({ engine: null, romName: file.name, romData: romData });
 
-const init = async () => {
-    await Promise.all([initBase(), initCanvas()]);
-};
+            this.showToast(`Loaded ${file.name} ROM successfully!`);
+        });
+        document.addEventListener("dragover", async (event) => {
+            if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
+                return;
 
-const registerDrop = () => {
-    document.addEventListener("drop", async (event) => {
-        if (
-            !event.dataTransfer.files ||
-            event.dataTransfer.files.length === 0
-        ) {
-            return;
-        }
+            event.preventDefault();
 
-        event.preventDefault();
-        event.stopPropagation();
+            const overlay = document.getElementById("overlay")!;
+            overlay.classList.add("visible");
+        });
+        document.addEventListener("dragenter", async (event) => {
+            if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
+                return;
+            const overlay = document.getElementById("overlay")!;
+            overlay.classList.add("visible");
+        });
+        document.addEventListener("dragleave", async (event) => {
+            if (!event.dataTransfer!.items || event.dataTransfer!.items[0].type)
+                return;
+            const overlay = document.getElementById("overlay")!;
+            overlay.classList.remove("visible");
+        });
+    }
 
-        const overlay = document.getElementById("overlay");
-        overlay.classList.remove("visible");
+    registerKeys() {
+        document.addEventListener("keydown", (event) => {
+            const keyCode = KEYS[event.key];
+            if (keyCode !== undefined) {
+                this.gameBoy!.key_press(keyCode);
+                return;
+            }
 
-        const file = event.dataTransfer.files[0];
+            switch (event.key) {
+                case "+":
+                    this.setLogicFrequency(
+                        this.logicFrequency + FREQUENCY_DELTA
+                    );
+                    break;
+
+                case "-":
+                    this.setLogicFrequency(
+                        this.logicFrequency - FREQUENCY_DELTA
+                    );
+                    break;
+
+                case "Escape":
+                    this.minimize();
+                    break;
+            }
+        });
 
-        if (!file.name.endsWith(".gb")) {
-            showToast("This is probably not a Game Boy ROM file!", true);
-            return;
-        }
+        document.addEventListener("keyup", (event) => {
+            const keyCode = KEYS[event.key];
+            if (keyCode !== undefined) {
+                this.gameBoy!.key_lift(keyCode);
+                return;
+            }
+        });
+    }
 
-        const arrayBuffer = await file.arrayBuffer();
-        const romData = new Uint8Array(arrayBuffer);
+    registerButtons() {
+        const engine = document.getElementById("engine")!;
+        engine.addEventListener("click", () => {
+            const name = this.engine == "neo" ? "classic" : "neo";
+            this.start({ engine: name });
+            this.showToast(
+                `Game Boy running in engine "${name.toUpperCase()}" from now on!`
+            );
+        });
 
-        start({ engine: null, romName: file.name, romData: romData });
-
-        showToast(`Loaded ${file.name} ROM successfully!`);
-    });
-    document.addEventListener("dragover", async (event) => {
-        if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
-            return;
-
-        event.preventDefault();
-
-        const overlay = document.getElementById("overlay");
-        overlay.classList.add("visible");
-    });
-    document.addEventListener("dragenter", async (event) => {
-        if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
-            return;
-        const overlay = document.getElementById("overlay");
-        overlay.classList.add("visible");
-    });
-    document.addEventListener("dragleave", async (event) => {
-        if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
-            return;
-        const overlay = document.getElementById("overlay");
-        overlay.classList.remove("visible");
-    });
-};
+        const logicFrequencyPlus = document.getElementById(
+            "logic-frequency-plus"
+        )!;
+        logicFrequencyPlus.addEventListener("click", () => {
+            this.setLogicFrequency(this.logicFrequency + FREQUENCY_DELTA);
+        });
 
-const registerKeys = () => {
-    document.addEventListener("keydown", (event) => {
-        const keyCode = KEYS[event.key];
-        if (keyCode !== undefined) {
-            state.gameBoy.key_press(keyCode);
-            return;
-        }
+        const logicFrequencyMinus = document.getElementById(
+            "logic-frequency-minus"
+        )!;
+        logicFrequencyMinus.addEventListener("click", () => {
+            this.setLogicFrequency(this.logicFrequency - FREQUENCY_DELTA);
+        });
 
-        switch (event.key) {
-            case "+":
-                setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
-                break;
+        const buttonPause = document.getElementById("button-pause")!;
+        buttonPause.addEventListener("click", () => {
+            this.toggleRunning();
+        });
 
-            case "-":
-                setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
-                break;
+        const buttonReset = document.getElementById("button-reset")!;
+        buttonReset.addEventListener("click", () => {
+            this.reset();
+        });
 
-            case "Escape":
-                minimize();
-                break;
-        }
-    });
+        const buttonBenchmark = document.getElementById("button-benchmark")!;
+        buttonBenchmark.addEventListener("click", async () => {
+            const result = await this.showModal(
+                "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
+                "Confirm"
+            );
+            if (!result) return;
+            buttonBenchmark.classList.add("enabled");
+            this.pause();
+            try {
+                const initial = Date.now();
+                const count = 500000000;
+                for (let i = 0; i < count; i++) {
+                    this.gameBoy!.clock();
+                }
+                const delta = (Date.now() - initial) / 1000;
+                const frequency_mhz = count / delta / 1000 / 1000;
+                this.showToast(
+                    `Took ${delta.toFixed(
+                        2
+                    )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
+                        2
+                    )} Mhz)!`,
+                    undefined,
+                    7500
+                );
+            } finally {
+                this.resume();
+                buttonBenchmark.classList.remove("enabled");
+            }
+        });
 
-    document.addEventListener("keyup", (event) => {
-        const keyCode = KEYS[event.key];
-        if (keyCode !== undefined) {
-            state.gameBoy.key_lift(keyCode);
-            return;
-        }
-    });
-};
+        const buttonFullscreen = document.getElementById("button-fullscreen")!;
+        buttonFullscreen.addEventListener("click", () => {
+            this.maximize();
+        });
 
-const registerButtons = () => {
-    const engine = document.getElementById("engine");
-    engine.addEventListener("click", () => {
-        const name = state.engine == "neo" ? "classic" : "neo";
-        start({ engine: name });
-        showToast(
-            `Game Boy running in engine "${name.toUpperCase()}" from now on!`
-        );
-    });
-
-    const logicFrequencyPlus = document.getElementById("logic-frequency-plus");
-    logicFrequencyPlus.addEventListener("click", () => {
-        setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
-    });
-
-    const logicFrequencyMinus = document.getElementById(
-        "logic-frequency-minus"
-    );
-    logicFrequencyMinus.addEventListener("click", () => {
-        setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
-    });
-
-    const buttonPause = document.getElementById("button-pause");
-    buttonPause.addEventListener("click", () => {
-        toggleRunning();
-    });
-
-    const buttonReset = document.getElementById("button-reset");
-    buttonReset.addEventListener("click", () => {
-        reset();
-    });
-
-    const buttonBenchmark = document.getElementById("button-benchmark");
-    buttonBenchmark.addEventListener("click", async () => {
-        const result = await showModal(
-            "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
-            "Confirm"
-        );
-        if (!result) return;
-        buttonBenchmark.classList.add("enabled");
-        pause();
-        try {
-            const initial = Date.now();
-            const count = 500000000;
-            for (let i = 0; i < count; i++) {
-                state.gameBoy.clock();
+        const buttonKeyboard = document.getElementById("button-keyboard")!;
+        buttonKeyboard.addEventListener("click", () => {
+            const sectionKeyboard =
+                document.getElementById("section-keyboard")!;
+            const separatorKeyboard =
+                document.getElementById("separator-keyboard")!;
+            const sectionNarrative =
+                document.getElementById("section-narrative")!;
+            const separatorNarrative = document.getElementById(
+                "separator-narrative"
+            )!;
+            if (buttonKeyboard.classList.contains("enabled")) {
+                sectionKeyboard.style.display = "none";
+                separatorKeyboard.style.display = "none";
+                sectionNarrative.style.display = "block";
+                separatorNarrative.style.display = "block";
+                buttonKeyboard.classList.remove("enabled");
+            } else {
+                sectionKeyboard.style.display = "block";
+                separatorKeyboard.style.display = "block";
+                sectionNarrative.style.display = "none";
+                separatorNarrative.style.display = "none";
+                buttonKeyboard.classList.add("enabled");
             }
-            const delta = (Date.now() - initial) / 1000;
-            const frequency_mhz = count / delta / 1000 / 1000;
-            showToast(
-                `Took ${delta.toFixed(
-                    2
-                )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
-                    2
-                )} Mhz)!`,
-                undefined,
-                7500
-            );
-        } finally {
-            resume();
-            buttonBenchmark.classList.remove("enabled");
-        }
-    });
-
-    const buttonFullscreen = document.getElementById("button-fullscreen");
-    buttonFullscreen.addEventListener("click", () => {
-        maximize();
-    });
-
-    const buttonKeyboard = document.getElementById("button-keyboard");
-    buttonKeyboard.addEventListener("click", () => {
-        const sectionKeyboard = document.getElementById("section-keyboard");
-        const separatorKeyboard = document.getElementById("separator-keyboard");
-        const sectionNarrative = document.getElementById("section-narrative");
-        const separatorNarrative = document.getElementById(
-            "separator-narrative"
-        );
-        if (buttonKeyboard.classList.contains("enabled")) {
-            sectionKeyboard.style.display = "none";
-            separatorKeyboard.style.display = "none";
-            sectionNarrative.style.display = "block";
-            separatorNarrative.style.display = "block";
-            buttonKeyboard.classList.remove("enabled");
-        } else {
-            sectionKeyboard.style.display = "block";
-            separatorKeyboard.style.display = "block";
-            sectionNarrative.style.display = "none";
-            separatorNarrative.style.display = "none";
-            buttonKeyboard.classList.add("enabled");
-        }
-    });
-
-    const buttonDebug = document.getElementById("button-debug");
-    buttonDebug.addEventListener("click", () => {
-        const sectionDebug = document.getElementById("section-debug");
-        const separatorDebug = document.getElementById("separator-debug");
-        const sectionNarrative = document.getElementById("section-narrative");
-        const separatorNarrative = document.getElementById(
-            "separator-narrative"
-        );
-        if (buttonDebug.classList.contains("enabled")) {
-            sectionDebug.style.display = "none";
-            separatorDebug.style.display = "none";
-            sectionNarrative.style.display = "block";
-            separatorNarrative.style.display = "block";
-            buttonDebug.classList.remove("enabled");
-        } else {
-            sectionDebug.style.display = "block";
-            separatorDebug.style.display = "block";
-            sectionNarrative.style.display = "none";
-            separatorNarrative.style.display = "none";
-            buttonDebug.classList.add("enabled");
-
-            const canvasTiles = document.getElementById(
-                "canvas-tiles"
-            ) as HTMLCanvasElement;
-            const canvasTilesCtx = canvasTiles.getContext("2d");
-
-            const canvasImage = canvasTilesCtx.createImageData(
-                canvasTiles.width,
-                canvasTiles.height
-            );
-            const videoBuff = new DataView(canvasImage.data.buffer);
-
-            /**
-             * Draws the tile at the given index to the proper
-             * vertical offset in the given context and buffer.
-             *
-             * @param index The index of the sprite to be drawn.
-             * @param format The pixel format of the sprite.
-             */
-            const drawTile = (
-                index: number,
-                context: CanvasRenderingContext2D,
-                buffer: DataView,
-                format: PixelFormat = PixelFormat.RGB
-            ) => {
-                const pixels = state.gameBoy.get_tile_buffer(index);
-                const line = Math.floor(index / 16);
-                const column = index % 16;
-                let offset =
-                    (line * canvasTiles.width * 8 + column * 8) *
-                    PixelFormat.RGBA;
-                let counter = 0;
-                for (let index = 0; index < pixels.length; index += format) {
-                    const color =
-                        (pixels[index] << 24) |
-                        (pixels[index + 1] << 16) |
-                        (pixels[index + 2] << 8) |
-                        (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
-                    buffer.setUint32(offset, color);
-
-                    counter++;
-                    if (counter == 8) {
-                        counter = 0;
-                        offset += (canvasTiles.width - 7) * PixelFormat.RGBA;
-                    } else {
-                        offset += PixelFormat.RGBA;
+        });
+
+        const buttonDebug = document.getElementById("button-debug")!;
+        buttonDebug.addEventListener("click", () => {
+            const sectionDebug = document.getElementById("section-debug")!;
+            const separatorDebug = document.getElementById("separator-debug")!;
+            const sectionNarrative =
+                document.getElementById("section-narrative")!;
+            const separatorNarrative = document.getElementById(
+                "separator-narrative"
+            )!;
+            if (buttonDebug.classList.contains("enabled")) {
+                sectionDebug.style.display = "none";
+                separatorDebug.style.display = "none";
+                sectionNarrative.style.display = "block";
+                separatorNarrative.style.display = "block";
+                buttonDebug.classList.remove("enabled");
+            } else {
+                sectionDebug.style.display = "block";
+                separatorDebug.style.display = "block";
+                sectionNarrative.style.display = "none";
+                separatorNarrative.style.display = "none";
+                buttonDebug.classList.add("enabled");
+
+                const canvasTiles = document.getElementById(
+                    "canvas-tiles"
+                ) as HTMLCanvasElement;
+                const canvasTilesCtx = canvasTiles.getContext("2d")!;
+                canvasTilesCtx.imageSmoothingEnabled = false;
+
+                const canvasImage = canvasTilesCtx.createImageData(
+                    canvasTiles.width,
+                    canvasTiles.height
+                );
+                const videoBuff = new DataView(canvasImage.data.buffer);
+
+                /**
+                 * Draws the tile at the given index to the proper
+                 * vertical offset in the given context and buffer.
+                 *
+                 * @param index The index of the sprite to be drawn.
+                 * @param format The pixel format of the sprite.
+                 */
+                const drawTile = (
+                    index: number,
+                    context: CanvasRenderingContext2D,
+                    buffer: DataView,
+                    format: PixelFormat = PixelFormat.RGB
+                ) => {
+                    const pixels = this.gameBoy!.get_tile_buffer(index);
+                    const line = Math.floor(index / 16);
+                    const column = index % 16;
+                    let offset =
+                        (line * canvasTiles.width * 8 + column * 8) *
+                        PixelFormat.RGBA;
+                    let counter = 0;
+                    for (
+                        let index = 0;
+                        index < pixels.length;
+                        index += format
+                    ) {
+                        const color =
+                            (pixels[index] << 24) |
+                            (pixels[index + 1] << 16) |
+                            (pixels[index + 2] << 8) |
+                            (format == PixelFormat.RGBA
+                                ? pixels[index + 3]
+                                : 0xff);
+                        buffer.setUint32(offset, color);
+
+                        counter++;
+                        if (counter == 8) {
+                            counter = 0;
+                            offset +=
+                                (canvasTiles.width - 7) * PixelFormat.RGBA;
+                        } else {
+                            offset += PixelFormat.RGBA;
+                        }
                     }
+                    context.putImageData(canvasImage, 0, 0);
+                };
+
+                for (let index = 0; index < 384; index++) {
+                    drawTile(index, canvasTilesCtx, videoBuff);
                 }
-                context.putImageData(canvasImage, 0, 0);
-            };
 
-            for (let index = 0; index < 384; index++) {
-                drawTile(index, canvasTilesCtx, videoBuff);
+                const vram = this.gameBoy!.vram_eager();
+                const step = 16;
+                for (let index = 0; index < vram.length; index += step) {
+                    let line = `${(index + 0x8000)
+                        .toString(16)
+                        .padStart(4, "0")}`;
+                    for (let j = 0; j < step; j++) {
+                        line += ` ${vram[index + j]
+                            .toString(16)
+                            .padStart(2, "0")}`;
+                    }
+                    console.info(line);
+                }
             }
+        });
 
-            const vram = state.gameBoy.vram_eager();
-            const step = 16;
-            for (let index = 0; index < vram.length; index += step) {
-                let line = `${(index + 0x8000).toString(16).padStart(4, "0")}`;
-                for (let j = 0; j < step; j++) {
-                    line += ` ${vram[index + j].toString(16).padStart(2, "0")}`;
-                }
-                console.info(line);
+        const buttonInformation =
+            document.getElementById("button-information")!;
+        buttonInformation.addEventListener("click", () => {
+            const sectionDiag = document.getElementById("section-diag")!;
+            const separatorDiag = document.getElementById("separator-diag")!;
+            if (buttonInformation.classList.contains("enabled")) {
+                sectionDiag.style.display = "none";
+                separatorDiag.style.display = "none";
+                buttonInformation.classList.remove("enabled");
+            } else {
+                sectionDiag.style.display = "block";
+                separatorDiag.style.display = "block";
+                buttonInformation.classList.add("enabled");
             }
-        }
-    });
-
-    const buttonInformation = document.getElementById("button-information");
-    buttonInformation.addEventListener("click", () => {
-        const sectionDiag = document.getElementById("section-diag");
-        const separatorDiag = document.getElementById("separator-diag");
-        if (buttonInformation.classList.contains("enabled")) {
-            sectionDiag.style.display = "none";
-            separatorDiag.style.display = "none";
-            buttonInformation.classList.remove("enabled");
-        } else {
-            sectionDiag.style.display = "block";
-            separatorDiag.style.display = "block";
-            buttonInformation.classList.add("enabled");
-        }
-    });
-
-    const buttonTheme = document.getElementById("button-theme");
-    buttonTheme.addEventListener("click", () => {
-        state.background_index =
-            (state.background_index + 1) % BACKGROUNDS.length;
-        const background = BACKGROUNDS[state.background_index];
-        setBackground(background);
-    });
-
-    const buttonUploadFile = document.getElementById(
-        "button-upload-file"
-    ) as HTMLInputElement;
-    buttonUploadFile.addEventListener("change", async () => {
-        if (!buttonUploadFile.files || buttonUploadFile.files.length === 0) {
-            return;
-        }
+        });
 
-        const file = buttonUploadFile.files[0];
+        const buttonTheme = document.getElementById("button-theme")!;
+        buttonTheme.addEventListener("click", () => {
+            this.background_index =
+                (this.background_index + 1) % BACKGROUNDS.length;
+            const background = BACKGROUNDS[this.background_index];
+            this.setBackground(background);
+        });
 
-        const arrayBuffer = await file.arrayBuffer();
-        const romData = new Uint8Array(arrayBuffer);
+        const buttonUploadFile = document.getElementById(
+            "button-upload-file"
+        ) as HTMLInputElement;
+        buttonUploadFile.addEventListener("change", async () => {
+            if (
+                !buttonUploadFile.files ||
+                buttonUploadFile.files.length === 0
+            ) {
+                return;
+            }
 
-        buttonUploadFile.value = "";
+            const file = buttonUploadFile.files[0];
 
-        start({ engine: null, romName: file.name, romData: romData });
+            const arrayBuffer = await file.arrayBuffer();
+            const romData = new Uint8Array(arrayBuffer);
 
-        showToast(`Loaded ${file.name} ROM successfully!`);
-    });
-};
+            buttonUploadFile.value = "";
 
-const registerKeyboard = () => {
-    const keyboard = document.getElementById("keyboard");
-    const keys = keyboard.getElementsByClassName("key");
+            this.start({ engine: null, romName: file.name, romData: romData });
 
-    keyboard.addEventListener("touchstart", function (event) {
-        event.preventDefault();
-        event.stopPropagation();
-    });
+            this.showToast(`Loaded ${file.name} ROM successfully!`);
+        });
+    }
 
-    keyboard.addEventListener("touchend", function (event) {
-        event.preventDefault();
-        event.stopPropagation();
-    });
+    // @todo this should be converted into a component
+    registerKeyboard() {
+        const keyboard = document.getElementById("keyboard")!;
+        const keys = keyboard.getElementsByClassName("key");
 
-    Array.prototype.forEach.call(keys, (k: Element) => {
-        k.addEventListener("mousedown", function (event) {
-            const keyCode = KEYS[this.textContent.toLowerCase()];
-            //state.gameBoy.key_press_ws(keyCode); @todo
+        keyboard.addEventListener("touchstart", function (event) {
             event.preventDefault();
             event.stopPropagation();
         });
 
-        k.addEventListener("touchstart", function (event) {
-            const keyCode = KEYS[this.textContent.toLowerCase()];
-            //state.gameBoy.key_press_ws(keyCode); @todo
+        keyboard.addEventListener("touchend", function (event) {
             event.preventDefault();
             event.stopPropagation();
         });
 
-        k.addEventListener("mouseup", function (event) {
-            const keyCode = KEYS[this.textContent.toLowerCase()];
-            //state.gameBoy.key_lift_ws(keyCode); @todo
-            event.preventDefault();
-            event.stopPropagation();
+        Array.prototype.forEach.call(keys, (k: Element) => {
+            k.addEventListener(
+                "mousedown",
+                function (this: HTMLElement, event) {
+                    const keyCode = KEYS[this.textContent!.toLowerCase()];
+                    //this.gameBoy.key_press_ws(keyCode); @todo
+                    event.preventDefault();
+                    event.stopPropagation();
+                }
+            );
+
+            k.addEventListener(
+                "touchstart",
+                function (this: HTMLElement, event) {
+                    const keyCode = KEYS[this.textContent!.toLowerCase()];
+                    //this.gameBoy.key_press_ws(keyCode); @todo
+                    event.preventDefault();
+                    event.stopPropagation();
+                }
+            );
+
+            k.addEventListener("mouseup", function (this: HTMLElement, event) {
+                const keyCode = KEYS[this.textContent!.toLowerCase()];
+                //this.gameBoy.key_lift_ws(keyCode); @todo
+                event.preventDefault();
+                event.stopPropagation();
+            });
+
+            k.addEventListener("touchend", function (this: HTMLElement, event) {
+                const keyCode = KEYS[this.textContent!.toLowerCase()];
+                //this.gameBoy.key_lift_ws(keyCode); @todo
+                event.preventDefault();
+                event.stopPropagation();
+            });
         });
+    }
 
-        k.addEventListener("touchend", function (event) {
-            const keyCode = KEYS[this.textContent.toLowerCase()];
-            //state.gameBoy.key_lift_ws(keyCode); @todo
-            event.preventDefault();
-            event.stopPropagation();
+    registerCanvas() {
+        const canvasClose = document.getElementById("canvas-close")!;
+        canvasClose.addEventListener("click", () => {
+            this.minimize();
         });
-    });
-};
+    }
 
-const registerCanvas = () => {
-    const canvasClose = document.getElementById("canvas-close");
-    canvasClose.addEventListener("click", () => {
-        minimize();
-    });
-};
+    registerToast() {
+        const toast = document.getElementById("toast")!;
+        toast.addEventListener("click", () => {
+            toast.classList.remove("visible");
+        });
+    }
 
-const registerToast = () => {
-    const toast = document.getElementById("toast");
-    toast.addEventListener("click", () => {
-        toast.classList.remove("visible");
-    });
-};
+    registerModal() {
+        const modalClose = document.getElementById("modal-close")!;
+        modalClose.addEventListener("click", () => {
+            this.hideModal(false);
+        });
 
-const registerModal = () => {
-    const modalClose = document.getElementById("modal-close");
-    modalClose.addEventListener("click", () => {
-        hideModal(false);
-    });
-
-    const modalCancel = document.getElementById("modal-cancel");
-    modalCancel.addEventListener("click", () => {
-        hideModal(false);
-    });
-
-    const modalConfirm = document.getElementById("modal-confirm");
-    modalConfirm.addEventListener("click", () => {
-        hideModal(true);
-    });
-
-    document.addEventListener("keydown", (event) => {
-        if (event.key === "Escape") {
-            hideModal(false);
-        }
-    });
-};
+        const modalCancel = document.getElementById("modal-cancel")!;
+        modalCancel.addEventListener("click", () => {
+            this.hideModal(false);
+        });
 
-const initBase = async () => {
-    const background = BACKGROUNDS[state.background_index];
-    setBackground(background);
-    setVersion(info.version);
-};
+        const modalConfirm = document.getElementById("modal-confirm")!;
+        modalConfirm.addEventListener("click", () => {
+            this.hideModal(true);
+        });
 
-const initCanvas = async () => {
-    // initializes the off-screen canvas that is going to be
-    // used in the drawing process
-    state.canvas = document.createElement("canvas");
-    state.canvas.width = DISPLAY_WIDTH;
-    state.canvas.height = DISPLAY_HEIGHT;
-    state.canvasCtx = state.canvas.getContext("2d");
-
-    state.canvasScaled = document.getElementById(
-        "engine-canvas"
-    ) as HTMLCanvasElement;
-    state.canvasScaled.width =
-        state.canvasScaled.width * window.devicePixelRatio;
-    state.canvasScaled.height =
-        state.canvasScaled.height * window.devicePixelRatio;
-    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);
-};
+        document.addEventListener("keydown", (event) => {
+            if (event.key === "Escape") {
+                this.hideModal(false);
+            }
+        });
+    }
 
-const updateCanvas = (
-    pixels: Uint8Array,
-    format: PixelFormat = PixelFormat.RGB
-) => {
-    let offset = 0;
-    for (let index = 0; index < pixels.length; index += format) {
-        const color =
-            (pixels[index] << 24) |
-            (pixels[index + 1] << 16) |
-            (pixels[index + 2] << 8) |
-            (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
-        state.videoBuff.setUint32(offset, color);
-        offset += PixelFormat.RGBA;
+    async initBase() {
+        const background = BACKGROUNDS[this.background_index];
+        this.setBackground(background);
+        this.setVersion(info.version);
     }
-    state.canvasCtx.putImageData(state.image, 0, 0);
-    state.canvasScaledCtx.drawImage(state.canvas, 0, 0);
-};
 
-const clearCanvas = async (
-    color = PIXEL_UNSET_COLOR,
-    { image = null as string, imageScale = 1 } = {}
-) => {
-    state.canvasScaledCtx.fillStyle = `#${color.toString(16).toUpperCase()}`;
-    state.canvasScaledCtx.fillRect(
-        0,
-        0,
-        state.canvasScaled.width,
-        state.canvasScaled.height
-    );
-
-    // in case an image was requested then uses that to load
-    // an image at the center of the screen properly scaled
-    if (image) {
-        const img = await new Promise<HTMLImageElement>((resolve) => {
-            const img = new Image();
-            img.onload = () => {
-                resolve(img);
-            };
-            img.src = image;
-        });
-        const [imgWidth, imgHeight] = [
-            img.width * imageScale * window.devicePixelRatio,
-            img.height * imageScale * window.devicePixelRatio
-        ];
-        const [x0, y0] = [
-            state.canvasScaled.width / 2 - imgWidth / 2,
-            state.canvasScaled.height / 2 - imgHeight / 2
-        ];
-        state.canvasScaledCtx.setTransform(1, 0, 0, 1, 0, 0);
-        try {
-            state.canvasScaledCtx.drawImage(img, x0, y0, imgWidth, imgHeight);
-        } finally {
-            state.canvasScaledCtx.scale(
-                state.canvasScaled.width / state.canvas.width,
-                state.canvasScaled.height / state.canvas.height
-            );
+    async initCanvas() {
+        // initializes the off-screen canvas that is going to be
+        // used in the drawing process
+        this.canvas = document.createElement("canvas");
+        this.canvas.width = DISPLAY_WIDTH;
+        this.canvas.height = DISPLAY_HEIGHT;
+        this.canvasCtx = this.canvas.getContext("2d")!;
+
+        this.canvasScaled = document.getElementById(
+            "engine-canvas"
+        ) as HTMLCanvasElement;
+        this.canvasScaled.width =
+            this.canvasScaled.width * window.devicePixelRatio;
+        this.canvasScaled.height =
+            this.canvasScaled.height * window.devicePixelRatio;
+        this.canvasScaledCtx = this.canvasScaled.getContext("2d")!;
+
+        this.canvasScaledCtx.scale(
+            this.canvasScaled.width / this.canvas.width,
+            this.canvasScaled.height / this.canvas.height
+        );
+        this.canvasScaledCtx.imageSmoothingEnabled = false;
+
+        this.image = this.canvasCtx.createImageData(
+            this.canvas.width,
+            this.canvas.height
+        );
+        this.videoBuff = new DataView(this.image.data.buffer);
+    }
+
+    updateCanvas(pixels: Uint8Array, format: PixelFormat = PixelFormat.RGB) {
+        let offset = 0;
+        for (let index = 0; index < pixels.length; index += format) {
+            const color =
+                (pixels[index] << 24) |
+                (pixels[index + 1] << 16) |
+                (pixels[index + 2] << 8) |
+                (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
+            this.videoBuff!.setUint32(offset, color);
+            offset += PixelFormat.RGBA;
         }
+        this.canvasCtx!.putImageData(this.image!, 0, 0);
+        this.canvasScaledCtx!.drawImage(this.canvas!, 0, 0);
     }
-};
 
-const showToast = async (message: string, error = false, timeout = 3500) => {
-    const toast = document.getElementById("toast");
-    toast.classList.remove("error");
-    if (error) toast.classList.add("error");
-    toast.classList.add("visible");
-    toast.textContent = message;
-    if (state.toastTimeout) clearTimeout(state.toastTimeout);
-    state.toastTimeout = setTimeout(() => {
-        toast.classList.remove("visible");
-        state.toastTimeout = null;
-    }, timeout);
-};
+    async clearCanvas(
+        color = PIXEL_UNSET_COLOR,
+        {
+            image = null,
+            imageScale = 1
+        }: { image?: string | null; imageScale?: number } = {}
+    ) {
+        this.canvasScaledCtx!.fillStyle = `#${color
+            .toString(16)
+            .toUpperCase()}`;
+        this.canvasScaledCtx!.fillRect(
+            0,
+            0,
+            this.canvasScaled!.width,
+            this.canvasScaled!.height
+        );
 
-const showModal = async (
-    message: string,
-    title = "Alert"
-): Promise<boolean> => {
-    const modalContainer = document.getElementById("modal-container");
-    const modalTitle = document.getElementById("modal-title");
-    const modalText = document.getElementById("modal-text");
-    modalContainer.classList.add("visible");
-    modalTitle.textContent = title;
-    modalText.innerHTML = message.replace(/\n/g, "<br/>");
-    const result = (await new Promise((resolve) => {
-        global.modalCallback = resolve;
-    })) as boolean;
-    return result;
-};
+        // in case an image was requested then uses that to load
+        // an image at the center of the screen properly scaled
+        if (image) {
+            const img = await new Promise<HTMLImageElement>((resolve) => {
+                const img = new Image();
+                img.onload = () => {
+                    resolve(img);
+                };
+                img.src = image;
+            });
+            const [imgWidth, imgHeight] = [
+                img.width * imageScale * window.devicePixelRatio,
+                img.height * imageScale * window.devicePixelRatio
+            ];
+            const [x0, y0] = [
+                this.canvasScaled!.width / 2 - imgWidth / 2,
+                this.canvasScaled!.height / 2 - imgHeight / 2
+            ];
+            this.canvasScaledCtx!.setTransform(1, 0, 0, 1, 0, 0);
+            try {
+                this.canvasScaledCtx!.drawImage(
+                    img,
+                    x0,
+                    y0,
+                    imgWidth,
+                    imgHeight
+                );
+            } finally {
+                this.canvasScaledCtx!.scale(
+                    this.canvasScaled!.width / this.canvas!.width,
+                    this.canvasScaled!.height / this.canvas!.height
+                );
+            }
+        }
+    }
 
-const hideModal = async (result = true) => {
-    const modalContainer = document.getElementById("modal-container");
-    modalContainer.classList.remove("visible");
-    if (global.modalCallback) global.modalCallback(result);
-    global.modalCallback = null;
-};
+    async showToast(message: string, error = false, timeout = 3500) {
+        const toast = document.getElementById("toast")!;
+        toast.classList.remove("error");
+        if (error) toast.classList.add("error");
+        toast.classList.add("visible");
+        toast.textContent = message;
+        if (this.toastTimeout) clearTimeout(this.toastTimeout);
+        this.toastTimeout = setTimeout(() => {
+            toast.classList.remove("visible");
+            this.toastTimeout = null;
+        }, timeout);
+    }
 
-const setVersion = (value: string) => {
-    document.getElementById("version").textContent = value;
-};
+    async showModal(message: string, title = "Alert"): Promise<boolean> {
+        const modalContainer = document.getElementById("modal-container")!;
+        const modalTitle = document.getElementById("modal-title")!;
+        const modalText = document.getElementById("modal-text")!;
+        modalContainer.classList.add("visible");
+        modalTitle.textContent = title;
+        modalText.innerHTML = message.replace(/\n/g, "<br/>");
+        const result = (await new Promise((resolve) => {
+            global.modalCallback = resolve;
+        })) as boolean;
+        return result;
+    }
 
-const setEngine = (name: string, upper = true) => {
-    name = upper ? name.toUpperCase() : name;
-    document.getElementById("engine").textContent = name;
-};
+    async hideModal(result = true) {
+        const modalContainer = document.getElementById("modal-container")!;
+        modalContainer.classList.remove("visible");
+        if (global.modalCallback) global.modalCallback(result);
+        global.modalCallback = null;
+    }
 
-const setRom = (name: string, data: Uint8Array) => {
-    state.romName = name;
-    state.romData = data;
-    state.romSize = data.length;
-    document.getElementById("rom-name").textContent = name;
-    document.getElementById("rom-size").textContent = String(data.length);
-};
+    setVersion(value: string) {
+        document.getElementById("version")!.textContent = value;
+    }
 
-const setLogicFrequency = (value: number) => {
-    if (value < 0) showToast("Invalid frequency value!", true);
-    value = Math.max(value, 0);
-    state.logicFrequency = value;
-    document.getElementById("logic-frequency").textContent = String(value);
-};
+    setEngine(name: string, upper = true) {
+        name = upper ? name.toUpperCase() : name;
+        document.getElementById("engine")!.textContent = name;
+    }
 
-const setFps = (value: number) => {
-    if (value < 0) showToast("Invalid FPS value!", true);
-    value = Math.max(value, 0);
-    state.fps = value;
-    document.getElementById("fps-count").textContent = String(value);
-};
+    setRom(name: string, data: Uint8Array) {
+        this.romName = name;
+        this.romData = data;
+        this.romSize = data.length;
+        //@todo update this one
+        //Component.get<KeyValue>("diag:rom-name").value = name;
+        //Component.get<KeyValue>("diag:rom-size").value = `${data.length} bytes`;
+    }
 
-const setBackground = (value: string) => {
-    document.body.style.backgroundColor = `#${value}`;
-    document.getElementById(
-        "footer-background"
-    ).style.backgroundColor = `#${value}f2`;
-};
+    setLogicFrequency(value: number) {
+        if (value < 0) this.showToast("Invalid frequency value!", true);
+        value = Math.max(value, 0);
+        this.logicFrequency = value;
+        document.getElementById("logic-frequency")!.textContent = String(value);
+    }
 
-const toggleRunning = () => {
-    if (state.paused) {
-        resume();
-    } else {
-        pause();
+    setFps(value: number) {
+        if (value < 0) this.showToast("Invalid FPS value!", true);
+        value = Math.max(value, 0);
+        this.fps = value;
+        //@todo
+        //Component.get<KeyValue>("diag:framerate").value = `${value} FPS`;
     }
-};
 
-const pause = () => {
-    state.paused = true;
-    const buttonPause = document.getElementById("button-pause");
-    const img = buttonPause.getElementsByTagName("img")[0];
-    const span = buttonPause.getElementsByTagName("span")[0];
-    buttonPause.classList.add("enabled");
-    // @ts-ignore: ts(2580)
-    img.src = require("./res/play.svg");
-    span.textContent = "Resume";
-};
+    setBackground(value: string) {
+        document.body.style.backgroundColor = `#${value}`;
+        document.getElementById(
+            "footer-background"
+        )!.style.backgroundColor = `#${value}f2`;
+    }
 
-const resume = () => {
-    state.paused = false;
-    state.nextTickTime = new Date().getTime();
-    const buttonPause = document.getElementById("button-pause");
-    const img = buttonPause.getElementsByTagName("img")[0];
-    const span = buttonPause.getElementsByTagName("span")[0];
-    buttonPause.classList.remove("enabled");
-    // @ts-ignore: ts(2580)
-    img.src = require("./res/pause.svg");
-    span.textContent = "Pause";
-};
+    toggleRunning() {
+        if (this.paused) {
+            this.resume();
+        } else {
+            this.pause();
+        }
+    }
 
-const toggleWindow = () => {
-    maximize();
-};
+    pause() {
+        this.paused = true;
+        const buttonPause = document.getElementById("button-pause")!;
+        const img = buttonPause.getElementsByTagName("img")[0];
+        const span = buttonPause.getElementsByTagName("span")[0];
+        buttonPause.classList.add("enabled");
+        // @ts-ignore: ts(2580)
+        img.src = require("./res/play.svg");
+        span.textContent = "Resume";
+    }
+
+    resume() {
+        this.paused = false;
+        this.nextTickTime = new Date().getTime();
+        const buttonPause = document.getElementById("button-pause")!;
+        const img = buttonPause.getElementsByTagName("img")[0];
+        const span = buttonPause.getElementsByTagName("span")[0];
+        buttonPause.classList.remove("enabled");
+        // @ts-ignore: ts(2580)
+        img.src = require("./res/pause.svg");
+        span.textContent = "Pause";
+    }
+
+    /**
+     * Resets the emulator machine to the start state and loads
+     * the ROM that is currently set in the emulator.
+     */
+    reset() {
+        this.start({ engine: null });
+    }
+
+    toggleWindow() {
+        this.maximize();
+    }
+
+    /**
+     * Maximizes the emulator's viewport taking up all the available
+     * window space. This method is responsible for keeping the aspect
+     * ratio of the emulator canvas according to the width/height ratio.
+     */
+    maximize() {
+        const canvasContainer = document.getElementById("canvas-container")!;
+        canvasContainer.classList.add("fullscreen");
+
+        window.addEventListener("resize", this.crop);
+
+        this.crop();
+    }
 
-const maximize = () => {
-    const canvasContainer = document.getElementById("canvas-container");
-    canvasContainer.classList.add("fullscreen");
+    /**
+     * Restore the emulator's viewport to the minimal size, should make all
+     * the other emulator's meta-information (info, buttons, etc.) visible.
+     */
+    minimize() {
+        const canvasContainer = document.getElementById("canvas-container")!;
+        const engineCanvas = document.getElementById("engine-canvas")!;
+        canvasContainer.classList.remove("fullscreen");
+        engineCanvas.style.width = "";
+        engineCanvas.style.height = "";
+        window.removeEventListener("resize", this.crop);
+    }
 
-    window.addEventListener("resize", crop);
+    crop() {
+        // @todo let's make this more flexible
+        const engineCanvas = document.getElementById("engine-canvas")!;
+
+        // calculates the window ratio as this is fundamental to
+        // determine the proper way to crop the fullscreen
+        const windowRatio = window.innerWidth / window.innerHeight;
+
+        // in case the window is wider (more horizontal than the base ratio)
+        // this means that we must crop horizontally
+        if (windowRatio > DISPLAY_RATIO) {
+            engineCanvas.style.width = `${
+                window.innerWidth * (DISPLAY_RATIO / windowRatio)
+            }px`;
+            engineCanvas.style.height = `${window.innerHeight}px`;
+        } else {
+            engineCanvas.style.width = `${window.innerWidth}px`;
+            engineCanvas.style.height = `${
+                window.innerHeight * (windowRatio / DISPLAY_RATIO)
+            }px`;
+        }
+    }
 
-    crop();
+    async fetchRom(romPath: string): Promise<[string, Uint8Array]> {
+        // extracts the name of the ROM from the provided
+        // path by splitting its structure
+        const romPathS = romPath.split(/\//g);
+        let romName = romPathS[romPathS.length - 1].split("?")[0];
+        const romNameS = romName.split(/\./g);
+        romName = `${romNameS[0]}.${romNameS[romNameS.length - 1]}`;
+
+        // loads the ROM data and converts it into the
+        // target byte array buffer (to be used by WASM)
+        const response = await fetch(ROM_PATH);
+        const blob = await response.blob();
+        const arrayBuffer = await blob.arrayBuffer();
+        const romData = new Uint8Array(arrayBuffer);
+
+        // returns both the name of the ROM and the data
+        // contents as a byte array
+        return [romName, romData];
+    }
+}
+
+type Global = {
+    modalCallback: Function | null;
 };
 
-const minimize = () => {
-    const canvasContainer = document.getElementById("canvas-container");
-    const engineCanvas = document.getElementById("engine-canvas");
-    canvasContainer.classList.remove("fullscreen");
-    engineCanvas.style.width = null;
-    engineCanvas.style.height = null;
-    window.removeEventListener("resize", crop);
+//@todo check if this is really required
+const global: Global = {
+    modalCallback: null
 };
 
-const crop = () => {
-    const engineCanvas = document.getElementById("engine-canvas");
-
-    // calculates the window ratio as this is fundamental to
-    // determine the proper way to crop the fulscreen
-    const windowRatio = window.innerWidth / window.innerHeight;
-
-    // in case the window is wider (more horizontal than the base ratio)
-    // this means that we must crop horizontaly
-    if (windowRatio > DISPLAY_RATIO) {
-        engineCanvas.style.width = `${
-            window.innerWidth * (DISPLAY_RATIO / windowRatio)
-        }px`;
-        engineCanvas.style.height = `${window.innerHeight}px`;
-    } else {
-        engineCanvas.style.width = `${window.innerWidth}px`;
-        engineCanvas.style.height = `${
-            window.innerHeight * (windowRatio / DISPLAY_RATIO)
-        }px`;
+declare global {
+    interface Window {
+        panic: (message: string) => void;
     }
-};
+}
 
-const reset = () => {
-    start({ engine: null });
+window.panic = (message: string) => {
+    console.error(message);
 };
 
-const fetchRom = async (romPath: string): Promise<[string, Uint8Array]> => {
-    // extracts the name of the ROM from the provided
-    // path by splitting its structure
-    const romPathS = romPath.split(/\//g);
-    let romName = romPathS[romPathS.length - 1].split("?")[0];
-    const romNameS = romName.split(/\./g);
-    romName = `${romNameS[0]}.${romNameS[romNameS.length - 1]}`;
-
-    // loads the ROM data and converts it into the
-    // target byte array buffer (to be used by WASM)
-    const response = await fetch(ROM_PATH);
-    const blob = await response.blob();
-    const arrayBuffer = await blob.arrayBuffer();
-    const romData = new Uint8Array(arrayBuffer);
-
-    // returns both the name of the ROM and the data
-    // contents as a byte array
-    return [romName, romData];
+const wasm = async () => {
+    await _wasm();
+    GameBoy.set_panic_hook_ws();
 };
 
 (async () => {
-    await main();
+    (globalThis as any).__VUE_OPTIONS_API__ = true;
+    (globalThis as any).__VUE_PROD_DEVTOOLS__ = false;
+
+    createApp(App).mount("#app");
+
+    const emulator = new Emulator();
+    await emulator.main();
 })();
diff --git a/examples/web/package.json b/examples/web/package.json
index 3b25a29a46c25f7a5e97cc0e436f3f5220333503..cb5ebe5407f63490f73e6551f1c2fd59881f0dc2 100644
--- a/examples/web/package.json
+++ b/examples/web/package.json
@@ -17,8 +17,10 @@
     "source": "index.ts",
     "devDependencies": {
         "@parcel/transformer-typescript-tsc": "^2.6.2",
+        "@parcel/transformer-vue": "^2.6.2",
         "parcel": "^2.6.2",
         "prettier": "^2.7.1",
-        "typescript": "^4.7.4"
+        "typescript": "^4.7.4",
+        "vue": "^3.2.37"
     }
 }
diff --git a/examples/web/tsconfig.json b/examples/web/tsconfig.json
index 54b8f23f78f97da010f950ac602328134bcce54a..8fbd457e4f663d87bd2c211a14d2354d6413c4c5 100644
--- a/examples/web/tsconfig.json
+++ b/examples/web/tsconfig.json
@@ -6,6 +6,12 @@
         "allowSyntheticDefaultImports": true,
         "target": "es6",
         "noImplicitAny": true,
+        "noImplicitThis": true,
+        "alwaysStrict": true,
+        "strictBindCallApply": true,
+        "strictNullChecks": true,
+        "strictFunctionTypes": true,
+        "strictPropertyInitialization": true,
         "sourceMap": true,
         "outDir": ".",
         "baseUrl": ".",
diff --git a/examples/web/vue/app.vue b/examples/web/vue/app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ee279da326f393316fb3d8b8bd9aa4fd1da78b85
--- /dev/null
+++ b/examples/web/vue/app.vue
@@ -0,0 +1,23 @@
+<template>
+    <div v-bind:class="'hello'" v-on:click="() => count++">Hello This {{ name }} {{ count}}!</div>
+</template>
+
+<style scoped>
+.hello {
+    cursor: pointer;
+    user-select: none;
+}
+</style>
+
+<script>
+export const App = {
+    data() {
+        return {
+            name: "Vue",
+            count: 1
+        };
+    }
+};
+
+export default App;
+</script>
diff --git a/examples/web/vue/components/button.vue b/examples/web/vue/components/button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fe4c1679e018f02dfbe6817ecca95552f214e41a
--- /dev/null
+++ b/examples/web/vue/components/button.vue
@@ -0,0 +1,16 @@
+<template>
+    <div v-on:click="() => count++">Hello {{ name }} {{ count}}!</div>
+</template>
+
+<script>
+export const Button = {
+    data() {
+        return {
+            name: "Vue",
+            count: 1
+        };
+    }
+};
+
+export default Button;
+</script>