Files
chat_grid/client/src/render/canvasRenderer.ts

120 lines
4.1 KiB
TypeScript
Raw Normal View History

2026-02-20 08:16:43 -05:00
import { GRID_SIZE, type GameState, type PeerState, type WorldItem } from '../state/gameState';
export class CanvasRenderer {
private readonly ctx: CanvasRenderingContext2D;
private squarePixelSize: number;
private gridSize: number;
2026-02-20 08:16:43 -05:00
constructor(private readonly canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to create 2D context');
}
this.ctx = ctx;
this.gridSize = GRID_SIZE;
this.squarePixelSize = canvas.width / this.gridSize;
}
setGridSize(gridSize: number): void {
if (!Number.isInteger(gridSize) || gridSize <= 0) return;
this.gridSize = gridSize;
this.squarePixelSize = this.canvas.width / this.gridSize;
2026-02-20 08:16:43 -05:00
}
draw(state: GameState): void {
const { ctx } = this;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.strokeStyle = '#374151';
for (let i = 0; i <= this.gridSize; i += 1) {
2026-02-20 08:16:43 -05:00
ctx.beginPath();
ctx.moveTo(i * this.squarePixelSize, 0);
ctx.lineTo(i * this.squarePixelSize, this.canvas.height);
ctx.moveTo(0, i * this.squarePixelSize);
ctx.lineTo(this.canvas.width, i * this.squarePixelSize);
ctx.stroke();
}
for (const peer of state.peers.values()) {
this.drawObject(peer, '#f87171', peer.nickname);
}
for (const item of state.items.values()) {
if (item.carrierId) continue;
this.drawItem(item);
}
this.drawObject(state.player, '#34d399', state.player.nickname);
if (state.mode === 'nickname' || state.mode === 'chat' || state.mode === 'itemPropertyEdit') {
const label =
state.mode === 'nickname' ? 'New Nickname' : state.mode === 'chat' ? 'Message' : 'Property Value';
this.drawTextOverlay(state, label);
}
}
private drawObject(obj: Pick<PeerState, 'x' | 'y' | 'nickname'>, color: string, name: string): void {
const drawX = obj.x * this.squarePixelSize;
const drawY = this.canvas.height - (obj.y * this.squarePixelSize) - this.squarePixelSize;
this.ctx.fillStyle = color;
this.ctx.fillRect(drawX, drawY, this.squarePixelSize, this.squarePixelSize);
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Courier New';
this.ctx.textAlign = 'center';
this.ctx.fillText(name, drawX + this.squarePixelSize / 2, drawY - 5);
}
private drawTextOverlay(state: GameState, label: string): void {
const { ctx } = this;
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, this.canvas.height / 2 - 30, this.canvas.width, 60);
ctx.fillStyle = 'white';
ctx.font = '24px Courier New';
ctx.textAlign = 'center';
const text = `${label}: ${state.nicknameInput}`;
const textMetrics = ctx.measureText(text);
const preCursorText = `${label}: ${state.nicknameInput.substring(0, state.cursorPos)}`;
const preCursorWidth = ctx.measureText(preCursorText).width;
const textX = this.canvas.width / 2;
ctx.fillText(text, textX, this.canvas.height / 2);
if (state.cursorVisible) {
ctx.fillRect(textX - textMetrics.width / 2 + preCursorWidth, this.canvas.height / 2 - 20, 2, 24);
}
}
private drawItem(item: WorldItem): void {
const drawX = item.x * this.squarePixelSize;
const drawY = this.canvas.height - (item.y * this.squarePixelSize) - this.squarePixelSize;
this.ctx.fillStyle =
item.type === 'radio_station'
? '#fbbf24'
: item.type === 'wheel'
? '#f97316'
: item.type === 'piano'
? '#c4b5fd'
: item.type === 'clock'
? '#86efac'
: item.type === 'widget'
? '#22d3ee'
: '#60a5fa';
2026-02-20 08:16:43 -05:00
this.ctx.fillRect(drawX, drawY, this.squarePixelSize, this.squarePixelSize);
this.ctx.fillStyle = '#111827';
this.ctx.font = 'bold 12px Courier New';
this.ctx.textAlign = 'center';
this.ctx.fillText(
item.type === 'radio_station'
? 'R'
: item.type === 'wheel'
? 'W'
: item.type === 'piano'
? 'P'
: item.type === 'clock'
? 'C'
: item.type === 'widget'
? 'B'
: 'D',
drawX + this.squarePixelSize / 2,
drawY + 13,
);
2026-02-20 08:16:43 -05:00
}
}