// 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); // Hold piece preview const holdDiv = document.createElement('div'); holdDiv.className = 'board-info'; const holdCanvas = document.createElement('canvas'); holdCanvas.id = `hold-${playerId}`; holdCanvas.width = 80; holdCanvas.height = 80; holdCanvas.style.width = '80px'; holdCanvas.style.height = '80px'; holdDiv.innerHTML = 'HOLD:'; holdDiv.appendChild(holdCanvas); boardDiv.appendChild(holdDiv); this.container.appendChild(boardDiv); this.boards.set(playerId, { element: boardDiv, canvas: canvas, nextCanvas: nextCanvas, holdCanvas: holdCanvas, info: infoDiv, ctx: canvas.getContext('2d'), nextCtx: nextCanvas.getContext('2d'), holdCtx: holdCanvas.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, holdCtx, 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 ghost piece (landing preview) if (gameState && gameState.currentPiece && !gameState.gameOver && gameState.ghostY !== null) { this.drawGhostPiece(ctx, gameState.currentPiece, gameState.ghostY); } // 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); } // Draw hold piece if (gameState && gameState.holdPiece) { this.drawHoldPiece(holdCtx, gameState.holdPiece, gameState.canHold); } else if (holdCtx) { // Clear hold canvas if no hold piece holdCtx.fillStyle = '#000'; holdCtx.fillRect(0, 0, holdCtx.canvas.width, holdCtx.canvas.height); } // Update stats if (gameState) { const linesSpan = info.querySelector('.lines'); if (linesSpan) { linesSpan.textContent = gameState.lines; } } // Update board state classes (preserving main/spectator/position classes) const wasMain = element.classList.contains('main'); const wasSpectator = element.classList.contains('spectator'); const positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; const wasPos = positions.some(p => element.classList.contains(p)); element.classList.remove('active', 'eliminated'); if (wasMain) element.classList.add('main'); if (wasSpectator) element.classList.add('spectator'); if (wasPos) { positions.forEach(p => { if (element.classList.contains(p)) element.classList.add(p); }); } 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); } drawGhostPiece(ctx, piece, ghostY) { 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 = ghostY + row; if (y >= 0) { this.drawGhostCell(ctx, x, y, piece.color); } } } } } drawGhostCell(ctx, x, y, color) { const px = x * CELL_SIZE; const py = y * CELL_SIZE; // Vibrant outlined border effect ctx.strokeStyle = color + 'cc'; // 80% opacity border ctx.lineWidth = 3; ctx.strokeRect(px + 2, py + 2, CELL_SIZE - 4, CELL_SIZE - 4); // More visible fill inside ctx.fillStyle = color + '40'; // 25% opacity ctx.fillRect(px + 3, py + 3, CELL_SIZE - 6, CELL_SIZE - 6); } 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); } } } } drawHoldPiece(ctx, piece, canHold) { // Clear ctx.fillStyle = '#000'; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); if (!piece) return; // Gray out if cannot hold again this turn const alpha = canHold ? 1.0 : 0.4; // 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 + Math.floor(alpha * 255).toString(16).padStart(2, '0'); ctx.fillRect(x, y, 20, 20); if (canHold) { 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 main class from all boards this.boards.forEach((boardData, id) => { boardData.element.classList.remove('active', 'main'); boardData.element.classList.remove('spectator', 'top-left', 'top-right', 'bottom-left', 'bottom-right'); }); // Add main class to current player const mainBoard = this.boards.get(playerId); if (mainBoard) { mainBoard.element.classList.add('main'); } // Add spectator class to other boards with positions const positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; let posIndex = 0; this.boards.forEach((boardData, id) => { if (id !== playerId) { boardData.element.classList.add('spectator'); if (posIndex < positions.length) { boardData.element.classList.add(positions[posIndex]); posIndex++; } } }); } 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;