Migrating the project to typescript.

This commit is contained in:
Sergiotarxz 2023-03-22 13:53:16 +01:00
parent d7b9d9d664
commit 13a948b21a
15 changed files with 292 additions and 201 deletions

View File

@ -1,7 +0,0 @@
import React from 'react';
import {MIN_WIDTH, MIN_HEIGHT} from '/constants';
export default function CanvasGBAEmulator(props) {
return (<canvas ref={props.canvasRef} width={MIN_WIDTH} height={MIN_HEIGHT}></canvas>);
}

View File

@ -0,0 +1,11 @@
import * as React from 'react';
import {MIN_WIDTH, MIN_HEIGHT} from '/constants';
export interface CanvasGBAEmulatorProps {
canvasRef: React.RefObject<HTMLCanvasElement>
}
export default function CanvasGBAEmulator(props: CanvasGBAEmulatorProps) {
return (<canvas ref={props.canvasRef} width={MIN_WIDTH} height={MIN_HEIGHT}></canvas>);
}

View File

@ -1,13 +0,0 @@
import React from 'react';
export default function CenterElement(props) {
let hidden = props.hidden;
if (hidden == null) {
hidden = false;
}
return (
<div style={{
display: hidden ? 'none':''
}} className="center-content">{props.children}</div>
);
}

View File

@ -0,0 +1,22 @@
import * as React from 'react';
export interface CenterElementProps {
hidden?: boolean | undefined,
children?: React.ReactNode,
}
type IHash = {
[id: string]: string;
}
export default function CenterElement(props: CenterElementProps) {
const styles: IHash = {};
let hidden = props.hidden;
if (hidden == null) {
hidden = false;
}
styles["display"] = hidden ? 'none' : '';
return (
<div style={styles} className="center-content">{props.children}</div>
);
}

View File

@ -1,6 +1,12 @@
import React from 'react';
import * as React from 'react';
export default function FormSelectFiles(props) {
export interface FormSelectFilesProps {
refInputRom: React.RefObject<HTMLInputElement>;
refInputSaveState: React.RefObject<HTMLInputElement>;
onStartEmulation: React.MouseEventHandler<HTMLInputElement>;
}
export default function FormSelectFiles(props: FormSelectFilesProps) {
const inputRom = props.refInputRom ? props.refInputRom : React.useRef(null);
const inputSaveState = props.refInputSaveState ? props.refInputSaveState : React.useRef(null);
const onStartEmulation = props.onStartEmulation;
@ -15,7 +21,7 @@ export default function FormSelectFiles(props) {
Savestate (A ss file from mgba...)
<input type="file" ref={inputSaveState} name="savestate"/>
</label>
<input type="button" value="Start emulation" onClick={onStartEmulation} ref={props.startEmulationButton}/>
<input type="button" value="Start emulation" onClick={onStartEmulation}/>
</form>
);
}

View File

@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import CenterElement from '/components/center-element';
import FormSelectFiles from '/components/form-select-files';
@ -8,9 +8,32 @@ import Endian from '/endian';
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants';
function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHiddenFormSelectFiles, canvas}) {
type setBooleanCallback = (c: boolean) => boolean;
export interface handleClickStartEmulationButtonObjectArgs {
e: React.MouseEvent<HTMLInputElement>;
inputRom: HTMLInputElement | null;
inputSaveState: HTMLInputElement | null;
setHiddenFormSelectFiles: (c: setBooleanCallback) => void;
canvas: HTMLCanvasElement | null;
printingFrame: boolean;
setPrintingFrame: (c: setBooleanCallback) => void;
};
function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHiddenFormSelectFiles, canvas, printingFrame, setPrintingFrame}: handleClickStartEmulationButtonObjectArgs) {
if (canvas == null) {
alert('Canvas does not exists?');
return;
}
const ctx = canvas.getContext('2d')
e.preventDefault();
if (ctx == null) {
alert('Unable to create canvas context, doing nothing');
return;
}
if (inputRom == null || inputSaveState == null || inputRom.files == null || inputSaveState.files == null) {
alert('Unable to read the files ');
return;
}
if (inputRom.files.length == 0) {
alert('There is no rom still');
return;
@ -23,42 +46,37 @@ function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHidden
const savestate_file = inputSaveState.files[0];
rom_file.arrayBuffer().then((rom_buffer) => {
savestate_file.arrayBuffer().then((savestate_buffer) => {
setHiddenFormSelectFiles((c: boolean) => true);
const rom_array = new Uint8Array(rom_buffer);
const savestate_array = new Uint8Array(savestate_buffer);
const websocket = new WebSocket(`ws://localhost:3000/ws`);
websocket.binaryType = 'arraybuffer';
websocket.onclose = (message) => console.log('CLOSE', message);
websocket.onopen = () => {
websocket.onclose = (message) => {
setHiddenFormSelectFiles(c => false);
console.log('Closing websocket.');
}
websocket.onopen = () => {
console.log('Opened websocket.');
sendHello(websocket, rom_array, savestate_array);
};
websocket.addEventListener('message', (event) => onWebSocketPacket(event, canvas, ctx));
setPrintingFrame(c => false);
websocket.addEventListener('message', (event) => {
onWebSocketPacket(event, canvas, ctx, printingFrame, setPrintingFrame)
});
});
});
}
function concatU8Array(array1, array2) {
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 u64ToByteArrayBigEndian(input_number) {
const buffer = new ArrayBuffer(8);
const buffer8 = new Uint8Array(buffer);
const buffer64 = new BigUint64Array(buffer);
buffer64[0] = input_number;
if (Endian.isLittleEndian()) {
buffer8.reverse();
}
return buffer8;
}
function sendPacket(websocket, id, raw_data) {
function sendPacket(websocket: WebSocket, id: bigint, raw_data: Uint8Array) {
const packet_u8 = concatU8Array(
concatU8Array(u64ToByteArrayBigEndian(id), u64ToByteArrayBigEndian(BigInt(raw_data.length))),
concatU8Array(Endian.u64ToByteArrayBigEndian(id), Endian.u64ToByteArrayBigEndian(BigInt(raw_data.length))),
raw_data
);
const packet_buffer = packet_u8.buffer;
@ -66,50 +84,48 @@ function sendPacket(websocket, id, raw_data) {
websocket.send(packet_buffer);
}
function sendHello(websocket, rom_array, savestate_array) {
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(u64ToByteArrayBigEndian(length_rom), rom_array),
u64ToByteArrayBigEndian(length_savestate)
concatU8Array(Endian.u64ToByteArrayBigEndian(length_rom), rom_array),
Endian.u64ToByteArrayBigEndian(length_savestate)
),
savestate_array
);
sendPacket(websocket, PACKET_ID_HELLO, raw_data);
}
function onWebSocketPacket(event, canvas, ctx) {
function onWebSocketPacket(event: MessageEvent, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) {
const buffer = event.data;
let packet_u8 = new Uint8Array(buffer);
const id = byteArrayToU64BigEndian(packet_u8.slice(0, 8));
let packet_u8: Uint8Array | null = new Uint8Array(buffer);
const id = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8));
packet_u8 = packet_u8.slice(8, packet_u8.length);
const size = byteArrayToU64BigEndian(packet_u8.slice(0, 8));
const size = Endian.byteArrayToU64BigEndian(packet_u8.slice(0, 8));
const raw_data = packet_u8.slice(8, packet_u8.length);
packet_u8 = null;
switch (id) {
case PACKET_ID_SEND_FRAME:
handleSendFrame(raw_data, canvas, ctx);
handleSendFrame(raw_data, canvas, ctx, printingFrame, setPrintingFrame);
break;
default:
console.log(`Received unknown packet ${id}`);
}
}
let printing_frame = false;
function handleSendFrame(raw_data, canvas, ctx) {
if (printing_frame) {
function handleSendFrame(raw_data: Uint8Array, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean, setPrintingFrame: (c: setBooleanCallback) => void) {
if (printingFrame) {
return;
}
printing_frame = true;
let data = raw_data;
const stride = byteArrayToU32BigEndian(data.slice(0, 4));
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 = byteArrayToU32BigEndian(data.slice(0, 8));
const output_buffer_size = Endian.byteArrayToU64BigEndian(data.slice(0, 8));
data = data.slice(8, data.length);
console.log(data.length / 4 / MIN_WIDTH);
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++) {
@ -120,56 +136,48 @@ function handleSendFrame(raw_data, canvas, ctx) {
img_data_u8[i] = data[i];
}
data = null;
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx));
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx, printingFrame));
}
function byteArrayToU32BigEndian(input_array) {
if (Endian.isLittleEndian()) {
input_array = input_array.reverse();
}
const buffer = input_array.buffer;
const output_u32_array = new Uint32Array(buffer);
return output_u32_array[0];
}
function byteArrayToU64BigEndian(input_array) {
if (Endian.isLittleEndian()) {
input_array = input_array.reverse();
}
const buffer = input_array.buffer;
const output_u64_array = new BigUint64Array(buffer);
return output_u64_array[0];
}
function drawBitmap(bitmap, canvas, ctx) {
function drawBitmap(bitmap: ImageBitmap, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, printingFrame: boolean) {
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
printing_frame = false;
printingFrame = false;
}
export default function Page() {
const screenDimensions = useScreenDimensions();
const emulatorDimensions = calculateSizeEmulator(screenDimensions);
const canvasRef = React.useRef(null);
function resizeCanvas(node) {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
function resizeCanvas() {
const canvas = canvasRef.current;
if (canvas) {
canvas.width = emulatorDimensions.width;
canvas.height = emulatorDimensions.height;
const ctx = canvas.getContext('2d')
fillBlack(canvas, ctx);
if (canvas == null) {
return;
}
if (emulatorDimensions.width === undefined || emulatorDimensions.height === undefined) {
return;
}
canvas.width = emulatorDimensions.width;
canvas.height = emulatorDimensions.height;
const ctx = canvas.getContext('2d')
if (ctx == null) {
return;
}
fillBlack(canvas, ctx);
};
const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState(false);
const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState<boolean>(false);
const [printingFrame, setPrintingFrame] = React.useState<boolean>(false);
React.useEffect(resizeCanvas, [emulatorDimensions]);
const refInputRom = React.useRef(null);
const refInputSaveState = React.useRef(null);
const onStartEmulation = (e) => {
const refInputRom = React.useRef<HTMLInputElement | null>(null);
const refInputSaveState = React.useRef<HTMLInputElement | null>(null);
const onStartEmulation = (e: React.MouseEvent<HTMLInputElement>) => {
handleClickStartEmulationButton({
e: e,
setHiddenFormSelectFiles: setHiddenFormSelectFiles,
inputRom: refInputRom.current,
inputSaveState: refInputSaveState.current,
canvas: canvasRef.current,
setPrintingFrame: setPrintingFrame,
printingFrame: printingFrame,
});
};
return (
@ -198,7 +206,7 @@ function getScreenDimensions() {
}
function useScreenDimensions() {
const [screenDimensions, setScreenDimensions] = React.useState(getScreenDimensions());
const [screenDimensions, setScreenDimensions] = React.useState<EmulatorDimensions>(getScreenDimensions());
React.useEffect(() => {
function onResize() {
@ -215,17 +223,25 @@ function useScreenDimensions() {
return screenDimensions;
}
function fillBlack(canvas, ctx) {
function fillBlack(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.fill();
}
function calculateSizeEmulator(screenDimensions) {
export interface EmulatorDimensions {
width?: number;
height?: number;
};
function calculateSizeEmulator(screenDimensions: EmulatorDimensions): EmulatorDimensions {
if (screenDimensions.width === undefined || screenDimensions.height === undefined) {
console.error(screenDimensions, 'screenDimensions has undefined fields');
return {};
}
const width = screenDimensions.width;
const height = screenDimensions.height * 0.75;
const emulatorDimensions = {};
const emulatorDimensions: EmulatorDimensions = {};
if (width < MIN_WIDTH || height < MIN_HEIGHT) {
return {
width: MIN_WIDTH,

View File

@ -3,6 +3,11 @@ export const MIN_HEIGHT = 160;
export const PACKET_ID_HELLO = 0n;
export const PACKET_ID_SEND_FRAME = 1n;
export default class Constants {
public static MIN_WIDTH: number = MIN_WIDTH;
public static MIN_HEIGHT: number = MIN_HEIGHT;
public static PACKET_ID_HELLO: bigint = PACKET_ID_HELLO;
public static PACKET_ID_SEND_FRAME: bigint = PACKET_ID_SEND_FRAME;
};
Constants.MIN_WIDTH = MIN_WIDTH;
Constants.MIN_HEIGHT = MIN_HEIGHT;

View File

@ -1,18 +0,0 @@
"use strict";
let littleEndian = true;
(()=>{
let buf = new ArrayBuffer(4);
let buf8 = new Uint8ClampedArray(buf);
let data = new Uint32Array(buf);
data[0] = 0xdeadbeef;
if(buf8[0] === 0xde){
littleEndian = false;
}
})()
export default class Endian {
static isLittleEndian() {
return littleEndian;
}
}

40
js-src/endian.ts Normal file
View File

@ -0,0 +1,40 @@
"use strict";
let littleEndian = true; (()=>{ let buf = new ArrayBuffer(4); let buf8 = new
Uint8ClampedArray(buf); let data = new Uint32Array(buf); data[0] = 0xdeadbeef;
if(buf8[0] === 0xde){ littleEndian = false; } })()
export default class Endian {
static isLittleEndian() {
return littleEndian;
}
static byteArrayToU32BigEndian(inputArray: Uint8Array) {
if (Endian.isLittleEndian()) {
inputArray = inputArray.reverse();
}
const buffer = inputArray.buffer;
const outputU32Array = new Uint32Array(buffer);
return outputU32Array[0];
}
static byteArrayToU64BigEndian(inputArray: Uint8Array) {
if (Endian.isLittleEndian()) {
inputArray = inputArray.reverse();
}
const buffer = inputArray.buffer;
const outputU64Array = new BigUint64Array(buffer);
return outputU64Array[0];
}
static u64ToByteArrayBigEndian(inputNumber: bigint) {
const buffer = new ArrayBuffer(8);
const buffer8 = new Uint8Array(buffer);
const buffer64 = new BigUint64Array(buffer);
buffer64[0] = inputNumber;
if (Endian.isLittleEndian()) {
buffer8.reverse();
}
return buffer8;
}
}

View File

@ -1,12 +0,0 @@
"use strict";
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import Endian from '/endian';
import Page from '/components/page';
const body = document.querySelector('body');
const app = document.createElement('div');
body.appendChild(app);
const root = ReactDOMClient.createRoot(app);
root.render(<Page/>);

18
js-src/index.tsx Normal file
View File

@ -0,0 +1,18 @@
"use strict";
import * as React from 'react';
import * as ReactDOMClient from 'react-dom/client';
import Endian from '/endian';
import Page from '/components/page';
const body = document.querySelector('body');
if (body != null) {
fillBody(body);
}
function fillBody(body: HTMLElement) {
const app = document.createElement('div');
body.appendChild(app);
const root = ReactDOMClient.createRoot(app);
root.render(<Page/>);
}

View File

@ -11,8 +11,12 @@
"license": "MIT",
"devDependencies": {
"@babel/preset-react": "^7.18.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"babel-loader": "^9.1.2",
"file-loader": "^6.2.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.2",
"url-loader": "^4.1.1",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2"

File diff suppressed because one or more lines are too long

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"outDir": "./public/js/",
"noImplicitAny": true,
"module": "es2020",
"target": "es2020",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"*": ["js-src/*"]
}
},
"include": ["js-src/*.ts", "js-src/*/*.ts" ]
}

View File

@ -1,23 +1,25 @@
const path = require('path');
module.exports = {
entry: './js-src/index.jsx',
entry: './js-src/index.tsx',
mode: 'development',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public/js/'),
},
resolve: {
extensions: [
'.js',
'.jsx',
],
extensions: [ '.js', '.jsx','.ts', '.tsx' ],
roots: [
path.resolve(__dirname, 'js-src/')
]
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.jpe?g|png$/,
exclude: /node_modules/,