Skip to content

Commit

Permalink
feat: improve virtual controller ui
Browse files Browse the repository at this point in the history
  • Loading branch information
arianrhodsandlot committed Oct 17, 2023
1 parent 8c7180e commit bfff3ca
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 53 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"devDependencies": {
"@arianrhodsandlot/eslint-config": "0.8.3",
"@iconify/json": "2.2.128",
"@iconify/json": "2.2.129",
"@iconify/tailwind": "0.1.3",
"@playwright/test": "1.39.0",
"@sergeymakinen/vite-plugin-html-minimize": "1.0.6",
Expand Down
20 changes: 10 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 6 additions & 7 deletions src/core/classes/emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import delay from 'delay'
import { Nostalgist } from 'nostalgist'
import { cdnHost, vendorsInfo } from '../constants/dependencies'
import { systemCoreMap } from '../constants/systems'
import { defaultRetroarchCoresConfig } from '../helpers/retroarch'
import { defaultRetroarchCoresConfig, getRetroarchConfig } from '../helpers/retroarch'
import { type Rom } from './rom'

interface EmulatorConstructorOptions {
Expand All @@ -15,7 +15,7 @@ interface EmulatorConstructorOptions {
retroarchConfig?: Record<string, string>
}

const defaultStyle = {
const defaultStyle: Partial<CSSStyleDeclaration> = {
backgroundImage:
'repeating-linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000), repeating-linear-gradient(45deg, #000 25%, #222 25%, #222 75%, #000 75%, #000)',
backgroundPosition: '0 0,15px 15px',
Expand Down Expand Up @@ -88,17 +88,16 @@ export class Emulator {
...(this.additionalFiles?.map(({ name, blob }) => ({ fileName: name, fileContent: blob })) || []),
]
const bios = this.biosFiles?.map(({ name, blob }) => ({ fileName: name, fileContent: blob })) || []
const retroarchConfig = { ...getRetroarchConfig(), ...this.retroarchConfig }
const retroarchCoreConfig = { ...defaultRetroarchCoresConfig[this.core], ...this.coreConfig?.[this.core] }
this.nostalgist = await Nostalgist.launch({
style: this.style,
element: this.canvas,
core: this.core,
rom,
bios,
retroarchConfig: this.retroarchConfig,
retroarchCoreConfig: {
...defaultRetroarchCoresConfig[this.core],
...this.coreConfig?.[this.core],
},
retroarchConfig,
retroarchCoreConfig,
respondToGlobalEvents: false,

async waitForInteraction({ done }) {
Expand Down
1 change: 1 addition & 0 deletions src/views/components/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const isGameRunningAtom = atom(false)
export const isGameLaunchingAtom = atom(false)
export const isGameLaunchedAtom = atom((get) => get(isGameRunningAtom) && !get(isGameLaunchingAtom))
export const isGameIdleAtom = atom((get) => !get(isGameRunningAtom) && !get(isGameLaunchingAtom))
export const showMenuOverlayAtom = atom(false)
1 change: 0 additions & 1 deletion src/views/components/home-screen/game-menus/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { atom } from 'jotai'

export const showMenuOverlayAtom = atom(false)
export const shouldFocusStatesListAtom = atom(false)
export const previousFocusedElementAtom = atom<HTMLElement | null>(null)
4 changes: 2 additions & 2 deletions src/views/components/home-screen/game-menus/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { AnimatePresence, motion } from 'framer-motion'
import { useAtom, useAtomValue } from 'jotai'
import { useCallback, useEffect } from 'react'
import { onPress, pauseGame, resumeGame } from '../../../../core'
import { isGameLaunchedAtom } from '../../atoms'
import { previousFocusedElementAtom, showMenuOverlayAtom } from './atoms'
import { isGameLaunchedAtom, showMenuOverlayAtom } from '../../atoms'
import { previousFocusedElementAtom } from './atoms'
import { MenuEntryButton } from './menu-entry-button'
import { MenuOverlay } from './menu-overlay'

Expand Down
15 changes: 11 additions & 4 deletions src/views/components/home-screen/game-menus/menu-entry-button.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
import { AnimatePresence, motion } from 'framer-motion'
import { useAtomValue } from 'jotai'
import { detectHasRunningGame } from '../../../../core'
import { isGameRunningAtom } from '../../atoms'
import { showMenuOverlayAtom } from './atoms'
import { isGameRunningAtom, showMenuOverlayAtom } from '../../atoms'
import { useMouseMoving } from './hooks'

function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
}

export function MenuEntryButton({ onClick }: { onClick: () => void }) {
const isGameRunning = useAtomValue(isGameRunningAtom)
const showMenuOverlay = useAtomValue(showMenuOverlayAtom)
const { isMouseMoving } = useMouseMoving({ timeout: 3000 })

const showMenuEntryButton = isGameRunning && detectHasRunningGame() && isMouseMoving && !showMenuOverlay

if (isTouchDevice()) {
return null
}

return (
<AnimatePresence>
{showMenuEntryButton ? (
<motion.div
animate={{ opacity: 1 }}
className='pointer-events-none fixed inset-0 z-20 flex justify-center'
className='pointer-events-none fixed inset-0 z-20 flex items-end justify-center bg-black/50'
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
transition={{ duration: 0.1 }}
>
<div
aria-hidden
className='mt-20 flex h-16 w-16 cursor-pointer items-center justify-center rounded-full bg-white text-rose-700 shadow-xl'
className='mb-20 flex h-16 w-16 cursor-pointer items-center justify-center rounded-full bg-white text-rose-700 shadow-xl'
onClick={onClick}
>
<span className='icon-[mdi--cog-pause-outline] h-8 w-8' />
Expand Down
3 changes: 2 additions & 1 deletion src/views/components/home-screen/game-menus/menu-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { useEffect, useRef, useState } from 'react'
import { isUsingDummy, onCancel, resumeGame } from '../../../../core'
import { isUsingDemo } from '../../../../core/exposed/is-using-demo'
import { SpatialNavigation } from '../../../lib/spatial-navigation'
import { previousFocusedElementAtom, shouldFocusStatesListAtom, showMenuOverlayAtom } from './atoms'
import { showMenuOverlayAtom } from '../../atoms'
import { previousFocusedElementAtom, shouldFocusStatesListAtom } from './atoms'
import { StatesList } from './states-list'

const menuButtonClassNames =
Expand Down
3 changes: 2 additions & 1 deletion src/views/components/home-screen/game-menus/menu-overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { useAsyncFn } from 'react-use'
import { loadGameState, restartGame, resumeGame, saveGameState } from '../../../../core'
import { showMenuOverlayAtom } from '../../atoms'
import { BouncingEllipsis } from '../../common/bouncing-ellipsis'
import { LightInputButton } from '../../common/light-input-button'
import { LoadingScreen } from '../../common/loading-screen'
import { useExit } from '../hooks'
import { previousFocusedElementAtom, showMenuOverlayAtom } from './atoms'
import { previousFocusedElementAtom } from './atoms'
import { MenuItems } from './menu-items'

export function MenuOverlay() {
Expand Down
4 changes: 2 additions & 2 deletions src/views/components/home-screen/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { exitGame } from '../../../core'
import { emitter } from '../../lib/emitter'
import { isGameRunningAtom } from '../atoms'
import { isGameRunningAtom, showMenuOverlayAtom } from '../atoms'
import { maskAtom } from './atoms'
import { previousFocusedElementAtom, showMenuOverlayAtom } from './game-menus/atoms'
import { previousFocusedElementAtom } from './game-menus/atoms'

export function useExit() {
const setShowMenuOverlay = useSetAtom(showMenuOverlayAtom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isGameLaunchedAtom } from '../../atoms'
import { VirtualControllerButtons } from './virtual-controller-buttons'

function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
}

export function VirtualController() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,58 @@
import { clsx } from 'clsx'
import { motion } from 'framer-motion'
import { type UIEvent, useState } from 'react'
import { pressController } from '../../../../core'

interface VirtualButtonProps {
name: string
name?: string
onTap?: () => void
}

export function VirtualButton({ name }: VirtualButtonProps) {
export function VirtualButton({ name, onTap }: VirtualButtonProps) {
const [pressing, setPressing] = useState(false)

function onPress(event: UIEvent<HTMLDivElement>) {
event.stopPropagation()
setPressing(true)
pressController(name, 'down')
if (name) {
for (const n of name.split(',')) {
pressController(n, 'down')
}
}

onTap?.()
}

function onRelease(event: UIEvent<HTMLDivElement>) {
event.stopPropagation()
if (pressing) {
setPressing(false)
pressController(name, 'up')
if (name) {
for (const n of name.split(',')) {
pressController(n, 'up')
}
}
}
}

return (
<div
<motion.div
animate={{ opacity: 1 }}
aria-hidden
className={clsx(
'flex h-full w-full select-none items-center justify-center text-transparent',
pressing ? 'bg-white' : 'bg-transparent',
)}
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
onMouseDown={onPress}
onMouseLeave={onRelease}
onMouseUp={onRelease}
onTouchEnd={onRelease}
onTouchStart={onPress}
transition={{ duration: 0.2 }}
>
{name}
</div>
</motion.div>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { useSetAtom } from 'jotai'
import { useEffect, useState } from 'react'
import { showMenuOverlayAtom } from '../../atoms'
import { VirtualButton } from './virtual-button'

export function VirtualControllerButtons() {
const [show, setShow] = useState(false)
const setShowMenuOverlayAtom = useSetAtom(showMenuOverlayAtom)

function onTapMenuButton() {
setShowMenuOverlayAtom(true)
setShow(false)
}

useEffect(() => {
function toggleShow(event: TouchEvent) {
Expand All @@ -28,50 +36,69 @@ export function VirtualControllerButtons() {
}

return (
<div className='fixed bottom-0 z-[11] w-full px-4 pb-12'>
<div className='fixed bottom-0 z-[11] w-full pb-10 portrait:px-4 landscape:px-10'>
<div className='flex w-full items-end justify-between'>
<div className='flex flex-col items-center'>
<div className='h-12 w-12 overflow-hidden rounded-t-md bg-white/50'>
<VirtualButton name='up' />
<div className='flex flex-col items-center overflow-hidden rounded-3xl border-8 border-gray-500 bg-white/60'>
<div className='flex'>
<div className='h-12 w-12 overflow-hidden'>
<VirtualButton name='up,left' />
</div>
<div className='h-12 w-12 overflow-hidden rounded-t bg-white/80'>
<VirtualButton name='up' />
</div>
<div className='h-12 w-12 overflow-hidden'>
<VirtualButton name='up,right' />
</div>
</div>
<div className='flex'>
<div className='h-12 w-12 overflow-hidden rounded-l-md bg-white/50'>
<div className='h-12 w-12 overflow-hidden rounded-l bg-white/80'>
<VirtualButton name='left' />
</div>
<div className='h-12 w-12 overflow-hidden bg-white/50' />
<div className='h-12 w-12 overflow-hidden rounded-r-md bg-white/50'>
<div className='h-12 w-12 overflow-hidden bg-white/80' />
<div className='h-12 w-12 overflow-hidden rounded-r bg-white/80'>
<VirtualButton name='right' />
</div>
</div>
<div className='h-12 w-12 overflow-hidden rounded-b-md bg-white/50'>
<VirtualButton name='down' />
<div className='flex'>
<div className='h-12 w-12 overflow-hidden'>
<VirtualButton name='down,left' />
</div>
<div className='h-12 w-12 overflow-hidden rounded-b bg-white/80 '>
<VirtualButton name='down' />
</div>
<div className='h-12 w-12 overflow-hidden'>
<VirtualButton name='down,right' />
</div>
</div>
</div>

<div className='flex flex-col items-center'>
<div className='h-14 w-14 overflow-hidden rounded-full bg-white/50'>
<div className='h-16 w-16 overflow-hidden rounded-full border-8 border-gray-500 bg-white/80'>
<VirtualButton name='x' />
</div>
<div className='flex'>
<div className='h-14 w-14 overflow-hidden rounded-full bg-white/50'>
<div className='h-16 w-16 overflow-hidden rounded-full border-8 border-gray-500 bg-white/80'>
<VirtualButton name='y' />
</div>
<div className='h-14 w-14 overflow-hidden' />
<div className='h-14 w-14 overflow-hidden rounded-full bg-white/50'>
<div className='h-16 w-16 overflow-hidden' />
<div className='h-16 w-16 overflow-hidden rounded-full border-8 border-gray-500 bg-white/80'>
<VirtualButton name='a' />
</div>
</div>
<div className='h-14 w-14 overflow-hidden rounded-full bg-white/50'>
<div className='h-16 w-16 overflow-hidden rounded-full border-8 border-gray-500 bg-white/80'>
<VirtualButton name='b' />
</div>
</div>
</div>

<div className='mt-4 flex items-center justify-center gap-8'>
<div className='h-6 w-16 overflow-hidden rounded-md bg-white/50'>
<div className='mt-10 flex items-center justify-center gap-10'>
<div className='h-8 w-20 overflow-hidden rounded-md border-8 border-gray-500 bg-white/80'>
<VirtualButton name='select' />
</div>
<div className='h-6 w-16 overflow-hidden rounded-md bg-white/50'>
<div className='h-12 w-12 overflow-hidden rounded-full border-8 border-gray-500 bg-white/80'>
<VirtualButton onTap={onTapMenuButton} />
</div>
<div className='h-8 w-20 overflow-hidden rounded-md border-8 border-gray-500 bg-white/80'>
<VirtualButton name='start' />
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/views/styles/index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

body
font-family: 'Noto Sans', system-ui, -apple-system, BlinkMacSystemFont, sans-serif
user-select: none

:focus
@apply outline-none
Expand Down

0 comments on commit bfff3ca

Please sign in to comment.