diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c3e07b9e26049c73f5bf89adbf83f93f334083e..bbcc71bbc4b239fee0a1d053d3adfa810e868798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * +## [0.4.1] - 2022-11-06 + +### Added + +* Logic frequency control using on click UI and keyboard +* Support for on screen keyboard for Game Boy +* Support for remote ROM loading using URL - [#3](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/3) + ## [0.4.0] - 2022-11-01 ### Added diff --git a/Cargo.toml b/Cargo.toml index 0d90a81bb287fdf92180c5d9a707f86bf3850f5d..fda46c6004ff25d43384067fad3f1b56f5af0d79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "boytacean" description = "A Game Boy emulator that is written in Rust." -version = "0.4.0" +version = "0.4.1" authors = ["João Magalhães <joamag@gmail.com>"] license = "Apache-2.0" repository = "https://gitlab.stage.hive.pt/joamag/boytacean" diff --git a/README.md b/README.md index c335a31d80f17ac402dda3c5b528cc264d345419..dd8077ba3b31b08c74609b59f3f9e7363703a131 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,11 @@ cd dist && python3 -m http.server * [YouTube - The Ultimate Game Boy Talk (33c3)](https://www.youtube.com/watch?v=HyzD8pNlpwI) +### Other + +* [GitHub - gbdev/awesome-gbdev](https://github.com/gbdev/awesome-gbdev) +* [GitHub - Hacktix/Bootix](https://github.com/Hacktix/Bootix) + ## License Boyacian is currently licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/). diff --git a/examples/sdl/Cargo.toml b/examples/sdl/Cargo.toml index 0c50371b7608b756ffeefdf6b45191188bd41829..ed2ae54d57afa13e4df15ed26ac841567cc95d73 100644 --- a/examples/sdl/Cargo.toml +++ b/examples/sdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boytacean-sdl" -version = "0.4.0" +version = "0.4.1" authors = ["João Magalhães <joamag@gmail.com>"] description = "Game Boy Emulator SDL (Desktop) Application" license = "Apache-2.0" diff --git a/examples/web/index.css b/examples/web/index.css index d8dd1b1daf19c6ae8772aadec8ad2ac13a9123ab..771be0c42ea9be25aae313c8b59c041e3faa17aa 100644 --- a/examples/web/index.css +++ b/examples/web/index.css @@ -24,6 +24,7 @@ a:hover { html { margin: 0px 0px 0px 0px; padding: 0px 0px 0px 0px; + touch-action: pan-x pan-y; } body { diff --git a/examples/web/index.ts b/examples/web/index.ts index cd8afa06d3e5d20999bd0bc491096e640098a657..4da7b07c62f02fba865a0bfefc4571d69d126b6e 100644 --- a/examples/web/index.ts +++ b/examples/web/index.ts @@ -1,6 +1,6 @@ import { Emulator, - Observable, + EmulatorBase, PixelFormat, RomInfo, startApp @@ -17,11 +17,11 @@ import info from "./package.json"; declare const require: any; -const LOGIC_HZ = 600; -const VISUAL_HZ = 60; +const LOGIC_HZ = 4194304; +const VISUAL_HZ = 59.7275; const IDLE_HZ = 10; -const FREQUENCY_DELTA = 60; +const FREQUENCY_DELTA = 400000; const SAMPLE_RATE = 2; @@ -46,6 +46,17 @@ const KEYS: Record<string, number> = { s: PadKey.B }; +const KEYS_NAME: Record<string, number> = { + ArrowUp: PadKey.Up, + ArrowDown: PadKey.Down, + ArrowLeft: PadKey.Left, + ArrowRight: PadKey.Right, + Start: PadKey.Start, + Select: PadKey.Select, + A: PadKey.A, + B: PadKey.B +}; + const ARROW_KEYS: Record<string, boolean> = { ArrowUp: true, ArrowDown: true, @@ -60,7 +71,7 @@ const ROM_PATH = require("../../res/roms/20y.gb"); * and "joins" all the elements together to bring input/output * of the associated machine. */ -class GameboyEmulator extends Observable implements Emulator { +class GameboyEmulator extends EmulatorBase implements Emulator { /** * The Game Boy engine (probably coming from WASM) that * is going to be used for the emulation. @@ -71,7 +82,7 @@ class GameboyEmulator extends Observable implements Emulator { * The descriptive name of the engine that is currently * in use to emulate the system. */ - private engine: string | null = null; + private _engine: string | null = null; private logicFrequency: number = LOGIC_HZ; private visualFrequency: number = VISUAL_HZ; @@ -89,6 +100,11 @@ class GameboyEmulator extends Observable implements Emulator { private cartridge: Cartridge | null = null; async main() { + // parses the current location URL as retrieves + // some of the "relevant" GET parameters for logic + const params = new URLSearchParams(window.location.search); + const romUrl = params.get("url"); + // initializes the WASM module, this is required // so that the global symbols become available await wasm(); @@ -99,7 +115,7 @@ class GameboyEmulator extends Observable implements Emulator { // boots the emulator subsystem with the initial // ROM retrieved from a remote data source - await this.boot({ loadRom: true }); + await this.boot({ loadRom: true, romPath: romUrl || undefined }); // the counter that controls the overflowing cycles // from tick to tick operation @@ -122,7 +138,11 @@ class GameboyEmulator extends Observable implements Emulator { let currentTime = new Date().getTime(); try { - pending = this.tick(currentTime, pending); + pending = this.tick( + currentTime, + pending, + Math.round(this.logicFrequency / this.visualFrequency) + ); } catch (err) { // sets the default error message to be displayed // to the user, this value may be overridden in case @@ -231,7 +251,7 @@ class GameboyEmulator extends Observable implements Emulator { // 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) { + 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); @@ -277,7 +297,7 @@ class GameboyEmulator extends Observable implements Emulator { // 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); + ({ name: romName, data: romData } = await this.fetchRom(romPath)); } else if (romName === null || romData === null) { [romName, romData] = [this.romName, this.romData]; } @@ -308,7 +328,7 @@ class GameboyEmulator extends Observable implements Emulator { // updates the name of the currently selected engine // to the one that has been provided (logic change) - if (engine) this.engine = engine; + if (engine) this._engine = engine; // updates the complete set of global information that // is going to be displayed @@ -340,11 +360,11 @@ class GameboyEmulator extends Observable implements Emulator { switch (event.key) { case "+": - this.logicFrequency += FREQUENCY_DELTA; + this.frequency += FREQUENCY_DELTA; break; case "-": - this.logicFrequency -= FREQUENCY_DELTA; + this.frequency -= FREQUENCY_DELTA; break; } }); @@ -367,27 +387,35 @@ class GameboyEmulator extends Observable implements Emulator { this.cartridge = cartridge; } - getName() { + get name(): string { return "Boytacean"; } - getDevice(): string { + get device(): string { return "Game Boy"; } - getDeviceUrl(): string { + get deviceUrl(): string { return "https://en.wikipedia.org/wiki/Game_Boy"; } - getVersion() { + get engines() { + return ["neo"]; + } + + get engine() { + return this._engine; + } + + get version(): string { return info.version; } - getVersionUrl() { + get versionUrl(): string { return "https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md"; } - getPixelFormat(): PixelFormat { + get pixelFormat(): PixelFormat { return PixelFormat.RGB; } @@ -397,11 +425,11 @@ class GameboyEmulator extends Observable implements Emulator { * * @returns The current pixel data for the emulator display. */ - getImageBuffer(): Uint8Array { + get imageBuffer(): Uint8Array { return this.gameBoy!.frame_buffer_eager(); } - getRomInfo(): RomInfo { + get romInfo(): RomInfo { return { name: this.romName || undefined, data: this.romData || undefined, @@ -414,7 +442,17 @@ class GameboyEmulator extends Observable implements Emulator { }; } - getFramerate(): number { + get frequency(): number { + return this.logicFrequency; + } + + set frequency(value: number) { + value = Math.max(value, 0); + this.logicFrequency = value; + this.trigger("frequency", value); + } + + get framerate(): number { return this.fps; } @@ -443,6 +481,18 @@ class GameboyEmulator extends Observable implements Emulator { this.boot({ engine: null }); } + keyPress(key: string) { + const keyCode = KEYS_NAME[key]; + if (!keyCode) return; + this.gameBoy!.key_press(keyCode); + } + + keyLift(key: string) { + const keyCode = KEYS_NAME[key]; + if (!keyCode) return; + this.gameBoy!.key_lift(keyCode); + } + benchmark(count = 50000000) { let cycles = 0; this.pause(); @@ -464,7 +514,9 @@ class GameboyEmulator extends Observable implements Emulator { } } - private async fetchRom(romPath: string): Promise<[string, Uint8Array]> { + private async fetchRom( + romPath: string + ): Promise<{ name: string; data: Uint8Array }> { // extracts the name of the ROM from the provided // path by splitting its structure const romPathS = romPath.split(/\//g); @@ -474,14 +526,17 @@ class GameboyEmulator extends Observable implements Emulator { // 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 response = await fetch(romPath); 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]; + return { + name: romName, + data: romData + }; } } diff --git a/examples/web/package.json b/examples/web/package.json index 3f3fb0ce3f4a4c3b674407b58fddcf502b685f06..da7ea87072bc57e228782b75098f250e9cbd07b4 100644 --- a/examples/web/package.json +++ b/examples/web/package.json @@ -1,6 +1,6 @@ { "name": "boytacean-web", - "version": "0.4.0", + "version": "0.4.1", "description": "The web version of Boytacean", "repository": { "type": "git", diff --git a/examples/web/react/app.css b/examples/web/react/app.css index 4c1786700a5d9be248c7a518c125744177b3972e..d44f28a65d33844a5029ac0909c352465c2cb860 100644 --- a/examples/web/react/app.css +++ b/examples/web/react/app.css @@ -14,6 +14,6 @@ @media only screen and (max-width: 1120px) { .app .display-container { - margin-top: 0px; + margin-top: 14px; } } diff --git a/examples/web/react/app.tsx b/examples/web/react/app.tsx index e8b7269c0320e20342cdce9b3617d380f92f7acd..4111def049b5b342346e685e55fc172ae2103c55 100644 --- a/examples/web/react/app.tsx +++ b/examples/web/react/app.tsx @@ -28,7 +28,7 @@ import { import "./app.css"; -export type Callback<T> = (owner: T, params?: Record<string, any>) => void; +export type Callback<T> = (owner: T, params?: any) => void; /** * Abstract class that implements the basic functionality @@ -54,7 +54,7 @@ export class Observable { this.events[event] = callbacks; } - trigger(event: string, params?: Record<string, any>) { + trigger(event: string, params?: any) { const callbacks = this.events[event] ?? []; callbacks.forEach((c) => c(this, params)); } @@ -87,78 +87,77 @@ export interface ObservableI { */ export interface Emulator extends ObservableI { /** - * Obtains the descriptive name of the emulator. - * - * @returns The descriptive name of the emulator. + * The descriptive name of the emulator. */ - getName(): string; + get name(): string; /** - * Obtains the name of the name of the hardware that - * is being emulated by the emulator (eg: Super Nintendo). - * - * @returns The name of the hardware that is being - * emulated. + * The name of the the hardware that is being emulated + * by the emulator (eg: Super Nintendo). */ - getDevice(): string; + get device(): string; - getDeviceUrl?(): string; + get deviceUrl(): string | undefined; /** - * Obtains a semantic version string for the current - * version of the emulator. + * A semantic version string for the current version + * of the emulator. * - * @returns The semantic version string. * @see {@link https://semver.org} */ - getVersion(): string; + get version(): string; /** - * Obtains a URL to the page describing the current version - * of the emulator. - * - * @returns A URL to the page describing the current version + * The URL to the page describing the current version * of the emulator. */ - getVersionUrl?(): string; + get versionUrl(): string; + + /** + * The complete set of engine names that can be used + * in the re-boot operation. + */ + get engines(): string[]; + + /** + * The name of the current execution engine being used + * by the emulator. + */ + get engine(): string | null; /** - * Obtains the pixel format of the emulator's display + * The pixel format of the emulator's display * image buffer (eg: RGB). - * - * @returns The pixel format used for the emulator's - * image buffer. */ - getPixelFormat(): PixelFormat; + get pixelFormat(): PixelFormat; /** - * Obtains the complete image buffer as a sequence of + * Gets 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. */ - getImageBuffer(): Uint8Array; + get imageBuffer(): Uint8Array; /** - * Obtains information about the ROM that is currently + * Gets information about the ROM that is currently + * loaded in the emulator, using a structure containing + * the 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. */ - getRomInfo(): RomInfo; + get romInfo(): RomInfo; /** - * Returns the current logic framerate of the running - * emulator. - * - * @returns The current logic framerate of the running - * emulator. + * The current CPU frequency (logic) of the emulator, + * should impact other elements of the emulator. */ - getFramerate(): number; + get frequency(): number; + set frequency(value: number); + + /** + * The current logic framerate of the running emulator. + */ + get framerate(): number; getTile(index: number): Uint8Array; @@ -187,6 +186,10 @@ export interface Emulator extends ObservableI { */ reset(): void; + keyPress(key: string): void; + + keyLift(key: string): void; + /** * Runs a benchmark operation in the emulator, effectively * measuring the performance of it. @@ -199,6 +202,16 @@ export interface Emulator extends ObservableI { benchmark(count?: number): BenchmarkResult; } +export class EmulatorBase extends Observable { + get deviceUrl(): string | undefined { + return undefined; + } + + get versionUrl(): string | undefined { + return undefined; + } +} + /** * Enumeration that describes the multiple pixel * formats and the associated size in bytes. @@ -213,6 +226,10 @@ type AppProps = { backgrounds?: string[]; }; +const isTouchDevice = () => { + return "ontouchstart" in window || navigator.maxTouchPoints > 0; +}; + export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { const [paused, setPaused] = useState(false); const [fullscreen, setFullscreen] = useState(false); @@ -226,7 +243,7 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { const [toastText, setToastText] = useState<string>(); const [toastError, setToastError] = useState(false); const [toastVisible, setToastVisible] = useState(false); - const [keyboardVisible, setKeyboardVisible] = useState(false); + const [keyboardVisible, setKeyboardVisible] = useState(isTouchDevice()); const [infoVisible, setInfoVisible] = useState(true); const [debugVisible, setDebugVisible] = useState(false); @@ -265,8 +282,7 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { } }; const onBooted = () => { - const romInfo = emulator.getRomInfo(); - setRomInfo(romInfo); + setRomInfo(emulator.romInfo); setPaused(false); }; const onMessage = ( @@ -322,7 +338,7 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { // Game Boy only (using the emulator interface) if (!file.name.endsWith(".gb")) { showToast( - `This is probably not a ${emulator.getDevice()} ROM file!`, + `This is probably not a ${emulator.device} ROM file!`, true ); return; @@ -400,18 +416,33 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { const onEngineChange = (engine: string) => { emulator.boot({ engine: engine.toLowerCase() }); showToast( - `${emulator.getDevice()} running in engine "${engine}" from now on!` + `${emulator.device} running in engine "${engine}" from now on!` ); }; + const onFrequencyChange = (value: number) => { + emulator.frequency = value * 1000 * 1000; + }; + const onFrequencyReady = (handler: (value: number) => void) => { + emulator.bind("frequency", (emulator: Emulator, frequency: number) => { + handler(frequency / 1000000); + }); + }; const onMinimize = () => { setFullscreen(!fullscreen); }; + const onKeyDown = (key: string) => { + console.info(key); + emulator.keyPress(key); + }; + const onKeyUp = (key: string) => { + emulator.keyLift(key); + }; const onDrawHandler = (handler: DrawHandler) => { if (frameRef.current) return; frameRef.current = true; emulator.bind("frame", () => { - handler(emulator.getImageBuffer(), PixelFormat.RGB); - setFramerate(emulator.getFramerate()); + handler(emulator.imageBuffer, PixelFormat.RGB); + setFramerate(emulator.framerate); }); }; const onClearHandler = (handler: ClearHandler) => { @@ -456,28 +487,28 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { </div> } > + {keyboardVisible && ( + <Section separatorBottom={true}> + <KeyboardGB onKeyDown={onKeyDown} onKeyUp={onKeyUp} /> + </Section> + )} <Title - text={emulator.getName()} - version={emulator.getVersion()} + text={emulator.name} + version={emulator.version} versionUrl={ - emulator.getVersionUrl - ? emulator.getVersionUrl() - : undefined + emulator.versionUrl ? emulator.versionUrl : undefined } iconSrc={require("../res/thunder.png")} ></Title> <Section> <Paragraph> This is a{" "} - {emulator.getDeviceUrl ? ( - <Link - href={emulator.getDeviceUrl()} - target="_blank" - > - {emulator.getDevice()} + {emulator.deviceUrl ? ( + <Link href={emulator.deviceUrl} target="_blank"> + {emulator.device} </Link> ) : ( - emulator.getDevice() + emulator.device )}{" "} emulator built using the{" "} <Link href="https://www.rust-lang.org" target="_blank"> @@ -504,11 +535,6 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { ROM. </Paragraph> </Section> - {keyboardVisible && ( - <Section> - <KeyboardGB /> - </Section> - )} {debugVisible && ( <Section> <h3>VRAM Tiles</h3> @@ -526,7 +552,9 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { name={"Engine"} valueNode={ <ButtonSwitch - options={["NEO", "CLASSIC"]} + options={emulator.engines.map((e) => + e.toUpperCase() + )} size={"large"} style={["simple"]} onChange={onEngineChange} @@ -542,7 +570,11 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { key="rom-size" name={"ROM Size"} value={ - romInfo.name ? `${romInfo.size} bytes` : "-" + romInfo.size + ? `${new Intl.NumberFormat().format( + romInfo.size + )} bytes` + : "-" } /> <Pair @@ -550,11 +582,13 @@ export const App: FC<AppProps> = ({ emulator, backgrounds = ["264653"] }) => { name={"CPU Frequency"} valueNode={ <ButtonIncrement - value={4.19} - delta={0.1} + value={emulator.frequency / 1000 / 1000} + delta={0.4} min={0} suffix={"MHz"} decimalPlaces={2} + onChange={onFrequencyChange} + onReady={onFrequencyReady} /> } /> diff --git a/examples/web/react/components/button-increment/button-increment.tsx b/examples/web/react/components/button-increment/button-increment.tsx index 62a9071ad02633bc663095c388215871d660fc24..f80609384b0773bcebe46c63187b363725261184 100644 --- a/examples/web/react/components/button-increment/button-increment.tsx +++ b/examples/web/react/components/button-increment/button-increment.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; import Button from "../button/button"; import "./button-increment.css"; @@ -16,6 +16,7 @@ type ButtonIncrementProps = { onClick?: () => void; onBeforeChange?: (value: number) => boolean; onChange?: (value: number) => void; + onReady?: (setValue: (value: number) => void) => void; }; export const ButtonIncrement: FC<ButtonIncrementProps> = ({ @@ -30,25 +31,31 @@ export const ButtonIncrement: FC<ButtonIncrementProps> = ({ style = ["simple", "border"], onClick, onBeforeChange, - onChange + onChange, + onReady }) => { const [valueState, setValue] = useState(value); const classes = () => ["button-increment", size, ...style].join(" "); + useEffect(() => { + onReady && onReady((value) => setValue(value)); + }, []); const _onMinusClick = () => { - const valueNew = valueState - delta; + let valueNew = valueState - delta; if (onBeforeChange) { if (!onBeforeChange(valueNew)) return; } - if (min !== undefined && valueNew < min) return; + if (min !== undefined) valueNew = Math.max(min, valueNew); + if (valueNew === valueState) return; setValue(valueNew); if (onChange) onChange(valueNew); }; const _onPlusClick = () => { - const valueNew = valueState + delta; + let valueNew = valueState + delta; if (onBeforeChange) { if (!onBeforeChange(valueNew)) return; } - if (max !== undefined && valueNew > max) return; + if (max !== undefined) valueNew = Math.min(max, valueNew); + if (valueNew === valueState) return; setValue(valueNew); if (onChange) onChange(valueNew); }; diff --git a/examples/web/react/components/keyboard-chip8/keyboard-chip8.css b/examples/web/react/components/keyboard-chip8/keyboard-chip8.css index 01f35fef038dc2cc39305784fdfde7dca61efb1d..60e0d8f80e26b585c403aace668127daf5d66001 100644 --- a/examples/web/react/components/keyboard-chip8/keyboard-chip8.css +++ b/examples/web/react/components/keyboard-chip8/keyboard-chip8.css @@ -38,8 +38,9 @@ height: 48px; line-height: 46px; margin-right: 14px; + min-width: 48px; + padding: 0px 6px 0px 6px; text-align: center; - width: 48px; } .keyboard-chip8 .key:last-child { diff --git a/examples/web/react/components/keyboard-gb/dpad.svg b/examples/web/react/components/keyboard-gb/dpad.svg deleted file mode 100644 index 8115b91717cc9d018b30805c4836162b78d9670d..0000000000000000000000000000000000000000 --- a/examples/web/react/components/keyboard-gb/dpad.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="700pt" height="700pt" version="1.1" viewBox="0 0 700 700" fill="#ffffff" stroke="#ffffff" xmlns="http://www.w3.org/2000/svg"><path d="m338.45 240.62c3.1953 2.8047 7.3008 4.3516 11.551 4.3516s8.3555-1.5469 11.551-4.3516l78.75-70c3.7773-3.3164 5.9414-8.0977 5.9492-13.125v-105c0-4.6406-1.8438-9.0938-5.125-12.375s-7.7344-5.125-12.375-5.125h-157.5c-4.6406 0-9.0938 1.8438-12.375 5.125s-5.125 7.7344-5.125 12.375v105c0.007812 5.0273 2.1719 9.8086 5.9492 13.125zm-49.699-170.62h122.5v79.625l-61.25 54.426-61.25-54.426zm72.801 249.38c-3.1953-2.8047-7.3008-4.3516-11.551-4.3516s-8.3555 1.5469-11.551 4.3516l-78.75 70c-3.7773 3.3164-5.9414 8.0977-5.9492 13.125v105c0 4.6406 1.8438 9.0938 5.125 12.375s7.7344 5.125 12.375 5.125h157.5c4.6406 0 9.0938-1.8438 12.375-5.125s5.125-7.7344 5.125-12.375v-105c-0.007812-5.0273-2.1719-9.8086-5.9492-13.125zm49.699 170.62h-122.5v-79.625l61.25-54.426 61.25 54.426zm-100.62-221.55-70-78.75c-3.3164-3.7773-8.0977-5.9414-13.125-5.9492h-105c-4.6406 0-9.0938 1.8438-12.375 5.125s-5.125 7.7344-5.125 12.375v157.5c0 4.6406 1.8438 9.0938 5.125 12.375s7.7344 5.125 12.375 5.125h105c5.0273-0.007812 9.8086-2.1719 13.125-5.9492l70-78.75c2.8047-3.1953 4.3516-7.3008 4.3516-11.551s-1.5469-8.3555-4.3516-11.551zm-91 72.801h-79.625v-122.5h79.625l54.426 61.25zm357.88-157.5h-105c-5.0273 0.007812-9.8086 2.1719-13.125 5.9492l-70 78.75c-2.8047 3.1953-4.3516 7.3008-4.3516 11.551s1.5469 8.3555 4.3516 11.551l70 78.75c3.3164 3.7773 8.0977 5.9414 13.125 5.9492h105c4.6406 0 9.0938-1.8438 12.375-5.125s5.125-7.7344 5.125-12.375v-157.5c0-4.6406-1.8438-9.0938-5.125-12.375s-7.7344-5.125-12.375-5.125zm-17.5 157.5h-79.625l-54.426-61.25 54.426-61.25h79.625z"/></svg> diff --git a/examples/web/react/components/keyboard-gb/keyboard-gb.css b/examples/web/react/components/keyboard-gb/keyboard-gb.css index 56f4b8dacbe21740773829853af511b26e7eb671..2039d4932b21ae19676cbc75571eb7fad2800e81 100644 --- a/examples/web/react/components/keyboard-gb/keyboard-gb.css +++ b/examples/web/react/components/keyboard-gb/keyboard-gb.css @@ -38,22 +38,155 @@ height: 48px; line-height: 46px; margin-right: 14px; + min-width: 48px; + padding: 0px 8px 0px 8px; text-align: center; - width: 48px; } .keyboard-gb .key:last-child { margin-right: 0px; } -.keyboard-gb .key:hover { +.keyboard-gb .key.pressed { background-color: #50cb93; } -.keyboard-gb .key:active { - background-color: #2a9d8f; +.keyboard-gb > .dpad { + float: left; + text-align: center; + width: 180px; +} + +.keyboard-gb > .dpad > .dpad-top { + margin-bottom: -3px; +} + +.keyboard-gb > .dpad > .dpad-bottom { + margin-top: -3px; +} + +.keyboard-gb > .dpad .key { + border: none; + padding: 0px 0px 0px 0px; + font-size: 24px; +} + +.keyboard-gb > .dpad .key.pressed { + background-color: #50cb93; +} + +.keyboard-gb > .dpad .key.up { + border-bottom: 3px solid transparent; + border-left: 3px solid #ffffff; + border-radius: 6px 6px 0px 0px; + -o-border-radius: 6px 6px 0px 0px; + -ms-border-radius: 6px 6px 0px 0px; + -moz-border-radius: 6px 6px 0px 0px; + -khtml-border-radius: 6px 6px 0px 0px; + -webkit-border-radius: 6px 6px 0px 0px; + border-right: 3px solid #ffffff; + border-top: 3px solid #ffffff; +} + +.keyboard-gb > .dpad .key.left { + border-bottom: 3px solid #ffffff; + border-left: 3px solid #ffffff; + border-radius: 6px 0px 0px 6px; + -o-border-radius: 6px 0px 0px 6px; + -ms-border-radius: 6px 0px 0px 6px; + -moz-border-radius: 6px 0px 0px 6px; + -khtml-border-radius: 6px 0px 0px 6px; + -webkit-border-radius: 6px 0px 0px 6px; + border-right: 3px solid transparent; + border-top: 3px solid #ffffff; + margin-right: 44px; +} + +.keyboard-gb > .dpad .key.right { + border-bottom: 3px solid #ffffff; + border-left: 3px solid transparent; + border-radius: 0px 6px 6px 0px; + -o-border-radius: 0px 6px 6px 0px; + -ms-border-radius: 0px 6px 6px 0px; + -moz-border-radius: 0px 6px 6px 0px; + -khtml-border-radius: 0px 6px 6px 0px; + -webkit-border-radius: 0px 6px 6px 0px; + border-right: 3px solid #ffffff; + border-top: 3px solid #ffffff; + margin-right: 0px; + margin-left: -3px; +} + +.keyboard-gb > .dpad .key.center { + border-radius: 0px 0px 0px 0px; + -o-border-radius: 0px 0px 0px 0px; + -ms-border-radius: 0px 0px 0px 0px; + -moz-border-radius: 0px 0px 0px 0px; + -khtml-border-radius: 0px 0px 0px 0px; + -webkit-border-radius: 0px 0px 0px 0px; + margin-right: 0px; + pointer-events: none; +} + +.keyboard-gb > .dpad .key.down { + border-bottom: 3px solid #ffffff; + border-left: 3px solid #ffffff; + border-radius: 0px 0px 6px 6px; + -o-border-radius: 0px 0px 6px 6px; + -ms-border-radius: 0px 0px 6px 6px; + -moz-border-radius: 0px 0px 6px 6px; + -khtml-border-radius: 0px 0px 6px 6px; + -webkit-border-radius: 0px 0px 6px 6px; + border-right: 3px solid #ffffff; + border-top: 3px solid transparent; +} + +.keyboard-gb > .action { + float: right; + margin-right: 18px; +} + +.keyboard-gb > .action > .key { + border-radius: 32px 32px 32px 32px; + -o-border-radius: 32px 32px 32px 32px; + -ms-border-radius: 32px 32px 32px 32px; + -moz-border-radius: 32px 32px 32px 32px; + -khtml-border-radius: 32px 32px 32px 32px; + -webkit-border-radius: 32px 32px 32px 32px; + border-width: 3px; + font-size: 30px; + height: 58px; + line-height: 52px; + width: 58px; + font-weight: 900; +} + +.keyboard-gb > .action > .key.a { + position: relative; + right: 0px; + top: 10px; +} + +.keyboard-gb > .action > .key.b { + position: relative; + right: 2px; + top: 50px; +} + +.keyboard-gb > .options { + margin-top: 32px; +} + +.keyboard-gb > .options > .key { + font-size: 14px; + height: 30px; + line-height: 26px; + margin-left: 16px; + margin-right: 16px; + min-width: 100px; + font-weight: 900; } -.keyboard-gb .dpad { - width: 120px; +.keyboard-gb > .break { + clear: both; } diff --git a/examples/web/react/components/keyboard-gb/keyboard-gb.tsx b/examples/web/react/components/keyboard-gb/keyboard-gb.tsx index 6358673061284ede95093ec10915277fa9c0681b..dddeaa6c9ffb28c1e5f61d8c2f9b3033db425159 100644 --- a/examples/web/react/components/keyboard-gb/keyboard-gb.tsx +++ b/examples/web/react/components/keyboard-gb/keyboard-gb.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, useState } from "react"; import "./keyboard-gb.css"; @@ -7,34 +7,89 @@ declare const require: any; type KeyboardGBProps = { style?: string[]; onKeyDown?: (key: string) => void; + onKeyUp?: (key: string) => void; }; -export const KeyboardGB: FC<KeyboardGBProps> = ({ style = [], onKeyDown }) => { +export const KeyboardGB: FC<KeyboardGBProps> = ({ + style = [], + onKeyDown, + onKeyUp +}) => { const classes = () => ["keyboard", "keyboard-gb", ...style].join(" "); - const renderKey = (key: string) => { + const renderKey = ( + key: string, + keyName?: string, + styles: string[] = [] + ) => { + const [pressed, setPressed] = useState(false); return ( <span - className="key" - key={key} - onKeyDown={() => onKeyDown && onKeyDown(key)} + className={["key", pressed ? "pressed" : "", ...styles].join( + " " + )} + key={keyName || key} + onMouseDown={(event) => { + setPressed(true); + onKeyDown && onKeyDown(keyName || key); + event.stopPropagation(); + event.preventDefault(); + }} + onMouseUp={(event) => { + setPressed(false); + onKeyUp && onKeyUp(keyName || key); + event.stopPropagation(); + event.preventDefault(); + }} + onMouseLeave={(event) => { + if (!pressed) return; + setPressed(false); + onKeyUp && onKeyUp(keyName || key); + event.stopPropagation(); + event.preventDefault(); + }} + onTouchStart={(event) => { + setPressed(true); + onKeyDown && onKeyDown(keyName || key); + event.stopPropagation(); + event.preventDefault(); + }} + onTouchEnd={(event) => { + setPressed(false); + onKeyUp && onKeyUp(keyName || key); + event.stopPropagation(); + event.preventDefault(); + }} > {key} </span> ); }; return ( - <div className={classes()}> - <div className="keyboard-line"> - <img className="dpad" alt="dpad" src={require("./dpad.svg")} /> + <div + className={classes()} + onTouchStart={(e) => e.preventDefault()} + onTouchEnd={(e) => e.preventDefault()} + > + <div className="dpad"> + <div className="dpad-top"> + {renderKey("🡑", "ArrowUp", ["up"])} + </div> + <div> + {renderKey("ðŸ¡", "ArrowLeft", ["left"])} + {renderKey("🡒", "ArrowRight", ["right"])} + </div> + <div className="dpad-bottom"> + {renderKey("🡓", "ArrowDown", ["down"])} + </div> </div> - <div className="keyboard-line"> - {["Q", "W", "E", "R"].map((k) => renderKey(k))} + <div className="action"> + {renderKey("B", "B", ["b"])} + {renderKey("A", "A", ["a"])} </div> - <div className="keyboard-line"> - {["A", "S", "D", "F"].map((k) => renderKey(k))} - </div> - <div className="keyboard-line"> - {["Z", "X", "C", "V"].map((k) => renderKey(k))} + <div className="break"></div> + <div className="options"> + {renderKey("START", "Start", ["start"])} + {renderKey("SELECT", "Select", ["select"])} </div> </div> ); diff --git a/examples/web/react/components/section/section.tsx b/examples/web/react/components/section/section.tsx index 91a84184a041858745c50f06805b13b4521d6c25..b6a0df50e36c9890bc33e642f6bcd2923adf9fab 100644 --- a/examples/web/react/components/section/section.tsx +++ b/examples/web/react/components/section/section.tsx @@ -5,12 +5,14 @@ import "./section.css"; type SectionProps = { children: ReactNode; separator?: boolean; + separatorBottom?: boolean; style?: string[]; }; export const Section: FC<SectionProps> = ({ children, separator = true, + separatorBottom = false, style = [] }) => { const classes = () => ["section", ...style].join(" "); @@ -18,6 +20,7 @@ export const Section: FC<SectionProps> = ({ <div className={classes()}> {separator && <div className="separator"></div>} <div className="section-contents">{children}</div> + {separatorBottom && <div className="separator"></div>} </div> ); }; diff --git a/examples/web/react/components/tiles/tiles.tsx b/examples/web/react/components/tiles/tiles.tsx index 8ecc57d3b5442758914256df11f0074b0740aaee..e879f383585faaab2a29889295ec78793182cb5c 100644 --- a/examples/web/react/components/tiles/tiles.tsx +++ b/examples/web/react/components/tiles/tiles.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, useEffect, useRef } from "react"; import { PixelFormat } from "../../app"; import Canvas, { CanvasStructure } from "../canvas/canvas"; @@ -18,6 +18,14 @@ export const Tiles: FC<TilesProps> = ({ style = [] }) => { const classes = () => ["tiles", ...style].join(" "); + const intervalsRef = useRef<number>(); + useEffect(() => { + return () => { + if (intervalsRef.current) { + clearInterval(intervalsRef.current); + } + }; + }, []); const onCanvas = (structure: CanvasStructure) => { const drawTiles = () => { for (let index = 0; index < 384; index++) { @@ -26,7 +34,7 @@ export const Tiles: FC<TilesProps> = ({ } }; drawTiles(); - setInterval(() => drawTiles(), interval); + intervalsRef.current = setInterval(() => drawTiles(), interval); }; return ( <div className={classes()}>