Canvas 是 HTML5 提供的一种新的标签形式 - 画布标签
通过名字就不难理解这个标签的意思,就和一张白纸一样,我们想呈现什么样的画面都可以(前提你需要一根画笔 – JavaScript)
Canvas 是一个矩形画布,可以使用 JavaScript 在画布上进行作画,控制每一个像素,也就是说他本身不具备绘画功能,是需要 JavaScript 来进行作画的,Canvas 非常的丰富多彩,拥有多种的绘制路径,矩形,图形,字符,图像等等方法,使得 Web 网页更加的美丽
首先我们说 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 标签的一些常用属性:
不建议使用 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()
上面我们已经画出了俩条相连接的线段,接下来我们想画出第三条线以形成一个三角形,有俩种方案
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"
/** @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()
假如现在我们有一个需求,需要画出三条颜色不同的线段
乍一想好像很简单,这样不就可以实现了吗?
/** @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 的基于状态绘图这么一个理念
,那么我们在创建一个新状态不就可以了吗?正好我们拥有这个 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()
/** @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, 半径, 角度起始位置, 角度结束位置, 逆时针绘制)
实践出真知,我们画一个圆弧:
/** @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()
/** @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)
COOL !! – 太有成就感了
像 EaChars 这样的图表库,当鼠标悬停在上方时可以显示对应标题以及百分比数据,那么问题来了,这些文字该怎么实现呢?
怎么把文字放在一个合适的位置?假如我们的需求是将文字放置在每一个扇形的夹角中间位置,这个我们应该怎么去实现
这就不得不谈论到我们的三角函数了
就以上面的图来说
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)
/** @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)
/** @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()
哈哈哈,请允许我这样,我对美女没有抵抗力
其实对于图片的绘制还有很多的参数:
ctx.drawImage(
图片,
裁剪图片的左上角X坐标,
裁剪图片的左上角Y坐标,
裁剪图片的宽度,
裁剪图片的高度,
图片所绘制的X坐标,
图片所绘制的Y坐标,
图片的宽度,
图片的高度
)
额…好像确实真的有点多了,不过为了帧动画我非常愿意学,帧动画可以说是游戏必备,其实帧动画就是一个精灵图,对于精灵图大家应该都不会陌生,话不多说,我们就拿下面这张图来做一个例子:
/** @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资源库