起步
Canvas 是一个可以使用 JavaScript 来绘制图形的 HTML 元素。下面我们来创建一个简单的例子:
<!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>Canvas</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(10, 10, 150, 100)
</script>
</body>
</html>
在上面的代码中,我们通过document.querySelector('#canvas') 获得 canvas 元素的引用。接着,使用HTMLCanvasElement.getContext() 方法获取 canvas 元素的渲染上下文,通过它来进行图形的绘制。context.fillStyle = 'red' 用红色来填充图形。context.fillRect(10, 10, 150, 100) 绘制一个长方形,并将它的左上角放在 (10, 10),把它的大小设置成宽 150 高 100。
这里需要注意的是:
canvas有默认的高度(150px)和宽度(300px),可以通过width和height两个属性设置它的宽高。- 不能通过 CSS 来设置
canvas标签的宽高,不然内容会被拉伸。
图形
直线
绘制直线需要了解三个函数:
moveTo(x, y)起点坐标 (x, y)lineTo(x, y)下一个点的坐标 (x, y)stroke()将所有坐标连起来
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.moveTo(50, 50)
context.lineTo(250, 250)
context.lineTo(300, 400)
context.stroke()
context.moveTo(200, 100)
context.lineTo(400, 100)
context.stroke()
三角形
画三条直线拼在一起就是一个三角形了
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.moveTo(100, 50)
context.lineTo(400, 250)
context.lineTo(100, 250)
context.lineTo(100, 50)
context.stroke()
矩形
矩形就不要手动连线了,Canvas API 给提供了三种绘制矩形的方法:
-
strokeRect(x, y, width, height)绘制一个矩形的边框。 -
fillRect(x, y, width, height)绘制一个填充的矩形。 -
clearRect(x, y, width, height)清除指定矩形区域。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.strokeRect(50, 50, 200, 150)
context.fillRect(100, 150, 200, 100)
context.clearRect(100, 175, 100, 50)
圆
使用 arc(x, y, radius, startAngle, endAngle, counterclockwise) 来绘制圆或圆弧:
x和y为圆心坐标,radius为半径。startAngle开始角度,endAngle结束角度,两者单位均为弧度。counterclockwise绘制的角度,false为顺时针(默认),true为逆时针。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.arc(100, 100, 80, Math.PI * 0.4, Math.PI * 2)
context.stroke()
再画一个圆试试:
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.arc(100, 100, 80, Math.PI * 0.4, Math.PI * 2)
context.arc(250, 100, 80, 0, Math.PI * 2)
context.stroke()
这似乎不是我们想要的结果,先画的圆和后画的圆被连在了一起,其实这是因为在咱们每次新建路径的时候都需要开启和闭合路径(beginPath 和 closePath),这样不同路径之间才不会相互干扰。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
// 关闭路径后绘制
context.beginPath()
context.arc(100, 100, 60, 0, Math.PI)
context.closePath()
context.stroke()
// 关闭路径前绘制
context.beginPath()
context.arc(250, 100, 60, 0, Math.PI)
context.stroke()
context.closePath()
// 填充
context.beginPath()
context.arc(400, 100, 60, 2, Math.PI * 2)
context.closePath()
context.fill()
现在就正常了,stroke 是通过线条来绘制图形的轮廓,而 fill 是填充路径的内容区域绘制实心的图形,同时我们注意到关闭路径后用 stroke 绘制和关闭前是不一样的。
除了 arc 外,还可以通过 arcTo(x1, y1, x2, y2, radius) 来绘制圆弧:
x1和y1为控制点坐标,x2和y2为结束点的坐标。- 开始点的坐标一般为
moveTo或lineTo提供。 radius为半径。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.beginPath()
context.moveTo(100, 100)
context.lineTo(200, 100)
context.arcTo(300, 100, 300, 200, 100)
context.stroke()
context.closePath()
context.fillStyle = 'red'
context.beginPath()
context.arc(100, 100, 3, 0, Math.PI * 2)
context.fill()
context.beginPath()
context.arc(200, 100, 3, 0, Math.PI * 2)
context.fill()
context.beginPath()
context.arc(300, 100, 3, 0, Math.PI * 2)
context.fill()
context.beginPath()
context.arc(300, 200, 3, 0, Math.PI * 2)
context.fill()
context.beginPath()
context.arc(200, 200, 3, 0, Math.PI * 2)
context.fill()
椭圆
使用 ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) 来绘制椭圆:
x和y为圆心坐标,radiusX为x轴半径,radiusY为y轴半径。rotation椭圆的旋转角度,startAngle开始角度,endAngle结束角度,三者单位均为弧度。counterclockwise绘制的角度,false为顺时针(默认),true为逆时针。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.beginPath()
context.ellipse(100, 100, 60, 30, 0, 0, Math.PI * 2)
context.closePath()
context.stroke()
context.beginPath()
context.ellipse(250, 100, 60, 30, Math.PI * 0.5, 0, Math.PI * 1.5)
context.stroke()
context.closePath()
context.beginPath()
context.ellipse(400, 120, 60, 60, 0, Math.PI, Math.PI * 2)
context.fill()
context.closePath()
贝塞尔曲线
二次贝塞尔曲线 quadraticCurveTo(cpx, cpy, x, y):cpx 和 cpy 为控制点坐标,x 和 y 为结束点坐标,起点坐标一般为 moveTo 或 lineTo 提供。
三次贝塞尔曲线 bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) 有两个控制点。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.beginPath()
context.moveTo(50, 50)
context.quadraticCurveTo(150, 150, 200, 50)
context.stroke()
context.beginPath()
context.moveTo(280, 100)
context.bezierCurveTo(380, 20, 400, 290, 480, 250)
context.stroke()
const drawDot = (x, y) => {
context.beginPath()
context.arc(x, y, 3, 0, Math.PI * 2)
context.fill()
}
context.fillStyle = 'red'
drawDot(50, 50)
drawDot(150, 150)
drawDot(200, 50)
drawDot(280, 100)
drawDot(380, 20)
drawDot(400, 290)
drawDot(480, 250)
样式
线条的样式
-
使用
lineWidth来设置线条的宽度,默认为1,单位是 px。 -
使用
lineCap = 'butt'(默认) | 'square' | 'round'来设置线条开始和结尾处的样式。 -
使用
lineJoin = 'miter'(默认) | 'round' | 'bevel'来设置线条拐角的样式。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.beginPath()
context.moveTo(50, 20)
context.lineTo(450, 20)
context.stroke()
context.beginPath()
context.lineWidth = 6
context.lineCap = 'square'
context.moveTo(50, 60)
context.lineTo(450, 60)
context.stroke()
context.beginPath()
context.lineWidth = 18
context.lineCap = 'round'
context.moveTo(50, 100)
context.lineTo(450, 100)
context.stroke()
context.beginPath()
context.lineJoin = 'round'
context.moveTo(50, 150)
context.lineTo(150, 150)
context.lineTo(200, 250)
context.stroke()
context.beginPath()
context.lineJoin = 'bevel'
context.moveTo(250, 250)
context.lineTo(300, 150)
context.lineTo(400, 150)
context.stroke()
虚线
使用 setLineDash(segments) 方法可以将描边设置成虚线,segments 是一个数组,有三种情况:
- 一个值
[10],实线和空白长度都是 10px。 - 两个值
[10, 20],实线长度是 10px,空白长度是 20px。 - 三个值
[20, 5, 10],实线 20px,空白 5px,实线 10px,空白 20px,实线 5px,空白 10px...
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.lineWidth = 10
context.beginPath()
context.setLineDash([10])
context.moveTo(50, 50)
context.lineTo(450, 50)
context.stroke()
context.beginPath()
context.setLineDash([10, 20])
context.moveTo(50, 150)
context.lineTo(450, 150)
context.stroke()
context.beginPath()
context.setLineDash([20, 5, 10])
context.moveTo(50, 230)
context.lineTo(450, 230)
context.stroke()
context.beginPath()
context.setLineDash([20, 5, 10])
// 起点往左偏移10px
context.lineDashOffset = 10
context.moveTo(50, 270)
context.lineTo(450, 270)
context.stroke()
透明度
设置 globalAlpha 属性或者使用有透明度的样式都可以绘制有透明度的图形。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.beginPath()
context.fillStyle = 'rgba(192, 168, 100, 0.25)'
context.fillRect(50, 40, 200, 100)
context.beginPath()
context.strokeStyle = 'rgba(10, 10, 10, 0.25)'
context.strokeRect(50, 160, 200, 100)
context.beginPath()
context.globalAlpha = 0.25
context.fillStyle = 'green'
context.arc(380, 150, 80, 0, Math.PI * 2)
context.fill()
渐变
线性渐变 createLinearGradient(x1, y1, x2, y2),径向渐变 createRadialGradient(x0, y0, r0, x1, y1, r1),两种渐变都需要用 addColorStop(offset, color) 方法来添加颜色。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
const gradient1 = context.createLinearGradient(50, 20, 400, 20)
gradient1.addColorStop(0, '#fce38a')
gradient1.addColorStop(1, '#f38181')
context.beginPath()
context.fillStyle = gradient1
context.fillRect(50, 20, 400, 20)
const gradient2 = context.createLinearGradient(50, 60, 400, 20)
// 从0.5才开始渐变
gradient2.addColorStop(0.5, '#fce38a')
gradient2.addColorStop(1, '#f38181')
context.beginPath()
context.fillStyle = gradient2
context.fillRect(50, 60, 400, 20)
const gradient3 = context.createRadialGradient(150, 180, 50, 150, 180, 10)
gradient3.addColorStop(0, '#1abc9c')
gradient3.addColorStop(0.25, '#3498db')
gradient3.addColorStop(0.5, '#9b59b6')
gradient3.addColorStop(0.75, '#7f8c8d')
gradient3.addColorStop(1, '#dfe4ea')
context.beginPath()
context.fillStyle = gradient3
context.fillRect(90, 120, 120, 120)
非零环绕规则
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.beginPath()
// 顺时针 计数器 +1
context.arc(150, 150, 50, 0, Math.PI * 2)
// 逆时针 计数器 -1 而它又经过外层圆 外层圆的计数器为 1 最终内层的值为 0 所以不会被填充
context.arc(150, 150, 25, 0, Math.PI * 2, true)
context.fill()
context.beginPath()
// 顺时针
context.moveTo(250, 100)
context.lineTo(400, 100)
context.lineTo(400, 200)
context.lineTo(250, 200)
context.lineTo(250, 100)
// 逆时针
context.moveTo(300, 125)
context.lineTo(300, 175)
context.lineTo(350, 175)
context.lineTo(350, 125)
context.lineTo(300, 125)
context.fill()
文本
填充和描边
使用 fillText(text, x, y, maxWidth) 方法绘制字体;使用 strokeText(text, x, y, maxWidth) 方法绘制描边字体,strokeStyle 可以设置描边颜色:
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.font = '60px consolas'
context.strokeStyle = 'green'
context.strokeText('Hello World!', 50, 100)
context.fillText('Hello World!', 50, 200)
样式
font = '10px sans-serif'(默认值)绘制文本的样式。textAlign = 'left' | 'right' | 'center' | 'start'(默认值) | 'end'文本对齐方式。direction = 'inherit'(默认值) | 'ltr' | 'rtl',ltr表示文本从左向右,rtl表示文本从右向左。textBaseline,基线对齐选项,决定文字垂直方向的对齐方式。
测量
使用 measureText() 方法测量文本,返回一个 TextMetrics 对象。
阴影
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.font = '50px consolas'
// 设置阴影颜色
context.shadowColor = '#cccccc'
// 设置填充颜色
context.fillStyle = '#ee7934'
// X轴上的阴影
context.shadowOffsetX = 10
// Y轴上的阴影
context.shadowOffsetY = 10
// 阴影的模糊程度
context.shadowBlur = 5
context.fillText('Hi Canvas !', 100, 50)
context.fillRect(100, 100, 200, 60)
context.shadowOffsetX = -10
context.shadowOffsetY = -10
context.shadowBlur = 5
context.fillText('Hi Canvas !', 100, 250)
图片
在 canvas 中可以使用 drawImage() 来绘制图片。
drawImage(image, dx, dy),image是要渲染的图片对象,dx dy是图片左上角的坐标。drawImage(image, dx, dy, dw, dh),dw dh是图片的宽度和高度。drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh),截取图片,sx sy是裁剪框的左上角坐标,sw sh是裁剪框的宽高。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
const image = new Image()
image.src = 'https://image-1304160910.file.myqcloud.com/avatar_2.png'
image.onload = () => {
context.drawImage(image, 100, 100, 500, 500, 30, 50, 200, 200)
context.drawImage(image, 260, 50, 200, 200)
}
变形
状态的保存和恢复
save():保存画布的所有状态,restore():恢复上次的状态。状态存储在栈中,调用 save() 状态入栈保存状态,调用 restore() 将保存状态从栈中弹出。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(50, 40, 400, 50)
context.save()
context.fillStyle = 'green'
context.fillRect(50, 120, 400, 50)
context.restore()
context.fillRect(50, 210, 400, 50)
移动、旋转和缩放
-
移动:
translate(x, y)。 -
旋转:
rotate(angle),顺时针旋转,单位是弧度。 -
缩放:
scale(x, y),默认值为 1。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.fillStyle = '#a29bfe'
context.save()
context.save()
// 原点变成(50, 50)
context.translate(50, 50)
context.fillRect(0, 0, 100, 50)
context.restore()
// 逆时针旋转 30°
context.rotate(-Math.PI / 6)
context.fillRect(100, 200, 50, 50)
context.restore()
// x轴缩放2倍,y轴不变
context.scale(2, 1)
context.fillRect(150, 200, 50, 50)
Transform
-
transform(a, b, c, d, e, f这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵:-
a水平方向的缩放 -
b竖直方向的倾斜偏移 -
c水平方向的倾斜偏移 -
d竖直方向的缩放 -
e水平方向的移动 -
f竖直方向的移动
-
-
setTransform(a, b, c, d, e, f)将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用transform方法。 -
resetTransform()方法为重置当前变形为单位矩阵,效果等同于setTransform(1, 0, 0, 1, 0, 0)。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
const sin = Math.sin(Math.PI / 6)
const cos = Math.cos(Math.PI / 6)
context.translate(150, 150)
for (let i = 0, c = 0; i <= 12; i++) {
c = Math.floor(255 / 12 * i)
context.fillStyle = 'rgb(' + c + ',' + c + ',' + c + ')'
context.fillRect(0, 0, 100, 10)
context.transform(cos, sin, -sin, cos, 0, 0)
}
context.setTransform(1, sin, 0, 1, 300, 100)
context.fillStyle = 'rgba(255, 128, 255, 0.5)'
context.fillRect(0, 0, 100, 100)
合成和裁剪
合成
合成的图形受限于绘制的顺序。如果我们不想受限于绘制的顺序,那么我们可以利用 globalCompositeOperation 属性来改变这种情况。
Compositing 示例 - Web API 接口参考 | MDN (mozilla.org)
裁剪
裁剪的作用是遮罩,用来隐藏不需要的部分,所有在路径以外的部分都不会在 canvas 上绘制出来。
clip(),将当前正在构建的路径转换为当前的裁剪路径。clip(path, fillRule),path为需要剪切的Path2D路径,fillRule为判断是在路径内还是在路径外,允许的值有nonzero:非零环绕原则(默认值),evenodd:奇偶环绕原则。
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
const image = new Image()
image.src = 'https://image-1304160910.file.myqcloud.com/avatar_2.png'
image.onload = () => {
context.arc(150, 150, 100, 0, Math.PI * 2)
context.clip()
context.drawImage(image, 50, 50, 200, 200)
// 效果一致
// const path2d = new Path2D()
// path2d.arc(150, 150, 100, 0, Math.PI * 2)
// context.clip(path2d)
// context.drawImage(image, 50, 50, 200, 200)
}
下载图片
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
const image = new Image()
image.src = 'https://image-1304160910.file.myqcloud.com/avatar_2.png'
image.onload = () => {
context.arc(150, 150, 100, 0, Math.PI * 2)
context.clip()
context.drawImage(image, 50, 50, 200, 200)
}
const button = document.createElement('button')
button.innerText = '下载图片'
button.style.display = 'block'
document.body.appendChild(button)
button.addEventListener('click', () => {
const base64 = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.download = '图片.png'
a.href = base64
a.click()
})
本文的所有代码位于:stitch007/canvas (github.com)