Adding GBC support and keyboard navigation.
This commit is contained in:
parent
10f04ce059
commit
7ffeb360e7
@ -1,11 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
|
|
||||||
import {MIN_WIDTH, MIN_HEIGHT} from '/constants';
|
import { MIN_WIDTH_GBA, MIN_HEIGHT_GBA } from '@msgba/constants'
|
||||||
|
|
||||||
export interface CanvasGBAEmulatorProps {
|
export interface CanvasGBAEmulatorProps {
|
||||||
canvasRef: React.RefObject<HTMLCanvasElement>
|
canvasRef: React.RefObject<HTMLCanvasElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CanvasGBAEmulator(props: CanvasGBAEmulatorProps) {
|
export default function CanvasGBAEmulator (props: CanvasGBAEmulatorProps): JSX.Element {
|
||||||
return (<canvas ref={props.canvasRef} width={MIN_WIDTH} height={MIN_HEIGHT}></canvas>);
|
return (<canvas ref={props.canvasRef} width={MIN_WIDTH_GBA} height={MIN_HEIGHT_GBA}></canvas>)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { sendKeyDown } from '@msgba/packet'
|
|||||||
|
|
||||||
export interface OverlayControlsProps {
|
export interface OverlayControlsProps {
|
||||||
firstMenuElement: React.RefObject<HTMLAnchorElement>
|
firstMenuElement: React.RefObject<HTMLAnchorElement>
|
||||||
|
controlsRef: React.RefObject<HTMLDivElement>
|
||||||
setHiddenMenu: (c: boolean) => void
|
setHiddenMenu: (c: boolean) => void
|
||||||
webSocket: WebSocket | null
|
webSocket: WebSocket | null
|
||||||
};
|
};
|
||||||
@ -19,15 +20,9 @@ interface ControlValue {
|
|||||||
|
|
||||||
type ControlMap = Record<string, ControlValue>
|
type ControlMap = Record<string, ControlValue>
|
||||||
|
|
||||||
export default function OverlayControls ({ firstMenuElement, setHiddenMenu, webSocket }: OverlayControlsProps): JSX.Element {
|
export default function OverlayControls ({ firstMenuElement, setHiddenMenu, webSocket, controlsRef }: OverlayControlsProps): JSX.Element {
|
||||||
function showOverlayMenu (): void {
|
function showOverlayMenu (): void {
|
||||||
setHiddenMenu(false)
|
setHiddenMenu(false)
|
||||||
setTimeout(() => {
|
|
||||||
if (firstMenuElement.current == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
firstMenuElement.current.focus()
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
const onGoingTouches = new Map<number, number>()
|
const onGoingTouches = new Map<number, number>()
|
||||||
|
|
||||||
@ -212,6 +207,11 @@ export default function OverlayControls ({ firstMenuElement, setHiddenMenu, webS
|
|||||||
'Enter', 'Space', 'ArrowUp', 'ArrowDown',
|
'Enter', 'Space', 'ArrowUp', 'ArrowDown',
|
||||||
'ArrowLeft', 'ArrowRight']
|
'ArrowLeft', 'ArrowRight']
|
||||||
function onPressControl (e: React.KeyboardEvent<HTMLDivElement>): void {
|
function onPressControl (e: React.KeyboardEvent<HTMLDivElement>): void {
|
||||||
|
if (e.code === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
showOverlayMenu()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (webSocket == null) {
|
if (webSocket == null) {
|
||||||
console.log('There is not websocket')
|
console.log('There is not websocket')
|
||||||
return
|
return
|
||||||
@ -235,7 +235,7 @@ export default function OverlayControls ({ firstMenuElement, setHiddenMenu, webS
|
|||||||
}
|
}
|
||||||
document.onselectstart = () => false
|
document.onselectstart = () => false
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} className="overlay" onKeyDown={onPressControl} onKeyUp={onUnpressControl}>
|
<div ref={controlsRef} tabIndex={-1} className="overlay" onKeyDown={onPressControl} onKeyUp={onUnpressControl}>
|
||||||
<div className="vertical-padding">
|
<div className="vertical-padding">
|
||||||
</div>
|
</div>
|
||||||
<div className="controls" onTouchStart={touchStartControls} onTouchMove={touchMoveControls} onTouchEnd={touchEndControls}>
|
<div className="controls" onTouchStart={touchStartControls} onTouchMove={touchMoveControls} onTouchEnd={touchEndControls}>
|
||||||
|
@ -9,7 +9,7 @@ import { sendHello, handleSendFrame } from '@msgba/packet'
|
|||||||
|
|
||||||
import Endian from '@msgba/endian'
|
import Endian from '@msgba/endian'
|
||||||
|
|
||||||
import { MIN_WIDTH, MIN_HEIGHT, PACKET_ID_SEND_FRAME } from '@msgba/constants'
|
import { MIN_WIDTH_GBA, MIN_HEIGHT_GBA, MIN_WIDTH_GBC, MIN_HEIGHT_GBC, PACKET_ID_SEND_FRAME } from '@msgba/constants'
|
||||||
|
|
||||||
export interface handleClickStartEmulationButtonObjectArgs {
|
export interface handleClickStartEmulationButtonObjectArgs {
|
||||||
e: React.MouseEvent<HTMLInputElement>
|
e: React.MouseEvent<HTMLInputElement>
|
||||||
@ -20,12 +20,16 @@ export interface handleClickStartEmulationButtonObjectArgs {
|
|||||||
setHiddenMenu: (c: boolean) => void
|
setHiddenMenu: (c: boolean) => void
|
||||||
setHiddenFormSelectFiles: (c: boolean) => void
|
setHiddenFormSelectFiles: (c: boolean) => void
|
||||||
setWebSocket: (c: WebSocket | null) => void
|
setWebSocket: (c: WebSocket | null) => void
|
||||||
|
isGBC: boolean
|
||||||
|
setIsGBC: (c: boolean) => void
|
||||||
|
controlsRef: React.RefObject<HTMLDivElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickStartEmulationButton ({
|
function handleClickStartEmulationButton ({
|
||||||
e, inputRom, inputSaveState, canvas,
|
e, inputRom, inputSaveState, canvas,
|
||||||
setEmulationStarted, setHiddenMenu,
|
setEmulationStarted, setHiddenMenu,
|
||||||
setHiddenFormSelectFiles, setWebSocket
|
setHiddenFormSelectFiles, setWebSocket,
|
||||||
|
isGBC, setIsGBC, controlsRef
|
||||||
}: handleClickStartEmulationButtonObjectArgs): void {
|
}: handleClickStartEmulationButtonObjectArgs): void {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (canvas.current == null) {
|
if (canvas.current == null) {
|
||||||
@ -71,7 +75,7 @@ function handleClickStartEmulationButton ({
|
|||||||
setHiddenFormSelectFiles(true)
|
setHiddenFormSelectFiles(true)
|
||||||
}
|
}
|
||||||
webSocket.addEventListener('message', (event) => {
|
webSocket.addEventListener('message', (event) => {
|
||||||
onWebSocketPacket(event, canvas.current, ctx)
|
onWebSocketPacket(event, canvas.current, ctx, isGBC, setIsGBC)
|
||||||
})
|
})
|
||||||
}).catch((c: string) => {
|
}).catch((c: string) => {
|
||||||
console.log('Unable to convert file to array_buffer')
|
console.log('Unable to convert file to array_buffer')
|
||||||
@ -83,7 +87,8 @@ function handleClickStartEmulationButton ({
|
|||||||
|
|
||||||
function onWebSocketPacket (event: MessageEvent,
|
function onWebSocketPacket (event: MessageEvent,
|
||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement | null,
|
||||||
ctx: CanvasRenderingContext2D): void {
|
ctx: CanvasRenderingContext2D,
|
||||||
|
isGBC: boolean, setIsGBC: (c: boolean) => void): void {
|
||||||
const buffer = event.data
|
const buffer = event.data
|
||||||
let packetU8: Uint8Array | null = new Uint8Array(buffer)
|
let packetU8: Uint8Array | null = new Uint8Array(buffer)
|
||||||
const id = Endian.byteArrayToU64BigEndian(packetU8.slice(0, 8))
|
const id = Endian.byteArrayToU64BigEndian(packetU8.slice(0, 8))
|
||||||
@ -93,7 +98,7 @@ function onWebSocketPacket (event: MessageEvent,
|
|||||||
packetU8 = null
|
packetU8 = null
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case PACKET_ID_SEND_FRAME:
|
case PACKET_ID_SEND_FRAME:
|
||||||
handleSendFrame(rawData, canvas, ctx)
|
handleSendFrame(rawData, canvas, setIsGBC)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
console.log(`Received unknown packet ${id}`)
|
console.log(`Received unknown packet ${id}`)
|
||||||
@ -101,8 +106,9 @@ function onWebSocketPacket (event: MessageEvent,
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Page (): JSX.Element {
|
export default function Page (): JSX.Element {
|
||||||
const screenDimensions = useScreenDimensions()
|
const [isGBC, setIsGBC] = React.useState<boolean>(false)
|
||||||
const emulatorDimensions = calculateSizeEmulator(screenDimensions)
|
const screenDimensions = useScreenDimensions(isGBC)
|
||||||
|
const emulatorDimensions = calculateSizeEmulator(screenDimensions, isGBC)
|
||||||
const canvasRef = React.useRef<HTMLCanvasElement>(null)
|
const canvasRef = React.useRef<HTMLCanvasElement>(null)
|
||||||
function resizeCanvas (): void {
|
function resizeCanvas (): void {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
@ -127,6 +133,29 @@ export default function Page (): JSX.Element {
|
|||||||
const [emulationStarted, setEmulationStarted] = React.useState<boolean>(false)
|
const [emulationStarted, setEmulationStarted] = React.useState<boolean>(false)
|
||||||
const [hiddenMenu, setHiddenMenu] = React.useState<boolean>(true)
|
const [hiddenMenu, setHiddenMenu] = React.useState<boolean>(true)
|
||||||
const [webSocket, setWebSocket] = React.useState<WebSocket | null>(null)
|
const [webSocket, setWebSocket] = React.useState<WebSocket | null>(null)
|
||||||
|
|
||||||
|
const controlsRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('Focusing the main screen')
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hiddenFormSelectFiles) {
|
||||||
|
if (refInputRom.current != null) {
|
||||||
|
refInputRom.current.focus()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!hiddenMenu) {
|
||||||
|
if (firstMenuElement.current != null) {
|
||||||
|
firstMenuElement.current.focus()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlsRef.current != null && hiddenMenu) {
|
||||||
|
controlsRef.current.focus()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}, [hiddenMenu, hiddenFormSelectFiles])
|
||||||
const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>): void => {
|
const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>): void => {
|
||||||
handleClickStartEmulationButton({
|
handleClickStartEmulationButton({
|
||||||
e,
|
e,
|
||||||
@ -136,7 +165,10 @@ export default function Page (): JSX.Element {
|
|||||||
canvas: canvasRef,
|
canvas: canvasRef,
|
||||||
setHiddenMenu,
|
setHiddenMenu,
|
||||||
setHiddenFormSelectFiles,
|
setHiddenFormSelectFiles,
|
||||||
setWebSocket
|
setWebSocket,
|
||||||
|
isGBC,
|
||||||
|
setIsGBC,
|
||||||
|
controlsRef
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const firstMenuElement = React.useRef<HTMLAnchorElement>(null)
|
const firstMenuElement = React.useRef<HTMLAnchorElement>(null)
|
||||||
@ -144,7 +176,7 @@ export default function Page (): JSX.Element {
|
|||||||
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(false)
|
const [isFullscreen, setIsFullscreen] = React.useState<boolean>(false)
|
||||||
return (
|
return (
|
||||||
<div ref={screenRef}>
|
<div ref={screenRef}>
|
||||||
<OverlayControls firstMenuElement={firstMenuElement}
|
<OverlayControls controlsRef={controlsRef} firstMenuElement={firstMenuElement}
|
||||||
setHiddenMenu={setHiddenMenu} webSocket={webSocket}/>
|
setHiddenMenu={setHiddenMenu} webSocket={webSocket}/>
|
||||||
<OverlayMenu hiddenMenu={hiddenMenu}
|
<OverlayMenu hiddenMenu={hiddenMenu}
|
||||||
setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted}
|
setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted}
|
||||||
@ -171,7 +203,7 @@ function getScreenDimensions (): EmulatorDimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useScreenDimensions (): EmulatorDimensions {
|
function useScreenDimensions (isGBC: boolean): EmulatorDimensions {
|
||||||
const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions())
|
const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions())
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -184,7 +216,7 @@ function useScreenDimensions (): EmulatorDimensions {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', onResize)
|
window.removeEventListener('resize', onResize)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [isGBC])
|
||||||
return screenDimensions
|
return screenDimensions
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +232,7 @@ export interface EmulatorDimensions {
|
|||||||
height?: number
|
height?: number
|
||||||
};
|
};
|
||||||
|
|
||||||
function calculateSizeEmulator (screenDimensions: EmulatorDimensions): EmulatorDimensions {
|
function calculateSizeEmulator (screenDimensions: EmulatorDimensions, isGBC: boolean): EmulatorDimensions {
|
||||||
if (screenDimensions.width === undefined || screenDimensions.height === undefined) {
|
if (screenDimensions.width === undefined || screenDimensions.height === undefined) {
|
||||||
console.error(screenDimensions, 'screenDimensions has undefined fields')
|
console.error(screenDimensions, 'screenDimensions has undefined fields')
|
||||||
return {}
|
return {}
|
||||||
@ -208,20 +240,22 @@ function calculateSizeEmulator (screenDimensions: EmulatorDimensions): EmulatorD
|
|||||||
const width = screenDimensions.width
|
const width = screenDimensions.width
|
||||||
const height = screenDimensions.height
|
const height = screenDimensions.height
|
||||||
const emulatorDimensions: EmulatorDimensions = {}
|
const emulatorDimensions: EmulatorDimensions = {}
|
||||||
if (width < MIN_WIDTH || height < MIN_HEIGHT) {
|
const minWidth = !isGBC ? MIN_WIDTH_GBA : MIN_WIDTH_GBC
|
||||||
|
const minHeight = !isGBC ? MIN_HEIGHT_GBA : MIN_HEIGHT_GBC
|
||||||
|
if (width < minWidth || height < minHeight) {
|
||||||
return {
|
return {
|
||||||
width: MIN_WIDTH,
|
width: minWidth,
|
||||||
height: MIN_HEIGHT
|
height: minHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ratioWidth = width / MIN_WIDTH
|
const ratioWidth = width / minWidth
|
||||||
const ratioHeight = height / MIN_HEIGHT
|
const ratioHeight = height / minHeight
|
||||||
if (ratioWidth < ratioHeight) {
|
if (ratioWidth < ratioHeight) {
|
||||||
emulatorDimensions.width = MIN_WIDTH * ratioWidth
|
emulatorDimensions.width = minWidth * ratioWidth
|
||||||
emulatorDimensions.height = MIN_HEIGHT * ratioWidth
|
emulatorDimensions.height = minHeight * ratioWidth
|
||||||
} else {
|
} else {
|
||||||
emulatorDimensions.height = MIN_HEIGHT * ratioHeight
|
emulatorDimensions.height = minHeight * ratioHeight
|
||||||
emulatorDimensions.width = MIN_WIDTH * ratioHeight
|
emulatorDimensions.width = minWidth * ratioHeight
|
||||||
}
|
}
|
||||||
return emulatorDimensions
|
return emulatorDimensions
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export const MIN_WIDTH = 240
|
export const MIN_WIDTH_GBA = 240
|
||||||
export const MIN_HEIGHT = 160
|
export const MIN_HEIGHT_GBA = 160
|
||||||
|
export const MIN_WIDTH_GBC = 160
|
||||||
|
export const MIN_HEIGHT_GBC = 144
|
||||||
export const PACKET_ID_HELLO = 0n
|
export const PACKET_ID_HELLO = 0n
|
||||||
export const PACKET_ID_SEND_FRAME = 1n
|
export const PACKET_ID_SEND_FRAME = 1n
|
||||||
export const PACKET_ID_KEY_DOWN = 2n
|
export const PACKET_ID_KEY_DOWN = 2n
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Endian from '@msgba/endian'
|
import Endian from '@msgba/endian'
|
||||||
import { MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_KEY_DOWN, PACKET_ID_SAVE_REQUEST } from '@msgba/constants'
|
import { MIN_WIDTH_GBC, MIN_WIDTH_GBA, MIN_HEIGHT_GBA, MIN_HEIGHT_GBC, PACKET_ID_HELLO, PACKET_ID_KEY_DOWN, PACKET_ID_SAVE_REQUEST } from '@msgba/constants'
|
||||||
|
|
||||||
function concatU8Array (array1: Uint8Array, array2: Uint8Array): Uint8Array {
|
function concatU8Array (array1: Uint8Array, array2: Uint8Array): Uint8Array {
|
||||||
const finalArray = new Uint8Array(array1.length + array2.length)
|
const finalArray = new Uint8Array(array1.length + array2.length)
|
||||||
@ -47,18 +47,33 @@ export function sendPacket (websocket: WebSocket, id: bigint, rawData: Uint8Arra
|
|||||||
websocket.send(packetBuffer)
|
websocket.send(packetBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleSendFrame (rawData: Uint8Array, canvas: HTMLCanvasElement | null, ctx: CanvasRenderingContext2D): void {
|
export function handleSendFrame (rawData: Uint8Array, canvas: HTMLCanvasElement | null, setIsGBC: (c: boolean) => void): void {
|
||||||
console.log('Reachs here')
|
|
||||||
if (canvas == null) {
|
if (canvas == null) {
|
||||||
console.log('No canvas')
|
console.log('No canvas')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (ctx == null) {
|
||||||
|
console.log('No context')
|
||||||
|
return
|
||||||
|
}
|
||||||
let data: Uint8Array | null = rawData
|
let data: Uint8Array | null = rawData
|
||||||
|
const stride = Endian.byteArrayToU32BigEndian(data.slice(0, 4))
|
||||||
|
let isGBC = false
|
||||||
|
if (stride === 160) {
|
||||||
|
isGBC = true
|
||||||
|
setIsGBC(true)
|
||||||
|
} else {
|
||||||
|
setIsGBC(false)
|
||||||
|
}
|
||||||
data = data.slice(4, data.length)
|
data = data.slice(4, data.length)
|
||||||
const outputBufferSize = Endian.byteArrayToU64BigEndian(data.slice(0, 8))
|
const outputBufferSize = Endian.byteArrayToU64BigEndian(data.slice(0, 8))
|
||||||
// TODO: This number conversion is not great. Is there other option?
|
// TODO: This number conversion is not great. Is there other option?
|
||||||
data = data.slice(8, Number(outputBufferSize))
|
data = data.slice(8, Number(outputBufferSize))
|
||||||
const imgData = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT)
|
let imgData = ctx.createImageData(MIN_WIDTH_GBA, MIN_HEIGHT_GBA)
|
||||||
|
if (isGBC) {
|
||||||
|
imgData = ctx.createImageData(MIN_WIDTH_GBC, MIN_HEIGHT_GBC)
|
||||||
|
}
|
||||||
const imgDataU8 = new Uint8Array(imgData.data.buffer)
|
const imgDataU8 = new Uint8Array(imgData.data.buffer)
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
if (i % 4 === 3) {
|
if (i % 4 === 3) {
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user