Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】

文章目录

  • 初识 Canvas
    • 什么是 Canvas?
    • 学会了 Canvas 我可以做什么?
  • 开始学习 Canvas
    • 闭合路径
    • 线条颜色
    • 线的宽度
    • 填充
    • Canvas 基础实践应用(1) - 绘制表格
    • Canvas - 什么是基于状态绘图
    • 闭合矩形的快速绘制
    • 圆形的快速绘制
    • Canvas 基础实践应用(2) - 绘制扇形图
      • 文字填充 - 角度求坐标
      • 文字填充 - 绘制方法
    • Canvas 基础实践应用(3) - 绘制文字 + 扇形图完结
    • 图片的绘制
      • 帧动画

初识 Canvas


什么是 Canvas?

Canvas 是 HTML5 提供的一种新的标签形式 - 画布标签

通过名字就不难理解这个标签的意思,就和一张白纸一样,我们想呈现什么样的画面都可以(前提你需要一根画笔 – JavaScript)

Canvas 是一个矩形画布,可以使用 JavaScript 在画布上进行作画,控制每一个像素,也就是说他本身不具备绘画功能,是需要 JavaScript 来进行作画的,Canvas 非常的丰富多彩,拥有多种的绘制路径,矩形,图形,字符,图像等等方法,使得 Web 网页更加的美丽

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第1张图片


学会了 Canvas 我可以做什么?

  1. 游戏开发:比如我们将要制作的 2D 游戏《保卫家园》就是使用 canvas 开发的一款 2D 游戏,不过说回来 canvas 开发游戏在基于 Web 图像显示方面是比之前的 Flash 更加立体的,更加细节,在流畅度以及跨平台方面比 Flash 更加的友好,当然我自己并不是一个游戏开发者,这只是一个爱好,我本身是一名前端开发者,我感觉游戏开发又是另一个领域了,我们需要学习一些数学知识,物理知识,几何学等等,当然《保卫家园》并没有使用到很多的专业基础知识,仅仅涉及到了一些三角函数,勾股定理这些高中数学知识
  2. 开发可视化数据图表:这个不需要我过多的阐述了,百度开源的 Echart 就是使用 canvas 进行绘制的
  3. banner 广告:我本身也经常的去浏览网站,所以提到广告字眼我就很讨厌,说到 banner 广告,我们不得不又一次提到 Flash ,那个时候还没有智能手机,HTML5 技术可以在 banner 广告上发挥不可一世的作用,所以用 Canvas 实现动态广告是很合适的
  4. 未来走向预测:模拟器(WebGL),远程计算机,图形编辑器,纯 Canvas 移动应用

开始学习 Canvas

首先我们说 Canvas 是 HTML5 的标签,那么我们新建画布的第一步就是创建一个 Cnavas 标签:

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>Canvastitle>
	head>
	<body>
		
		<canvas>canvas>
	body>
html>

那么接下来就是 canvas 标签的一些常用属性

  • width
  • hright

不建议使用 css 设置宽高,会被拉伸

<canvas class="canvas" width="800" height="600">canvas>

JavaScript 创建 canvas 环境上下文

// 开启 canvas 环境(其实没有很大的必要 - 至少可以拥有一些 API 智能提示)
/** @type {HTMLCanvasElement} */

// 拿到 canvas节点
const canvas = document.querySelector(".canvas")

// 拿到 canvas 上下文(开启 2d 环境)
const ctx = canvas.getContext("2d")
// 如果开启 3D 环境的话,我们可以写入 webgl

Canvas 坐标轴是怎样的?

这里我就不做图示了,很简单啊,就是直角坐标系的第四象限嘛,Y 轴向下为正,X 轴向右为正,这很合理,和浏览器渲染方向是一样的(从左上角开始渲染)

小试牛刀 - 画俩条连接线段

// ctx 就是canvas上下文嘛,moveTo - 移动到 --(直角坐标系第四象限x:100,y::100)的位置
ctx.moveTo(100, 100)
// 现在我们已经有了一个点了(页面无显示)

// 以上一个点拉长线条
ctx.lineTo(200, 100)

// 继续拉长当前线条到指定位置
ctx.lineTo(100, 200)

// 此时页面还无显示(我们只是把路径绘制出来了,没有描线)

// 所以我们这一步填充线条
ctx.stroke()

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第2张图片

闭合路径

上面我们已经画出了俩条相连接的线段,接下来我们想画出第三条线以形成一个三角形,有俩种方案

  • 我们可以继续使用 lineTo 进行一个拉伸
  • 我们可以使用 closePath 进行一个闭合路径
// 闭合路径
ctx.closePath()

线条颜色

我们可以在描边之前给到这些线条一些自定义颜色 strokeStyle

ctx.strokeStyle = "yellow"
// or
ctx.strokeStyle = "rgba(98,98,98,0.5)"

线的宽度

依旧是在描边之前定义 lineWidth

ctx.lineWidth = 10

线的宽度是以初始坐标点为基准进行内外扩展的,也就是说,我们给到的 10 像素宽,以 100 ~ 105 为一半宽度,100 ~ 95 为一半宽度,共同组合为了 10 像素的宽度

填充

填充在描边之后进行,对上面刚创建好的闭合图形进行一个填充 fill

默认颜色为黑色,我们可以自定义颜色

ctx.fill()

自定义填充颜色的话,我们需要在填充之前进行定义 fillStyle

ctx.fillStyle = "green"

Canvas 基础实践应用(1) - 绘制表格

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const lineFunc = function () {
	// 每条横线的间隔:
	const restHeight = 10
	const restWidth = 20

	ctx.lineWidth = 0.5

	// 绘制横线
	for (let h = 0; h < canvas.height / restHeight; h++) {
		ctx.moveTo(0, restHeight * h)
		ctx.lineTo(canvas.width, restHeight * h)
	}
	// 竖线
	for (let w = 0; w < canvas.width / restWidth; w++) {
		ctx.moveTo(restWidth * w, 0)
		ctx.lineTo(restWidth * w, canvas.height)
	}
	ctx.stroke()
}

lineFunc()

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第3张图片


Canvas - 什么是基于状态绘图

假如现在我们有一个需求,需要画出三条颜色不同的线段

乍一想好像很简单,这样不就可以实现了吗?

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const lineFunc = function () {
	// first line
	ctx.strokeStyle = "#D36118"
	ctx.lineWidth = 16

	ctx.moveTo(100, 100)
	ctx.lineTo(300, 100)

	ctx.stroke()

	// second line
	ctx.strokeStyle = "#8B95C6"
	ctx.moveTo(100, 200)
	ctx.lineTo(300, 200)
	ctx.stroke()
}

lineFunc()

看似没有错误,我们打开页面看一下发现俩条线的颜色都变成了最后我们赋值的样式颜色,这可不妙呀,我们想要画的是三条颜色不同的线段

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第4张图片

其实这个问题就出自于 Canvas 的基于状态绘图这么一个理念,那么我们在创建一个新状态不就可以了吗?正好我们拥有这个 API – beginPath

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const lineFunc = function () {
	// first line
	ctx.strokeStyle = "#D36118"
	ctx.lineWidth = 16

	ctx.moveTo(100, 100)
	ctx.lineTo(300, 100)

	ctx.stroke()

	// second line
	ctx.beginPath()
	ctx.strokeStyle = "#8B95C6"
	ctx.moveTo(100, 200)
	ctx.lineTo(300, 200)
	ctx.stroke()
}

lineFunc()

对于这个状态的理解非常重要,也就是相当于开启了一个新的样式环境,我们绘制的第一条线其实是有一个默认状态的,如果之后绘图不开启新的状态,那么样式依旧会使用默认状态的样式,如果我们使用了 beginPath 开启了一个新的状态,那么默认状态依旧会进行继承,但是这个继承不是强制继承,如果之后的状态中的样式没有做出相应的修改才会进行继承,之后的状态样式一旦修改了,也就是与默认状态样式冲突了,那么基于状态这个理念,样式会进行一个覆盖,当前的状态样式覆盖点默认状态样式

所以我们继续完成这个需求:

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const lineFunc = function () {
	// first line
	ctx.strokeStyle = "#D36118"
	ctx.lineWidth = 16

	ctx.moveTo(100, 100)
	ctx.lineTo(300, 100)

	ctx.stroke()

	// second line
	ctx.beginPath()
	ctx.strokeStyle = "#8B95C6"
	ctx.moveTo(100, 200)
	ctx.lineTo(300, 200)
	ctx.stroke()

	// third line
	ctx.beginPath()
	ctx.strokeStyle = "#F0F64E"
	ctx.moveTo(100, 300)
	ctx.lineTo(300, 300)
	ctx.stroke()
}

lineFunc()

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第5张图片


闭合矩形的快速绘制

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const lineFunc = function () {
	// rect语法:rect(X,Y,width,height)

	ctx.rect(0, 0, 200, 100)
	ctx.stroke()
}

lineFunc()

或者我们还可以直接绘制一个矩形:

// 描边矩形
ctx.strokeRect(0, 0, 200, 100)
// 填充矩形
ctx.strokeRect(0, 0, 200, 100)

清楚矩形:

ctx.clearRect(0, 0, 200, 100)

圆形的快速绘制

ctx.arc(x, y, 半径, 角度起始位置, 角度结束位置, 逆时针绘制)

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第6张图片

实践出真知,我们画一个圆弧:

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const arcFunc = function () {
	// 弧度转角度:deg  * Math.PI / 180
	ctx.arc(500, 500, 100, (0 * Math.PI) / 180, (360 * Math.PI) / 180, false)
	ctx.stroke()

	ctx.beginPath()
	// 从圆心点绘制圆弧
	ctx.moveTo(300, 300)

	ctx.arc(300, 300, 50, (0 * Math.PI) / 180, (37 * Math.PI) / 180)
	ctx.closePath()
	ctx.stroke()

	// 逆时针绘制
	ctx.beginPath()
	// 从圆心点绘制圆弧
	ctx.moveTo(150, 150)

	ctx.arc(150, 150, 50, (0 * Math.PI) / 180, (37 * Math.PI) / 180, true)
	ctx.closePath()
	// 填充
	ctx.fill()

	ctx.stroke()
}

arcFunc()

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第7张图片


Canvas 基础实践应用(2) - 绘制扇形图

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

// 假设从后端请求来的数据
const data = [
	{
		match: 1,
		color: "#EFC544",
		title: "完成事项",
	},
	{
		match: 2,
		color: "#290D48",
		title: "完成事项",
	},
	{
		match: 1,
		color: "#A05AF9",
		title: "完成事项",
	},
	{
		match: 6,
		color: "#43BDE5",
		title: "完成事项",
	},
]

const arcFunc = function (x, y, radius, data) {
	let deg = -90

	data.forEach((item) => {
		ctx.beginPath()
		ctx.moveTo(x, y)

		let angle = item.match * 36

		ctx.fillStyle = item.color

		let startAngle = (deg * Math.PI) / 180
		let endAngle = ((deg + angle) * Math.PI) / 180
		ctx.arc(x, y, radius, startAngle, endAngle)

		ctx.fill()
		deg += angle

		ctx.closePath()
		ctx.stroke()
	})
}

arcFunc(300, 300, 100, data)

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第8张图片

COOL !! – 太有成就感了


文字填充 - 角度求坐标

像 EaChars 这样的图表库,当鼠标悬停在上方时可以显示对应标题以及百分比数据,那么问题来了,这些文字该怎么实现呢?

怎么把文字放在一个合适的位置?假如我们的需求是将文字放置在每一个扇形的夹角中间位置,这个我们应该怎么去实现

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第9张图片

这就不得不谈论到我们的三角函数了

就以上面的图来说

x = 300 + Math.cos((deg * Math.PI) / 180) * (R + 20)

y = 300 + Math.sin((deg * Math.PI) / 180) * (r + 20)

文字填充 - 绘制方法

// 空心文字(无填充)
ctx.strokeText("我想绘制这样一段文字", 500, 400)

// 返回包含指定文本宽度的对象
// ctx.measureText('Hello-Canvas')

/******************************/
ctx.moveTo(500, 500)
ctx.fillStyle = "#290D48"
// 字体样式
ctx.font = "20px 幼圆"
// 字体底线对齐绘制基线
ctx.textBaseline = "bottom"
// 字体对齐方式
ctx.textAlign = "center"
// 填充文字
ctx.fillText("你好 画板 -- Hello Canvas", 300, 500)

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第10张图片


Canvas 基础实践应用(3) - 绘制文字 + 扇形图完结

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

// 假设从后端请求来的数据
const data = [
	{
		match: 1,
		color: "#EFC544",
		title: "待完成事项",
	},
	{
		match: 2,
		color: "#290D48",
		title: "以完成事项",
	},
	{
		match: 1,
		color: "#A05AF9",
		title: "可选完成事项",
	},
	{
		match: 6,
		color: "#43BDE5",
		title: "必要完成事项",
	},
]

const arcFunc = function (x, y, radius, data) {
	let deg = -90

	data.forEach((item) => {
		ctx.beginPath()
		ctx.moveTo(x, y)

		let angle = item.match * 36

		ctx.fillStyle = item.color

		let startAngle = (deg * Math.PI) / 180
		let endAngle = ((deg + angle) * Math.PI) / 180
		ctx.arc(x, y, radius, startAngle, endAngle)

		// 绘制文字
		let text = `${item.title}:${item.match * 10}%`
		let textAngle = deg + (1 / 2) * angle
		let x0 = x + Math.cos((textAngle * Math.PI) / 180) * (radius + 30)
		let y0 = y + Math.sin((textAngle * Math.PI) / 180) * (radius + 30)

		// 在左半边的文字正常显示
		if (textAngle > 90 && textAngle < 270) ctx.textAlign = "end"

		ctx.font = "16px 幼圆"
		ctx.fillText(text, x0, y0)

		ctx.fill()
		deg += angle

		ctx.closePath()
		ctx.stroke()
	})
}

arcFunc(300, 300, 100, data)

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第11张图片


图片的绘制

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1000
canvas.height = 800
canvas.style.border = "3px solid #000"

const imgFunc = function () {
	// 创建图片实例(或者说是获取到了图片的DOM对象)
	const img_01 = new Image()
	img_01.src = "./wallhaven-2879mg.png"

	// 当我们的图片加载完毕之后再将图片绘制到 canvas 中
	img_01.addEventListener("load", () => {
		// 参数:图片,X轴位置,Y轴位置,宽度,高度
		ctx.drawImage(img_01, 100, 100, 500, 260)
		// 或许我们可以进行一个等比拉伸
		ctx.drawImage(img_01, 300, 300, 500, 500 * (img_01.height / img_01.width))
	})
}

imgFunc()

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第12张图片

哈哈哈,请允许我这样,我对美女没有抵抗力


帧动画

其实对于图片的绘制还有很多的参数:

ctx.drawImage(
	图片,
	裁剪图片的左上角X坐标,
	裁剪图片的左上角Y坐标,
	裁剪图片的宽度,
	裁剪图片的高度,
	图片所绘制的X坐标,
	图片所绘制的Y坐标,
	图片的宽度,
	图片的高度
)

额…好像确实真的有点多了,不过为了帧动画我非常愿意学,帧动画可以说是游戏必备,其实帧动画就是一个精灵图,对于精灵图大家应该都不会陌生,话不多说,我们就拿下面这张图来做一个例子:

Canvas 从 0 到 1 -- 开发 2D 游戏《保卫家园》-- 【上篇】_第13张图片

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1200
canvas.height = 800
canvas.style.border = "3px solid #000"

const imgFunc = function () {}
const img = new Image()
img.src = "./怪兽.png"
img.addEventListener("load", () => {
	let frameIndex = 0
	setInterval(() => {
		ctx.clearRect(0, 0, canvas.width, canvas.height) // 清除上一步的墨迹(从0,0位置开始清除canvas的宽高)
		ctx.drawImage(img, frameIndex * 900, 0, 900, 900, 300, 300, 200, 200)

		frameIndex++
		if (frameIndex === 8) frameIndex = 0
	}, 100)
})
imgFunc()

让我们再来完善一下小人物的动作,比如 jump – 跳

/** @type {HTMLCanvasElement} */

const canvas = document.querySelector(".canvas")
const ctx = canvas.getContext("2d")

canvas.width = 1200
canvas.height = 800
canvas.style.border = "3px solid #000"

const imgFunc = function () {}
const img = new Image()
img.src = "./怪兽.png"

// 定义高度
let peopleHeight = 300

img.addEventListener("load", () => {
	let frameIndex = 0
	setInterval(() => {
		ctx.clearRect(0, 0, canvas.width, canvas.height) // 清除上一步的墨迹(从0,0位置开始清除canvas的宽高)
		ctx.drawImage(
			img,
			frameIndex * 900,
			0,
			900,
			900,
			300,
			peopleHeight,
			200,
			200
		)

		frameIndex++
		if (frameIndex === 8) frameIndex = 0
	}, 100)
})
imgFunc()

// jump
const btn = document.querySelector(".button")

btn.addEventListener("click", () => {
	if (peopleHeight === 300) {
		const moveInterval = setInterval(() => {
			peopleHeight--
			if (peopleHeight === 100) {
				clearInterval(moveInterval)
				const backInterval = setInterval(() => {
					peopleHeight++
					if (peopleHeight === 300) clearInterval(backInterval)
				}, 4)
			}
		}, 2)
	}
})

好吧,我承认自己写了一个回调地狱,不过功能却简单的实现了

这是上篇,因为作者本人写的慢,所以先将上篇写出来并发布了,之后我们会完整的做一个 2d游戏,并且我会将源代码以及资料免费上传到CSDN资源库

你可能感兴趣的:(JS,HTML+CSS,游戏,javascript,前端,canvas,html5)