Adding functional controls.

This commit is contained in:
Sergiotarxz 2023-03-25 15:20:18 +01:00
parent 282a4b671d
commit 93cc25adf5
7 changed files with 213 additions and 86 deletions

View File

@ -1,12 +1,14 @@
import * as React from 'react';
import {HOME_BUTTON_IMAGE} from '/constants';
import {sendKeyDown} from '/packet';
export interface OverlayControlsProps {
firstMenuElement: React.RefObject<HTMLAnchorElement>,
setHiddenMenu: (c: boolean) => void,
webSocket: WebSocket | null;
};
export default function OverlayControls({firstMenuElement, setHiddenMenu}: OverlayControlsProps) {
export default function OverlayControls({firstMenuElement, setHiddenMenu, webSocket}: OverlayControlsProps) {
function showOverlayMenu() {
setHiddenMenu(false);
setTimeout(() => {
@ -16,20 +18,103 @@ export default function OverlayControls({firstMenuElement, setHiddenMenu}: Overl
firstMenuElement.current.focus();
}, 100);
}
const [onGoingTouches, setOnGoingTouches] = React.useState<{[id: string]: number}>({});
function mouseDown(e: React.MouseEvent<HTMLAnchorElement> | React.TouchEvent<HTMLAnchorElement>, key: number) {
e.preventDefault();
if (webSocket == null) {
console.log('There is not websocket');
return;
}
sendKeyDown(webSocket, true, key);
}
function touchStart(e: React.TouchEvent<HTMLAnchorElement>, key: number) {
e.preventDefault();
if (webSocket == null) {
console.log('There is not websocket');
return;
}
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const idx = touch.identifier;
onGoingTouches[idx] = key;
sendKeyDown(webSocket, true, key);
}
}
function touchEnd(e: React.TouchEvent<HTMLAnchorElement|HTMLDivElement>) {
e.preventDefault();
if (webSocket == null) {
console.log('There is not websocket');
return;
}
for (let i = 0; i < e.changedTouches.length; i++) {
const touch = e.changedTouches[i];
const idx = touch.identifier;
const key = onGoingTouches[idx];
delete onGoingTouches[idx];
sendKeyDown(webSocket, false, key);
}
}
function mouseUp(e: React.MouseEvent<HTMLAnchorElement> | React.TouchEvent<HTMLAnchorElement>, key: number) {
e.preventDefault();
if (webSocket == null) {
console.log('There is not websocket');
return;
}
sendKeyDown(webSocket, false, key);
}
document.onselectstart = () => false;
return (
<div className="overlay">
<div className="overlay" onTouchEnd={touchEnd}>
<div className="vertical-padding">
</div>
<div className="controls">
<a href="#" className="gear control" onClick={showOverlayMenu}>
<a tabIndex={-1} className="gear control" onClick={showOverlayMenu} onTouchStart={showOverlayMenu}>
<img src={HOME_BUTTON_IMAGE} alt="Go to menu. (House icon)"/>
</a>
<a href="#" className="control-a control control-button">A</a>
<a href="#" className="control-b control control-button">B</a>
<a href="#" className="control-up control control-pad-button">&#94;</a>
<a href="#" className="control-down control control-pad-button">v</a>
<a href="#" className="control-left control control-pad-button">&lt;</a>
<a href="#" className="control-right control control-pad-button">&gt;</a>
<a tabIndex={-1}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, 0)}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, 0)}
onTouchStart={(e: React.TouchEvent<HTMLAnchorElement>) => touchStart(e, 0)}
onTouchEnd={(e: React.TouchEvent<HTMLAnchorElement>) => touchEnd(e)}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault()}
className="control-a control control-button">A</a>
<a tabIndex={-1}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, 1)}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, 1)}
onTouchStart={(e: React.TouchEvent<HTMLAnchorElement>) => touchStart(e, 1)}
onTouchEnd={(e: React.TouchEvent<HTMLAnchorElement>) => touchEnd(e)}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault()}
className="control-b control control-button">B</a>
<a tabIndex={-1}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, 6)}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, 6)}
onTouchStart={(e: React.TouchEvent<HTMLAnchorElement>) => touchStart(e, 6)}
onTouchEnd={(e: React.TouchEvent<HTMLAnchorElement>) => touchEnd(e)}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault()}
className="control-up control control-pad-button">&#94;</a>
<a tabIndex={-1}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, 7)}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, 7)}
onTouchStart={(e: React.TouchEvent<HTMLAnchorElement>) => touchStart(e, 7)}
onTouchEnd={(e: React.TouchEvent<HTMLAnchorElement>) => touchEnd(e)}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault()}
className="control-down control control-pad-button">v</a>
<a tabIndex={-1}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, 8)}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, 8)}
onTouchStart={(e: React.TouchEvent<HTMLAnchorElement>) => touchStart(e, 8)}
onTouchEnd={(e: React.TouchEvent<HTMLAnchorElement>) => touchEnd(e)}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault()}
className="control-left control control-pad-button">&lt;</a>
<a tabIndex={-1}
onMouseDown={(e: React.MouseEvent<HTMLAnchorElement>) => mouseDown(e, 9)}
onMouseUp={(e: React.MouseEvent<HTMLAnchorElement>) => mouseUp(e, 9)}
onTouchStart={(e: React.TouchEvent<HTMLAnchorElement>) => touchStart(e, 9)}
onTouchEnd={(e: React.TouchEvent<HTMLAnchorElement>) => touchEnd(e)}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => e.preventDefault()}
className="control-right control control-pad-button">&gt;</a>
</div>
</div>
);

View File

@ -7,28 +7,28 @@ import OverlayControls from '/components/overlay-controls';
import OverlayMenu from '/components/overlay-menu';
import OverlaySelectFiles from '/components/overlay-select-files';
import CloseButton from '/components/close-button';
import {sendHello, handleSendFrame} from '/packet';
import Endian from '/endian';
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants';
type setBooleanCallback = (c: boolean) => boolean;
export interface handleClickStartEmulationButtonObjectArgs {
e: React.MouseEvent<HTMLInputElement>;
inputRom: HTMLInputElement | null;
inputSaveState: HTMLInputElement | null;
canvas: HTMLCanvasElement | null;
printingFrame: boolean;
setPrintingFrame: (c: setBooleanCallback) => void;
setPrintingFrame: (c: boolean) => void;
setEmulationStarted: (c: boolean) => void,
setHiddenMenu: (c: boolean) => void;
setHiddenFormSelectFiles: (c: boolean) => void;
setWebSocket: (c: WebSocket) => void;
};
function handleClickStartEmulationButton({e, inputRom, inputSaveState, canvas, printingFrame,
setPrintingFrame, setEmulationStarted, setHiddenMenu,
setHiddenFormSelectFiles}: handleClickStartEmulationButtonObjectArgs) {
setHiddenFormSelectFiles, setWebSocket}: handleClickStartEmulationButtonObjectArgs) {
e.preventDefault();
if (canvas == null) {
alert('Canvas does not exists?');
@ -57,60 +57,29 @@ function handleClickStartEmulationButton({e, inputRom, inputSaveState, canvas, p
savestate_file.arrayBuffer().then((savestate_buffer) => {
const rom_array = new Uint8Array(rom_buffer);
const savestate_array = new Uint8Array(savestate_buffer);
const websocket = new WebSocket(`ws://${window.location.host}/ws`);
websocket.binaryType = 'arraybuffer';
websocket.onclose = (message) => {
const webSocket = new WebSocket(`ws://${window.location.host}/ws`);
setWebSocket(webSocket);
webSocket.binaryType = 'arraybuffer';
webSocket.onclose = (message) => {
setEmulationStarted(false);
console.log('Closing websocket.');
}
websocket.onopen = () => {
webSocket.onopen = () => {
console.log('Opened websocket.');
setEmulationStarted(true);
sendHello(websocket, rom_array, savestate_array);
sendHello(webSocket, rom_array, savestate_array);
setHiddenMenu(true);
setHiddenFormSelectFiles(true);
};
setPrintingFrame(c => false);
websocket.addEventListener('message', (event) => {
setPrintingFrame(false);
webSocket.addEventListener('message', (event) => {
onWebSocketPacket(event, canvas, ctx, printingFrame, setPrintingFrame)
});
});
});
}
function concatU8Array(array1: Uint8Array, array2: Uint8Array) {
const final_array = new Uint8Array(array1.length + array2.length);
final_array.set(array1);
final_array.set(array2, array1.length);
return final_array;
}
function sendPacket(websocket: WebSocket, id: bigint, raw_data: Uint8Array) {
const packet_u8 = concatU8Array(
concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(raw_data.length))),
raw_data
);
const packet_buffer = packet_u8.buffer;
console.log('Sending packet');
websocket.send(packet_buffer);
}
function sendHello(websocket: WebSocket, rom_array: Uint8Array, savestate_array: Uint8Array) {
console.log('Sending hello.');
const length_rom = BigInt(rom_array.length);
const length_savestate = BigInt(savestate_array.length);
const raw_data =
concatU8Array(
concatU8Array(
concatU8Array(Endian.u64ToByteArrayBigEndian(length_rom), rom_array),
Endian.u64ToByteArrayBigEndian(length_savestate)
),
savestate_array
);
sendPacket(websocket, PACKET_ID_HELLO, raw_data);
}
function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) {
function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: boolean) => void) {
const buffer = event.data;
let packet_u8: Uint8Array | null = new Uint8Array(buffer);
const id = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8));
@ -127,34 +96,6 @@ function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement, ctx:
}
}
function handleSendFrame(raw_data: Uint8Array, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) {
if (printingFrame) {
return;
}
setPrintingFrame(c => true);
let data: Uint8Array | null = raw_data;
const stride = Endian.byteArrayToU32BigEndian(data.slice(0, 4));
data = data.slice(4, data.length);
const output_buffer_size = Endian.byteArrayToU64BigEndian(data.slice(0, 8));
data = data.slice(8, data.length);
const img_data = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT);
const img_data_u8 = new Uint8Array(img_data.data.buffer);
for (let i = 0; i<data.length; i++) {
if (i % 4 == 3) {
img_data_u8[i] = 255;
continue;
}
img_data_u8[i] = data[i];
}
data = null;
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx, printingFrame));
}
function drawBitmap(bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean) {
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
printingFrame = false;
}
export default function Page() {
const screenDimensions = useScreenDimensions();
const emulatorDimensions = calculateSizeEmulator(screenDimensions);
@ -182,6 +123,7 @@ export default function Page() {
const refInputSaveState = React.useRef<HTMLInputElement | null>(null);
const [emulationStarted, setEmulationStarted] = React.useState<boolean>(false);
const [hiddenMenu, setHiddenMenu] = React.useState<boolean>(true);
const [webSocket, setWebSocket] = React.useState<WebSocket | null>(null);
const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>) => {
handleClickStartEmulationButton({
e: e,
@ -193,6 +135,8 @@ export default function Page() {
printingFrame: printingFrame,
setHiddenMenu: setHiddenMenu,
setHiddenFormSelectFiles: setHiddenFormSelectFiles,
setWebSocket: setWebSocket,
});
};
const firstMenuElement = React.useRef<HTMLAnchorElement>(null);
@ -201,7 +145,7 @@ export default function Page() {
return (
<div ref={screenRef}>
<OverlayControls firstMenuElement={firstMenuElement}
setHiddenMenu={setHiddenMenu}/>
setHiddenMenu={setHiddenMenu} webSocket={webSocket}/>
<OverlayMenu hiddenMenu={hiddenMenu}
setHiddenMenu={setHiddenMenu} emulationStarted={emulationStarted}
setHiddenFormSelectFiles={setHiddenFormSelectFiles} screenRef={screenRef}

View File

@ -2,6 +2,7 @@ export const MIN_WIDTH = 240;
export const MIN_HEIGHT = 160;
export const PACKET_ID_HELLO = 0n;
export const PACKET_ID_SEND_FRAME = 1n;
export const PACKET_ID_KEY_DOWN = 2n;
export const CLOSE_BUTTON_IMAGE: string = "/img/close.png";
export const HOME_BUTTON_IMAGE: string = "/img/home.png";

View File

@ -37,4 +37,15 @@ export default class Endian {
}
return buffer8;
}
static u32ToByteArrayBigEndian(inputNumber: number) {
const buffer = new ArrayBuffer(4);
const buffer8 = new Uint8Array(buffer);
const buffer32 = new Uint32Array(buffer);
buffer32[0] = inputNumber;
if (Endian.isLittleEndian()) {
buffer8.reverse();
}
return buffer8;
}
}

72
js-src/packet.ts Normal file
View File

@ -0,0 +1,72 @@
import Endian from '/endian';
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME, PACKET_ID_KEY_DOWN} from '/constants';
function concatU8Array(array1: Uint8Array, array2: Uint8Array) {
const final_array = new Uint8Array(array1.length + array2.length);
final_array.set(array1);
final_array.set(array2, array1.length);
return final_array;
}
export function sendHello(websocket: WebSocket, rom_array: Uint8Array, savestate_array: Uint8Array) {
console.log('Sending hello.');
const length_rom = BigInt(rom_array.length);
const length_savestate = BigInt(savestate_array.length);
const raw_data =
concatU8Array(
concatU8Array(
concatU8Array(Endian.u64ToByteArrayBigEndian(length_rom), rom_array),
Endian.u64ToByteArrayBigEndian(length_savestate)
),
savestate_array
);
sendPacket(websocket, PACKET_ID_HELLO, raw_data);
}
export function sendKeyDown(websocket: WebSocket, isDown: boolean, key: number) {
console.log('Sending keyDown.');
console.log(PACKET_ID_KEY_DOWN);
const isDownArray = new Uint8Array(1);
isDownArray[0] = isDown ? 1: 0;
const rawData = concatU8Array(isDownArray, Endian.u32ToByteArrayBigEndian(key));
console.log(rawData.length);
sendPacket(websocket, PACKET_ID_KEY_DOWN, rawData);
}
export function sendPacket(websocket: WebSocket, id: bigint, raw_data: Uint8Array) {
const packet_u8 = concatU8Array(
concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(raw_data.length))),
raw_data
);
const packet_buffer = packet_u8.buffer;
console.log('Sending packet');
websocket.send(packet_buffer);
}
export function handleSendFrame(raw_data: Uint8Array, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: boolean) => void) {
if (printingFrame) {
return;
}
setPrintingFrame(true);
let data: Uint8Array | null = raw_data;
const stride = Endian.byteArrayToU32BigEndian(data.slice(0, 4));
data = data.slice(4, data.length);
const output_buffer_size = Endian.byteArrayToU64BigEndian(data.slice(0, 8));
data = data.slice(8, data.length);
const img_data = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT);
const img_data_u8 = new Uint8Array(img_data.data.buffer);
for (let i = 0; i<data.length; i++) {
if (i % 4 == 3) {
img_data_u8[i] = 255;
continue;
}
img_data_u8[i] = data[i];
}
data = null;
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx, printingFrame));
}
function drawBitmap(bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean) {
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
printingFrame = false;
}

View File

@ -1,3 +1,7 @@
*:focus {
outline: none;
}
body {
background: #0E0E10;
color: #F5F5F5;

File diff suppressed because one or more lines are too long