用到的技术
- es6
- canvas
canvas
Canvas API 提供了一个通过JavaScript 和 HTML的元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
初识canvas
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no" /> <title>初识canvas.html</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } canvas { background-color: aqua; } </style> </head> <body> <canvas></canvas> </body> </html>
可以看到 canvas是一个 默认宽高为 300*150
、内联的一个元素。
画直线 lineTo
以下代码画出一个对角线
// 1 获取dom元素 const canvas = document.querySelector('canvas'); // 2 获取画布对象 const ctx = canvas.getContext('2d'); // 3 设定起点 ctx.moveTo(0, 1); // 4 设定终点 ctx.lineTo(300, 150); // 5 描边 ctx.stroke();
改画布大小
需要注意的是,想要修改 canvas标签的宽度和高度,不能直接在css中改,否则是拉伸canvas。只能在标签的属性上通过 width 和 height来改
错误示例
canvas { width: 600px; height: 300px; }
正确示例
<canvas width="600" height="300"></canvas>
修改颜色 strokeStyle
当我们不想要默认的黑色线条时,可以通过 strokeStyle来修改颜色
ctx.strokeStyle = 'pink'; ctx.stroke();
画圆弧
// arc(圆心坐标x,圆心坐标y,半径长度,开始弧度,结束弧度) ctx.arc(300, 150, 100, 0, Math.PI * 2); ctx.stroke();
也可以修改为 填充的圆弧 fill
ctx.arc(300, 150, 100, 0, Math.PI * 2); ctx.fill();
如果想要修改填充的颜色的话 就要使用 fillStyle
ctx.fillStyle = 'yellow'; ctx.fill();
show time
下列的代码示例,有注释的表示是新增的代码
搭建好静态结构
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no" /> <title>五子棋</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { text-align: center; height: 100vh; } canvas { /* 可以自己写一个背景颜色来代替以下四行代码 */ background-image: url(./images/20211227092309212.jpg); background-repeat: no-repeat; background-size: cover; background-position: center; } </style> </head> <body> <canvas></canvas> </body> </html>
新建一个类,声明相关方法
下列的代码示例,有注释的表示是新增的代码
主要有四个功能
- 初始化相关属性 如 棋盘宽高等
- 声明描绘棋盘的方法
- 声明点击棋盘,进行下棋的方法
- 声明下棋时,判断是否胜利的方法
class Gobang { /** 构造函数 */ constructor() {} /** 初始化画布 */ init() {} /** 描绘棋盘 */ drawCheckerBroad() {} /** canvas绑定点击事件,实现下棋功能 */ checkerBroadEvent() {} /** 判断是否胜利 * */ victory() {} }
初始化画布
下列的代码示例,有注释的表示是新增的代码
这里面需要初始化的属性有
需求是想要的棋盘 一共 18*18个格子,那么就需要用到19条线,每一个格子大小为20*20
this.canvas
用来在后面绑定点击事件使用this.checkerBoradWidth
用来声明一个格子多大this.lineNums
用来声明棋盘一个要画多少条线段this.blaorwh
用来声明当前下棋的选手是黄色还是白色this.chessMartrixs
棋盘位置矩阵,用来记录下棋的棋子位置和判断是否胜利
init() { // 格子的宽度宽度 this.checkerBoradWidth = 20; this.lineNums = 19; // 白色还是黄色 为黄色 this.blaorwh = '黄色'; // 存放棋子的位置 二维数组 19*19 this.chessMartrixs = []; for (let i = 0; i <= this.lineNums; i++) { this.chessMartrixs.push( new Array(this.lineNums).fill(0, 0, this.lineNums) ); } this.canvas = document.querySelector('canvas'); // 设置 画布的大小 刚好等于20个格子的大小 this.canvas.width = this.checkerBoradWidth * (this.lineNums - 1); this.canvas.height = this.checkerBoradWidth * (this.lineNums - 1); this.ctx = this.canvas.getContext('2d'); }
描绘棋盘
下列的代码示例,有注释的表示是新增的代码
需求是想要的棋盘 一共 18*18个格子,那么就需要用到19条线,每一个格子大小为20*20
// 开辟新的路径 this.ctx.beginPath(); // 设定线条颜色 this.ctx.strokeStyle = '#ccc'; // 遍历描绘 棋盘上的每一条线 19*19条线 for (let i = 0; i < this.lineNums; i++) { // 描绘横线 checkerBoradWidth 为 棋盘 格子的宽度 this.ctx.moveTo(0, i * this.checkerBoradWidth); this.ctx.lineTo( // lineNums 为 线条的个数 this.lineNums * this.checkerBoradWidth, i * this.checkerBoradWidth ); // 描绘竖线 this.ctx.moveTo(i * this.checkerBoradWidth, 0); this.ctx.lineTo( i * this.checkerBoradWidth, this.lineNums * this.checkerBoradWidth ); } this.ctx.stroke();
然后在 构造函数中分别调用 初始化画布和描绘棋盘的方法,然后实例化
constructor() { this.init(); this.drawCheckerBroad(); }
实例化
const gobang = new Gobang();
出现效果
点击棋盘,进行下棋
下列的代码示例,有注释的表示是新增的代码
通过给canvas标签绑定点击事件,获取鼠标坐标,然后就地画圆即可
checkerBroadEvent() { // 注意要使用箭头函数,否则容易导致 this指向问题 this.canvas.addEventListener('click', (e) => { // 获取鼠标在画布中的坐标 const x = e.offsetX; const y = e.offsetY; // 每次点击 重新开辟路径 this.ctx.beginPath(); // 根据点击的坐标绘制 棋子 this.ctx.arc(x, y, this.checkerBoradWidth / 2, 0, 2 * Math.PI); // 判断白棋还是黑棋 if (this.blaorwh === '黄色') { // 黄色 this.ctx.fillStyle = 'yellow'; } else { // 白色 描边 否则容易和背景混淆 this.ctx.fillStyle = '#fff'; this.ctx.stroke(); } this.ctx.fill(); this.blaorwh = this.blaorwh === '黄色' ? '白色' : '黄色'; }); }
在构造函数中也调用它
constructor() { this.init(); this.drawCheckerBroad(); this.checkerBroadEvent(); // 鼠标点击下棋功能 }
这时候会出现两个问题
- 下棋的位置不是在线段的中心,有可能歪了
- 同一个位置,可以放无数个棋子(需要判断是否已经有棋子)
我们需要解决它
修正下棋的标准位置
下列的代码示例,有注释的表示是新增的代码
主要通过判断当前下棋的坐标位置和水平或竖直方向上的哪一条线段的位置近,那么就设置下棋的位置等于距离最近的线段的坐标。
/** canvas绑定点击事件,实现下棋功能 */ checkerBroadEvent() { // 注意要使用箭头函数,否则容易导致 this指向问题 this.canvas.addEventListener('click', (e) => { const x = e.offsetX; const y = e.offsetY; // 设定画圆弧的圆心坐标 需要加上每一个格子20px大小的 let arcX, arcY; // 用来记录 棋子的坐标点 如 (0,1) (1,3) 等 for (let i = 0; i <= this.lineNums; i++) { // 判断鼠标的x坐标靠近哪一条线 水平方向 if ( Math.abs(x - i * this.checkerBoradWidth) <= this.checkerBoradWidth / 2 ) { arcX = i * this.checkerBoradWidth; } // 判断鼠标的y坐标靠近哪一条边线 垂直方向 if ( Math.abs(y - i * this.checkerBoradWidth) <= this.checkerBoradWidth / 2 ) { arcY = i * this.checkerBoradWidth; } } // 如果点出了其他位置,直接返回 if (arcX === undefined || arcY === undefined) return; this.ctx.beginPath(); // 。。。。。 }); }
判断当前位置是否已经有棋子了
下列的代码示例,有注释的表示是新增的代码
使用一个二维数组 this.checkerBoradWidth ,记录下棋的坐标位置,当再次下棋时,判断当前位置是否有棋子来实现功能
checkerBroadEvent() { this.canvas.addEventListener('click', (e) => { const x = e.offsetX; const y = e.offsetY; let arcX, arcY; // 用来记录 棋子的坐标点 把当前棋子的位置记录在 棋子矩阵中 let chessX, chessY; for (let i = 0; i <= this.lineNums; i++) { if ( Math.abs(x - i * this.checkerBoradWidth) <= this.checkerBoradWidth / 2 ) { arcX = i * this.checkerBoradWidth; // 记录当前棋子的位置 用来做下棋时的位置重复判断 chessX = i; } if ( Math.abs(y - i * this.checkerBoradWidth) <= this.checkerBoradWidth / 2 ) { arcY = i * this.checkerBoradWidth; // 记录当前棋子的位置 用来做下棋时的位置重复判断 chessY = i; } } // 如果点出了其他位置,直接返回 if (arcX === undefined || arcY === undefined) return; // 如果有人下过了,那么就返回 if (this.chessMartrixs[chessY][chessX]) return; // 把当前棋子位置存一份到 矩阵中 this.chessMartrixs[chessY][chessX] = this.blaorwh; this.ctx.beginPath(); this.ctx.arc( arcX, arcY, this.checkerBoradWidth / 2, 0, 2 * Math.PI ); if (this.blaorwh === '黄色') { this.ctx.fillStyle = 'yellow'; } else { this.ctx.fillStyle = '#fff'; this.ctx.stroke(); } this.ctx.fill(); this.blaorwh = this.blaorwh === '黄色' ? '白色' : '黄色'; }); }
判断选手胜利-重点
我们实现判断选手是否胜利的思路如下
以下四条轴中,任意一条轴(由两条短轴连接而成)上有5个同色的棋子就表示胜利
通过观察以上图表,得出一个规律:我们需要设置一个判断棋子是否相连的关键数组
[[-1, -1], [-1, 0], [1, -1], [0, 1]]
- 比如我们对上面的数组进行遍历 比如 可以获取到 第一个元素
[-1,-1]
- 同时我们对
[-1,-1]
进行取反 ,便也得到[1,1]
,那么便可以确定一条长轴
- 此时,我们拿
[-1,-1]
单独开启遍历,计算 这边方向的棋子个数
- 同时,我们拿
[1,1]
也开始遍历,计算这边方向的棋子个数
- 如果两者加起来大于等于 5 ,便表示胜利
代码
/** * 判断选手是否胜利 */ victory(matrix, x, y, borw) { // 判断胜利条件的4个轴方向 let victoryUnits = [ [-1, -1], [-1, 0], [1, -1], [0, 1], ]; const getLineNums = (matrix, direcsx, x, y, borw) => { // 用来统计相连的同色的棋子的数量 let results = 0; for (let i = 0; i < 5; i++) { const vicx = x + i * direcsx[0]; const vicy = y + i * direcsx[1]; // 判断位置没有超出 if ( matrix[vicy] !== undefined && matrix[vicy][vicx] !== undefined && borw === matrix[vicy][vicx] ) { // 判断是否和当前的颜色相等 results++; } else { break; } } return results; }; // 判断是否胜利 const result = victoryUnits.some((item) => { // 统计一侧短轴的同色棋子的个数 const n1 = getLineNums(matrix, item, x, y, borw); // 统计另一侧短轴的同色棋子的个数 const n2 = getLineNums( matrix, item.map((v) => -v), x, y, borw ); // 因为自己也算一个了 return n1 + n2 - 1 >= 5; }); return result; }
完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no" /> <title>H5-五子棋</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { text-align: center; height: 100vh; } canvas { background-image: url(./images/20211227092309212.jpg); background-repeat: no-repeat; background-size: cover; background-position: center; } </style> </head> <body> <canvas></canvas> <script> class Gobang { constructor() { this.init(); this.drawCheckerBroad(); this.checkerBroadEvent(); } init() { this.checkerBoradWidth = 20; this.lineNums = 19; this.blaorwh = '黄色'; this.chessMartrixs = []; for (let i = 0; i <= this.lineNums; i++) { this.chessMartrixs.push( new Array(this.lineNums).fill(0, 0, this.lineNums) ); } this.canvas = document.querySelector('canvas'); this.canvas.width = this.checkerBoradWidth * (this.lineNums - 1); this.canvas.height = this.checkerBoradWidth * (this.lineNums - 1); this.ctx = this.canvas.getContext('2d'); } drawCheckerBroad() { this.ctx.beginPath(); this.ctx.strokeStyle = '#ccc'; for (let i = 0; i < this.lineNums; i++) { this.ctx.moveTo(i * this.checkerBoradWidth, 0); this.ctx.lineTo( i * this.checkerBoradWidth, this.lineNums * this.checkerBoradWidth ); this.ctx.moveTo(0, i * this.checkerBoradWidth); this.ctx.lineTo( this.lineNums * this.checkerBoradWidth, i * this.checkerBoradWidth ); } this.ctx.stroke(); } checkerBroadEvent() { this.canvas.addEventListener('click', (e) => { const x = e.offsetX; const y = e.offsetY; let arcX, arcY; let chessX, chessY; for (let i = 0; i <= this.lineNums; i++) { if ( Math.abs(x - i * this.checkerBoradWidth) <= this.checkerBoradWidth / 2 ) { arcX = i * this.checkerBoradWidth; chessX = i; } if ( Math.abs(y - i * this.checkerBoradWidth) <= this.checkerBoradWidth / 2 ) { arcY = i * this.checkerBoradWidth; chessY = i; } } if (arcX === undefined || arcY === undefined) return; if (this.chessMartrixs[chessY][chessX]) return; this.chessMartrixs[chessY][chessX] = this.blaorwh; this.ctx.beginPath(); this.ctx.arc( arcX, arcY, this.checkerBoradWidth / 2, 0, 2 * Math.PI ); if (this.blaorwh === '黄色') { this.ctx.fillStyle = 'yellow'; } else { this.ctx.fillStyle = '#fff'; this.ctx.stroke(); } this.ctx.fill(); if ( this.victory(this.chessMartrixs, chessX, chessY, this.blaorwh) ) { console.log(this.blaorwh + '胜利'); return; } this.blaorwh = this.blaorwh === '黄色' ? '白色' : '黄色'; }); } victory(matrix, x, y, borw) { let victoryUnits = [ [-1, -1], [-1, 0], [1, -1], [0, 1], ]; const getLineNums = (matrix, direcsx, x, y, borw) => { let results = 0; for (let i = 0; i < 5; i++) { const vicx = x + i * direcsx[0]; const vicy = y + i * direcsx[1]; if ( matrix[vicy] !== undefined && matrix[vicy][vicx] !== undefined && borw === matrix[vicy][vicx] ) { results++; } else { break; } } return results; }; const result = victoryUnits.some((item) => { const n1 = getLineNums(matrix, item, x, y, borw); const n2 = getLineNums( matrix, item.map((v) => -v), x, y, borw ); return n1 + n2 - 1 >= 5; }); return result; } } const gobang = new Gobang(); </script> </body> </html>