// Canvas Renderer for Tetris
class TetrisRenderer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.boards = new Map();
}
createPlayerBoard(playerId, playerName) {
const boardDiv = document.createElement('div');
boardDiv.className = 'player-board';
boardDiv.id = `board-${playerId}`;
boardDiv.dataset.playerId = playerId;
// Main game canvas
const canvas = document.createElement('canvas');
canvas.id = `canvas-${playerId}`;
canvas.width = BOARD_WIDTH * CELL_SIZE;
canvas.height = BOARD_HEIGHT * CELL_SIZE;
canvas.style.width = `${BOARD_WIDTH * CELL_SIZE}px`;
canvas.style.height = `${BOARD_HEIGHT * CELL_SIZE}px`;
boardDiv.appendChild(canvas);
// Player info
const infoDiv = document.createElement('div');
infoDiv.className = 'board-info';
infoDiv.innerHTML = `
${playerName}
Lines: 0
`;
boardDiv.appendChild(infoDiv);
// Next piece preview
const nextDiv = document.createElement('div');
nextDiv.className = 'board-info';
const nextCanvas = document.createElement('canvas');
nextCanvas.id = `next-${playerId}`;
nextCanvas.width = 80;
nextCanvas.height = 80;
nextCanvas.style.width = '80px';
nextCanvas.style.height = '80px';
nextDiv.innerHTML = 'NEXT:';
nextDiv.appendChild(nextCanvas);
boardDiv.appendChild(nextDiv);
this.container.appendChild(boardDiv);
this.boards.set(playerId, {
element: boardDiv,
canvas: canvas,
nextCanvas: nextCanvas,
info: infoDiv,
ctx: canvas.getContext('2d'),
nextCtx: nextCanvas.getContext('2d')
});
return boardDiv;
}
removePlayerBoard(playerId) {
const board = this.boards.get(playerId);
if (board) {
board.element.remove();
this.boards.delete(playerId);
}
}
renderPlayer(playerId, gameState) {
const boardData = this.boards.get(playerId);
if (!boardData) return;
const { ctx, nextCtx, element, info } = boardData;
// Clear canvas
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// Draw grid lines
this.drawGrid(ctx);
// Draw locked pieces
if (gameState && gameState.board) {
this.drawBoard(ctx, gameState.board);
}
// Draw current piece
if (gameState && gameState.currentPiece && !gameState.gameOver) {
this.drawPiece(ctx, gameState.currentPiece);
}
// Draw next piece
if (gameState && gameState.nextPiece) {
this.drawNextPiece(nextCtx, gameState.nextPiece);
}
// Update stats
if (gameState) {
const linesSpan = info.querySelector('.lines');
if (linesSpan) {
linesSpan.textContent = gameState.lines;
}
}
// Update board state classes
element.classList.remove('active', 'eliminated');
if (gameState && gameState.eliminated) {
element.classList.add('eliminated');
}
}
drawGrid(ctx) {
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
for (let x = 0; x <= BOARD_WIDTH; x++) {
ctx.beginPath();
ctx.moveTo(x * CELL_SIZE, 0);
ctx.lineTo(x * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE);
ctx.stroke();
}
for (let y = 0; y <= BOARD_HEIGHT; y++) {
ctx.beginPath();
ctx.moveTo(0, y * CELL_SIZE);
ctx.lineTo(BOARD_WIDTH * CELL_SIZE, y * CELL_SIZE);
ctx.stroke();
}
}
drawBoard(ctx, board) {
for (let row = 0; row < BOARD_HEIGHT; row++) {
for (let col = 0; col < BOARD_WIDTH; col++) {
const color = board[row][col];
if (color) {
this.drawCell(ctx, col, row, color);
}
}
}
}
drawPiece(ctx, piece) {
for (let row = 0; row < piece.shape.length; row++) {
for (let col = 0; col < piece.shape[row].length; col++) {
if (piece.shape[row][col]) {
const x = piece.x + col;
const y = piece.y + row;
if (y >= 0) {
this.drawCell(ctx, x, y, piece.color);
}
}
}
}
}
drawCell(ctx, x, y, color) {
const px = x * CELL_SIZE;
const py = y * CELL_SIZE;
// Main fill
ctx.fillStyle = color;
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
// Bevel effect
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(px, py, CELL_SIZE, 3);
ctx.fillRect(px, py, 3, CELL_SIZE);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(px, py + CELL_SIZE - 3, CELL_SIZE, 3);
ctx.fillRect(px + CELL_SIZE - 3, py, 3, CELL_SIZE);
// Border
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 1;
ctx.strokeRect(px, py, CELL_SIZE, CELL_SIZE);
}
drawNextPiece(ctx, piece) {
// Clear
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// Center the piece
const pieceWidth = piece.shape[0].length * 20;
const pieceHeight = piece.shape.length * 20;
const offsetX = (ctx.canvas.width - pieceWidth) / 2;
const offsetY = (ctx.canvas.height - pieceHeight) / 2;
for (let row = 0; row < piece.shape.length; row++) {
for (let col = 0; col < piece.shape[row].length; col++) {
if (piece.shape[row][col]) {
const x = offsetX + col * 20;
const y = offsetY + row * 20;
ctx.fillStyle = piece.color;
ctx.fillRect(x, y, 20, 20);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(x, y, 20, 3);
ctx.fillRect(x, y, 3, 20);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x, y + 17, 20, 3);
ctx.fillRect(x + 17, y, 3, 20);
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, 20, 20);
}
}
}
}
setActivePlayer(playerId) {
// Remove active class from all boards
this.boards.forEach((boardData) => {
boardData.element.classList.remove('active');
});
// Add to current player
const boardData = this.boards.get(playerId);
if (boardData) {
boardData.element.classList.add('active');
}
}
triggerShake(playerId) {
const boardData = this.boards.get(playerId);
if (boardData) {
boardData.element.classList.remove('shake');
void boardData.element.offsetWidth; // Trigger reflow
boardData.element.classList.add('shake');
}
}
triggerFlash(playerId) {
const boardData = this.boards.get(playerId);
if (boardData) {
boardData.element.classList.remove('flash');
void boardData.element.offsetWidth; // Trigger reflow
boardData.element.classList.add('flash');
}
}
updatePlayerInfo(playerId, name, score, lines) {
const boardData = this.boards.get(playerId);
if (boardData) {
const nameSpan = boardData.info.querySelector('.name');
if (nameSpan) nameSpan.textContent = name;
const linesSpan = boardData.info.querySelector('.lines');
if (linesSpan) linesSpan.textContent = lines;
}
}
clearAll() {
this.boards.forEach((boardData) => {
boardData.element.remove();
});
this.boards.clear();
}
}
window.TetrisRenderer = TetrisRenderer;