render current player in secondary color

This commit is contained in:
Vinzenz Schroeter 2024-04-16 00:07:44 +02:00
parent fbaad86555
commit c4c4eb6358
13 changed files with 255 additions and 72 deletions

View file

@ -1,7 +1,3 @@
TANK_DOMAIN=vinzenz-lpt2
VITE_TANK_API=http://$TANK_DOMAIN
VITE_TANK_WS=ws://$TANK_DOMAIN
VITE_TANK_SCREEN_URL=$VITE_TANK_WS/screen
VITE_TANK_CONTROLS_URL=$VITE_TANK_WS/controls
VITE_TANK_PLAYER_URL=$VITE_TANK_API/player

View file

@ -41,7 +41,7 @@ export default function App() {
{nameId.name !== '' &&
<Button onClick={() => setNameId(getNewNameId)} text='logout'/>}
</Row>
<ClientScreen logout={logout} theme={theme}/>
<ClientScreen logout={logout} theme={theme} playerId={nameId.id}/>
{nameId.name === '' && <JoinForm setNameId={setNameId} clientId={nameId.id}/>}
<Row className='GadgetRows'>
{isLoggedIn && <Controls playerId={nameId.id} logout={logout}/>}

View file

@ -2,9 +2,13 @@ import useWebSocket from 'react-use-websocket';
import {useEffect, useRef} from 'react';
import './ClientScreen.css';
import {hslToString, Theme} from "./theme.ts";
import {Guid} from "./Guid.ts";
const pixelsPerRow = 352;
const pixelsPerCol = 160;
const observerMessageSize = pixelsPerCol * pixelsPerRow / 8;
const isPlayerMask = 1;
function getIndexes(bitIndex: number) {
return {
@ -19,42 +23,75 @@ function normalizeColor(context: CanvasRenderingContext2D, color: string) {
return context.getImageData(0, 0, 1, 1).data;
}
function drawPixelsToCanvas(pixels: Uint8Array, canvas: HTMLCanvasElement, theme: Theme) {
const drawContext = canvas.getContext('2d');
if (!drawContext)
throw new Error('could not get draw context');
function drawPixelsToCanvas({context, width, height, pixels, additional, foreground, background, playerColor}: {
context: CanvasRenderingContext2D,
width: number,
height: number,
pixels: Uint8ClampedArray,
additional: Uint8ClampedArray | null,
background: Uint8ClampedArray,
foreground: Uint8ClampedArray,
playerColor: Uint8ClampedArray
}) {
let additionalDataIndex = 0;
let additionalDataByte: number | null = null;
const nextPixelColor = (isOn: boolean) => {
if (!isOn)
return background;
if (!additional)
return foreground;
const colorPrimary = normalizeColor(drawContext, hslToString(theme.primary));
const colorBackground = normalizeColor(drawContext, hslToString(theme.background));
let info;
if (additionalDataByte === null) {
additionalDataByte = additional[additionalDataIndex];
additionalDataIndex++;
info = additionalDataByte;
} else {
info = additionalDataByte >> 4;
additionalDataByte = null;
}
const imageData = drawContext.getImageData(0, 0, canvas.width, canvas.height, {colorSpace: 'srgb'});
if ((info & isPlayerMask) != 0) {
return playerColor;
}
return foreground;
}
const imageData = context.getImageData(0, 0, width, height, {colorSpace: 'srgb'});
const data = imageData.data;
for (let y = 0; y < canvas.height; y++) {
const rowStartPixelIndex = y * pixelsPerRow;
for (let x = 0; x < canvas.width; x++) {
const pixelIndex = rowStartPixelIndex + x;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIndex = y * pixelsPerRow + x;
const {byteIndex, bitInByteIndex} = getIndexes(pixelIndex);
const mask = (1 << bitInByteIndex);
const isOn = (pixels[byteIndex] & mask) !== 0;
const color = isOn ? colorPrimary : colorBackground;
const isOn = (pixels[byteIndex] & (1 << bitInByteIndex)) !== 0;
const color = nextPixelColor(isOn);
for (let colorChannel of [0, 1, 2, 3])
data[pixelIndex * 4 + colorChannel] = color[colorChannel];
}
}
drawContext.putImageData(imageData, 0, 0);
context.putImageData(imageData, 0, 0);
}
export default function ClientScreen({logout, theme}: { logout: () => void, theme: Theme }) {
export default function ClientScreen({logout, theme, playerId}: {
logout: () => void,
theme: Theme,
playerId?: Guid
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const url = new URL('/screen', import.meta.env.VITE_TANK_WS);
if (playerId)
url.searchParams.set('player', playerId);
const {
lastMessage,
sendMessage,
getWebSocket
} = useWebSocket(import.meta.env.VITE_TANK_SCREEN_URL, {
} = useWebSocket(url.toString(), {
onError: logout,
shouldReconnect: () => true,
});
@ -69,7 +106,34 @@ export default function ClientScreen({logout, theme}: { logout: () => void, them
if (canvasRef.current === null)
throw new Error('canvas null');
drawPixelsToCanvas(new Uint8Array(lastMessage.data), canvasRef.current, theme);
const canvas = canvasRef.current;
const drawContext = canvas.getContext('2d');
if (!drawContext)
throw new Error('could not get draw context');
const colorBackground = normalizeColor(drawContext, hslToString(theme.background));
const colorPrimary = normalizeColor(drawContext, hslToString(theme.primary));
const colorSecondary = normalizeColor(drawContext, hslToString(theme.secondary));
let pixels = new Uint8ClampedArray(lastMessage.data);
let additionalData: Uint8ClampedArray | null = null;
if (pixels.length > observerMessageSize) {
additionalData = pixels.slice(observerMessageSize);
pixels = pixels.slice(0, observerMessageSize);
}
console.log('', {pixelLength: pixels.length, additionalLength: additionalData?.length});
drawPixelsToCanvas({
context: drawContext,
width: canvas.width,
height: canvas.height,
pixels,
additional: additionalData,
background: colorBackground,
foreground: colorPrimary,
playerColor: colorSecondary
});
sendMessage('');
}, [lastMessage, canvasRef.current, theme]);

View file

@ -6,7 +6,7 @@ export default function Controls({playerId, logout}: {
playerId: string,
logout: () => void
}) {
const url = new URL(import.meta.env.VITE_TANK_CONTROLS_URL);
const url = new URL('controls', import.meta.env.VITE_TANK_WS);
url.searchParams.set('playerId', playerId);
const {
sendMessage,

View file

@ -38,7 +38,7 @@ export async function fetchTyped<T>({url, method}: { url: URL; method: string; }
}
export function postPlayer({name, id}: NameId) {
const url = new URL(import.meta.env.VITE_TANK_PLAYER_URL);
const url = new URL('/player', import.meta.env.VITE_TANK_API);
url.searchParams.set('name', name);
url.searchParams.set('id', id);
@ -46,7 +46,7 @@ export function postPlayer({name, id}: NameId) {
}
export function getPlayer(id: Guid) {
const url = new URL(import.meta.env.VITE_TANK_PLAYER_URL);
const url = new URL('/player', import.meta.env.VITE_TANK_API);
url.searchParams.set('id', id);
return fetchTyped<Player>({url, method: 'GET'});