canvas 入门
canvas 是 HTML5 新增的一个标签, 表示画布,canvas 也是 h5 的画布技术, 我们通过代码的方式在画布上描绘一个图像
canvas标签
要进行 canvas 绘图, 首先我们先要了解到 canvas 标签,是 html5 推出的一个标签 。
<html>
<head>
...
</head>
<body>
<canvas></canvas>
</body>
</html>
- canvas 默认是一个行内块元素
- canvas 默认画布大小是 300 * 150
- canvas 默认没有边框, 背景默认为无色透明
canvas画布大小
我们在绘图之前, 先要确定一个画布的大小,因为画布默认是按照比例调整,所以我们调整宽度或者高度的时候, 调整一个, 另一个自然会按照比例自己调整,我们也可以宽高一起调整
调整画布大小有两种方案
- 第一种 : 通过 css 样式 ( 不推荐 )
<html>
<head>
<style>
canvas {
width: 1000px;
height: 500px;
}
</style>
</head>
<body>
<canvas></canvas>
</body>
</html>
- 第二种 : 通过标签属性 ( 推荐 )
<html>
<head>
...
</head>
<body>
<canvas width="1000" height="500"></canvas>
</body>
</html>
** **
两种方案的区别 通过 css 样式的调整方案, 不推荐,是因为这个方案其实并没有设置了画布的大小,而是把原先 300 * 150 的画布, 将它的可视窗口变成了 1000 * 500 所以真实画布并没有放大, 只是可视程度变大了
举个例子 : 就是你把一个 300 * 150 的图片, 放大到 1000 * 500 的大小来看,所以这个方式我们极其不推荐。
通过属性的调整方案, 推荐,这个才是真正的调整画布的的大小,也就是我们会在一个 1000 * 500 的画布上进行绘制
画布的坐标 canvas 画布是和我们 css 的坐标系一样的,从 canvas 的左上角为 0 0 左边, 分别向右向下延伸为正方向
canvas初体验
准备工作已经完成了, 我们可以开始体验一下绘制了,其实 canvas 画布很简单, 就和我们 windows 电脑的画板工具是一样的道理。
思考 : 我们在 windows 这个画板上绘制内容的时候,我们一定是先选定一个工具 ( 画笔, 矩形, 圆形, ... ),设定好样式 ( 粗细, 颜色 ),然后开始绘制。
其实在 canvas 绘制也是一个道理,拿到一个画布工具箱,从工具箱中选定工具,设定样式,开始绘制,初体验步骤。
index.html
<html>
<head>
...
</head>
<body>
<canvas id="canvas" width="600" height="300"></canvas>
<script src="./index.js"></script>
</body>
</html>
** **
index.js
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
// 语法: canvas 元素.getContext('2d')
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制
// 2-1. 讲画笔移动到一个指定位置开始下笔
// 语法: 工具箱.moveTo(x轴坐标, y轴坐标)
ctx.moveTo(100, 100)
// 2-2. 将笔移动到一个指定位置, 画下一条轨迹
// 注意: 这里是没有显示的, 因为只是画了一个轨迹
// 语法: 工具箱.lineTo(x轴坐标, y轴坐标)
ctx.lineTo(300, 100)
// 2-3. 设定本条线的样式
// 设定线的宽度
// 语法: 工具箱.lineWidth = 数字
ctx.lineWidth = 10
// 设定线的颜色
// 语法: 工具箱.strokeStyle = '颜色'
ctx.strokeStyle = '#000'
// 2-4. 描边
// 把上边画下的痕迹按照设定好的样式描绘下来
// 语法: 工具箱.stroke()
ctx.stroke()
至此我们的第一个线段就绘制完毕, 画布上就会出现一条线段
-
从坐标 ( 100, 100 ) 绘制到坐标 ( 300, 100 )
-
线段长度为 200px
-
线段宽度为 10px
-
线段颜色为 '#000' ( 黑色 )
canvas线宽颜色问题
刚才我们经过了初体验, 画了一个线段,看似没有问题, 也出现了线段, 但是其实内在是有一些问题的,我们先来观察,这次我们再来画一个线段
- 从坐标 ( 100, 100 ) 绘制到坐标 ( 300, 100 )
- 线段长度为 200px
- 线段宽度为 1px
- 线段颜色为 '#000' ( 黑色 )
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制
ctx.moveTo(100, 100)
// 2-2. 将笔移动到一个指定位置, 画下一条轨迹
ctx.lineTo(300, 100)
// 2-3. 设定本条线的样式
// 设定线的宽度
ctx.lineWidth = 10
// 设定线的颜色
ctx.strokeStyle = '#000'
// 2-4. 描边
ctx.stroke()
效果出现了, 没有什么问题,只是看上去不太像 1px, 而且颜色有些浅,不着急, 我们再来画一个线段。
-
从坐标 ( 100, 100 ) 绘制到坐标 ( 300, 100 )
-
线段长度为 200px
-
线段宽度为 2px
-
线段颜色为 '#000' ( 黑色 )
这个时候问题就出现了,两次画出来的线段, 一次设置 1px 一次设置 2px,感觉上 线宽度 一样,两次画出来的线段, 两次都是设置为 '#000' 的颜色,但是感觉上颜色不太一样。
这是因为浏览器在描述内容的时候, 最小的描述单位是 1px,我们来模拟一下浏览器绘制的内容,假设这是我们浏览器描述的画布中的像素点。
我们来做一个坐标的标记
现在呢不关注线的长度和坐标, 我们就画一个宽度为 1px 的线段
我们来剖析一下问题,因为在描绘这个线段的时候, 会把线段的最中心点放在这个像素点位上,也就是说, 在描述线宽的时候, 实际上会从 0.5px 的位置绘制到 1.5px 的位置,合计描述宽度为 1px,但是浏览器的最小描述为 1px,这里说的不是最小宽度为 1px, 是浏览器不能在非整数像素开始描述,也就是说浏览器没办法从 0.5 开始绘制, 也没有办法绘制到 1.5 停止,那么就只能是从 0 开始绘制到 2,所以线宽就会变成 2px 了。
因为本身一个像素的黑色被强制拉伸到两个像素宽度, 所以颜色就会变浅,就像我们一杯墨水, 倒在一个杯子里面就是黑色,但是到在一个杯子里面的时候, 又倒进去一杯水, 颜色就会变浅,实际描绘出来的样子。
这就变成了我们刚才看到的样子,所以, 我们在进行 canvas 绘制内容的时候, 涉及到线段的时候,我们一般不会把线段宽度设置成奇数, 一般都是偶数的。
canvas绘制平行线
刚才我们绘制了线段, 接下来我们来绘制一个平行线, 也就是两个线段,小伙伴: " 一个简单的效果, 想到就搞 "
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 开始绘制第二个线段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
没有问题, 效果实现了。
接下来, 咱们稍微增加一下需求,第一个线段线宽 2px, 黑色,第二个线段线宽 10px, 红色。这也简单啊, 稍微修改一下
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 开始绘制第二个线段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 10
ctx.strokeStyle = 'red'
ctx.stroke()
这是什么鬼, 为什么两个线段都变了, 不是应该只改变一个吗 ?
这是因为我们并没有告诉他这是两个不一样的线段,所以在设置线段样式的时候, 会默认按照最后一次设置的样式来绘制所有的线段,我们要想让第一个线段绘制完毕以后, 和第二个没有关系,我们需要告诉画布, 我的这个线段结束了, 后面的不要和我扯上关系.
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 结束之前的绘制内容
// 语法: 工具箱.beginPath()
ctx.beginPath()
// 4.. 开始绘制第二个线段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 10
ctx.strokeStyle = 'red'
ctx.stroke()
这样才是我们的需求
canvas绘制三角形
画完了线段, 咱们就来画一个简单的图形, 画一个三角形,其实就是由三个线段组成, 用三个线段围成一个封闭图形即可
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 三角形第一个点
ctx.moveTo(100, 100)
// 三角形第二个点
ctx.lineTo(200, 100)
// 三角形第三个点
ctx.lineTo(200, 200)
// 回到第一个点
ctx.lineTo(100, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
// 描边
ctx.stroke()
看似没啥问题, 一个三角形就出来了,但是我们仔细观察一下三角形的第一个角。
因为这是两个线段, 只是画到了一个点, 不可能重叠出一个 尖儿~~,这个时候, 我们就不能这样绘制三角形了,当我们要绘制闭合图形的时候,我们不要手动绘制最后一个路径, 而是描述出形状,通过 canvas 让它自动闭合。
首先, 我们绘制出形状, 不要闭合最终路径
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 三角形第一个点
ctx.moveTo(100, 100)
// 三角形第二个点
ctx.lineTo(200, 100)
// 三角形第三个点
ctx.lineTo(200, 200)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
// 描边
ctx.stroke()
接下来, 让 canvas 来帮我们闭合这个封闭图形
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineTo(200, 200)
// 自动闭合图形
// 语法: 工具箱.closePath()
ctx.closePath()
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
// 描边
ctx.stroke()
这个时候, 我们发现一个正常的三角形就出现了 注意 : 闭合路径,closePath() 这个方法,是从当前坐标点, 直接用线段的方式连接到 modeTo() 的位置,也就是从当前坐标点直接连接到开始坐标点。
canvas线段两端的样式
canvas 中, 是可以设置线段两端的样子的,我们先来画三个平行线
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
ctx.stroke()
ctx.beginPath()
// 第二个线段
ctx.moveTo(100, 150)
ctx.lineTo(200, 150)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
ctx.stroke()
ctx.beginPath()
// 第三个线段
ctx.moveTo(100, 200)
ctx.lineTo(200, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
ctx.stroke()
ctx.beginPath()
接下来, 我们开始设置两端样式
- 语法: 工具箱.lineCap = '值'
- 值 : => butt 无 => round 圆 => square 方
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
// 两端样式设置为 butt
ctx.lineCap = 'butt'
ctx.stroke()
ctx.beginPath()
// 第二个线段
ctx.moveTo(100, 150)
ctx.lineTo(200, 150)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
// 两端样式设置为 round
ctx.lineCap = 'round'
ctx.stroke()
ctx.beginPath()
// 第三个线段
ctx.moveTo(100, 200)
ctx.lineTo(200, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
// 两端样式设置为 'square'
ctx.lineCap = 'square'
ctx.stroke()
ctx.beginPath()
square 和 round 会让线段稍稍变长,线段端点样式的颜色会和线段颜色保持一致
canvas线段拐点的样式
canvas 在绘制线段拐角的时候, 会自动进行闭合拐角,我们也可以通过设置, 来设置一下拐角的样子,先来绘制三个带有拐角的线段。
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 150)
ctx.lineTo(100, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
ctx.stroke()
ctx.beginPath()
// 第二个线段
ctx.moveTo(200, 100)
ctx.lineTo(300, 150)
ctx.lineTo(200, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
ctx.stroke()
ctx.beginPath()
// 第三个线段
ctx.moveTo(300, 100)
ctx.lineTo(400, 150)
ctx.lineTo(300, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
ctx.stroke()
ctx.beginPath()
canvas 对于线段拐点默认的样式就是尖角拐点,我们可以进行一些设置来改变
-
语法: 工具箱.lineJoin = '值'
-
值:
=> miter 默认尖角拐点
=> round 创建圆角拐点
=> bevel 创建斜角拐点
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 150)
ctx.lineTo(100, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
// 默认拐点
ctx.lineJoin = 'miter'
ctx.stroke()
ctx.beginPath()
// 第二个线段
ctx.moveTo(200, 100)
ctx.lineTo(300, 150)
ctx.lineTo(200, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
// 圆角拐点
ctx.lineJoin = 'round'
ctx.stroke()
ctx.beginPath()
// 第三个线段
ctx.moveTo(300, 100)
ctx.lineTo(400, 150)
ctx.lineTo(300, 200)
ctx.lineWidth = 10
ctx.strokeStyle = '#000'
// 斜角拐点
ctx.lineJoin = 'bevel'
ctx.stroke()
ctx.beginPath()
canvas填充
在 canvas 中, 一旦你画出封闭图形以后,我们不光可以描边, 也可以进行填充, 也就是填满颜色,先来画一个矩形吧。
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineTo(200, 200)
ctx.lineTo(100, 200)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.closePath()
ctx.stroke()
我们这里用的是描边( 工具箱.stroke() ),是按照痕迹把路线描绘下来,在 canvas 内, 除了描边, 还有一个叫做填充,咱们再来一个矩形, 这次我们不进行描边, 而是进行填充。
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineTo(200, 200)
ctx.lineTo(100, 200)
// 3. 填充
// 语法: 工具箱.fill()
ctx.fill()
这样, 就会按照我们绘制的路线, 以填充的形式出现一个封闭图形
注意 : 填充的时候可以不进行图形闭合, 会自动闭合图形以后进行填充,填充的时候也可以设置填充颜色的设置。
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineTo(200, 200)
ctx.lineTo(100, 200)
// 3. 填充
// 3-1. 填充颜色设置
// 语法: 工具箱.fillStyle = '值'
ctx.fillStyle = 'skyblue'
// 3-2. 填充
ctx.fill()
填充是可以和描边一起使用的
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制第一个线段
// 第一个线段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineTo(200, 200)
ctx.lineTo(100, 200)
// 自动闭合图形
ctx.closePath()
// 3. 描边
// 3-1. 设置描边样式
ctx.lineWidth = 4
ctx.strokeStyle = 'orange'
// 3-2. 描边
ctx.stroke()
// 4. 填充
// 4-1. 填充颜色设置
ctx.fillStyle = 'skyblue'
// 4-2. 填充
ctx.fill()
canvas的填充规则
我们发现, 到现在为止, canvas 的绘制, 描边, 填充都很简单,但是接下来的内容可能稍微复杂一些了,我们要说一下 canvas 的填充规则,我们管 canvas 的填充规则叫做 非零填充
例子 一下子可能说不明白, 我们先来看一个例子,绘制一个 "回" 形
注意一个细节 : 我们绘制的过程,里面的小正方形我们会按照 顺时针 的方向绘制,外面的大正方形我们也会按照 顺时针 的方向绘制。
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制里面的小正方形
ctx.moveTo(200, 100)
ctx.lineTo(300, 100)
ctx.lineTo(300, 200)
ctx.lineTo(200, 200)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 开始绘制外面的大正方形
ctx.moveTo(150, 50)
ctx.lineTo(350, 50)
ctx.lineTo(350, 250)
ctx.lineTo(150, 250)
ctx.stroke()
两个正方形都是这个方向绘制的, 我们接下来把描边的线去掉,我们来进行一下填充看看。
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制里面的小正方形
ctx.moveTo(200, 100)
ctx.lineTo(300, 100)
ctx.lineTo(300, 200)
ctx.lineTo(200, 200)
// ctx.lineWidth = 2
// ctx.strokeStyle = '#000'
// ctx.stroke()
// 3. 开始绘制外面的大正方形
ctx.moveTo(150, 50)
ctx.lineTo(350, 50)
ctx.lineTo(350, 250)
ctx.lineTo(150, 250)
// ctx.stroke()
// 4. 填充
ctx.fill()
我们发现, 两个都被填充了,这是因为, 在填充的时候, 就是会一次性把所有的内容都会填充好
注意 : 和是否闭合路径 ( 工具箱.closePath() ) 没有关系,和里外正方形的绘制先后顺序没有关系。那是怎么回事呢 ?
例子 先不管是怎么回事, 我们再来看一个例子,还是绘制一个 "回" 形 注意一个细节 :
我们绘制的过程,里面的小正方形我们会按照 逆时针 的方向绘制,外面的大正方形我们也会按照 顺时针 的方向绘制
// 0. 获取到页面上的 canvas 标签元素节点
const canvasEle = document.querySelector('#canvas')
// 1. 获取当前这个画布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 开始绘制里面的小正方形
ctx.moveTo(200, 100)
ctx.lineTo(200, 200)
ctx.lineTo(300, 200)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 开始绘制外面的大正方形
ctx.moveTo(150, 50)
ctx.lineTo(350, 50)
ctx.lineTo(350, 250)
ctx.lineTo(150, 250)
ctx.stroke()
这回两个矩形绘制的时候, 方向不一样了,我们再来填充一次试试看
我们会发现, 和刚才填充出来的结果不一样了,这又是怎么回事呢 ?难道和顺时针逆时针有关系吗 ?
非零填充 其实我们的填充和顺时针逆时针有关系, 但是不是简单的顺逆时针的问题 概念 : 从任何一个区域向画布最外层移动,按照经历最短边计算,其中经历的顺时针的边记录为 +1,经历逆时针的边记录为 -1,只要最终总和不为 零, 那么该区域填充,如果最终总和为 零, 那么该区域不填充,听起来很麻烦, 咱们来画布上看一下效果就好了,这次我们绘制一个稍微复杂一些的图形。
这是两个矩形对接在一起, 一个是顺时针绘制, 一个是逆时针绘制,我们来分析一下看看,首先, 最左侧封闭图形。
如果走最短的路线出来的话, 会经历一条顺时针的边,记录为 +1,最终为 +1,所以该区域填充,然后, 最右侧封闭图形。
经历最短路线出来的话, 会经历一条逆时针的边,记录为 -1,最终为 -1,所以该区域填充,最后, 中间的封闭图形。
经历最短路线出来的会, 必然会经历一条顺时针的边和一条逆时针的边,顺时针记录为 +1,逆时针记录为 -1,最终为 0,所以该区域不填充,此时我们对当前图形进行填充后观察。
这就是最后填充好的样子
转载自:https://juejin.cn/post/7251792725070757947