game/js/renderer.js

144 lines
4.5 KiB
JavaScript

/*
* Canvas 渲染:木色棋盘、立体棋子、悬停预览、最后一手标记、获胜连线。
* 不持有游戏状态,每次 render(game, hover) 根据传入状态整体重绘。
*/
(function () {
const SIZE = 15;
const STAR_POINTS = [[3, 3], [11, 3], [3, 11], [11, 11], [7, 7]];
function createRenderer(canvas) {
const ctx = canvas.getContext('2d');
let cssSize = 600;
let margin = 0;
let cell = 0;
function resize() {
const rect = canvas.getBoundingClientRect();
cssSize = Math.max(200, Math.floor(rect.width));
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(cssSize * dpr);
canvas.height = Math.round(cssSize * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
margin = cssSize / (SIZE + 1);
cell = (cssSize - margin * 2) / (SIZE - 1);
}
function toPixel(x, y) {
return { px: margin + x * cell, py: margin + y * cell };
}
function drawBoard() {
const bg = ctx.createLinearGradient(0, 0, cssSize, cssSize);
bg.addColorStop(0, '#e3b473');
bg.addColorStop(1, '#d19a52');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, cssSize, cssSize);
ctx.strokeStyle = '#7a5a2e';
ctx.lineWidth = 1;
for (let i = 0; i < SIZE; i++) {
const p = margin + i * cell;
ctx.beginPath();
ctx.moveTo(margin, p);
ctx.lineTo(cssSize - margin, p);
ctx.moveTo(p, margin);
ctx.lineTo(p, cssSize - margin);
ctx.stroke();
}
// 外框加粗一圈
ctx.lineWidth = 2;
ctx.strokeRect(margin, margin, cell * (SIZE - 1), cell * (SIZE - 1));
ctx.fillStyle = '#5a3f1c';
for (const [sx, sy] of STAR_POINTS) {
const { px, py } = toPixel(sx, sy);
ctx.beginPath();
ctx.arc(px, py, Math.max(2.5, cell * 0.09), 0, Math.PI * 2);
ctx.fill();
}
}
function drawStone(x, y, color, alpha) {
const { px, py } = toPixel(x, y);
const r = cell * 0.42;
ctx.save();
ctx.globalAlpha = alpha;
if (alpha === 1) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.35)';
ctx.shadowBlur = r * 0.35;
ctx.shadowOffsetY = r * 0.12;
}
const g = ctx.createRadialGradient(px - r * 0.35, py - r * 0.35, r * 0.1, px, py, r);
if (color === 1) {
g.addColorStop(0, '#6a6a6a');
g.addColorStop(1, '#0d0d0d');
} else {
g.addColorStop(0, '#ffffff');
g.addColorStop(1, '#c9c4ba');
}
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(px, py, r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function render(game, hover) {
drawBoard();
for (let y = 0; y < SIZE; y++) {
for (let x = 0; x < SIZE; x++) {
if (game.board[y][x] !== 0) drawStone(x, y, game.board[y][x], 1);
}
}
// 最后一手红点标记(有获胜连线时不画,避免遮挡)
const last = game.moveHistory[game.moveHistory.length - 1];
if (last && !game.winningLine) {
const { px, py } = toPixel(last.x, last.y);
ctx.fillStyle = '#e6493a';
ctx.beginPath();
ctx.arc(px, py, Math.max(2.5, cell * 0.1), 0, Math.PI * 2);
ctx.fill();
}
// 获胜连线
if (game.winningLine) {
const a = toPixel(game.winningLine[0].x, game.winningLine[0].y);
const b = toPixel(
game.winningLine[game.winningLine.length - 1].x,
game.winningLine[game.winningLine.length - 1].y
);
ctx.save();
ctx.strokeStyle = 'rgba(230, 60, 45, 0.9)';
ctx.lineWidth = cell * 0.14;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(a.px, a.py);
ctx.lineTo(b.px, b.py);
ctx.stroke();
ctx.restore();
}
// 悬停预览:半透明显示当前行棋方棋子
if (hover && !game.gameOver && game.board[hover.y][hover.x] === 0) {
drawStone(hover.x, hover.y, game.currentPlayer, 0.4);
}
}
// 把鼠标事件换算成最近的交叉点,超出棋盘返回 null
function cellFromEvent(evt) {
const rect = canvas.getBoundingClientRect();
const x = Math.round((evt.clientX - rect.left - margin) / cell);
const y = Math.round((evt.clientY - rect.top - margin) / cell);
if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) return null;
return { x, y };
}
resize();
return { render, resize, cellFromEvent };
}
window.GomokuRenderer = { createRenderer };
})();