// CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { // if (w < 2 * r) r = w / 2; // if (h < 2 * r) r = h / 2; // this.beginPath(); // this.moveTo(x+r, y); // this.arcTo(x+w, y, x+w, y+h, r); // this.arcTo(x+w, y+h, x, y+h, r); // this.arcTo(x, y+h, x, y, r); // this.arcTo(x, y, x+w, y, r); // this.closePath(); // return this; // } function startGame() { const canvas = document.querySelector("canvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.font = "24px Montserrat" ctx.imageSmoothingEnabled = true canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (x < 0 || y < 0 || x > rect.width || y > rect.height) return; // Adjust for potential canvas scaling (e.g. high DPI) const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; socket.emit("cursor", x * scaleX, y * scaleY); }) canvas.addEventListener('click', e => click(true, e, canvas)) canvas.addEventListener('contextmenu', e => click(false, e, canvas)) gameLoop(ctx) } /** * * @param {boolean} left * @param {PointerEvent} e * @param {HTMLCanvasElement} canvas */ function click(left, e, canvas) { e.preventDefault() const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (x < 0 || y < 0 || x > rect.width || y > rect.height) return; // Adjust for potential canvas scaling (e.g. high DPI) const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; socket.emit("click", left, Math.floor((x * scaleX - offsetX()) / CELL_SIZE), Math.floor((y * scaleY - offsetY()) / CELL_SIZE)); } /** * * @param {CanvasRenderingContext2D} ctx */ function gameLoop(ctx) { ctx.save() ctx.clearRect(0, 0, 800, 600) for (let x = 0; x < data.field.length; x++) for (let y = 0; y < data.field[x].length; y++) drawCell(ctx, x, y) const x = 400 const y = 600 - (600 - (CELL_SIZE * MAX_HEIGTH)) * 0.5 ctx.textAlign = 'center' ctx.textBaseline = 'middle' ctx.fillStyle = getColor('fg') const w = data.field.length const h = w && data.field[0].length ctx.fillText(`${data.text} 🚩 ${data.flags} 💣 ${data.bombs} Поле ${w}x${h}`.trim(), x, y) for (const sid of Object.keys(data.cursors)) drawCursor(ctx, sid) ctx.restore(); window.requestAnimationFrame(() => gameLoop(ctx)) } const HALF_CELL_SIZE = 16 const CELL_SIZE = HALF_CELL_SIZE * 2 const MAX_WIDTH = 25 const MAX_HEIGTH = 15 function offsetX() { return (MAX_WIDTH - data.field.length) * HALF_CELL_SIZE } function offsetY() { return (MAX_HEIGTH - (data.field.length && data.field[0].length)) * HALF_CELL_SIZE } /** * * @param {CanvasRenderingContext2D} ctx * @param {number} cx * @param {number} cy */ function drawCell(ctx, cx, cy) { let cell = data.field[cx][cy] switch (cell) { case "": case "🚩": case "❓": ctx.fillStyle = getColor("bgCellClosed") break; case " ": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": ctx.fillStyle = getColor("bgCellOpen") break default: ctx.fillStyle = getColor("bgCellBomb") break } ctx.strokeStyle = getColor("bg") ctx.lineWidth = 4 const x = cx * CELL_SIZE + offsetX(); const y = cy * CELL_SIZE + offsetY(); ctx.fillRect(x, y, CELL_SIZE, CELL_SIZE) ctx.strokeRect(x, y, CELL_SIZE, CELL_SIZE) switch (cell) { case "": case " ": return case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": ctx.fillStyle = getColor('fgCell' + cell) break default: ctx.fillStyle = "white" break } ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.fillText(cell, x + HALF_CELL_SIZE, y + HALF_CELL_SIZE) } /** * * @param {CanvasRenderingContext2D} ctx * @param {string} sid * @param {number} x * @param {number} y * @param {number} w * @param {number} h */ function drawCursor(ctx, sid, w = 15, h = 20) { const { x, y, nickname } = data.cursors[sid]; const a = sid.charCodeAt(0) % 256; const b = sid.charCodeAt(1) % 256; const c = sid.charCodeAt(2) % 3; const aStr = a < 16 ? "0" + a.toString(16) : a.toString(16); const bStr = b < 16 ? "0" + b.toString(16) : b.toString(16); let color = "#"; switch (c) { case 0: color += "ff" + aStr + bStr; break; case 1: color += aStr + "ff" + bStr; break; default: color += aStr + bStr + "ff"; break; } ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + w, y + h * 0.6); ctx.lineTo(x + w * 0.6, y + h * 0.5); ctx.lineTo(x + w, y + h * 0.9); ctx.lineTo(x + w * 0.9, y + h); ctx.lineTo(x + w * 0.5, y + h * 0.6); ctx.lineTo(x + w * 0.6, y + h); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = "black" ctx.lineWidth = 1 ctx.stroke(); ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.textBaseline = 'top'; ctx.fillStyle = "#00000077"; const { width: tw, ideographicBaseline: th } = ctx.measureText(nickname) ctx.beginPath(); ctx.roundRect(x + w * 2, y, tw + w * 2, Math.abs(th), [w]) ctx.fill() ctx.fillStyle = "#ffffff" ctx.fillText(nickname, x + w * 3, y) }