Skip to content
Snippets Groups Projects
Verified Commit 98492c22 authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

feat: initial support for drawing to display

parent 3d738d0e
No related branches found
No related tags found
1 merge request!9Version 0.4.0 🍾
Pipeline #1339 passed
import { Emulator, startApp } from "./react/app";
import { Emulator, PixelFormat, startApp } from "./react/app";
import { default as _wasm, GameBoy, PadKey, PpuMode } from "./lib/boytacean.js";
import info from "./package.json";
......@@ -43,13 +43,6 @@ const KEYS: Record<string, number> = {
const ROM_PATH = require("../../res/roms/20y.gb");
// Enumeration that describes the multiple pixel
// formats and the associated size in bytes.
enum PixelFormat {
RGB = 3,
RGBA = 4
}
/**
* Top level class that controls the emulator behaviour
* and "joins" all the elements together to bring input/output
......@@ -995,6 +988,20 @@ class GameboyEmulator implements Emulator {
this.start({ engine: null });
}
/**
* Returns the array buffer that contains the complete set of
* pixel data that is going to be drawn.
*
* @returns The current pixel data for the emulator display.
*/
getImageBuffer(): Uint8Array {
return this.gameBoy!.frame_buffer_eager();
}
getPixelFormat(): PixelFormat {
return PixelFormat.RGB;
}
toggleWindow() {
this.maximize();
}
......
......@@ -9,6 +9,7 @@ import {
ButtonIncrement,
ButtonSwitch,
Display,
DrawHandler,
Footer,
Info,
Link,
......@@ -21,6 +22,11 @@ import {
import "./app.css";
/**
* Top level interface that declares the main abstract
* interface of an emulator structured entity.
* Should allow typical hardware operations to be performed.
*/
export interface Emulator {
getName(): string;
getVersion(): string;
......@@ -29,6 +35,17 @@ export interface Emulator {
pause(): void;
resume(): void;
reset(): void;
getImageBuffer(): Uint8Array;
getPixelFormat(): PixelFormat;
}
/**
* Enumeration that describes the multiple pixel
* formats and the associated size in bytes.
*/
export enum PixelFormat {
RGB = 3,
RGBA = 4
}
type AppProps = {
......@@ -53,6 +70,11 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
const onThemeClick = () => {
setBackgroundIndex((backgroundIndex + 1) % backgrounds.length);
};
const onDrawHAndler = (handler: DrawHandler) => {
setTimeout(() => {
handler(emulator.getImageBuffer(), PixelFormat.RGB);
}, 3000);
};
useEffect(() => {
document.body.style.backgroundColor = `#${getBackground()}`;
});
......@@ -67,7 +89,7 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
<PanelSplit
left={
<div>
<Display />
<Display onDrawHandler={onDrawHAndler} />
</div>
}
>
......
import React, { FC, useState } from "react";
import React, { FC, useRef, useEffect } from "react";
import { PixelFormat } from "../../app";
import "./display.css";
declare const require: any;
/**
* Function that handles a draw operation into a
* certain drawing context.
*/
export type DrawHandler = (pixels: Uint8Array, format: PixelFormat) => void;
type DisplayOptions = {
width: number;
height: number;
logicWidth: number;
logicHeight: number;
scale?: number;
};
......@@ -14,18 +23,53 @@ type DisplayProps = {
options?: DisplayOptions;
size?: string;
style?: string[];
onDrawHandler?: (caller: DrawHandler) => void;
};
type CanvasContents = {
canvasCtx: CanvasRenderingContext2D;
canvasBuffer: HTMLCanvasElement;
canvasBufferCtx: CanvasRenderingContext2D;
imageData: ImageData;
videoBuffer: DataView;
};
export const Display: FC<DisplayProps> = ({
options = {},
options = { width: 320, height: 288, logicWidth: 160, logicHeight: 144 },
size = "small",
style = []
style = [],
onDrawHandler
}) => {
options = { ...options, ...{ width: 320, height: 288 } };
const classes = () => ["display", size, ...style].join(" ");
options = {
...options,
...{ width: 320, height: 288, logicWidth: 160, logicHeight: 144 }
};
if (!options.scale) {
options.scale = window.devicePixelRatio ? window.devicePixelRatio : 1;
}
let canvasContents: CanvasContents | null = null;
const classes = () => ["display", size, ...style].join(" ");
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (canvasRef.current && !canvasContents) {
canvasContents = initCanvas(
options.logicWidth,
options.logicHeight,
canvasRef.current
);
}
});
if (onDrawHandler) {
onDrawHandler((pixels: Uint8Array, format: PixelFormat) => {
if (!canvasContents) return;
updateCanvas(canvasContents, pixels, format);
});
}
return (
<div id="display" className={classes()}>
<span id="display-close" className="magnify-button canvas-close">
......@@ -37,6 +81,7 @@ export const Display: FC<DisplayProps> = ({
</span>
<div className="display-frame">
<canvas
ref={canvasRef}
id="display-canvas"
className="display-canvas"
width={options.width * options.scale}
......@@ -47,4 +92,58 @@ export const Display: FC<DisplayProps> = ({
);
};
const initCanvas = (
width: number,
height: number,
canvas: HTMLCanvasElement
): CanvasContents => {
// initializes the off-screen canvas that is going to be
// used in the drawing process, this is used essentially for
// performance reasons as it provides a way to draw pixels
// in the original size instead of the target one
const canvasBuffer = document.createElement("canvas");
canvasBuffer.width = width;
canvasBuffer.height = height;
const canvasBufferCtx = canvasBuffer.getContext("2d")!;
const imageData = canvasBufferCtx.createImageData(
canvasBuffer.width,
canvasBuffer.height
);
const videoBuffer = new DataView(imageData.data.buffer);
const canvasCtx = canvas.getContext("2d")!;
canvasCtx.scale(
canvas.width / canvasBuffer.width,
canvas.height / canvasBuffer.height
);
canvasCtx.imageSmoothingEnabled = false;
return {
canvasCtx: canvasCtx,
canvasBuffer: canvasBuffer,
canvasBufferCtx: canvasBufferCtx,
imageData: imageData,
videoBuffer: videoBuffer
};
};
const updateCanvas = (
canvasContents: CanvasContents,
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);
canvasContents.videoBuffer.setUint32(offset, color);
offset += PixelFormat.RGBA;
}
canvasContents.canvasBufferCtx.putImageData(canvasContents.imageData, 0, 0);
canvasContents.canvasCtx.drawImage(canvasContents.canvasBuffer, 0, 0);
};
export default Display;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment