Newer
Older
import React, { FC, useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom/client";
ButtonIncrement,
ButtonSwitch,
export type Callback<T> = (owner: T) => void;
/**
* Abstract class that implements the basic functionality
* part of the definition of the Observer pattern.
*
* @see {@link https://en.wikipedia.org/wiki/Observer_pattern}
*/
export class Observable {
private events: Record<string, [Callback<this>]> = {};
bind(event: string, callback: Callback<this>) {
const callbacks = this.events[event] ?? [];
if (callbacks.includes(callback)) return;
callbacks.push(callback);
this.events[event] = callbacks;
}
trigger(event: string) {
const callbacks = this.events[event] ?? [];
callbacks.forEach((c) => c(this));
}
}
export type RomInfo = {
name?: string;
data?: Uint8Array;
size?: number;
extra?: Record<string, string | undefined>;
};
export interface ObservableI {
bind(event: string, callback: Callback<this>): void;
trigger(event: string): void;
}
/**
* 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 extends ObservableI {
/**
* Obtains the descriptive name of the emulator.
*
* @returns The descriptive name of the emulator.
*/
/**
* Obtains a semantic version string for the current
* version of the emulator.
*
* @returns The semantic version string.
* @see {@link https://semver.org}
*/
/**
* Obtains a URL to the page describing the current version
* of the emulator.
*
* @returns A URL to the page describing the current version
* of the emulator.
*/
/**
* Obtains the pixel format of the emulator's display
* image buffer (eg: RGB).
*
* @returns The pixel format used for the emulator's
* image buffer.
*/
/**
* Obtains the complete image buffer as a sequence of
* bytes that respects the current pixel format from
* `getPixelFormat()`. This method returns an in memory
* pointer to the heap and not a copy.
*
* @returns The byte based image buffer that respects
* the emulator's pixel format.
*/
/**
* Obtains information about the ROM that is currently
* loaded in the emulator.
*
* @returns Structure containing the information about
* the ROM that is currently loaded in the emulator.
*/
/**
* Returns the current logic framerate of the running
* emulator.
*
* @return The current logic framerate of the running
* emulator.
*/
/**
* Toggle the running state of the emulator between paused
* and running, prevents consumers from the need to access
* the current running state of the emulator to implement
* a logic toggle.
*/
toggleRunning(): void;
pause(): void;
resume(): void;
/**
* Resets the emulator machine to the start state and
* re-loads the ROM that is currently set in the emulator.
*/
}
/**
* Enumeration that describes the multiple pixel
* formats and the associated size in bytes.
*/
export enum PixelFormat {
RGB = 3,
RGBA = 4
backgrounds?: string[];
};
export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => {
const [paused, setPaused] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
const [backgroundIndex, setBackgroundIndex] = useState(0);
const [romInfo, setRomInfo] = useState<RomInfo>({});
const [framerate, setFramerate] = useState(0);
const [keyaction, setKeyaction] = useState<string | null>(null);
const frameRef = useRef<boolean>(false);
const getPauseText = () => (paused ? "Resume" : "Pause");
const getPauseIcon = () =>
paused ? require("../res/play.svg") : require("../res/pause.svg");
const getBackground = () => backgrounds[backgroundIndex];
const onPauseClick = () => {
emulator.toggleRunning();
setPaused(!paused);
const onResetClick = () => {
emulator.reset();
};
setFullscreen(!fullscreen);
const onThemeClick = () => {
setBackgroundIndex((backgroundIndex + 1) % backgrounds.length);
};
const onMinimize = () => {
setFullscreen(!fullscreen);
};
if (frameRef.current) return;
frameRef.current = true;
handler(emulator.getImageBuffer(), PixelFormat.RGB);
setFramerate(emulator.getFramerate());
const onClearHandler = (handler: ClearHandler) => {
if (errorRef.current) return;
errorRef.current = true;
emulator.bind("error", async () => {
await handler(undefined, require("../res/storm.png"), 0.2);
});
};
const onKeyDown = (event: KeyboardEvent) => {};
useEffect(() => {
document.body.style.backgroundColor = `#${getBackground()}`;
}, [backgroundIndex]);
useEffect(() => {
switch (keyaction) {
case "Escape":
setFullscreen(false);
setKeyaction(null);
break;
case "Fullscreen":
setFullscreen(!fullscreen);
setKeyaction(null);
break;
}
}, [keyaction]);
useEffect(() => {
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
event.stopPropagation();
event.preventDefault();
}
if (event.key === "f" && event.ctrlKey === true) {
event.stopPropagation();
event.preventDefault();
const romInfo = emulator.getRomInfo();
setRomInfo(romInfo);
});
<Footer color={getBackground()}>
Built with ❤️ by{" "}
<Display
fullscreen={fullscreen}
onDrawHandler={onDrawHandler}
onMinimize={onMinimize}
/>
text={emulator.getName()}
version={emulator.getVersion()}
versionUrl={emulator.getVersionUrl()}
iconSrc={require("../res/thunder.png")}
></Title>
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<Section>
<Paragraph>
This is a{" "}
<Link
href="https://en.wikipedia.org/wiki/Game_Boy"
target="_blank"
>
Game Boy
</Link>{" "}
emulator built using the{" "}
<Link href="https://www.rust-lang.org" target="_blank">
Rust Programming Language
</Link>{" "}
and is running inside this browser with the help of{" "}
<Link href="https://webassembly.org/" target="_blank">
WebAssembly
</Link>
.
</Paragraph>
<Paragraph>
You can check the source code of it at{" "}
<Link
href="https://gitlab.stage.hive.pt/joamag/boytacean"
target="_blank"
>
GitLab
</Link>
.
</Paragraph>
<Paragraph>
TIP: Drag and Drop ROM files to the Browser to load the
ROM.
</Paragraph>
</Section>
<ButtonContainer>
<Button
text={getPauseText()}
image={getPauseIcon()}
imageAlt="pause"
onClick={onPauseClick}
/>
<Button
text={"Reset"}
image={require("../res/reset.svg")}
imageAlt="reset"
onClick={onResetClick}
/>
<Button
text={"Fullscreen"}
image={require("../res/maximise.svg")}
imageAlt="maximise"
onClick={onFullscreenClick}
/>
<Button
text={"Theme"}
image={require("../res/marker.svg")}
imageAlt="marker"
onClick={onThemeClick}
/>
</ButtonContainer>
<Pair
key="rom"
name={"ROM"}
value={romInfo.name ?? "-"}
/>
<Pair
key="rom-size"
name={"ROM Size"}
value={romInfo.name ? `${romInfo.size} bytes` : "-"}
/>
<Pair
key="rom-type"
name={"ROM Type"}
value={
romInfo.extra?.romType
? `${romInfo.extra?.romType}`
: "-"
}
/>
<Pair
key="framerate"
name={"Framerate"}
value={`${framerate} fps`}
/>
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
<Pair
key="button-tobias"
name={"Button Increment"}
valueNode={
<ButtonIncrement
value={200}
delta={100}
min={0}
suffix={"Hz"}
/>
}
/>
<Pair
key="button-cpu"
name={"Button Switch"}
valueNode={
<ButtonSwitch
options={["NEO", "CLASSIC"]}
size={"large"}
style={["simple"]}
onChange={(v) => alert(v)}
/>
}
/>
</Info>
</Section>
</PanelSplit>
export const startApp = (
element: string,
emulator: Emulator,
backgrounds: string[]
) => {
const elementRef = document.getElementById(element);
if (!elementRef) {
return;
}
const root = ReactDOM.createRoot(elementRef);
root.render(<App emulator={emulator} backgrounds={backgrounds} />);