我也用Canvas实现了一个五子棋 !

1,457 阅读5分钟

本文正在参加「金石计划」

前言: 对于 Canvas 这个标签我一直都是泛泛的了解了一下,于是这两天系统的学习了一下这个标签,然后在学习的过程中使用 Canvas 实现了这个五子棋。

效果图:

QQ图片20230331112411.png

千里之行,始于足下。学习使用Canvas制作这个五子棋游戏你要会的有:

  1. Canvas 的基本使用
  2. 使用 Canvas 绘制线条
  3. 使用 Canvas 圆弧
  4. 填充样式

Canvas 的基本使用

以下是使用 Javascript 创建一个 Canvas :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        // 直接使用script创建
        const canvas=document.createElement('canvas')  // 创建元素
        canvas.width=600  // 设置宽度
        canvas.height=400  // 设置高度
        document.body.append(canvas)
        // 获取画笔
        const context=canvas.getContext('2d')
        // 画图
        context.fillRect(100,100,200,200)
    </script>
</body>
</html>

效果图:

QQ图片20230331113510.png

以上就是使用脚本绘制一个最基本的 Canvas 画布。 注意:如果不设置 Canvas 的宽高,那么它默认的宽高是 300150

使用 Canvas 绘制线条

当然有了 Canvas 只是第一步,棋盘上面的网格需要我们使用横线纵横交错的画出来,所以第二步学习使用怎么画直线。 示例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <script>
        const canavs = document.createElement('canvas')  
        canavs.width = 600
        canavs.height = 400
        document.body.append(canavs)
        const context = canavs.getContext('2d')
        // 起点
        context.moveTo(100, 100)
        // 终点
        context.lineTo(300, 100)
        // 调用画线的方法
        context.stroke()
    </script>
</body>

</html>

画直线:

  1. 使用 moveTo() 方法把画笔移动到直线起点,简单来说就是绘制起点。
  2. 使用 lineTo() 方法把画笔移动到直线终点,简单来说就是绘制终点。
  3. 使用 stroke() 方法让画笔绘制线条,该实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。

使用 Canvas 绘制圆

有圆才能画出棋子,以下是一个例子:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        const canvas = document.createElement('canvas')
        canvas.width = 600
        canvas.height = 500
        document.body.append(canvas)
        const context = canvas.getContext('2d')

        // 画笑脸
        // 画大圆
        context.arc(300, 200, 100, 0, 360 * Math.PI / 180)
        context.stroke()

        // 画左眼
        // 在两个不相干的图形之间需要告诉context 重新生成一个新的路径
        context.beginPath()
        context.arc(250,150,20,0,360*Math.PI/180)
        context.stroke()
        // 有一个配合的api 闭合路径
        context.closePath()

        // 画右眼
        context.beginPath()
        context.arc(350,150,20,0,360*Math.PI/180)
        context.stroke()
        context.closePath()

        // 画鼻子
        context.beginPath()
        context.arc(300,200,10,0,360*Math.PI/180)
        context.stroke()
        context.closePath()

        // 嘴巴
        context.beginPath()
        context.arc(300,200,80,0,180*Math.PI/180)
        context.stroke()
        context.closePath()
    </script>
</body>

</html>

这个是效果图:

QQ图片20230331151404.png

上面用到关于画圆的api有:canvas.arc(),这个方法创建弧/曲线(用于创建圆或部分圆)。

语法:

context.arc(x,y,r,sAngle,eAngle,counterclockwise);
x圆的中心的 x 坐标。
y圆的中心的 y 坐标。
r圆的半径。
sAngle起始角,以弧度计。(弧的圆形的三点钟位置是 0 度)。
eAngle结束角,以弧度计。
counterclockwise可选。规定应该逆时针还是顺时针绘图。False = 顺时针,true = 逆时针。

填充样式

与填充样式有关的api有:

  • createLinearGradient() 方法创建放射状/圆形渐变对象。
  • createLinearGradient() 方法创建线性的渐变对象。
  • createConicGradient() 方法创建(圆形)梯度渐变对象。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
         const canvas=document.createElement('canvas')
        canvas.width=600
        canvas.height=600
        document.body.append(canvas)
        const context=canvas.getContext('2d')
        // 修改填充图形的颜色
        // context.fillStyle='#0a0'
        // let g= context.createLinearGradient(0,0,600,600)
        // g.addColorStop(0,'yellow')
        // g.addColorStop(1,'blue')
        // context.fillStyle=g


        let g= context.createRadialGradient(250,150,0,250,150,150)
        g.addColorStop(0,'yellow')
        g.addColorStop(1,'blue')
        context.fillStyle=g

        context.beginPath()
        context.arc(300,300,100,0,360*Math.PI/180)
        // 填充颜色
        context.fill()
        context.stroke()
        context.closePath()

        context.beginPath()
        context.arc(250,250,20,0,360*Math.PI/180)
        context.stroke()
        context.closePath()

        context.beginPath()
        context.arc(350,250,20,0,360*Math.PI/180)
        context.stroke()
        context.closePath()

        context.beginPath()
        context.ellipse(300,300,10,30,0,0,360*Math.PI/180)
        context.stroke()
        context.closePath()

        context.beginPath()
        context.arc(300,300,80,0,180*Math.PI/180)
        context.stroke()
        context.closePath()
    </script>
</body>
</html>

效果图:

QQ图片20230331152554.png

好了,现在你应该知道如何创建一个Canvas,如何使用Canvas来画直线,如何使用Canvas来画圆圈以及如何使用Canvas来填充图形颜色。那么要实现一个五子棋游戏就只要再加上一点JavaScript就大功告成了。

实现思路:首先创建一个 Canvas 对象,然后我们在Canvas上面开始作画,用横线画出棋盘,然后再点击棋盘上的某个位置生成棋子,再加上一点逻辑来判断是否满足五子成线,若成线即游戏结束,逻辑上来看大概就是这样子的。

所以首先创建一个大小为 800*800 的棋盘,然后循环的画出棋盘线。

<script>
// 创建画布
        const canvas = document.createElement('canvas')
        canvas.width = 800
        canvas.height = 800
        document.body.append(canvas)
        const context = canvas.getContext('2d')
         // 画棋盘 由横线和纵线组成
        // 循环画线,将线条画出来
        for (let i = 1; i < 16; i++) {
            // 将线段的开头表示出来
            context.moveTo(50, 50 * i)  // 横线
            context.lineTo(750, 50 * i)
            context.stroke()

            // 纵线
            context.moveTo(50 * i, 50)
            context.lineTo(50 * i, 750)
            context.stroke()
        }
        
</script>

效果图:

QQ图片20230331154847.png

当然我给这个棋盘加了一点样式。然后有了棋盘之后就是要有棋子,逻辑就是监听Canvas上面的点击事件,使用一个变量来控制棋子的颜色,点击棋盘上面的某个地方就绘制一个圆,根据控制棋子颜色的变量来给圆填充对应的颜色。

至于判断五子成线的状态:只要在落子的时候去循环的遍历该子的纵向,横向,自左上向右下,自右上到左下的四个方向有没有连续的五个相同颜色的棋子即可。

// 点击棋盘的时候,绘制一个圆
        canvas.addEventListener('click', (e) => {
            let { offsetX, offsetY } = e
            // 判断棋子点击的边界
            if (offsetX < 25 || offsetY < 25 || offsetX > 775 || offsetY > 775) {
                return
            }
            // 让棋子准确的落到棋盘
            let i = Math.floor((offsetX + 25) / 50)  // x轴坐标
            let j = Math.floor((offsetY + 25) / 50)  // y轴坐标
            // console.log(i, j);

            // 判断点击的棋盘当前是否已经有棋子了
            if (circles[i][j]) {
                alert('请勿重复落子')
                return
            }
            circles[i][j] = isBlack ? 'black' : 'white'
            // console.log(circles);
            let win = (checkY(i, j) || checkX(i, j) || checkLR(i, j) || checkRL(i, j))
            console.log(win);
            // 判断是否有人获胜



            let x = Math.floor((offsetX + 25) / 50) * 50
            let y = Math.floor((offsetY + 25) / 50) * 50
            context.beginPath()
            context.arc(x, y, 20, 0, 2 * Math.PI)

            // 根据棋子状态判断出下一个棋子的颜色
            // context.fillStyle = isBlack ? '#000' : '#fff'
            let tx = isBlack ? x - 10 : x + 10
            let ty = isBlack ? y - 10 : y + 10
            let g = context.createRadialGradient(tx, ty, 0, tx, ty, 30)
            g.addColorStop(0, isBlack ? '#ccc' : '#666')
            g.addColorStop(1, isBlack ? '#000' : '#fff')
            context.fillStyle = g
            context.fill()
            context.closePath()


            isBlack = !isBlack
            tip.innerText = isBlack ? '请黑棋落子' : '请白棋落子'
            if (win) {
                tip.innerText = isBlack ? '白子胜利' : '黑子胜利'
            }
        })
        // 纵向寻找五子
        function checkY(x, y) {
            let count = 0  // 控制下面循环的次数的变量
            let up = 0  // 记录向上找的次数
            let down = 0  // 记录向下找的次数
            let target = circles[x][y]  // 获取当前棋子的颜色

            let num = 1  // 初始只有一个连子


            while (count < 100) {  // 多次循环判断
                count++   
                if (num >= 5 || (circles[x][y - up] !== target && circles[x][y + down] !== target)) {  // 如果连子的个数大于等于5或者旁边的棋子颜色跟当前棋子的颜色不同,退出当前循环
                    break
                }

                up++
                if (circles[x][y - up] && circles[x][y - up] === target) {  // 如果该子的上面的位置有棋子并且颜色与当前棋子颜色相同
                    num++
                }
                circles[x][y - up]

                down++
                if (circles[x][y + down] && circles[x][y + down] === target) {
                    num++
                }

            }

            return num >= 5
        }

        // 横向寻找五子
        function checkX(x, y) {
            let count = 0
            let left = 0  // 记录向左找的次数
            let right = 0  // 记录向右找的次数
            let target = circles[x][y]

            let num = 1  // 初始只有一个连子


            while (count < 100) {
                count++
                if (num >= 5 || (circles[x - left][y] !== target && circles[x + right][y] !== target)) {
                    break
                }

                left++
                if (circles[x - left][y] && circles[x - right][y] === target) {
                    num++
                }
                circles[x - right][y]

                right++
                if (circles[x + right][y] && circles[x + right][y] === target) {
                    num++
                }

            }

            return num >= 5
        }

        // 左上角到右下角斜线判断五子
        function checkLR(x, y) {
            let count = 0
            let num = 1
            let lt = 0
            let rb = 0
            let target = circles[x][y]
            while (count < 100) {
                count++

                lt++
                if (circles[x - lt][y - lt] && circles[x - lt][y - lt] === target) {
                    num++
                }
                rb++
                if (circles[x + rb][y + rb] && circles[x + rb][y + rb] === target) {
                    num++
                }

                if (num >= 5 || (circles[x - lt][y - lt] !== target && circles[x + rb][y + rb] !== target)) {
                    break
                }
            }

            return num >= 5
        }
        // 左下角到右上角方向判断五子
        function checkRL(x, y) {
            let count = 0
            let num = 1
            let lb = 0
            let rt = 0
            let target = circles[x][y]
            while (count < 100) {
                count++

                lb++
                if (circles[x - lb][y + lb] && circles[x - lb][y + lb] === target) {
                    num++
                }
                rt++
                if (circles[x + rt][y - rt] && circles[x + rt][y - rt] === target) {
                    num++
                }

                if (num >= 5 || (circles[x +rt][y - rt] !== target && circles[x - lb][y + lb] !== target)) {
                    break
                }
            }

            return num >= 5
        }

完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        canvas {
            background: #b87c49;
            display: block;
            margin: 0 auto;
        }

        #tip {
            text-align: center;
            margin: 10px;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <div id="tip">
        请黑棋落子
    </div>
    <script>
        const canvas = document.createElement('canvas')
        canvas.width = 800
        canvas.height = 800
        document.body.append(canvas)
        const context = canvas.getContext('2d')

        let tip = document.getElementById('tip')

        // 画棋盘 由横线和纵线组成
        // 循环画线,将线条画出来
        for (let i = 1; i < 16; i++) {
            // 将线段的开头表示出来
            context.moveTo(50, 50 * i)  // 横线
            context.lineTo(750, 50 * i)
            context.stroke()

            // 纵线
            context.moveTo(50 * i, 50)
            context.lineTo(50 * i, 750)
            context.stroke()
        }

        // 使用一个变量来保存棋子颜色状态
        let isBlack = true

        // 解决重复落子问题
        let circles = []
        for (let i = 1; i < 16; i++) {
            circles[i] = []
        }

        // 点击棋盘的时候,绘制一个圆
        canvas.addEventListener('click', (e) => {
            let { offsetX, offsetY } = e
            // 判断棋子点击的边界
            if (offsetX < 25 || offsetY < 25 || offsetX > 775 || offsetY > 775) {
                return
            }
            // 让棋子准确的落到棋盘
            let i = Math.floor((offsetX + 25) / 50)  // x轴坐标
            let j = Math.floor((offsetY + 25) / 50)  // y轴坐标
            // console.log(i, j);

            // 判断点击的棋盘当前是否已经有棋子了
            if (circles[i][j]) {
                alert('请勿重复落子')
                return
            }
            circles[i][j] = isBlack ? 'black' : 'white'
            // console.log(circles);
            let win = (checkY(i, j) || checkX(i, j) || checkLR(i, j) || checkRL(i, j))
            console.log(win);
            // 判断是否有人获胜



            let x = Math.floor((offsetX + 25) / 50) * 50
            let y = Math.floor((offsetY + 25) / 50) * 50
            context.beginPath()
            context.arc(x, y, 20, 0, 2 * Math.PI)

            // 根据棋子状态判断出下一个棋子的颜色
            // context.fillStyle = isBlack ? '#000' : '#fff'
            let tx = isBlack ? x - 10 : x + 10
            let ty = isBlack ? y - 10 : y + 10
            let g = context.createRadialGradient(tx, ty, 0, tx, ty, 30)
            g.addColorStop(0, isBlack ? '#ccc' : '#666')
            g.addColorStop(1, isBlack ? '#000' : '#fff')
            context.fillStyle = g
            context.fill()
            context.closePath()


            isBlack = !isBlack
            tip.innerText = isBlack ? '请黑棋落子' : '请白棋落子'
            if (win) {
                tip.innerText = isBlack ? '白子胜利' : '黑子胜利'
            }
        })
        // 纵向寻找五子
        function checkY(x, y) {
            let count = 0  // 控制下面循环的次数的变量
            let up = 0  // 记录向上找的次数
            let down = 0  // 记录向下找的次数
            let target = circles[x][y]  // 获取当前棋子的颜色

            let num = 1  // 初始只有一个连子


            while (count < 100) {  // 多次循环判断
                count++   
                if (num >= 5 || (circles[x][y - up] !== target && circles[x][y + down] !== target)) {  // 如果连子的个数大于等于5或者旁边的棋子颜色跟当前棋子的颜色不同,退出当前循环
                    break
                }

                up++
                if (circles[x][y - up] && circles[x][y - up] === target) {  // 如果该子的上面的位置有棋子并且颜色与当前棋子颜色相同
                    num++
                }
                circles[x][y - up]

                down++
                if (circles[x][y + down] && circles[x][y + down] === target) {
                    num++
                }

            }

            return num >= 5
        }

        // 横向寻找五子
        function checkX(x, y) {
            let count = 0
            let left = 0  // 记录向左找的次数
            let right = 0  // 记录向右找的次数
            let target = circles[x][y]

            let num = 1  // 初始只有一个连子


            while (count < 100) {
                count++
                if (num >= 5 || (circles[x - left][y] !== target && circles[x + right][y] !== target)) {
                    break
                }

                left++
                if (circles[x - left][y] && circles[x - right][y] === target) {
                    num++
                }
                circles[x - right][y]

                right++
                if (circles[x + right][y] && circles[x + right][y] === target) {
                    num++
                }

            }

            return num >= 5
        }

        // 左上角到右下角斜线判断五子
        function checkLR(x, y) {
            let count = 0
            let num = 1
            let lt = 0
            let rb = 0
            let target = circles[x][y]
            while (count < 100) {
                count++

                lt++
                if (circles[x - lt][y - lt] && circles[x - lt][y - lt] === target) {
                    num++
                }
                rb++
                if (circles[x + rb][y + rb] && circles[x + rb][y + rb] === target) {
                    num++
                }

                if (num >= 5 || (circles[x - lt][y - lt] !== target && circles[x + rb][y + rb] !== target)) {
                    break
                }
            }

            return num >= 5
        }
        // 左下角到右上角方向判断五子
        function checkRL(x, y) {
            let count = 0
            let num = 1
            let lb = 0
            let rt = 0
            let target = circles[x][y]
            while (count < 100) {
                count++

                lb++
                if (circles[x - lb][y + lb] && circles[x - lb][y + lb] === target) {
                    num++
                }
                rt++
                if (circles[x + rt][y - rt] && circles[x + rt][y - rt] === target) {
                    num++
                }

                if (num >= 5 || (circles[x +rt][y - rt] !== target && circles[x - lb][y + lb] !== target)) {
                    break
                }
            }

            return num >= 5
        }
    </script>

</body>

</html>

效果:

动画.gif