// @ts-ignore No types available import { WasmBoy } from '@soapbox.pub/wasmboy'; import clsx from 'clsx'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { exitFullscreen, isFullscreen, requestFullscreen } from 'soapbox/features/ui/util/fullscreen'; import { HStack, IconButton } from './ui'; let gainNode: GainNode | undefined; interface IGameboy extends Pick, 'onFocus' | 'onBlur'> { /** Classname of the outer `
`. */ className?: string; /** URL to the ROM. */ src: string; /** Aspect ratio of the canvas. */ aspect?: 'normal' | 'stretched'; } /** Component to display a playable Gameboy emulator. */ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocus, onBlur, ...rest }) => { const node = useRef(null); const canvas = useRef(null); const [paused, setPaused] = useState(false); const [muted, setMuted] = useState(true); const [fullscreen, setFullscreen] = useState(false); const [showControls, setShowControls] = useState(true); async function init() { await WasmBoy.config(WasmBoyOptions, canvas.current!); await WasmBoy.loadROM(src); await play(); if (document.activeElement === canvas.current) { await WasmBoy.enableDefaultJoypad(); } else { await WasmBoy.disableDefaultJoypad(); } } const handleFocus: React.FocusEventHandler = useCallback(() => { WasmBoy.enableDefaultJoypad(); }, []); const handleBlur: React.FocusEventHandler = useCallback(() => { WasmBoy.disableDefaultJoypad(); }, []); const handleFullscreenChange = useCallback(() => { setFullscreen(isFullscreen()); }, []); const handleCanvasClick = useCallback(() => { setShowControls(!showControls); }, [showControls]); const pause = async () => { await WasmBoy.pause(); setPaused(true); }; const play = async () => { await WasmBoy.play(); setPaused(false); }; const togglePaused = () => paused ? play() : pause(); const toggleMuted = () => setMuted(!muted); const toggleFullscreen = () => { if (isFullscreen()) { exitFullscreen(); } else if (node.current) { requestFullscreen(node.current); } }; const handleDownload = () => { window.open(src); }; useEffect(() => { init(); return () => { WasmBoy.pause(); WasmBoy.disableDefaultJoypad(); }; }, []); useEffect(() => { document.addEventListener('fullscreenchange', handleFullscreenChange, true); return () => { document.removeEventListener('fullscreenchange', handleFullscreenChange, true); }; }, []); useEffect(() => { if (fullscreen) { node.current?.focus(); } }, [fullscreen]); useEffect(() => { if (gainNode) { gainNode.gain.value = muted ? 0 : 1; } }, [gainNode, muted]); return (
); }; const WasmBoyOptions = { headless: false, useGbcWhenOptional: true, isAudioEnabled: true, frameSkip: 1, audioBatchProcessing: true, timersBatchProcessing: false, audioAccumulateSamples: true, graphicsBatchProcessing: false, graphicsDisableScanlineRendering: false, tileRendering: true, tileCaching: true, gameboyFPSCap: 60, updateGraphicsCallback: false, updateAudioCallback: (audioContext: AudioContext, audioBufferSourceNode: AudioBufferSourceNode) => { gainNode = gainNode ?? audioContext.createGain(); audioBufferSourceNode.connect(gainNode); return gainNode; }, saveStateCallback: false, }; export default Gameboy;