From 2e1282bc2d4b6e89a5dbd7d679cebf8c6589a672 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 22 Nov 2023 22:20:19 -0600 Subject: [PATCH 01/13] Render a Game Boy player for GB/GBC attachments --- package.json | 1 + src/components/gameboy.tsx | 47 ++++++++++++++++++++++++++++++++ src/components/media-gallery.tsx | 24 ++++++++++++---- yarn.lock | 26 +++++++++++++++++- 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/components/gameboy.tsx diff --git a/package.json b/package.json index a6cfd4f568..b6144c5e9a 100644 --- a/package.json +++ b/package.json @@ -174,6 +174,7 @@ "vite-plugin-html": "^3.2.0", "vite-plugin-require": "^1.1.10", "vite-plugin-static-copy": "^0.17.0", + "wasmboy": "^0.7.1", "wicg-inert": "^3.1.1", "zod": "^3.21.4" }, diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx new file mode 100644 index 0000000000..ec66173046 --- /dev/null +++ b/src/components/gameboy.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useRef } from 'react'; +// @ts-ignore No types available +import { WasmBoy } from 'wasmboy'; + +interface IGameboy extends React.CanvasHTMLAttributes { + /** URL to the ROM. */ + src: string; +} + +/** Component to display a playable Gameboy emulator. */ +const Gameboy: React.FC = ({ src, ...rest }) => { + const canvas = useRef(null); + + async function init() { + await WasmBoy.config(WasmBoyOptions, canvas.current!); + await WasmBoy.loadROM(src); + await WasmBoy.play(); + } + + useEffect(() => { + init(); + }, []); + + return ( + + ); +}; + +const WasmBoyOptions = { + headless: false, + useGbcWhenOptional: true, + isAudioEnabled: false, + frameSkip: 1, + audioBatchProcessing: true, + timersBatchProcessing: false, + audioAccumulateSamples: true, + graphicsBatchProcessing: false, + graphicsDisableScanlineRendering: false, + tileRendering: true, + tileCaching: true, + gameboyFPSCap: 60, + updateGraphicsCallback: false, + updateAudioCallback: false, + saveStateCallback: false, +}; + +export { Gameboy }; \ No newline at end of file diff --git a/src/components/media-gallery.tsx b/src/components/media-gallery.tsx index 0d8e486833..2cf8ee139d 100644 --- a/src/components/media-gallery.tsx +++ b/src/components/media-gallery.tsx @@ -12,6 +12,8 @@ import { truncateFilename } from 'soapbox/utils/media'; import { isIOS } from '../is-mobile'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio'; +import { Gameboy } from './gameboy'; + import type { Property } from 'csstype'; import type { List as ImmutableList } from 'immutable'; @@ -141,8 +143,22 @@ const Item: React.FC = ({ } let thumbnail: React.ReactNode = ''; + const ext = attachment.url.split('.').pop()?.toLowerCase(); - if (attachment.type === 'unknown') { + if (attachment.type === 'unknown' && ['gb', 'gbc'].includes(ext!)) { + return ( +
1, + })} + key={attachment.id} + style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} + > + +
+ ); + } else if (attachment.type === 'unknown') { const filename = truncateFilename(attachment.url, MAX_FILENAME_LENGTH); const attachmentIcon = ( = ({ ); } else if (attachment.type === 'audio') { - const ext = attachment.url.split('.').pop()?.toUpperCase(); thumbnail = ( = ({ title={attachment.description} > - {ext} + {ext} ); } else if (attachment.type === 'video') { - const ext = attachment.url.split('.').pop()?.toUpperCase(); thumbnail = ( = ({ > - {ext} + {ext} ); } diff --git a/yarn.lock b/yarn.lock index a2d4c7e5dd..79e9ac9e26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3108,6 +3108,10 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +"audiobuffer-to-wav@git+https://github.com/torch2424/audiobuffer-to-wav.git#es-module-rollup": + version "1.0.0" + resolved "git+https://github.com/torch2424/audiobuffer-to-wav.git#8878a20c5cc7e457b113dabfb1781ad4178f9c62" + autoprefixer@^10.4.15: version "10.4.15" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.15.tgz#a1230f4aeb3636b89120b34a1f513e2f6834d530" @@ -5197,6 +5201,11 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idb@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/idb/-/idb-2.1.3.tgz#7b295fa1a46ab7851e42dd85543a271435f87fee" + integrity sha512-1He6QAuavrD38HCiJasi4lEEK87Y22ldFuM+ZHkp433n4Fd5jXjWKutClYFp8w4mgx3zgrjnWxL8dpjMzcQ+WQ== + idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" @@ -7138,7 +7147,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf@^3.1.0: +raf@^3.1.0, raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -7709,6 +7718,11 @@ responselike@^2.0.0: dependencies: lowercase-keys "^2.0.0" +responsive-gamepad@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/responsive-gamepad/-/responsive-gamepad-1.1.0.tgz#0173d05199e3c41c09f3b1b9fa8eb811dad8e585" + integrity sha512-njsJuKvany9eYjywXm8iorTeXeAAPqwMNaRWOo8jlh0iQboXgGPf6Z6bLGntELrfU+vR94jTPJYRW0Zzb2gaRA== + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -9040,6 +9054,16 @@ warning@^4.0.0, warning@^4.0.1, warning@^4.0.2: dependencies: loose-envify "^1.0.0" +wasmboy@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/wasmboy/-/wasmboy-0.7.1.tgz#5bbf0f0f386f8e9ea322a611689b889f9c3495d2" + integrity sha512-qgA3bIFAqioYs8kYXtsanIvedgZlZQf382zs3gNlZHIItsAnRzV70/Vp6cJxbK4FyaiG58ah8/g7OW3orrs9Lg== + dependencies: + audiobuffer-to-wav "git+https://github.com/torch2424/audiobuffer-to-wav.git#es-module-rollup" + idb "^2.1.3" + raf "^3.4.0" + responsive-gamepad "1.1.0" + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" From 2e7b773bb5b370067514b9a97f33e7149a6eb464 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 22 Nov 2023 22:39:45 -0600 Subject: [PATCH 02/13] Gameboy: release keyboard when canvas isn't focused --- src/components/gameboy.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index ec66173046..c3a67b3d4b 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; // @ts-ignore No types available import { WasmBoy } from 'wasmboy'; @@ -8,21 +8,39 @@ interface IGameboy extends React.CanvasHTMLAttributes { } /** Component to display a playable Gameboy emulator. */ -const Gameboy: React.FC = ({ src, ...rest }) => { +const Gameboy: React.FC = ({ src, onFocus, onBlur, ...rest }) => { const canvas = useRef(null); async function init() { await WasmBoy.config(WasmBoyOptions, canvas.current!); await WasmBoy.loadROM(src); await WasmBoy.play(); + + if (document.activeElement !== canvas.current) { + await WasmBoy.disableDefaultJoypad(); + } } + const handleFocus: React.FocusEventHandler = useCallback(() => { + WasmBoy.enableDefaultJoypad(); + }, []); + + const handleBlur: React.FocusEventHandler = useCallback(() => { + WasmBoy.disableDefaultJoypad(); + }, []); + useEffect(() => { init(); }, []); return ( - + ); }; From f7c624483ec2e034055c33f8fad04453f9234568 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 22 Nov 2023 22:44:23 -0600 Subject: [PATCH 03/13] Gameboy: improve remounting --- src/components/gameboy.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index c3a67b3d4b..7c9773fc59 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -16,7 +16,9 @@ const Gameboy: React.FC = ({ src, onFocus, onBlur, ...rest }) => { await WasmBoy.loadROM(src); await WasmBoy.play(); - if (document.activeElement !== canvas.current) { + if (document.activeElement === canvas.current) { + await WasmBoy.enableDefaultJoypad(); + } else { await WasmBoy.disableDefaultJoypad(); } } @@ -31,6 +33,11 @@ const Gameboy: React.FC = ({ src, onFocus, onBlur, ...rest }) => { useEffect(() => { init(); + + return () => { + WasmBoy.pause(); + WasmBoy.disableDefaultJoypad(); + }; }, []); return ( From 0bb50f492e5555963016b095dc14d9e792a063c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 22 Nov 2023 22:49:27 -0600 Subject: [PATCH 04/13] Lazy-load the Gameboy component --- src/components/gameboy.tsx | 2 +- src/components/media-gallery.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index 7c9773fc59..7753860806 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -69,4 +69,4 @@ const WasmBoyOptions = { saveStateCallback: false, }; -export { Gameboy }; \ No newline at end of file +export default Gameboy; \ No newline at end of file diff --git a/src/components/media-gallery.tsx b/src/components/media-gallery.tsx index 2cf8ee139d..198fee48ab 100644 --- a/src/components/media-gallery.tsx +++ b/src/components/media-gallery.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useState, useRef, useLayoutEffect } from 'react'; +import React, { useState, useRef, useLayoutEffect, Suspense } from 'react'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; @@ -12,11 +12,11 @@ import { truncateFilename } from 'soapbox/utils/media'; import { isIOS } from '../is-mobile'; import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media-aspect-ratio'; -import { Gameboy } from './gameboy'; - import type { Property } from 'csstype'; import type { List as ImmutableList } from 'immutable'; +const Gameboy = React.lazy(() => import('./gameboy')); + const ATTACHMENT_LIMIT = 4; const MAX_FILENAME_LENGTH = 45; @@ -155,7 +155,9 @@ const Item: React.FC = ({ key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} > - + }> + + ); } else if (attachment.type === 'unknown') { From 3b17bef9e5c69f3dab9261ec5580d89be847dc40 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 17:16:51 -0600 Subject: [PATCH 05/13] Switch to my fork of wasmboy --- package.json | 2 +- src/components/gameboy.tsx | 4 ++-- yarn.lock | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index b6144c5e9a..8484a627a8 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@reduxjs/toolkit": "^1.8.1", "@sentry/browser": "^7.74.1", "@sentry/react": "^7.74.1", + "@soapbox.pub/wasmboy": "^0.8.0", "@tabler/icons": "^2.0.0", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", @@ -174,7 +175,6 @@ "vite-plugin-html": "^3.2.0", "vite-plugin-require": "^1.1.10", "vite-plugin-static-copy": "^0.17.0", - "wasmboy": "^0.7.1", "wicg-inert": "^3.1.1", "zod": "^3.21.4" }, diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index 7753860806..47d8e060ad 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useEffect, useRef } from 'react'; // @ts-ignore No types available -import { WasmBoy } from 'wasmboy'; +import { WasmBoy } from '@soapbox.pub/wasmboy'; +import React, { useCallback, useEffect, useRef } from 'react'; interface IGameboy extends React.CanvasHTMLAttributes { /** URL to the ROM. */ diff --git a/yarn.lock b/yarn.lock index 79e9ac9e26..08b18ba00f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2100,6 +2100,16 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== +"@soapbox.pub/wasmboy@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@soapbox.pub/wasmboy/-/wasmboy-0.8.0.tgz#ca7e86b9c144af44530223536fdbcd29930953b1" + integrity sha512-feecE/YHmE7RdKe5IWzTCeLSaO3gF2XtHqAZWZ31YGzm0Fc8vAxuZC2Tyt0lrKRsS7rEs2L1SxT9oYAiEJcOZA== + dependencies: + audiobuffer-to-wav "git+https://github.com/torch2424/audiobuffer-to-wav.git#es-module-rollup" + idb "^2.1.3" + raf "^3.4.0" + responsive-gamepad "1.1.0" + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" @@ -9054,16 +9064,6 @@ warning@^4.0.0, warning@^4.0.1, warning@^4.0.2: dependencies: loose-envify "^1.0.0" -wasmboy@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/wasmboy/-/wasmboy-0.7.1.tgz#5bbf0f0f386f8e9ea322a611689b889f9c3495d2" - integrity sha512-qgA3bIFAqioYs8kYXtsanIvedgZlZQf382zs3gNlZHIItsAnRzV70/Vp6cJxbK4FyaiG58ah8/g7OW3orrs9Lg== - dependencies: - audiobuffer-to-wav "git+https://github.com/torch2424/audiobuffer-to-wav.git#es-module-rollup" - idb "^2.1.3" - raf "^3.4.0" - responsive-gamepad "1.1.0" - watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" From 33b2d19cd04465a6e26f0f0d726e04371c5742d9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 18:36:37 -0600 Subject: [PATCH 06/13] vite: don't optimize wasmboy in dev (fixes devserver) --- vite.config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 4f93c42d3d..273ebb87b4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,7 +12,7 @@ import { VitePWA } from 'vite-plugin-pwa'; import vitePluginRequire from 'vite-plugin-require'; import { viteStaticCopy } from 'vite-plugin-static-copy'; -export default defineConfig({ +export default defineConfig(({ command }) => ({ build: { assetsDir: 'packs', assetsInlineLimit: 0, @@ -29,6 +29,9 @@ export default defineConfig({ server: { port: 3036, }, + optimizeDeps: { + exclude: command === 'serve' ? ['@soapbox.pub/wasmboy'] : [], + }, plugins: [ checker({ typescript: true }), // @ts-ignore @@ -100,7 +103,7 @@ export default defineConfig({ environment: 'jsdom', setupFiles: 'src/jest/test-setup.ts', }, -}); +})); /** Return file as string, or return empty string if the file isn't found. */ function readFileContents(path: string) { From 5ee2c8a23c0196903536c19a436bc0819920688c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 20:54:24 -0600 Subject: [PATCH 07/13] Gameboy: render the canvas inside a container div --- src/components/gameboy.tsx | 29 ++++++++++++++++++++--------- src/components/media-gallery.tsx | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index 47d8e060ad..e7bbe9309f 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -1,14 +1,19 @@ // @ts-ignore No types available import { WasmBoy } from '@soapbox.pub/wasmboy'; +import clsx from 'clsx'; import React, { useCallback, useEffect, useRef } from 'react'; -interface IGameboy extends React.CanvasHTMLAttributes { +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 = ({ src, onFocus, onBlur, ...rest }) => { +const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocus, onBlur, ...rest }) => { const canvas = useRef(null); async function init() { @@ -41,13 +46,19 @@ const Gameboy: React.FC = ({ src, onFocus, onBlur, ...rest }) => { }, []); return ( - +
+ +
); }; diff --git a/src/components/media-gallery.tsx b/src/components/media-gallery.tsx index 198fee48ab..db7bfd1d60 100644 --- a/src/components/media-gallery.tsx +++ b/src/components/media-gallery.tsx @@ -156,7 +156,7 @@ const Item: React.FC = ({ style={{ position, float, left, top, right, bottom, height, width: `${width}%` }} > }> - +
); From 269636471072617354509fb5aacf92123fe71834 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 21:39:59 -0600 Subject: [PATCH 08/13] Gameboy: add controls and a pause buttom --- src/components/gameboy.tsx | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index e7bbe9309f..50ba4b08be 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -1,7 +1,9 @@ // @ts-ignore No types available import { WasmBoy } from '@soapbox.pub/wasmboy'; import clsx from 'clsx'; -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import { IconButton } from './ui'; interface IGameboy extends Pick, 'onFocus' | 'onBlur'> { /** Classname of the outer `
`. */ @@ -16,10 +18,12 @@ interface IGameboy extends Pick, ' const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocus, onBlur, ...rest }) => { const canvas = useRef(null); + const [paused, setPaused] = useState(false); + async function init() { await WasmBoy.config(WasmBoyOptions, canvas.current!); await WasmBoy.loadROM(src); - await WasmBoy.play(); + await play(); if (document.activeElement === canvas.current) { await WasmBoy.enableDefaultJoypad(); @@ -36,6 +40,18 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu WasmBoy.disableDefaultJoypad(); }, []); + const pause = async () => { + await WasmBoy.pause(); + setPaused(true); + }; + + const play = async () => { + await WasmBoy.play(); + setPaused(false); + }; + + const togglePaused = () => paused ? play() : pause(); + useEffect(() => { init(); @@ -46,7 +62,7 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu }, []); return ( -
+
= ({ className, src, aspect = 'normal', onFocu onBlur={onBlur ?? handleBlur} {...rest} /> + +
+ +
); }; From 48db472af57fe0e9e96b785e993203c6799ed68e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 21:58:04 -0600 Subject: [PATCH 09/13] Gameboy: add audio control, but it doesn't work correctly --- src/components/gameboy.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index 50ba4b08be..e19977730c 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -19,6 +19,7 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu const canvas = useRef(null); const [paused, setPaused] = useState(false); + const [muted, setMuted] = useState(true); async function init() { await WasmBoy.config(WasmBoyOptions, canvas.current!); @@ -52,6 +53,11 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu const togglePaused = () => paused ? play() : pause(); + const unmute = async () => { + await WasmBoy.resumeAudioContext(); + setMuted(false); + }; + useEffect(() => { init(); @@ -75,12 +81,17 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu {...rest} /> -
+
+
); @@ -89,7 +100,7 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu const WasmBoyOptions = { headless: false, useGbcWhenOptional: true, - isAudioEnabled: false, + isAudioEnabled: true, frameSkip: 1, audioBatchProcessing: true, timersBatchProcessing: false, From 4d840e02905154c6ce4207f07fbbe75b9ba4667d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 22:15:25 -0600 Subject: [PATCH 10/13] Gameboy: add a fullscreen button --- src/components/gameboy.tsx | 45 ++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index e19977730c..2edb520653 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -3,9 +3,11 @@ 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 { IconButton } from './ui'; -interface IGameboy extends Pick, 'onFocus' | 'onBlur'> { +interface IGameboy extends Pick, 'onFocus' | 'onBlur'> { /** Classname of the outer `
`. */ className?: string; /** URL to the ROM. */ @@ -16,10 +18,12 @@ interface IGameboy extends Pick, ' /** 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); async function init() { await WasmBoy.config(WasmBoyOptions, canvas.current!); @@ -33,14 +37,18 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu } } - const handleFocus: React.FocusEventHandler = useCallback(() => { + const handleFocus: React.FocusEventHandler = useCallback(() => { WasmBoy.enableDefaultJoypad(); }, []); - const handleBlur: React.FocusEventHandler = useCallback(() => { + const handleBlur: React.FocusEventHandler = useCallback(() => { WasmBoy.disableDefaultJoypad(); }, []); + const handleFullscreenChange = useCallback(() => { + setFullscreen(isFullscreen()); + }, []); + const pause = async () => { await WasmBoy.pause(); setPaused(true); @@ -58,6 +66,14 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu setMuted(false); }; + const toggleFullscreen = () => { + if (isFullscreen()) { + exitFullscreen(); + } else if (node.current) { + requestFullscreen(node.current); + } + }; + useEffect(() => { init(); @@ -67,17 +83,27 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu }; }, []); + useEffect(() => { + document.addEventListener('fullscreenchange', handleFullscreenChange, true); + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange, true); + }; + }, []); + return ( -
+
@@ -92,6 +118,11 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu onClick={unmute} src={muted ? require('@tabler/icons/volume-3.svg') : require('@tabler/icons/volume.svg')} /> +
); From a317152e3f007d1520d81a0d5d827561ee1aee93 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 22:28:29 -0600 Subject: [PATCH 11/13] Gameboy: tap the canvas to show/hide controls --- src/components/gameboy.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index 2edb520653..d5e371e4dc 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -24,6 +24,7 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu 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!); @@ -49,6 +50,10 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu setFullscreen(isFullscreen()); }, []); + const handleCanvasClick = useCallback(() => { + setShowControls(!showControls); + }, [showControls]); + const pause = async () => { await WasmBoy.pause(); setPaused(true); @@ -100,6 +105,7 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu > = ({ className, src, aspect = 'normal', onFocu {...rest} /> -
+
Date: Fri, 24 Nov 2023 22:39:42 -0600 Subject: [PATCH 12/13] Gameboy: focus the div after entering fullscreen --- src/components/gameboy.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index d5e371e4dc..457aeb74cf 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -95,11 +95,17 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu }; }, []); + useEffect(() => { + if (fullscreen) { + node.current?.focus(); + } + }, [fullscreen]); + return (
From 5aa69c917f3c39959c17a859227e5d7d47248ccd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 24 Nov 2023 23:18:15 -0600 Subject: [PATCH 13/13] Gameboy: make mute button work properly --- src/components/gameboy.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/gameboy.tsx b/src/components/gameboy.tsx index 457aeb74cf..bc8197fd1c 100644 --- a/src/components/gameboy.tsx +++ b/src/components/gameboy.tsx @@ -7,6 +7,8 @@ import { exitFullscreen, isFullscreen, requestFullscreen } from 'soapbox/feature import { IconButton } from './ui'; +let gainNode: GainNode | undefined; + interface IGameboy extends Pick, 'onFocus' | 'onBlur'> { /** Classname of the outer `
`. */ className?: string; @@ -65,11 +67,7 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu }; const togglePaused = () => paused ? play() : pause(); - - const unmute = async () => { - await WasmBoy.resumeAudioContext(); - setMuted(false); - }; + const toggleMuted = () => setMuted(!muted); const toggleFullscreen = () => { if (isFullscreen()) { @@ -101,6 +99,12 @@ const Gameboy: React.FC = ({ className, src, aspect = 'normal', onFocu } }, [fullscreen]); + useEffect(() => { + if (gainNode) { + gainNode.gain.value = muted ? 0 : 1; + } + }, [gainNode, muted]); + return (
= ({ className, src, aspect = 'normal', onFocu /> { + gainNode = gainNode ?? audioContext.createGain(); + audioBufferSourceNode.connect(gainNode); + return gainNode; + }, saveStateCallback: false, };