目前各种数据来看,前端未来在 数据可视化
和 AI
这两个领域会比较香,而 Canvas
是 数据可视化 在前端方面的基础技术。所以给大家唠唠Canvas这个魔幻工具。
Canvas 介绍
Canvas
中文名叫 “画布”,是HTML5
新增的一个标签。Canvas
允许开发者通过JS
在这个标签上 绘制各种图案 。Canvas
拥有多种绘制路径、矩形、圆形、字符以及图片的方法。Canvas
在某些情况下可以 “代替” 图片。Canvas
可用于动画、游戏、数据可视化、图片编辑器、实时视频处理等领域。
SVG 的区别
Canvas | SVG |
---|---|
用JS动态生成元素(一个HTML元素) | 用XML描述元素(类似HTML元素那样,可用多个元素来描述一个图形) |
位图(受屏幕分辨率影响) | 矢量图(不受屏幕分辨率影响) |
不支持事件 | 支持事件 |
数据发生变化需要重绘 | 不需要重绘 |
如果感觉上面的描述难懂,可以打开 AntV
旗下的图形编辑引擎做对比。G6 是使用 canvas
开发的,X6 是使用 svg
开发的。
个人建议是:如果要展示的数据量比较大,比如一条数据就是一个元素节点,那使用 canvas
会比较合适;如果用户操作的交互比较多,而且对清晰度有要求(矢量图),那么使用 svg
会比较合适。
入门
画 直线
基本步骤:
- 在
HTML
中创建canvas
元素 - 通过
js
获取canvas
标签 - 从
canvas
标签中获取到绘图工具 - 通过绘图工具,在
canvas
标签上绘制图形
<!-- 1、创建 canvas 元素 -->
<canvasid="c"width="300"height="200"style="border: 1px solid #ccc;"
></canvas><script>// 2、获取 canvas 对象const cnv = document.getElementById('c')// 3、获取 canvas 上下文环境对象const cxt = cnv.getContext('2d')// 4、绘制图形cxt.moveTo(100, 100) // 起点坐标 (x, y)cxt.lineTo(200, 100) // 终点坐标 (x, y)cxt.stroke() // 将起点和终点连接起来
</script>
1、默认宽高
canvas
有 默认的 宽度(300px) 和 高度(150px)
如果不在 canvas
上设置宽高,那 canvas
元素的默认宽度是300px,默认高度是150px。
2、设置 canvas 宽高
canvas
元素提供了 width
和 height
两个属性,可设置它的宽高。
需要注意的是,这两个属性只需传入数值,不需要传入单位(比如 px
等)。
<canvas width="600" height="400"></canvas>
3、不能通过 CSS 设置画布的宽高
使用 css
设置 canvas
的宽高,会出现 内容被拉伸 的不良后果!
<style>#c {width: 400px;height: 400px;border: 1px solid #ccc;}
</style><canvas id="c"></canvas><script>// 1、获取canvas对象const cnv = document.getElementById('c')// 2、获取canvas上下文环境对象const cxt = cnv.getContext('2d')// 3、绘制图形cxt.moveTo(100, 100) // 起点cxt.lineTo(200, 100) // 终点cxt.stroke() // 将起点和终点连接起来console.log(cnv.width) // 获取 canvas 的宽度,输出:300console.log(cnv.height) // 获取 canvas 的高度,输出:150
</script>
canvas
的默认宽度是300px,默认高度是150px。
- 如果使用
css
修改canvas
的宽高(比如本例变成 400px * 400px),那宽度就由 300px 拉伸到 400px,高度由 150px 拉伸到 400px。 - 使用
js
获取canvas
的宽高,此时返回的是canvas
的默认值。
4、线条 默认宽度 和 颜色
线条的默认宽度是 1px
,默认颜色是 黑色
。
但由于默认情况下 canvas
会将线条的中心点 和 像素的底部 对齐,因此就会导致显示效果是 2px
和非纯黑色问题。
5、IE兼容性高
暂时只有 IE 9
以上才支持 canvas
。但好消息是 IE
已经凉了。
如需兼容 IE 7 和 8
,可以使用 ExplorerCanvas 。但即使是使用了 ExplorerCanvas
仍然会有所限制,比如无法使用 fillText()
方法等。
基础图形
坐标系
Canvas
使用的是 W3C 坐标系 ,也就是遵循我们屏幕、报纸的 阅读习惯
,从上往下,从左往右。
W3C 坐标系 和 数学直角坐标系 的 X轴
是一样的,只是 Y轴
的反向相反。
W3C 坐标系 的 Y轴
正方向向下。
直线
一条直线
最简单的起步方式是画一条直线。这里所说的 “直线” 是几何学里的 “线段” 的意思。
需要用到这3个方法:
moveTo(x1, y1)
:起点坐标 (x, y)lineTo(x2, y2)
:下一个点的坐标 (x, y)stroke()
:将所有坐标用一条线连起来
起步阶段可以先这样理解。
<canvas id="c" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 绘制直线cxt.moveTo(50, 100) // 起点坐标cxt.lineTo(200, 50) // 下一个点的坐标cxt.stroke() // 将上面的坐标用一条线连接起来
</script>
上面的代码所呈现的效果,可以看下图解释(手不太聪明,画得不是很标准,希望能看懂)
多条直线
如需画多条直线,可以用会上面那几个方法。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(20, 100)cxt.lineTo(200, 100)cxt.stroke()cxt.moveTo(20, 120.5)cxt.lineTo(200, 120.5)cxt.stroke()
</script>
可以看到 两条线的 粗细不一样。
这是因为 默认情况下 canvas
会将线条的中心点和像素的底部对齐,所以会导致显示效果是 2px
和非纯黑色问题。
线的中心点会和画布像素点的底部对齐,所以会线中间是黑色的,但由于一个像素就不能再切割了,所以会有半个像素被染色,就变成了浅灰色。
所以如果你设置的 Y轴
值是一个整数,就会出现上面那种情况。
设置样式
lineWidth
:线的粗细strokeStyle
:线的颜色lineCap
:线帽。
默认:butt
; 圆形:round
; 方形:square
<canvas id="c" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 绘制直线cxt.moveTo(50, 50)cxt.lineTo(200, 50)// 修改直线的宽度cxt.lineWidth = 20// 修改直线的颜色cxt.strokeStyle = 'pink'// 修改直线两端样式cxt.lineCap = 'round' // 默认: butt; 圆形: round; 方形: squarecxt.stroke()
</script>
新开路径
开辟新路径的方法:
beginPath()
在绘制多条线段的同时,还要设置线段样式,通常需要开辟新路径。
要不然样式之间会相互污染。
比如这样:
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 第一条线cxt.moveTo(20, 100)cxt.lineTo(200, 100)cxt.lineWidth = 10cxt.strokeStyle = 'pink'cxt.stroke()// 第二条线cxt.moveTo(20, 120.5)cxt.lineTo(200, 120.5)cxt.stroke()
</script>
如果不想相互污染,需要做2件事:
- 使用
beginPath()
方法,重新开一个路径 - 设置新线段的样式(必须项)
如果上面2步却了其中1步都会有影响。
只使用 beginPath()
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 第一条线cxt.moveTo(20, 100)cxt.lineTo(200, 100)cxt.lineWidth = 10cxt.strokeStyle = 'pink'cxt.stroke()// 第二条线cxt.beginPath() // 重新开启一个路径cxt.moveTo(20, 120.5)cxt.lineTo(200, 120.5)cxt.stroke()
</script>
第一条线的样式会影响之后的线。
但如果使用了 beginPath()
,后面的线段不会影响前面的线段。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 第一条线cxt.moveTo(20, 100)cxt.lineTo(200, 100)cxt.stroke()// 第二条线cxt.beginPath() // 重新开启一个路径cxt.moveTo(20, 120.5)cxt.lineTo(200, 120.5)cxt.lineWidth = 4cxt.strokeStyle = 'red'cxt.stroke()
</script>
设置新线段的样式,没使用 beginPath()
的情况
这个情况会反过来,后面的线能影响前面的线。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 第一条线cxt.moveTo(20, 100)cxt.lineTo(200, 100)cxt.lineWidth = 10cxt.strokeStyle = 'pink'cxt.stroke()// 第二条线cxt.moveTo(20, 120.5)cxt.lineTo(200, 120.5)cxt.lineWidth = 4cxt.strokeStyle = 'red'cxt.stroke()
</script>
正确的做法
在设置 beginPath()
的同时,也各自设置样式。这样就能做到相互不影响了。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(20, 100)cxt.lineTo(200, 100)cxt.lineWidth = 10cxt.strokeStyle = 'pink'cxt.stroke()cxt.beginPath() // 重新开启一个路径cxt.moveTo(20, 120.5)cxt.lineTo(200, 120.5)cxt.lineWidth = 4cxt.strokeStyle = 'red'cxt.stroke()
</script>
折线
和 直线 差不多,都是使用 moveTo()
、lineTo()
和 stroke()
方法可以绘制折线。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(50, 200)cxt.lineTo(100, 50)cxt.lineTo(200, 200)cxt.lineTo(250, 50)cxt.stroke()
</script>
画这种折线,最好在草稿纸上画一个坐标系,自己计算并描绘一下每个点大概在什么什么位置,最后在 canvas
中看看效果。
矩形
根据前面的基础,我们可以 使用线段来描绘矩形,但 canvas
也提供了 rect()
等方法可以直接生成矩形。
使用线段描绘矩形
可以使用前面画线段的方法来绘制矩形
canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 绘制矩形cxt.moveTo(50, 50)cxt.lineTo(200, 50)cxt.lineTo(200, 120)cxt.lineTo(50, 120)cxt.lineTo(50, 50) // 需要闭合,又或者使用 closePath() 方法进行闭合,推荐使用 closePath()cxt.stroke()
</script>
使用 strokeRect()
描边矩形
strokeStyle
:设置描边的属性(颜色、渐变、图案)strokeRect(x, y, width, height)
:描边矩形(x和y是矩形左上角起点;width 和 height 是矩形的宽高)strokeStyle
必须写在strokeRect()
前面,不然样式不生效。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// strokeStyle 属性// strokeRect(x, y, width, height) 方法cxt.strokeStyle = 'pink'cxt.strokeRect(50, 50, 200, 100)
</script>
上面的代码可以这样理解
使用 fillRect()
填充矩形
fillRect()
和 strokeRect()
方法差不多,但 fillRect()
的作用是填充。
需要注意的是,fillStyle
必须写在 fillRect()
之前,不然样式不生效。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// fillStyle 属性// fillRect(x, y, width, height) 方法cxt.fillStyle = 'pink'cxt.fillRect(50, 50, 200, 100) // fillRect(x, y, width, height)
</script>
同时使用 strokeRect()
和 fillRect()
同时使用 strokeRect()
和 fillRect()
会产生描边和填充的效果
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.strokeStyle = 'red'cxt.strokeRect(50, 50, 200, 100) // strokeRect(x, y, width, height)cxt.fillStyle = 'yellow'cxt.fillRect(50, 50, 200, 100) // fillRect(x, y, width, height)
</script>
使用 rect()
生成矩形
rect()
和 fillRect() 、strokeRect()
的用法差不多,唯一的区别是:
strokeRect()
和 fillRect()
这两个方法调用后会立即绘制;rect()
方法被调用后,不会立刻绘制矩形,而是需要调用 stroke()
或 fill()
辅助渲染。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.strokeStyle = 'red'cxt.fillStyle = 'pink'cxt.rect(50, 50, 200, 100) // rect(x, y, width, height)cxt.stroke()cxt.fill()
</script>
等价公式:
cxt.strokeStyle = 'red',
cxt.rect(50, 50, 200, 100)
cxt.stroke()// 等价于
cxt.strokeStyle = 'red'
cxt.strokerect(50, 50, 200, 100)// -----------------------------cxt.fillStyle = 'hotpink'
cxt.rect(50, 50, 200, 100)
cxt.fill()// 等价于
cxt.fillStyle = 'yellowgreen'
cxt.fillRect(50, 50, 200, 100)
使用 clearRect()
清空矩形
使用 clearRect()
方法可以清空指定区域。
clearRect(x, y, width, height)
其语法和创建 cxt.rect()
差不多。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.fillStyle = 'pink' // 设置填充颜色cxt.fillRect(50, 50, 200, 200) // 填充矩形cxt.clearRect(60, 60, 180, 90) // 清空矩形
</script>
清空画布
canvas
画布元素是矩形,所以可以通过下面的代码把整个画布清空掉。
// 省略部分代码cxt.clearRect(0, 0, cnv.width, cnv.height)
要清空的区域:从画布左上角开始,直到画布的宽和画布的高为止。
多边形
Canvas
要画多边形,需要使用 moveTo()
、 lineTo()
和 closePath()
。
三角形
虽然三角形是常见图形,但 canvas
并没有提供类似 rect()
的方法来绘制三角形。
需要确定三角形3个点的坐标位置,然后使用 stroke()
或者 fill()
方法生成三角形。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(50, 50)cxt.lineTo(200, 50)cxt.lineTo(200, 200)// 注意点:如果使用 lineTo 闭合图形,是不能很好闭合拐角位的。cxt.lineTo(50, 50) // 闭合cxt.stroke()</script>
注意,默认情况下不会自动从最后一个点连接到起点。最后一步需要设置一下 cxt.lineTo(50, 50)
,让它与 cxt.moveTo(50, 50)
一样。这样可以让路径回到起点,形成一个闭合效果。
但这样做其实是有点问题的,而且也比较麻烦,要记住起始点坐标。
上面的闭合操作,如果遇到设置了 lineWidth
或者 lineJoin
就会有问题,比如:
// 省略部分代码
cxt.lineWidth = 20
当线段变粗后,起始点和结束点的链接处,拐角就出现“不正常”现象。
如果需要真正闭合,可以使用 closePath()
方法。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(50, 50)cxt.lineTo(200, 50)cxt.lineTo(200, 200)// 手动闭合cxt.closePath()cxt.lineJoin = 'miter' // 线条连接的样式。miter: 默认; bevel: 斜面; round: 圆角cxt.lineWidth = 20cxt.stroke()
</script>
使用 cxt.closePath()
可以自动将终点和起始点连接起来,此时看上去就正常多了。
菱形
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(150, 50)cxt.lineTo(250, 100)cxt.lineTo(150, 150)cxt.lineTo(50, 100)cxt.closePath()cxt.stroke()
</script>
要绘制 直线类型 的图形,在草稿纸上标记出起始点和每个拐角的点,然后再连线即可。相对曲线图形来说,直线图形是比较容易的。
圆形
绘制圆形的方法是 arc()
。
语法:
arc(x, y, r, sAngle, eAngle,counterclockwise)
x
和y
: 圆心坐标r
: 半径sAngle
: 开始角度eAngle
: 结束角度counterclockwise
: 绘制方向(true: 逆时针; false: 顺时针),默认 false
开始角度和结束角度,都是以弧度为单位。例如 180°就写成Math.PI
,360°写成Math.PI * 2
,以此类推。
在实际开发中,为了让自己或者别的开发者更容易看懂弧度的数值,1°应该写成 Math.PI / 180
。
- 100°:
100 * Math.PI / 180
- 110°:
110 * Math.PI / 180
- 241°:
241 * Math.PI / 180
注意:绘制圆形之前,必须先调用 beginPath()
方法!!!
在绘制完成之后,还需要调用 closePath()
方法!!!
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/440ffb9c8d2e4fc391ebdd0026b86145.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.beginPath()cxt.arc(150, 150, 80, 0, 360 * Math.PI / 180)cxt.closePath()cxt.stroke()
</script>
半圆
如果使用 arc()
方法画圆时,没做到刚好绕完一周(360°)就直接闭合路径,就会出现半圆的状态。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.beginPath()cxt.arc(150, 150, 100, 0, 180 * Math.PI / 180) // 顺时针cxt.closePath()cxt.stroke()
</script>
上面的代码中,cxt.arc
最后一个参数没传,默认是 false
,所以是顺时针绘制。
如果希望半圆的弧面在上方,可以将 cxt.arc
最后一个参数设置成 true
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.beginPath()cxt.arc(150, 150, 100, 0, 180 * Math.PI / 180, true)cxt.closePath()cxt.stroke()
</script>
弧线
使用 arc()
方法画半圆时,如果最后不调用 closePath()
方法,就不会出现闭合路径。也就是说,那是一条弧线。
在 canvas
中,画弧线有2中方法:arc()
和 arcTo()
。
arc() 画弧线
如果想画一条 0° ~ 30°
的弧线,可以这样写
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.beginPath()cxt.arc(150, 150, 100, 0, 30 * Math.PI / 180)cxt.stroke()
</script>
原理如下图所示,红线代表画出来的那条弧线。
arcTo() 画弧线
arcTo()
的使用方法会更加复杂,如果初学看不太懂的话可以先跳过,看完后面的再回来补补。
语法:
arcTo(cx, cy, x2, y2, radius)
cx
: 两切线交点的横坐标cy
: 两切线交点的纵坐标x2
: 结束点的横坐标y2
: 结束点的纵坐标radius
: 半径
其中,(cx, cy)
也叫控制点,(x2, y2)
也叫结束点。
是不是有点奇怪,为什么没有 x1
和 y1
?
(x1, y1)
是开始点,通常是由 moveTo()
或者 lineTo()
提供。
arcTo()
方法利用 开始点、控制点和结束点形成的夹角,绘制一段与夹角的两边相切并且半径为 radius
的圆弧。
举个例子
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.moveTo(40, 40)cxt.arcTo(120, 40, 120, 120, 80)cxt.stroke()
</script>
基础样式
描边 stroke()
前面讲到过,这里不再多说。
线条宽度 lineWidth
lineWidth
默认值是 1
,默认单位是 px
。
语法:
lineWidth = 线宽
线条颜色 strokeStyle
使用 strokeStyle
可以设置线条颜色
语法:
strokeStyle = 颜色值
线帽 lineCap
线帽指的是线段的开始和结尾处的样式,使用 lineCap
可以设置
语法:
lineCap = '属性值'
属性值包括:
butt
: 默认值,无线帽square
: 方形线帽round
: 圆形线帽
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 设置线宽,方便演示cxt.lineWidth = 16// 默认线帽 buttcxt.beginPath()cxt.moveTo(50, 60)cxt.lineTo(250, 60)cxt.stroke()// 方形线帽 squarecxt.beginPath()cxt.lineCap = 'square'cxt.moveTo(50, 150)cxt.lineTo(250, 150)cxt.stroke()// 圆形线帽 roundcxt.beginPath()cxt.lineCap = 'round'cxt.moveTo(50, 250)cxt.lineTo(250, 250)cxt.stroke()
</script>
使用 square
和 round
的话,会使线条变得稍微长一点点,这是给线条增加线帽的部分,这个长度在日常开发中需要注意。
线帽 只对线条的 开始 和 结尾 处产生作用,对 拐角 不会产生任何作用。
拐角样式 lineJoin
如果需要设置拐角样式,可以使用 lineJoin
。
语法:
lineJoin = '属性值'
属性值包括:
miter
: 默认值,尖角round
: 圆角bevel
: 斜角
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.lineWidth = 20// 默认,尖角cxt.moveTo(50, 40)cxt.lineTo(200, 40)cxt.lineTo(200, 90)cxt.stroke()// 斜角 bevelcxt.beginPath()cxt.moveTo(50, 140)cxt.lineTo(200, 140)cxt.lineTo(200, 190)cxt.lineJoin = 'bevel'cxt.stroke()// 圆角 roundcxt.beginPath()cxt.moveTo(50, 240)cxt.lineTo(200, 240)cxt.lineTo(200, 290)cxt.lineJoin = 'round'cxt.stroke()
</script>
虚线 setLineDash()
使用 setLineDash()
方法可以将描边设置成虚线。
语法:
setLineDash([])
需要传入一个数组,且元素是数值型。
虚线分3种情况
- 只传1个值
- 有2个值
- 有3个以上的值
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.lineWidth = 20cxt.strokeStyle = 'pink'cxt.moveTo(50, 50)cxt.lineTo(200, 50)cxt.setLineDash([10]) // 只传1个参数,实线与空白都是 10pxcxt.stroke()cxt.beginPath()cxt.moveTo(50, 100)cxt.lineTo(200, 100)cxt.setLineDash([10, 20]) // 2个参数,此时,实线是 10px, 空白 20pxcxt.stroke()cxt.beginPath()cxt.moveTo(50, 150)cxt.lineTo(200, 150)cxt.setLineDash([10, 20, 5]) // 传3个以上的参数,此例:10px实线,20px空白,5px实线,10px空白,20px实线,5px空白 ……cxt.stroke()
</script>
此外,还可以始终 cxt.getLineDash()
获取虚线不重复的距离;
用 cxt.lineDashOffset
设置虚线的偏移位。
填充
使用 fill()
可以填充图形,根据前面的例子应该掌握了如何使用 fill()
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.fillStyle = 'pink'cxt.rect(50, 50, 200, 100)cxt.fill()
</script>
可以使用 fillStyle
设置填充颜色,默认是黑色。
非零环绕填充
在使用 fill()
方法填充时,需要注意一个规则:非零环绕填充。
这样说有点复杂,先看看例子
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 外层矩形cxt.moveTo(50, 50)cxt.lineTo(250, 50)cxt.lineTo(250, 250)cxt.lineTo(50, 250)cxt.closePath()// 内层矩形cxt.moveTo(200, 100)cxt.lineTo(100, 100)cxt.lineTo(100, 200)cxt.lineTo(200, 200)cxt.closePath()cxt.fill()
</script>
请看看上面的代码,我画了2个矩形,它们都没有用 beginPath()
方法开辟新路径。
内层矩形是逆时针绘制的,所以内层的值是 -1
,它又经过外层矩形,而外层矩形是顺时针绘制,所以经过外层时值 +1
,最终内层的值为 0
,所以不会被填充。
文本
Canvas
提供了一些操作文本的方法。
为了方便演示,先了解一下在 Canvas
中如何给本文设置样式。
样式 font
和 CSS
设置 font
差不多,Canvas
也可以通过 font
设置样式。
语法:
cxt.font = 'font-style font-variant font-weight font-size/line-height font-family'
如果需要设置字号 font-size
,需要同时设置 font-family
。
cxt.font = '30px 宋体'
描边 strokeText()
使用 strokeText()
方法进行文本描边
语法:
strokeText(text, x, y, maxWidth)
text
: 字符串,要绘制的内容x
: 横坐标,文本左边要对齐的坐标(默认左对齐)y
: 纵坐标,文本底边要对齐的坐标maxWidth
: 可选参数,表示文本渲染的最大宽度(px),如果文本超出maxWidth
设置的值,文本会被压缩。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.font = '60px Arial' // 将字号设置成 60px,方便观察cxt.strokeText('雷猴', 30, 90)
</script>
设置描边颜色 strokeStyle
使用 strokeStyle
设置描边颜色。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.font = '60px Arial' // 将字号设置成 60px,方便观察cxt.strokeStyle = 'pink' // 设置文本描边颜色cxt.strokeText('雷猴', 30, 90)
</script>
填充 fillText
使用 fillText()
可填充文本。
语法和 strokeText()
一样。
fillText(text, x, y, maxWidth)
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.font = '60px Arial'cxt.fillText('雷猴', 30, 90)
</script>
设置填充颜色 fillStyle
使用 fillStyle
可以设置文本填充颜色。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')cxt.font = '60px Arial'cxt.fillStyle = 'pink'cxt.fillText('雷猴', 30, 90)
</script>
获取文本长度 measureText()
measureText().width
方法可以获取文本的长度,单位是 px
。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')let text = '雷猴'cxt.font = 'bold 40px Arial'cxt.fillText(text, 40, 80)console.log(cxt.measureText(text).width) // 80
</script>
水平对齐方式 textAlign
使用 textAlign
属性可以设置文字的水平对齐方式,一共有5个值可选
start
: 默认。在指定位置的横坐标开始。end
: 在指定坐标的横坐标结束。left
: 左对齐。right
: 右对齐。center
: 居中对齐。
红线是辅助参考线。
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 竖向的辅助线(参考线,在画布中间)cxt.moveTo(200, 0)cxt.lineTo(200, 400)cxt.strokeStyle = 'red'cxt.stroke()cxt.font = '30px Arial'// 横坐标开始位对齐cxt.textAlign = 'start' // 默认值,cxt.fillText('雷猴 start', 200, 40)// 横坐标结束位对齐cxt.textAlign = 'end' // 结束对齐cxt.fillText('雷猴 end', 200, 100)// 左对齐cxt.textAlign = 'left' // 左对齐cxt.fillText('雷猴 left', 200, 160)// 右对齐cxt.textAlign = 'right' // 右对齐cxt.fillText('雷猴 right', 200, 220)// 居中对齐cxt.textAlign = 'center' // 右对齐cxt.fillText('雷猴 center', 200, 280)
</script>
从上面的例子看,start
和 left
的效果好像是一样的,end
和 right
也好像是一样的。
在大多数情况下,它们的确一样。但在某些国家或者某些场合,阅读文字的习惯是 从右往左 时,start
就和 right
一样了,end
和 left
也一样。这是需要注意的地方。
垂直对齐方式 textBaseline
使用 textBaseline
属性可以设置文字的垂直对齐方式。
在使用 textBaseline
前,需要自行了解 css
的文本基线。
用一张网图解释一下基线
textBaseline
可选属性:
alphabetic
: 默认。文本基线是普通的字母基线。top
: 文本基线是em
方框的顶端。bottom
: 文本基线是em
方框的底端。middle
: 文本基线是em
方框的正中。hanging
: 文本基线是悬挂基线。
红线是辅助参考线。
<canvas id="c" width="800" height="300" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 横向的辅助线(参考线,在画布中间)cxt.moveTo(0, 150)cxt.lineTo(800, 150)cxt.strokeStyle = 'red'cxt.stroke()cxt.font = '20px Arial'// 默认 alphabeticcxt.textBaseline = 'alphabetic'cxt.fillText('雷猴 alphabetic', 10, 150)// 默认 topcxt.textBaseline = 'top'cxt.fillText('雷猴 top', 200, 150)// 默认 bottomcxt.textBaseline = 'bottom'cxt.fillText('雷猴 bottom', 320, 150)// 默认 middlecxt.textBaseline = 'middle'cxt.fillText('雷猴 middle', 480, 150)// 默认 hangingcxt.textBaseline = 'hanging'cxt.fillText('雷猴 hanging', 640, 150)
</script>
注意:在绘制文字的时候,默认是以文字的左下角作为参考点进行绘制
图片
在 Canvas
中可以使用 drawImage()
方法绘制图片。
渲染图片
渲染图片的方式有2中,一种是在JS里加载图片再渲染,另一种是把DOM里的图片拿到 canvas
里渲染。
渲染的语法:
drawImage(image, dx, dy)
image
: 要渲染的图片对象。dx
: 图片左上角的横坐标位置。dy
: 图片左上角的纵坐标位置。
JS版
在 JS
里加载图片并渲染,有以下几个步骤:
- 创建
Image
对象 - 引入图片
- 等待图片加载完成
- 使用
drawImage()
方法渲染图片
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')// 1 创建 Image 对象const image = new Image()// 2 引入图片image.src = './images/dog.jpg'// 3 等待图片加载完成image.onload = () => {// 4 使用 drawImage() 方法渲染图片cxt.drawImage(image, 30, 30)}
</script>
DOM版
<style>#dogImg {display: none;}
</style><img src="./images/dog.jpg" id="dogImg"/>
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')const image = document.getElementById('dogImg')cxt.drawImage(image, 70, 70)
</script>
因为图片是从 DOM
里获取到的,所以一般来说,只要在 window.onload
这个生命周期内使用 drawImage
都可以正常渲染图片。
本例使用了 css
的方式,把图片的 display
设置成 none
。主要担心被 <img>
影响到本例讲解。
设置图片宽高
前面的例子都是直接加载图片,图片默认的宽高是多少就加载多少。
如果需要指定图片宽高,可以在前面的基础上再添加两个参数:
drawImage(image, dx, dy, dw, dh)
image、 dx、 dy
的用法和前面一样。
dw
用来定义图片的宽度,dh
定义图片的高度。
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')const image = new Image()image.src = './images/dog.jpg'image.onload = () => {cxt.drawImage(image, 30, 30, 100, 100)}
</script>
我把图片的尺寸设为 100px * 100px,图片看上去比之前就小了很多。
截取图片 仅展示某部分
截图图片同样使用drawImage()
方法,只不过传入的参数数量比之前都多,而且顺序也有点不一样了。
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
以上参数缺一不可
image
: 图片对象sx
: 开始截取的横坐标sy
: 开始截取的纵坐标sw
: 截取的宽度sh
: 截取的高度dx
: 图片左上角的横坐标位置dy
: 图片左上角的纵坐标位置dw
: 图片宽度dh
: 图片高度
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas><script>const cnv = document.getElementById('c')const cxt = cnv.getContext('2d')const image = new Image()image.src = './images/dog.jpg'image.onload = () => {cxt.drawImage(image, 0, 0, 100, 100, 30, 30, 200, 200)}
</script>
Canvas内部元素 的 事件监听
canvas 内部 图形元素 不能像DOM元素一样方便的添加交互事件监听,因为canvas内不存在“元素”这个概念,它们仅仅是canvas绘制出来的图形。
这对于交互开发来说是一个必经障碍,想要监听图形的点击事件思路很简单,只要监听canvas元素本身的点击事件,再判断点击坐标位于哪一个图形内部,就变相实现了图形点击事件。
监听图像上 点击事件 的基本流程:
- 通过getBoundingClientRect方法获取canvas相对于可视区域的偏移量
- canvas点击事件触发时使用clientX、clientY方法获取点击位置,计算可得到点击位置在canvas中的坐标
- 清空画布
- 重绘画布,在绘制完每个图形的时候都执行一次isPointInPath方法判断点击位置是否在该图形中,决定是否要执行onClick事件
从上面可以看出,最重要的是 确定 点击坐标位于哪一个图形内部。
这里将介绍三种方法,判断坐标点是否位于某个canvas图形内部。
像素法
像素检测法的思路是,将canvas中的多个图形(如果有多个的话)分别离屏绘制,并用getImageData()方法分别获取到像素数据保存起来。当canvas元素监听到点击事件时,通过点击坐标可以直接推算出点击发生在canvas上的第几个像素,然后遍历前面保存的图形数据,看看这个像素的alpha值是不是0,如果是0说明落点不在当前图形内,否则就说明点到了这个图形。
根据点击坐标得到所点击的像素序号的方法:
像素序号 = (纵坐标-1) * canvas宽度 + 横坐标
比如在宽度为 5 的画布上点击坐标(3,3),根据上述公式得到像素序号是(3-1) * 5 + 3 = 18,如图所示:
因为canvas导出的图形数据是将每个像素以rgba的顺序存成4个数字组成的数组,所以想访问指定像素的alpha值,只要读取这个数组的第pIndex * 4 + 3个值就可以了,如果这个值不为0,说明该像素可见,也就是点击到了该图形。
这个方法是我认为思路最直接、结果最准确、而且对图形形状没有任何要求的方法,但这个方法有一个致命的局限,当图形需要在画布上移动时,要频繁的创建数据缓存才能保证检测结果准确,受到画布尺寸和图形数量的影响,getImageData()方法的性能会成为严重的瓶颈。所以如果canvas图形是静态的,这个方法非常适合,否则就不适合用这个方法了。
角度法
角度判断法的原理很容易理解,如果一个点在多边形内部,则该点与多边形所有顶点两两构成的夹角,相加应该刚好等于360°。
计算过程可以转变为以下三个步骤:
- 已知多边形顶点和已知坐标,将坐标与顶点两两组合成三点队列
- 已知三点求夹角,可以使用余玄定理
- 判断夹角之和是否360°
//计算两点距离
const getDistence = function (p1, p2) {return Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y))
};
//角度法判断点在多边形内部
const checkPointInPolyline = (point, polylinePoints) => {let totalA = 0;const A = point;for (let i = 0; i < polylinePoints.length; i++) {let B, C;if (i === polylinePoints.length - 1) {B = {x: polylinePoints[i][0],y: polylinePoints[i][1]};C = {x: polylinePoints[0][0],y: polylinePoints[0][1]};} else {B = {x: polylinePoints[i][0],y: polylinePoints[i][1]};C = {x: polylinePoints[i + 1][0],y: polylinePoints[i + 1][1]};}//计算角度const angleA = Math.acos((Math.pow(getDistence(A, C), 2) + Math.pow(getDistence(A, B), 2) - Math.pow(getDistence(B, C), 2)) / (2 * getDistence(A, C) * getDistence(A, B)))totalA += angleA}//判断角度之和return totalA === 2 * Math.PI
}
这个方法有一个局限性,就是图形必须是凸多边形。如果不是凸多边形需要先切割成凸多边形再计算,这就比较复杂了。
射线法
射线法是非常好用的方法,只要判断点与多边形一侧的交点个数为 奇数
,则点在多边形内部。
需要注意的是,只要数任何一侧的焦点个数就可以,比如左侧。
这个方法不限制多边形的类型,凸多边形、凹多边形甚至环形都可以。
const checkPointInPolyline = (point, polylinePoints) => {//射线法let leftSide = 0;const A = point;for (let i = 0; i < polylinePoints.length; i++) {let B, C;if (i === polylinePoints.length - 1) {B = {x: polylinePoints[i][0],y: polylinePoints[i][1]};C = {x: polylinePoints[0][0],y: polylinePoints[0][1]};} else {B = {x: polylinePoints[i][0],y: polylinePoints[i][1]};C = {x: polylinePoints[i + 1][0],y: polylinePoints[i + 1][1]};}//判断左侧相交let sortByY = [B.y, C.y].sort((a,b) => a-b)if (sortByY[0] < A.y && sortByY[1] > A.y){if(B.x<A.x || C.x < A.x){leftSide++}}}return leftSide % 2 === 1
}
射线法有一种特殊情况,当点在多变形的一条边上时需要特殊处理。但在实际项目中个人认为也可以不处理,因为如果用户刚好点在图形的边界上,那么程序认为他没有点到也是可以的。