本文正在参加「金石计划」
前言: 对于 Canvas 这个标签我一直都是泛泛的了解了一下,于是这两天系统的学习了一下这个标签,然后在学习的过程中使用 Canvas 实现了这个五子棋。
效果图:
千里之行,始于足下。学习使用Canvas制作这个五子棋游戏你要会的有:
- Canvas 的基本使用
- 使用 Canvas 绘制线条
- 使用 Canvas 圆弧
- 填充样式
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>
效果图:
以上就是使用脚本绘制一个最基本的 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>
画直线:
- 使用 moveTo() 方法把画笔移动到直线起点,简单来说就是绘制起点。
- 使用 lineTo() 方法把画笔移动到直线终点,简单来说就是绘制终点。
- 使用 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>
这个是效果图:
上面用到关于画圆的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>
效果图:
好了,现在你应该知道如何创建一个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>
效果图:
当然我给这个棋盘加了一点样式。然后有了棋盘之后就是要有棋子,逻辑就是监听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>
效果: