1
0
mirror of https://github.com/thib8956/tic-tac-toe-ws.git synced 2026-02-21 07:48:12 +00:00

feat: calculate size based on screen size

This commit is contained in:
2026-02-18 10:35:58 +01:00
parent 4277a0ca48
commit ae7c3a4662
3 changed files with 53 additions and 48 deletions

View File

@@ -1,9 +1,8 @@
import type { Click, Update, Message, Hello, EndGame, Spectate } from "common.js"; import type { Click, Update, Message, Hello, EndGame, Spectate } from "common.js";
const CELL_SIZE = 150;
const GRID_SIZE = CELL_SIZE * 3;
const SHAPE_SIZE = 100;
const ANIMATE_DURATION = 500; // ms const ANIMATE_DURATION = 500; // ms
const GRID_PADDING = 10;
const MESSAGE_PADDING = 20;
let address = "ws://localhost:1234"; let address = "ws://localhost:1234";
if (window.location.hostname !== "localhost") { if (window.location.hostname !== "localhost") {
@@ -34,18 +33,18 @@ let mySymbol: "x" | "o" | null = null;
let animationId: number; let animationId: number;
let canvasMsg: string = "Offline..."; let canvasMsg: string = "Offline...";
function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) { function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point, cellSize: number, gridSize: number) {
ctx.strokeStyle = "white"; ctx.strokeStyle = "white";
ctx.lineWidth = 5; ctx.lineWidth = 5;
for (let x = 1; x < 3; ++x) { for (let x = 1; x < 3; ++x) {
ctx.moveTo(origin.x + x * CELL_SIZE, 0 + origin.y); ctx.moveTo(origin.x + x * cellSize, origin.y);
ctx.lineTo(origin.x + x * CELL_SIZE, origin.y + GRID_SIZE); ctx.lineTo(origin.x + x * cellSize, origin.y + gridSize);
} }
for (let y = 1; y < 3; ++y) { for (let y = 1; y < 3; ++y) {
ctx.moveTo(origin.x, origin.y + y * CELL_SIZE); ctx.moveTo(origin.x, origin.y + y * cellSize);
ctx.lineTo(origin.x + GRID_SIZE, origin.y + y * CELL_SIZE); ctx.lineTo(origin.x + gridSize, origin.y + y * cellSize);
} }
ctx.stroke(); ctx.stroke();
@@ -54,22 +53,22 @@ function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) {
function resizeCanvas(ctx: CanvasRenderingContext2D) { function resizeCanvas(ctx: CanvasRenderingContext2D) {
ctx.canvas.width = window.innerWidth; ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight; ctx.canvas.height = window.innerHeight;
ctx.clearRect(0, 0, ctx.canvas.width, ctx. canvas.height); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
} }
function coordToGridIndex(origin: Point, clientPos: Point): [number, number] | undefined { function coordToGridIndex(origin: Point, clientPos: Point, cellSize: number): [number, number] | undefined {
// Coord relative to origin of the grid (origin) // Coord relative to origin of the grid (origin)
const pt = { x: clientPos.x - origin.x, y: clientPos.y - origin.y }; const pt = { x: clientPos.x - origin.x, y: clientPos.y - origin.y };
const gridIndex: [number, number] = [Math.floor(pt.x / CELL_SIZE), Math.floor(pt.y / CELL_SIZE)]; const gridIndex: [number, number] = [Math.floor(pt.x / cellSize), Math.floor(pt.y / cellSize)];
if (gridIndex[0] >= 0 && gridIndex[0] <= 2 && gridIndex[1] >= 0 && gridIndex[1] <= 2) { if (gridIndex[0] >= 0 && gridIndex[0] <= 2 && gridIndex[1] >= 0 && gridIndex[1] <= 2) {
return gridIndex; return gridIndex;
} }
return undefined; return undefined;
} }
function handlePendingEvts(ws: WebSocket, gridOrigin: Point) { function handlePendingEvts(ws: WebSocket, gridOrigin: Point, cellSize: number) {
for (const evt of pendingEvts) { for (const evt of pendingEvts) {
const gridIndex = coordToGridIndex(gridOrigin, evt); const gridIndex = coordToGridIndex(gridOrigin, evt, cellSize);
if (gridIndex) { if (gridIndex) {
const [x, y] = gridIndex; const [x, y] = gridIndex;
const msg: Click = { x, y }; const msg: Click = { x, y };
@@ -79,8 +78,8 @@ function handlePendingEvts(ws: WebSocket, gridOrigin: Point) {
pendingEvts = []; pendingEvts = [];
} }
function drawAnimatedCircle(ctx: CanvasRenderingContext2D, dt: number, x: number, y: number, hue: number) { function drawAnimatedCircle(ctx: CanvasRenderingContext2D, dt: number, x: number, y: number, hue: number, shapeSize: number) {
const radius = SHAPE_SIZE / 2; const radius = shapeSize / 2;
const end = dt*2*Math.PI/ANIMATE_DURATION; const end = dt*2*Math.PI/ANIMATE_DURATION;
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
@@ -92,41 +91,42 @@ function drawAnimatedCircle(ctx: CanvasRenderingContext2D, dt: number, x: number
ctx.restore(); ctx.restore();
} }
function drawAnimatedCross(ctx: CanvasRenderingContext2D, dt: number, x: number, y: number, hue: number) { function drawAnimatedCross(ctx: CanvasRenderingContext2D, dt: number, x: number, y: number, hue: number, shapeSize: number) {
const startPoint: Point = { x: x-SHAPE_SIZE/2, y: y-SHAPE_SIZE/2 }; const startPoint: Point = { x: x-shapeSize/2, y: y-shapeSize/2 };
const halfAnim = ANIMATE_DURATION/2; const halfAnim = ANIMATE_DURATION/2;
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y); ctx.moveTo(startPoint.x, startPoint.y);
const delta = SHAPE_SIZE*dt/halfAnim; const delta = shapeSize*dt/halfAnim;
if (delta < SHAPE_SIZE) { // draw \ if (delta < shapeSize) { // draw \
const d = Math.min(delta, SHAPE_SIZE); const d = Math.min(delta, shapeSize);
ctx.lineTo(startPoint.x + d, startPoint.y + d); ctx.lineTo(startPoint.x + d, startPoint.y + d);
} else { // draw / } else { // draw /
ctx.lineTo(startPoint.x + SHAPE_SIZE, startPoint.y + SHAPE_SIZE); // keep \ drawn ctx.lineTo(startPoint.x + shapeSize, startPoint.y + shapeSize); // keep \ drawn
ctx.moveTo(startPoint.x + SHAPE_SIZE, startPoint.y); ctx.moveTo(startPoint.x + shapeSize, startPoint.y);
const d = Math.min(delta - SHAPE_SIZE, SHAPE_SIZE); const d = Math.min(delta - shapeSize, shapeSize);
ctx.lineTo(startPoint.x + SHAPE_SIZE - d, startPoint.y + d); ctx.lineTo(startPoint.x + shapeSize - d, startPoint.y + d);
} }
ctx.lineWidth = 5; ctx.lineWidth = 5;
const percent = Math.floor(100*Math.min(delta, SHAPE_SIZE)/SHAPE_SIZE); const percent = Math.floor(100*Math.min(delta, shapeSize)/shapeSize);
ctx.strokeStyle = `hsla(${hue}, ${percent}%, 50%, 1)`; ctx.strokeStyle = `hsla(${hue}, ${percent}%, 50%, 1)`;
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
} }
function gridIndexToCoords(gridOrigin: Point, x: number, y: number): Point { function gridIndexToCoords(gridOrigin: Point, x: number, y: number, cellSize: number): Point {
const center = { const center = {
x: gridOrigin.x + x * CELL_SIZE + CELL_SIZE/2, x: gridOrigin.x + x * cellSize + cellSize/2,
y: gridOrigin.y + y * CELL_SIZE + CELL_SIZE/2 y: gridOrigin.y + y * cellSize + cellSize/2
}; };
return center; return center;
} }
function updateGridState(ctx: CanvasRenderingContext2D, time: number, gridOrigin: Point) { function updateGridState(ctx: CanvasRenderingContext2D, time: number, gridOrigin: Point, cellSize: number) {
const shapeSize = cellSize * 0.80;
for (let y = 0; y < 3; ++y) { for (let y = 0; y < 3; ++y) {
for (let x = 0; x < 3; ++x) { for (let x = 0; x < 3; ++x) {
const shape = grid[y*3+x]; const shape = grid[y*3+x];
@@ -135,16 +135,16 @@ function updateGridState(ctx: CanvasRenderingContext2D, time: number, gridOrigin
shape.time = time; shape.time = time;
} }
const p = gridIndexToCoords(gridOrigin, x, y); const p = gridIndexToCoords(gridOrigin, x, y, cellSize);
const dt = time - shape.time; const dt = time - shape.time;
switch (shape.kind) { switch (shape.kind) {
case "o": { case "o": {
drawAnimatedCircle(ctx, dt, p.x, p.y, shape.hue); drawAnimatedCircle(ctx, dt, p.x, p.y, shape.hue, shapeSize);
break; break;
} }
case "x": { case "x": {
drawAnimatedCross(ctx, dt, p.x, p.y, shape.hue); drawAnimatedCross(ctx, dt, p.x, p.y, shape.hue, shapeSize);
break; break;
} }
default: break; default: break;
@@ -156,19 +156,22 @@ function updateGridState(ctx: CanvasRenderingContext2D, time: number, gridOrigin
// update loop, called every frame // update loop, called every frame
function update(ctx: CanvasRenderingContext2D, time: number, ws: WebSocket) { function update(ctx: CanvasRenderingContext2D, time: number, ws: WebSocket) {
const cellSize = Math.floor(Math.min(ctx.canvas.width, ctx.canvas.height - MESSAGE_PADDING * 2 - GRID_PADDING) / 3);
const gridSize = cellSize * 3;
const gridOrigin: Point = { const gridOrigin: Point = {
x: ctx.canvas.width / 2 - GRID_SIZE / 2, x: (ctx.canvas.width - gridSize) / 2,
y: ctx.canvas.height / 2 - GRID_SIZE / 2 y: MESSAGE_PADDING + GRID_PADDING,
}; };
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "white"; ctx.fillStyle = "white";
ctx.font = "24px sans-serif"; ctx.font = "24px sans-serif";
ctx.fillText(canvasMsg, gridOrigin.x, gridOrigin.y - 50); ctx.fillText(canvasMsg, gridOrigin.x + MESSAGE_PADDING, MESSAGE_PADDING);
drawGridBackground(ctx, gridOrigin); drawGridBackground(ctx, gridOrigin, cellSize, gridSize);
handlePendingEvts(ws, gridOrigin); updateGridState(ctx, time, gridOrigin, cellSize);
updateGridState(ctx, time, gridOrigin); handlePendingEvts(ws, gridOrigin, cellSize);
animationId = window.requestAnimationFrame(t => update(ctx, t, ws)); animationId = window.requestAnimationFrame(t => update(ctx, t, ws));
} }
@@ -183,7 +186,7 @@ function init() {
canvas.addEventListener("click", (evt) => { canvas.addEventListener("click", (evt) => {
if (isSpectator) { if (isSpectator) {
console.debug("ignoring click in spectator mode"); console.warn("ignoring click in spectator mode");
return; return;
} }
const {clientX, clientY} = evt; const {clientX, clientY} = evt;
@@ -208,7 +211,7 @@ function init() {
try { try {
msg = JSON.parse(evt.data); msg = JSON.parse(evt.data);
} catch { } catch {
console.debug("received non-JSON message, ignoring"); console.warn("received non-JSON message, ignoring");
return; return;
} }
console.log(msg); console.log(msg);
@@ -261,9 +264,9 @@ function init() {
if (!isSpectator) { if (!isSpectator) {
canvasMsg = `Game reset... Id #${myId}, playing as ${mySymbol}`; canvasMsg = `Game reset... Id #${myId}, playing as ${mySymbol}`;
} }
grid = new Array(9); grid = new Array(9); // reset grid state
pendingEvts = []; pendingEvts = []; // reset pending events
resizeCanvas(ctx); resizeCanvas(ctx); // hack to avoid race condition on game reset
break; break;
} }
default: { default: {

View File

@@ -3,10 +3,10 @@
<head> <head>
<title>Hello websockets</title> <title>Hello websockets</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head> </head>
<body style="margin: 0; padding 0; overflow: hidden; background-color: #000000"> <body style="margin: 0; padding: 0; overflow: hidden; background-color: #000000; touch-action: none;">
<canvas id="game" width="800" height="600"></canvas> <canvas id="game"></canvas>
</body> </body>
<script type="module" src="client.js"></script> <script type="module" src="client.js"></script>
</html> </html>

View File

@@ -97,9 +97,11 @@ wss.on("connection", (ws, req) => {
currentPlayer = undefined; currentPlayer = undefined;
endGame = false; endGame = false;
for (const c of clients) { for (const c of clients) {
c.ws.send(JSON.stringify({ const m = JSON.stringify({
kind: "reset" kind: "reset"
} as Message)); } as Message);
console.log("sent response", m, c.id);
c.ws.send(m);
} }
for (const s of spectators) { for (const s of spectators) {